Top
Photo Drawings Software Writing Reading Memo Study Profile Bookmark 
Amethyst DumLambda PetitLambda Yamanba JSharp JFlat OOPS yash KumonoIto ML Toys 

MLでウィンドウマネージャ作成

X Window Systemのウィンドウマネージャを作成します。

以下のように各種の言語でウィンドウマネージャが実装されています。
Perlwm
perlで記述されています。
PLWM
Pythonで記述されています。
GWM
ウィンドウ操作に関するプリミティブを追加したLispインタプリタ上 に実装されています。
GwML
上のGWMに触発されて開発されました。
Camlで実装されています。また、カスタマイズもCamlで記述します。
Standard MLでもウィンドウマネージャを作ってみましょう。

X Window Systemの仕組み

X Window Systemはクライアント-サーバの形をとっています。 サーバはワークステーションやパソコン上で動作して、 ウィンドウやグラフィックスのディスプレイへの表示や、 キーボード/マウス入力を適切なウィンドウに振り分けたりといった ユーザとの直接のインターフェイスを実装します。 クライアントは、サーバと同じマシンあるいは別のマシン上で動作し、 サーバに対してディスプレイへの描画を要求したり、 ユーザの入力情報をサーバから受け取るなど xtermやxemacsなどはクライアントにあたります。
クライアントとサーバとは、TCP/IPなどのネットワーク上で 要求/応答を交わします。 この要求/応答のフォーマットおよび手順はXプロトコルと呼ばれる仕様で定義されています。 Xプロトコルについては こちらの文書 を参照してください。

Xプロトコルライブラリ

上のリストで挙げたperlwmは570行のPerlコードで構成されています。 また、 aewm はC言語で実装されていますが、コードサイズは1600行ほどに過ぎません。 当然、これらは機能がごく限られていますが、ウィンドウマネージャとして必要な最低限の機能は備えています。

ただし、これらのプログラムはいずれもXプロトコルを隠蔽するライブラリを前提としています。 perlwmはX11モジュールを別に必要としますし、 aewmはXlibを利用しています。 また、PLWMもpython-xlib を使用します。

実は、ウィンドウマネージャよりもこれらのXプロトコル抽象モジュールの方が 規模が大きく、内容も複雑なものとなっています。 perlwmが570行ですが、 PerlのX11モジュールは4300行もあります。 その他の言語向けのライブラリも、 python-xlibが9000行、 GwMLのプロトコル関係モジュールが15000行と、かなりの分量です。

一方、MLにはこれらと同等のライブラリが用意されていません。 したがって、ウィンドウマネージャ本体に取りかかる前に、 Xプロトコルを抽象化するモジュールを作成する必要があります。

SMLNJの配布パッケージに含まれているeXeneがXプロトコルを抽象化するモジュールを実装しています。 eXeneは、SMLで実装されたツールキットで、 XプロトコルメッセージをXサーバとの間で直接やり取りする方法をとっています。 Xプロトコルメッセージに関連するソースファイルは ${SMLNJPackageDir}/src/eXene/lib/protocolディレクトリに 収められています。
ただし、このeXeneのコードはCML(Concurrent ML)の機能やキャストなど SMLNJ固有の機能を使用しています。 また、今回作成するウィンドウマネージャが実装するのはごく小さな機能のみなので、 eXeneが提供するウィジェットセットやその他のリソース管理機能は不要です。 そこで、eXeneのごく低レベルなルーチンのみを借用し、これをもとに、Xプロトコルを薄く包むラッパー関数群を作成します。

ウィンドウマネージャ

以下のような最小機能/コードサイズのウィンドウマネージャを目標にします。

ウィンドウマネージャについては 「Xlibプログラミングマニュアル」にサンプル実装コード付きで解説されています。

多くのウィンドウマネージャが提供はつぎの機能を提供しています。

