†逆襲のOS/2†

VS3:スクリーンセーバーを作る

作成開始日 2006.08.17
最終更新日 2006.12.31

OS/2にはフリーのフツーのスクリーンセーバーがない。Hobbesは一通り覗いてみたけど、変なものや不完全なものや余分な機能が付いているものばかりで、ごくシンプルなものがない。そこで、自分で作ることにした。その名も「VS3 (Very Simple Screen Saver;VS-cube)」。開発ツールはSibyl、イメージとしてはWindowsの[My Picture Slideshow Screen Saver]のようなもの。

スクリーンセーバーの原理

さて、スクリーンセーバーとは;

@一定時間入力がないと自動的に起動し、
A画面いっぱいに動く画像などを表示し、
B何等かの入力があると停止して非表示となり、
C起動前にアクティブだったウィンドウにフォーカスを戻す

プログラムのこと。では、ステップ毎に問題点を明確にしていこう。

@一定時間入力がないと自動的に起動

いきなりだが、実はこれが今回の最大の難関だった。これはスクリーンセーバー単体では解決できない問題だ。そもそも、あるプログラムが、他のプログラムのキー入力の有無をチェックするなんて可能なのだろうか? ウィンドウがアクティブだというのは、そのウィンドウがキー入力のメッセージを独占するという事と同義語だから、非アクティブな状態で休眠しているスクリーンセーバーが、アクティブ・ウィンドウにキー入力があったかどうかなんて、知りようがない。