アプリケーションウィンドウの管理
ウィンドウの移動/サイズ変更/アイコン化など、 個々のアプリケーションウィンドウの外観を変更する機能をユーザに提供します。
デスクトップ共通部分の管理
ルートウィンドウを操作することにより、 デスクトップ全体に関わる たとえば ルートウィンドウがクリックされた際にポップアップメニューを表示したり、 ユーザの好みの画像を壁紙として設定します。
今回は、アプリケーションウィンドウの管理機能のみを実装することにします。 デスクトップ共通部分の管理については通常のXアプリケーションプログラミングと大差ないと思います (Xプログラミングはわたしは今回がはじめてなので推測ですが)。

以下に、フレームの作成、フレームの消去、フレームの移動/サイズ変更について手順を説明します。

フレームの作成

アプリケーションウィンドウの上部のタイトルバーや、 周囲にユーザがマウスでサイズ変更ができるよう枠を表示します。 これは、アプリケーションウィンドウのそれぞれに対応して、 ウィンドウ(これをフレームと呼びます)をルートウィンドウの直接の子ウィンドウとして作成し、 アプリケーションウィンドウの親を、ルートウィンドウからこのフレームに設定し直します。
この処理はつぎの二つのタイミングでおこなわれます。
ウィンドウマネージャが開始した時点
ウィンドウマネージャ開始前にすでに表示されているウィンドウのリストを QueryTreeリクエストにより取得し、 これらに対して上記の処理をおこないます。
ウィンドウマネージャ開始後にアプリケーションウィンドウが表示される時点
まず、ウィンドウマネージャ開始時に、ルートウィンドウを対象とし、 SubstructureRedirect をイベントマスクとして指定してChangeWindowAttributesリクエスト をサーバに送信しておきます。 これにより、ルートウィンドウを親とするウィンドウをアプリケーションが表示しようとする際に、 サーバはウィンドウマネージャにMapRequestを送信します。 この機会にウィンドウマネージャは上記のフレーム作成処理をおこないます。

フレームの消去

フレームウィンドウを生成する際にSubstructureNotifyをイベントマスクに設定します。 これにより、アプリケーションウィンドウが非表示となる際に、サーバから UnmapNotifyイベントがウィンドウマネージャに通知されます。 この時に、ウィンドウマネージャはこのアプリケーションウィンドウに対応するフレームを消去します。

フレームの移動/サイズ変更

フレームウィンドウを生成する際にButtonPressButtonMotionなど 適切なイベントマスクを指定しておけば、フレーム上でマウスボタンがクリックされた場合や マウスが移動した場合にサーバから通知が送られてきますので、 これに応じてフレームを移動したり、サイズを変更するなどの処理をおこないます。 必要に応じてアプリケーションウィンドウのサイズも変更します。

ウィンドウマネージャ by SML

Xプロトコルモジュール

まず、Xプロトコルを抽象化するモジュールを、 eXeneのソースをもとに作成します。

eXeneのソース(lib以下)は15000行ですが、これを5000行まで削りました。

Xストラクチャに、Xプロトコルの各リクエストに対応する関数群を定義しています。 これらの関数の引数はXプロトコルに記載されている仕様を直接表現しています。 たとえばGetWindowAttributesリクエストに対応して、 つぎの型の関数を定義しています。

- X.GetWindowAttributes;
val it = fn
  : X.xdisplay
    -> {win:X.xid}
       -> {all_event_mask:X.event_mask,
           backing_pixel:X.pixel,
           backing_planes:X.plane_mask,
           backing_store:X.backing_store,
           bit_gravity:X.gravity, colormap:X.xid option,
           do_not_propagate:X.event_mask,
           event_mask:X.event_mask, input_only:bool,
           map_is_installed:bool, map_state:X.map_state,
           override_redirect:bool, save_under:bool,
           visual:X.visual_id, win_gravity:X.gravity}

ただし、すべてのXプロトコルリクエストに対応して関数を用意しているわけではありません。 およそウィンドウマネージャの実装に必要な関数を用意しています。 x.smlをみれば、足りない関数を追加するのは簡単であることが分かるでしょう。
そのほかに、Xlibに相当するレベルの関数も少しですが定義しています。
たとえば
XMoveWindow(Display* display, Window w, int x, int y)
に相当する関数として
- X.MoveWindow;
val it = fn
  : X.xdisplay -> {pt:Geometry.point, win:X.xid} -> unit
を定義しています。

これらのモジュールのソースは、 以下のウィンドウマネージャのソースパッケージに同梱しています。

ver. 0.1

最初のバージョンは、ウィンドウマネージャ本体のサイズが250行です。

ウィンドウマネージャ本体のソース

次のスクリーンショットをみても分かるように、機能はごく限られています(perlwmやaewmより劣ります)。

タイトルバーをマウスでクリックすることで次のウィンドウ操作が可能です。

  • 左ボタンでクリックしてドラッグするとウィンドウを移動できます。
  • 中ボタンでクリックすると、ポインタがウィンドウの右下に移動します。そのままドラッグするとウィンドウのサイズが変更できます。
  • 右ボタンでクリックすると、ウィンドウを「強制」終了します。

ソース(winmgr-0.1.tar.gz)

テスト

テストは少々面倒です。

まず、ウィンドウマネージャを実行するディスプレイと開発をおこなうディスプレイとを別に用意する必要があります。 (開発をおこなうディスプレイ上でウィンドウマネージャを実行するのは危険です。)
一例として、わたしはつぎのような環境でテストをおこなっています。

左側のディスプレイはワークステーションに接続されており、 ウィンドウマネージャが動作しています。 右側のノートパソコン上では、 Astec-Xを利用してワークステーション上のエディタ(xemacs)のウィンドウを表示して、 ウィンドウマネージャの制御とデバッグ出力のチェックをおこなっています。
(VMWareやAstec-Xをうまく使えば一つの物理ディスプレイ上に複数の論理ディスプレイを表示できるかもしれません。)

つぎに、X-Windowシステムの設定が必要です。
$HOME/.xinitrcを次のような内容にしておきます。
xbiff > /dev/null 2>&1 &
kterm > /dev/null 2>&1  &
xclock > /dev/null 2>&1  &
exec sh -c 'while true;do sleep 1000;done'
最後の行は、通常ならばtwmwmakerなどウィンドウマネージャを指定します。 しかし、ウィンドウマネージャを実行すると、後からSMLウィンドウマネージャを実行することができなくなります。 SMLウィンドウマネージャを起動するよう、ここに指定してもよいのですが、 ウィンドウマネージャが落ちる度にX-Windowを再起動することになり時間がかかります。 そこで、この場合はshに、永久にループし続けるコマンドを指定しています。 Xウィンドウを使用せず、勝手に終了しないコマンドなら何でも構いません。

そして、Xウィンドウを起動します。 学校のワークステーションだと、普通はSunのウィンドウマネージャが起動されてしまいます。 そこで、ログインウィンドウ

の「オプション」コンボボックスで「コマンド行ログイン」を選択し、

is15e1uXX login: kiyoshiy
password: ********
      :
bash$ startx
のようにして、Xウィンドウを起動します。

そして、Astec-Xなどを使って、ターミナルをワークステーション上で起動して、 開発する側のディスプレイ上に表示します。 このターミナル上で、ワークステーションのディスプレイを指定してウィンドウマネージャを起動します。 (文章だと分かりにくいですが。)

bash-2.04$ sml-cm
Standard ML of New Jersey, ...
- CM.make'("Xprotocol/sources.cm");
[starting dependency analysis]
    :
val it = () : unit
- use "winmgr.sml";
[opening winmgr.sml]
    :
val it = () : unit
- winmgr ":0.0";

(上に示したような画面イメージは

bash$ xwd -root -display localhost:0 -out /tmp/dump.xwd
bash$ convert /tmp/dump.xwd /tmp/dump.jpg
により取得できます。)

ウィンドウマネージャを終了するには、SML上で ^C を入力します。 ただし、この方法ではXのセッションが残っているので、SMLのプロンプトに対して

 - X.closeXDisplay (valOf (!g_dpy)); 
と入力してセッションを切断する必要があります。

Xウィンドウを終わらせるには、shコマンドのプロセスを探して殺します。

bash$ ps -a | grep sh
   123 console  0:00 sh
bash$ kill -9 123
Xウィンドウが終了してコマンドプロンプトが表示されるはずです。