そこで、こういう場合には、アクティブ・ウィンドウへ送られる前のキー入力をフック(横取り)するという手法を使うのが一般的らしい。APIにはフック用の関数もちゃんと用意されている。でも、どのプログラムがアクティブになるかはわからないし、システム全体でフックするのは面倒臭そうだし、一つ間違うと大変なことになりそうなので今回は避けた。フック後の処理にバグがあれば、システム全体のキー入力が不可能になる可能性がある。そうなるとデバグがたいへんだ(HobbesにあるblackoutスクリーンセーバーのDLLを借用すれば簡単だろうが(^_^;)。そこで、その代わりとなるインチキな方法を考案した。これについては後述する。

A画面いっぱいに動く画像などを表示

これは比較的簡単だ。どんな画像をどんなふうに動かすかで難易度は大きく変わるが、いずれにしてもスクリーンセーバーの本質とはほとんど関係ない。というか、場合によっては何も表示しない真っ黒の画面でも、ディスプレイ保護(スクリーンセーブ)という役目は果たす。なお、Sibylで開発する際には、ウィンドウのタイトルバーを完全に表示しなくするように、ちょびっと特殊な細工をしなくてはならない(別項参照)。

B何等かの入力があると停止して非表示

これも比較的簡単。キー入力またはマウス移動のイベントハンドラに、描画の停止とフォームの非表示の命令を書くだけ。ただし、非表示(hide)にしただけでは、フォーカスはスクリーンセーバーがそのまま持ち続けている。これはちょっと失礼かつ不便だろう。ということで、次の少々厄介な問題を解決する必要がある。

C起動前にアクティブだったウィンドウにフォーカスを戻す

これもスクリーンセーバー単体の問題ではないのでけっこう難しい。実は、起動時にはスクリーンセーバーがフォーカスを強制取得する必要があるのだが(そうしないとアクティブ・ウィンドウを隠すことができない)、その際にはCaptureFocusメソッドを使用した。では、休眠時にフォーカスを手放すにはどうすればよいのか? CaptureFocusと対になっているKillFocusメソッドを使えば良さそうに見えるが、実はダメ。KillFocusは仮想メソッドで、プログラマが具体的なコードを実装しない限り、何もしないのである。

メソッド機 能
Show/Hide 表示/非表示が可能だが、フォーカスは動かない。
CaptureFocus/KillFocus フォーカスの取得/解放だが、解放のみ仮想メソッド。つまり、取得はできるが解放はできない。
Activate/Deactivate アクティブ化はフォーカス取得とほぼ同義だが、ActivateもDeactivateも仮想メソッドで、そのままでは何の効果もない。

早い話が、KillFocusもDeactivateもフォーカスを「手放したときのハンドラ」であって「手放すための命令」ではないわけ。というか、そもそも「フォーカスを手放す」ための命令は存在しない。そうではなくて、他のプログラムにフォーカスが移動するから、結果としてそのプログラムが「フォーカスを失う」のである。娘を出すには、嫁ぎ先を決めろ、ということだ。つまり、何等かの方法で、他のプログラムにフォーカスを設定してやればよい…のだが、そんなことできるの? SibylのSPCCでは無理だけど(アクセス違反が起きる)、API関数を叩けばできる。それが、WinSetFocus関数。

ただし、その場合も、フォーカスを設定する相手をどうやって特定するかという問題が残る。そこで、スクリーンセーバが起動するときに、それまでアクティブだったプログラムのハンドルを取得しておいて、休眠時にそのハンドルにフォーカスを返してやればよい。実家に戻っていた娘を、嫁ぎ先に返すようなもんだ。アクティブなウィンドウのハンドルを取得するには、WinQueryFocus関数を使用する。

var
  h: HWND;	//グローバルに宣言しておいて

Procedure....
Begin
  h:=WinQueryFocus(HWND_DESKTOP);//アクティブ・ウィンドウを記憶
  .....
End;

Procedure...
Begin
  ....
  WinSetFocus(HWND_DESKTOP,h);	//フォーカスを元のウィンドウに返す
End;

フック代わりのインチキな入力検出方法

で、最後の山は、キー入力やマウス入力のフックの代替手段。あくまでも邪道であることをお断りした上で……

◎マウス移動の検出

何等かの入力があるというのは、ユーザーがそのパソコンを使用しているということである。通常のPMアプリケーションであれば、ほぼ間違いなくマウスを使う。だから、一定時間(例えば1秒)ごとにマウスポインタの位置をチェックして、前回のチェック位置と異なっていれば、ユーザーがマウスを動かした証拠となる。ま、厳密に言えば、1秒ごとに同じ位置にマウスポインタを戻すという操作もできないわけではないので、理論上はこの方法ではチェックできないマウス操作も存在する。が、それは非現実的だろう。マウスの移動の感知に関しては、この方法は充分実用的だと思う。

Var
  p_now, p_pre: TPoint;		//マウスの現在位置、前回位置

Begin
  p_now:=Screen.MousePos;	//現在位置取得
  If (p_now<>p_pre) Then TimerSleep.Stop;
  If (p_now= p_pre) Then TimerSleep.Start;★ここちょっと再チェック
  p_pre:=p_now;			//現在位置を次回の前回位置に記憶
End;

◎キー入力の検出

しかし、マウス操作を伴わない、キー入力のみの操作もありうる。キーボード入力ではこの考え方は通用しない。最初、マウスポインタの代わりにカーソルの位置をチェックするという方法を思い付いたが、すべてのPMアプリケーションにテキストカーソルが存在しているわけではないし、そもそもDOS窓などでは全然役に立たない。そこで、カーソルではなく、キーボードそのものの状態をチェックする方法を考えた。APIには、メッセージ・キューとは別に、キーボードのハードウェア的な状態をチェックできる関数が用意されている。

しかし、キーボードの状態変化は瞬間的なものであるため検出が難しい。検出と変化のタイミングが一致すればよいが、そうでないと状態変化が検出できない。この点がマウスと大きく異なる。たとえば、マウスがある瞬間に(100,100)の座標あり、1秒後に(50,120)の座標にあれば、それは間違いなく「動いた」と言うことである。また、1秒後にも(100,100)にあれば、それは「動いていない」ということである。これは検出のタイミングに拘らず常に判別できる。厳密には、動いたあと(100,100)に戻って来たという可能性もないわけではないが、マウスの場合にはほとんどありえない状況だ。

しかし、キーボードではこうはいかない。キー入力が感知できるのは、そのキーを押した瞬間のみである。ある瞬間と1秒後のキーボードの状態が同じであったとしても、その間に入力がなかったという根拠にはならない。キーボードには「何も押していない」という基底状態が存在してしまっているからだ。チェック間隔を短くすれば変化を拾えるが、それではシステム負荷が大きくなり過ぎる。かと言って、間隔を長くすれば、チェックをすべてすり抜けてしまう可能性もある。が、要は確率だ。現実にはそこまで神経質に考える必要もなかろう。

例えばスクリーンセーバーが起動するまでの時間を10分とする。10分間は600秒であり、1秒に1回チェックしても600回になる。600回のチェックすべてで同じ状態(何もキーを押していない)なんていうのは、一本指入力でもないかぎり、ちょっと考えにくいだろう。600回に1回の割合で感知できれば、キーボードを使っている間中スクリーンセーバーは起動しない。充分実用的だ。それに、よしんば誤起動したところで、キー一発で元に戻るのだから実害はほとんどない。実のところ、これがこの手法を正当化しうる最大の理由だ。

となれば、あとは、すべてのキーの入力をどうやって感知するかだが、これにはWinSetKeyboardStatusTable関数を使用した。関数名からも判るように、本来は状態取得ではなく状態設定の関数だが、引数のフラグの設定によって取得にも使える。この関数は、キーの状態(byte型)を256個の配列に返す。で、この1つ1つをチェックするのは大変なので、すべての配列の値を足してチェックサムを作り、このチェックサムの異同でキー入力の有無をチェックすることにした。我ながら呆れた手抜きだが、実際に使ってみるとけっこうまともに機能する。

Var
  KeyState:ARRAY[0..256] OF BYTE;	//キーの状態テーブル
  sum_now, sum_pre: Integer		//現在と前回のチェックサム
  n: integer;

Begin
  sum_now:=0;
  WinSetKeyboardStateTable(HWND_DESKTOP,KeyState[1],FALSE);
  For n:=1 to 256 do
  Begin
    sum_now:=sum_now+KeyState[N];
  End;

  If (sum_now<>sum_pre) Then TimerSleep.Stop;
  If (sum_now= sum_pre) Then TimerSleep.Start;
  sum_pre:=sum_now;
End;

プログラムファイル

で、こんなんできました。ときどき変な動きをしますが、まあ、試作品の御愛嬌ということで。ちなみに、readme.txtはぎこちない英語です(^_^; 秘かにHobbesに投稿しようと思ってたもんで。


【逆襲のOS/2目次】 【ホーム】