VCL を使ったRAD開発環境では、 Delphi を起動したときにすでに TForm1 のインスタンスがつくられており、これが見かけ上のメインウィンドウになる。 TForm は、Forms.pas で定義されているVCL のクラスであるから、VCL を使わずに Delphi のオブジェクトパスカルだけで Windows のアプリケーションをつくるためには、まずメインウィンドウをつくることが最初の仕事になる。 本章では、パスカルのコードだけでメインウィンドウをつくり、そのコードについて解説する。
本章で見るようにメインウィンドウをコードでつくることは比較的簡単であるが、すべてのウィンドウズアプリケーションの基本的エッセンスはこれらのコードに詰め込まれているといっても過言ではないほど重要な意味を持っている。 VCL を使ったコーディングに慣れていると、このコードは煩雑で異様な感触を持つことだろう。 しかし、一度その意味を理解してしまうと、これらのコードがどれほど重要で、汎用性が高いものであるかすぐに気づくはずだ。
マイクロソフト社が提供するヘルプファイルの一つに Progtech.hlp がある。 これは Win32 の仕様に基づいたアプリケーション開発のテクニックの基本について説明したものだ。 この中に、Generic.c という最も基本的な Win32 プログラムがある。 これはC言語で書かれたもので、単に何もしない Window を表示するだけのアプリケーションである。 しかし、すべての Win32 アプリケーションの元になる重要なプログラムである。以下のリストは、これを Delphi の Object Pascal に書き直したものだ。
program Generic01; //--------------------------------------------------------------- // generic01.dpr: Source code for generic //--------------------------------------------------------------- uses windows, messages; //------------------------------------------------------------ // Event Handlers //------------------------------------------------------------ procedure MainPaint(hWindow:HWND; pt:TPaintStruct); begin SetBkMode(pt.hdc,TRANSPARENT); TextOut(pt.hdc,10,10,'Hello, World!',13 ); end; procedure MainDestroy(hWindow: HWND); begin PostQuitMessage(0); end; //------------------------------------------------------------ // Main Window Procedure //------------------------------------------------------------ function MainWndProc(hWindow: HWND; Msg: UINT; WParam: WPARAM; LParam: LPARAM): LRESULT; stdcall; export; var ps: TPaintStruct; begin Result := 0; case Msg of //--------------- WM_PAINT --------------- WM_PAINT: begin BeginPaint(hWindow, ps); MainPaint(hWindow,ps); EndPaint( hWindow, ps ); end; //--------------- WM_DESTROY ------------- WM_DESTROY: MainDestroy(hWindow); //-------- そのほかのメッセージの処理 ------ else begin result := DefWindowProc( hWindow, Msg, wParam, lParam ); exit; end; end; // case end; //------------------------------------------------------------ // Main Procedure //------------------------------------------------------------ var wc: TWndClass; hWindow: HWND; Msg: TMsg; begin wc.lpszClassName := 'GenericAppClass'; wc.lpfnWndProc := @MainWndProc; wc.style := CS_VREDRAW or CS_HREDRAW; wc.hInstance := hInstance; wc.hIcon := LoadIcon(0,IDI_APPLICATION); wc.hCursor := LoadCursor(0,IDC_ARROW); wc.hbrBackground := (COLOR_WINDOW+1); wc.lpszMenuName := nil; wc.cbClsExtra := 0; wc.cbWndExtra := 0; RegisterClass(wc); hWindow := CreateWindowEx(WS_EX_CONTROLPARENT or WS_EX_WINDOWEDGE, 'GenericAppClass', 'Generic01', WS_VISIBLE or WS_CLIPSIBLINGS or WS_CLIPCHILDREN or WS_OVERLAPPEDWINDOW, 200,100, 400,300, 0, 0, hInstance, nil); ShowWindow(hWindow,CmdShow); UpDateWindow(hWindow); //---- Message Loop --------------------- while GetMessage(Msg, 0, 0, 0) do begin TranslateMessage(Msg); DispatchMessage(Msg); end; Halt(Msg.wParam); end.
このプログラムは、プロジェクトファイル (*.dpr) だけでできている。 VCLを使ったRADプログラミングに慣れている人には、ずいぶん異様に見えると思うが、 Delphi の開発環境からコンパイルして実行することができる。 その手順を以下に示す。
実行時のイメージは、右図のようになる。
実行中に以下のことを確認してほしい。
このような機能は、ユーザーが通常のメインウィンドウの動作として期待している機能であり、Generic01.dpr で作成されたウィンドウで実現されていることが分かる。 とくに最小化するときやそれから復帰するとき、タスクバーに吸い込まれたり飛び出してくるようにアニメーションすることは重要だ。 RADプログラムでデフォルトで作成された TForm1 のインスタンスである Form1 は、実行中でもこのようなアニメーションをしない。( Delphi ver.5 ではアニメーション動作が改善されている。) このことは、トップレベルウィンドウとしては、Generic01.dpr で作成された方が Win32 の純正ウィンドウであることを示している。
Generic01.dpr を、パスカルの文法を通して見てみると、
と、要約される。 たったこれだけで一応のウィンドウを表示し、しかも前節のような通常のウィンドウとしての機能を実現しているのは驚異的とさえいえる。
ここでさらに注目されるのは、コンパイルされてできる実行形式(*.exe)ファイルのサイズである。 私の環境では、Delphi3.1 で 15.872キロバイト、Delphi2.0 では 10.240キロバイトであった。 RADベースでの TForm1 のインスタンスを表示するだけのプログラムのサイズが 170キロバイトを越えることに比較すれば、わずか10分の1以下のサイズであることは注目に値する。 VCL オブジェクトを使用しないことの重要な代償の一つである。 このような Object Pascal のコードを使えば、 Windows のメモ帳アプリケーション(notepad.exe , 私のバージョンではサイズは 37.816キロバイト)と等価なエディタを Delphi で作ることもできることが、納得できるであろう。
Generic01.dpr のコードを詳しく見ていこう。 Win32 の仕様のもとでウィンドウを作るためには、一般に次のような手順が必要である。
この各手順は、当然ながらメイン手続きで実装されている。 以下で詳しく見てみることにしよう。
マイクロソフトの言葉を借りれば Win32 システムはオブジェクト指向システムなので、ウィンドウを作成するには、まず、ウィンドウクラスを作成して登録することが必要である。Delphi のオブジェクトパスカルでは、このウィンドウクラスを表現するのは TWndClass レコードである。これは、windows.pas で以下のように定義されている。
type TWndClassA = packed record style: UINT; // ウィンドウスタイル lpfnWndProc: TFNWndProc; // ウィンドウ関数のアドレス cbClsExtra: Integer; // このクラスのエクストラバイト数 cbWndExtra: Integer; // このクラスのインスタンスのエクストラバイト数 hInstance: HINST; // アプリケーションのインスタンスハンドル hIcon: HICON; // このクラスのアイコンハンドル hCursor: HCURSOR; // クライアント領域で表示されるカーソルハンドル hbrBackground: HBRUSH; // クライアント領域の背景を塗りつぶすブラシハンドル lpszMenuName: PAnsiChar; // メニューリソースの名前 lpszClassName: PAnsiChar;// このクラスの名前 end; TWndClass = TWndClassA;
各フィールドについて、以下に調べてみる。
type UINT = Integer;
と定義されている。 Generic01.dpr では、
wc.style := CS_VREDRAW or CS_HREDRAW;
としている。 直接、数値3を指定しても同じであるが、このようにした方がコードは格段に読みやすい。 これは、一般的なウィンドウの定義である。
wc.lpfnWndProc := @MainWndProc;
となっている。アドレスを指定することに注意する。
wc.cbClsExtra := 0;
wc.cbWndExtra := 0;
type HINST = Integer;
OSは、このクラスが登録された時、どの実行単位が登録したのかこのフィールドを参照する。 C言語での Win32 アプリケーションのメイン関数は、OSから明示的にインスタンスハンドルを受け取る。 Delphi の Object Pascal の仕様では、 デフォルトでグローバル変数としての hInstance として参照できる。これは、プログラムのどの部分からも参照できる。
wc.hInstance := hInstance;
wc.hIcon := LoadIcon( 0, IDI_APPLICATION );
LoadIcon( ) APIの定義は、 windows.pas で、
function LoadIcon(hInstance: HINST; lpIconName: PChar): HICON; stdcall;
である。 標準アイコンの場合は、第一パラメータをゼロにする。 定義済みアイコンには、
IDI_APPLICATION, IDI_ASTERISK, IDI_EXCLAMATION, IDI_HAND, IDI_QUESTION, IDI_WINLOGO
がある。 名前から形を想像できるだろう。 Generic01.dpr でいろいろ試してほしい。 LoadIcon( ) API は、アイコンハンドルを戻す。
wc.hCursor := LoadCursor( 0, IDC_ARROW );
LoadCursor( ) APIの定義は、
function LoadCursor(hInstance: HINST; lpCursorName: PAnsiChar): HCURSOR; stdcall;
である。 定義済みカーソルの場合は、第一パラメータをゼロにする。定義済みカーソルには、
IDC_APPSTARTING, IDC_ARROW, IDC_CROSS, IDC_IBEAM, IDC_UPARROW, IDC_WAIT
などがある。
wc.hbrBackground := ( COLOR_WINDOW+1 );
として、システムカラーを指定している。 システムカラーには、他に
COLOR_ACTIVEBORDER, COLOR_ACTIVECAPTION, COLOR_APPWORKSPACE, COLOR_BACKGROUND, COLOR_BTNFACE, COLOR_BTNSHADOW
などがある。 Delphi の VCL RAD ベースの Color プロパティーの設定の時にあらわれるドロップダウンリストの下のほうにある clWindow や clBtnFace などがこれらに相当する。 Generic01.dpr のクライアント領域の色は、COLOR_WINDOW のシステムカラーで指定された色であり、カスタマイズされていなければ白色である。
wc.lpszMenuName := nil;
wc.lpszClassName := 'GenericAppClass';
と、文字列リテラルを使っている。 ウィンドウを作成するとき、もとにするクラスをこの名前で指定できる。
以上で、TWndClass 型の wc レコードの全フィールドが指定できた。 Generic01.dpr のこの部分を再掲する。
wc.lpszClassName := 'GenericAppClass'; wc.lpfnWndProc := @MainWndProc; wc.style := CS_VREDRAW or CS_HREDRAW; wc.hInstance := hInstance; wc.hIcon := LoadIcon(0,IDI_APPLICATION); wc.hCursor := LoadCursor(0,IDC_ARROW); wc.hbrBackground := (COLOR_WINDOW+1); wc.lpszMenuName := nil; wc.cbClsExtra := 0; wc.cbWndExtra := 0;
ここまでで定義した TWndClass 型のレコードをOSに登録する。 Generic01.dpr では、
RegisterClass( wc );
の一行である。 RegisterClass( ) APIは、
function RegisterClass(const lpWndClass: TWndClass): ATOM; stdcall;
と定義されていて、TWndClass 型のパラメータを指定すると、 ATOM を戻してくれる。 ATOM は、一意の文字列定数に対応づけられたOSが管理する16ビットの数値である。 Delphi では、
type ATOM = Word;
と定義されている。 ATOM を使って文字列定数を参照することができる。 RegisterClass( ) APIでは、クラス名に対応づけられた ATOM が返される。 通常、プログラマは、 今登録したクラス名を知っているので戻り値である ATOM を無視する。 RegisterClass( ) APIによるウィンドウクラスの登録は、登録したアプリケーションが終了するまで有効である。 RegisterClass( ) は失敗するとゼロを戻す。
ここまでで、TWndClass を定義し、それをOSに登録した。いよいよ、この登録したクラスを使って、ウィンドウを作成する。 Generic01.dprでは、
hWindow := CreateWindowEx(WS_EX_CONTROLPARENT or WS_EX_WINDOWEDGE, 'GenericAppClass', 'Generic01', WS_VISIBLE or WS_OVERLAPPEDWINDOW, 200,100, 300,200, 0, 0, hInstance, nil);
のコードに対応する。 CreateWindowEx( ) APIは、12個のパラメータを必要とする。
function CreateWindowEx(dwExStyle: DWORD; lpClassName: PChar; lpWindowName: PChar; dwStyle: DWORD; X, Y, nWidth, nHeight: Integer; hWndParent: HWND; hMenu: HMENU; hInstance: HINST; lpParam: Pointer): HWND; stdcall;
dwExStyle には、作成されるウィンドウの拡張スタイルを指定する。 windows.pas には、ここに指定できる定数が定義されている。
{ Extended Window Styles } WS_EX_DLGMODALFRAME = 1; // 二重境界を持つ WS_EX_NOPARENTNOTIFY = 4; // 親ウィンドウにWM_PARENTNOTIFYメッセージを送らない WS_EX_TOPMOST = 8; // 常に一番手前に表示 WS_EX_ACCEPTFILES = $10; // ドラグドロップファイルを受け付ける WS_EX_TRANSPARENT = $20; // 透明なウィンドウ WS_EX_MDICHILD = $40; // MDI子ウィンドウ WS_EX_TOOLWINDOW = $80; // ツールウィンドウ WS_EX_WINDOWEDGE = $100; // 境界が盛り上がったウィンドウ WS_EX_CLIENTEDGE = $200; // 境界はくぼんだ縁をもつ WS_EX_CONTEXTHELP = $400; // 状況依存ヘルプのための?マークを表示 WS_EX_RIGHT = $1000; // 右揃え(ヘブライ語とアラビア語で有効) WS_EX_LEFT = 0; // 左揃え (デフォルト) WS_EX_RTLREADING = $2000; // テキストは右から左へ WS_EX_LTRREADING = 0; // テキストは左から右へ(デフォルト) WS_EX_LEFTSCROLLBAR = $4000;// スクロールバーが左側に WS_EX_RIGHTSCROLLBAR = 0; // スクロールバーが右側に(デフォルト) WS_EX_CONTROLPARENT = $10000; // 子ウィンドウのフォーカスをTabキーで移動できる WS_EX_STATICEDGE = $20000; // 3D境界 WS_EX_APPWINDOW = $40000; // 最小化すると強制的にタスクバーに入る WS_EX_OVERLAPPEDWINDOW = (WS_EX_WINDOWEDGE or WS_EX_CLIENTEDGE); WS_EX_PALETTEWINDOW = (WS_EX_WINDOWEDGE or WS_EX_TOOLWINDOW or WS_EX_TOPMOST);
Generic01.dpr では、WS_EX_CONTROLPARENT or WS_EX_WINDOWEDGE を指定しているが、いろいろと試してみてほしい。
lpClassName は、作成の際にもとにするクラスの名前か、それに対応する ATOM を指定する。 Generic01.dpr では、登録したときの名前である 'GenericAppClass' を指定する。
lpWindowName は、作成するウィンドウの名前を指定する。 通常のタイトルバーがあるウィンドウの場合は、タイトルバーに表示される。
dwStyle は、作成するウィンドウのスタイルを指定する。ウィンドウスタイルは、以下のように、windows.pas で WS_XXX の形で定義済みのものを or 演算子で結合できる。 Win32 では、作成されるウィンドウを三つのタイプに分けることができる。 Generic01.dpr で作成したようなトップレベルのメインウィンドウは、オーバーラップウィンドウ(O)という。また、ダイアログボックスはポップアップウィンドウ(P)、ボタンやエディット、リストボックス、ツールバーなどのコントロールは子ウィンドウ(C)という。それぞれ、O、P、Cの場合に指定できる定数を以下に示した。
const { Window Styles } WS_OVERLAPPED = 0; // O、オーバーラップウィンドウを指定 WS_POPUP = $80000000; // P、ポップアップウィンドウを指定 WS_CHILD = $40000000; // C、子ウィンドウを指定 WS_MINIMIZE = $20000000; // O、最小化したウィンドウとして作成 WS_VISIBLE = $10000000; // O,P,C、可視状態で作成 WS_DISABLED = $8000000; // O,P,C、使用禁止の状態で作成 WS_CLIPSIBLINGS = $4000000;// C、兄弟ウィンドウの重なり部分を描画しない WS_CLIPCHILDREN = $2000000;// O,P,C、子ウィンドウの領域を描画しない WS_MAXIMIZE = $1000000; // O、最大化したウィンドウとして作成 WS_CAPTION = $C00000; // O,P,C、{ WS_BORDER or WS_DLGFRAME } WS_BORDER = $800000; // O,P,C、境界を持つ WS_DLGFRAME = $400000; // O,P,C、ダイアログ境界を持つ WS_VSCROLL = $200000; // O,P,C、垂直スクロールバーを持つ WS_HSCROLL = $100000; // O,P,C、水平スクロールバーを待つ WS_SYSMENU = $80000; // O,P,C、システムメニューを持つ WS_THICKFRAME = $40000; // O,P,C、サイズ変更できる太い境界を持つ WS_GROUP = $20000; // C、子トロールグループの最初のメンバーを指定 WS_TABSTOP = $10000; // C、Tabキーでフォーカスを獲得できる WS_MINIMIZEBOX = $20000; // O、最小化ボックスを表示 WS_MAXIMIZEBOX = $10000; // O、最大化ボックスを表示 WS_TILED = WS_OVERLAPPED; WS_ICONIC = WS_MINIMIZE; WS_SIZEBOX = WS_THICKFRAME; { Common Window Styles } WS_OVERLAPPEDWINDOW = (WS_OVERLAPPED or WS_CAPTION or WS_SYSMENU or WS_THICKFRAME or WS_MINIMIZEBOX or WS_MAXIMIZEBOX); WS_TILEDWINDOW = WS_OVERLAPPEDWINDOW; WS_POPUPWINDOW = (WS_POPUP or WS_BORDER or WS_SYSMENU); WS_CHILDWINDOW = (WS_CHILD);
Generic01.dpr では、トップレベルのウィンドウとしては一般的なスタイルである WS_OVERLAPPEDWINDOW と WS_VISIBLE を指定する。
X, Y, nWidth, nHeight パラメータは、作成するウィンドウの位置と大きさを指定する。 X は、画面の左上隅を原点とした右側向きのピクセル単位の水平座標、Y は、下向きの垂直座標である。 X には、CW_USEDEFAULT 値を指定でき、このとき Y の値は無視される。 同様に、nWidth, nHeight は ピクセル単位の幅と高さを指定する。 nWidth には、CW_USEDEFAULT 値を指定でき、このとき nHeight の値は無視される。
hWndParent は、作成するウィンドウが子ウィンドウの場合に、親のウィンドウハンドルを指定する。 Generic01.dpr では、トップレベルウィンドウを作成するので、親は無く、このパラメータにはゼロを指定する。
hMenu パラメータには、このウィンドウのメニューハンドルを指定する。 ここでは、メニューを使わないのでゼロを指定する。
hInstance は、このプログラムの実行単位を識別するインスタンスハンドルを指定する。 generic01.dpr では、デフォルトのグローバル変数である、hInstance を指定する。
最後のパラメータである lpParam は、ウィンドウ関数にこのウィンドウが作成されるときに送られる WM_NCCREATE や WM_CREATE メッセージとともに、何かデータを渡したいときに、そのデータを指すポインタを指定する。
CreateWindowEx( ) APIは、ウィンドウハンドルを戻す。 ウィンドウハンドルは、OSがウィンドウを一意に識別するための HWND 型の32ビットの数値である。
type HWND = Integer;
ウィンドウハンドルは、 多くの Win32 APIのパラメータとなっている重要な数値である。 ウィンドウハンドルを知っていれば、 そのウィンドウに関するほとんど全ての操作をAPIを通して実行することができる。
作成されたウィンドウは、すぐにデスクトップに表示されるわけではない。 WS_VISIBLE スタイルを指定しているからといって作成後すぐに表示されると期待してはいけない。 コードで明示的に表示することが必要なのである。 Generic01.dpr では、
ShowWindow( hWindow, CmdShow ); UpDateWindow(hWindow);
の部分に対応する。 ShowWindow( ) APIは、
function ShowWindow(hWnd: HWND; nCmdShow: Integer): BOOL; stdcall;
と宣言されている。 第一パラメータは、表示するウィンドウのハンドルを指定する。 第二パラメータは、表示の仕方を指定する。 通常、アプリケーションは、ショートカットをダブルクリックされたり、エクスプローラから実行ファイルをダブルクリックされたり、タスクバーなどの特別なランチャから起動されたりするが、起動動作そのものはOSの管理下にある。 OSは、ユーザが起動パラメータなどで指定した表示方式でアプリケーションを起動する。 アプリケーションはメイン手続きのパラメータとして、以下のような値のうちの一つをOSから受け取るので、それを ShowWindow( ) APIの第二パラメータとしてメインウィンドウを表示するとユーザーが期待したとおりの動作となる。
const SW_HIDE = 0; // ウィンドウを隠し、他のウィンドウにフォーカス SW_SHOWNORMAL = 1; // ウィンドウを通常サイズで表示し、フォーカスを与える SW_NORMAL = 1; SW_SHOWMINIMIZED = 2; // 最小化した状態で表示しフォーカスを与える SW_SHOWMAXIMIZED = 3; // 最大化した状態で表示しフォーカスを与える SW_MAXIMIZE = 3; SW_SHOWNOACTIVATE = 4; // 通常サイズで表示するがフォーカスを与えない SW_SHOW = 5; // 通常サイズで表示しフォーカスを与える SW_MINIMIZE = 6; // = SW_SHOWMINIMIZED SW_SHOWMINNOACTIVE = 7; // アイコン化して表示 SW_SHOWNA = 8; // 現在の状態のサイズで表示しフォーカスを与えない SW_RESTORE = 9; // = SW_SHOWNORMAL SW_SHOWDEFAULT = 10; // CreateProcessのパラメータ通りに表示 SW_MAX = 10;
表示のためのパラメータは、C言語ではメイン関数のパラメータとして明示的にOSから受け取るが、Delphi の Object Pascal の場合はデフォルトのグローバル変数 CmdShow として、コードの中で参照できる。 Generic01.dpr でも、このパラメータを指定している。 UpDateWindow( ) APIは無くても動作するが、多くのコード例では ShowWindow( ) とともにここで実行するように推奨している。 windows.pas では、
function UpdateWindow(hWnd: HWND): BOOL; stdcall;
と宣言されている。 この関数の役割は、パラメータで指定されたウィンドウのウィンドウ関数にメッセージを送って、クライアント領域の再描画を要求する事である。
ウィンドウが表示されたところでプログラムが終了しては意味がない。 従って、 次のコードは必然的にループ構造をとる。このループはただ終了の合図を待つあまり意味の無いループではなく、Win32 のアプリケーションでは、作成したウィンドウの動作を決定するともいえるメッセージを処理する重要な役割をする。Generic01.dpr では、
while GetMessage(Msg, 0, 0, 0) do begin TranslateMessage(Msg); DispatchMessage(Msg); end;
の部分である。このループは、GetMessage( ) APIが false を返した時に終了する事が分かるだろう。
GetMessage( ) の宣言は、
function GetMessage(var lpMsg: TMsg; hWnd: HWND; wMsgFilterMin, wMsgFilterMax: UINT): BOOL; stdcall;
である。 この関数の役割を調べるまえに、そもそもメッセージとは何なのかと思われる読者もいることだろう。 Win32 のOSでは、キーボードのキーが押された、マウスが移動した、ウィンドウのサイズが変更された、タイマがカウントアップした、Windows が終了されようとしている...といった、およそアプリケーションプログラマが知りたいだろうと考えられるありとあらゆる出来事を、メッセージという形でアプリケーションに知らせる。 プログラマがこれらの出来事に対処するコードを書くことで、アプリケーションが成り立つわけである。 メッセージの大半は、一時にドッと押し寄せてくるわけではなく、OSによって優先順位をつけられてメッセージキューというところに順序よく並んでいる。 このメッセージの待合室であるメッセージキューは、実行単位(スレッド)ごとにOSから一つずつ割り当てられている。 GetMessage( ) は、このメッセージキューからメッセージを取り出してくる。第一パラメータが取り出してきたメッセージに相当する。 TMsg 型レコードの定義は、
type TMsg = packed record hwnd: HWND; message: UINT; wParam: WPARAM; lParam: LPARAM; time: DWORD; pt: TPoint; end;
である。 hwnd は、このメッセージを受け取るウィンドウのハンドルである。第二フィールドの message が、メッセージの本体であり、どんな出来事が起こったのかを識別する。とはいえ、単なる32ビットの符号なし整数である。message.pas には、一般的なウィンドウメッセージである WM_*** で始まるものや、定義済みのウィンドウであるコントロールのためのメッセージなどの数百におよぶ定数が定義されている。 例えば、マウスの右ボタンがクリックされたときのメッセージは、
const WM_RBUTTONDOWN = $0204; WM_RBUTTONUP = $0205;
である。一つの出来事が一つのメッセージに対応しているわけではない。メッセージが別のメッセージを自動的に生成したり、複数のメッセージに分岐する事もある。 プログラマにとって重要なのは、どんなときどのようなメッセージが生成され、自分が必要としているメッセージは何なのかを知ることである。 wParam と lParam は、メッセージに付属している関連情報を与える。 その内容は、メッセージごとに異なる。 WPARAM型 と LPARAM型は、windows.pas で定義された単なる32ビット整数である。
type WPARAM = Longint; LPARAM = Longint;
TMsg の time フィールド変数は、このメッセージがキューに入った時刻を、Windows 起動後から経過した時間としてミリ秒単位で表したもの、 また pt フィールドは、 このメッセージが生成されたときのマウスの位置をスクリーン左上隅を原点として、ピクセル単位で表したものである。
GetMessage( ) APIの第二パラメータは、受け取るメッセージの宛先となるウィンドウのハンドルを指定する。 ここに特定のウィンドウのハンドルを指定すると、そのウィンドウに向けられたメッセージだけを受け取ることになる。 通常はゼロを指定して、この実行単位の関連する全てのウィンドウ向けのメッセージを受け取れるようにする。第三、第四パラメータは、受け取るメッセージの範囲を指定する。 前述のように、メッセージ本体は単なる符号なし整数なので、受け取る整数の最小値と最大値を指定することで範囲を指定する。 通常は、ともにゼロを指定して全てのメッセージを受け取れるようにする。
GetMessage( ) APIは、WM_QUIT ( = $0012) を受け取ったときだけ、false を返し、メッセージループが終了して、プログラムが終了する。 その他のメッセージのときは、TranslateMessage( ) APIに、TMsg型の変数を引き渡す。 TranslateMessage( ) は、
function TranslateMessage(const lpMsg: TMsg): BOOL; stdcall;
と定義されている。 この関数は、キーボードのキーを押したときに生成されるメッセージを受け取ったとき、デバイスドライバに基づいてキー入力を文字入力に翻訳して、WM_CHAR メッセージをメッセージキューに入れる(ポストする、という)。 その他のメッセージのときは、何もしない。
DispatchMessage( ) は、
function DispatchMessage(const lpMsg: TMsg): Longint; stdcall;
と定義され、 TMsg型の変数を受け取って、その宛先のウィンドウ関数にこのメッセージを送り出す。 この関数の働きにより、各ウィンドウのウィンドウ関数はメッセージを受け取ることができ、動作をカスタマイズする事ができるのである。 戻り値は、メッセージによって異なるが、通常、無視される。
このようにメッセージループを見てくると、たった四行のコードではあるが、アプリケーションの動作を決定するメッセージ伝達機構を、陰で支える重要なコードであることが分かるだろう。 メイン手続きの他のコードは、たった一回だけ実行されるのに対し、このコードは、ウィンドウが存在する限り、何千回、何万回も実行されるのである。
1-3-1 の項で定義したように、Delphi の Object Pascal では ウィンドウクラスは TWndClass型のレコードを使って登録する。 このレコードの第二フィールド変数である、
lpfnWndProc: TFNWndProc;
では、ウィンドウ関数のアドレスを指定した。 つまり、ウィンドウ関数は、このレコードをもとにして CreateWindow( ) APIで作られる個々のインスタンスであるウィンドウごとに指定するのではなく、もとになるウィンドウクラスごとに指定されるのである。 上記の TFNWndProc は、 windows.pas で
type TFarProc = Pointer; TFNWndProc = TFarProc;
と定義されている、単なる型なしポインタである。 Generic01.dpr では、MainWndProc( ) がウィンドウ関数である。ウィンドウ関数は、関連付けられたウィンドウにOSがメッセージを送るための入り口と考えることができる。 前にも指摘したように、Generic01.dpr では、MainWndProc( ) は、プログラマが書いたコードの中から呼び出されていない。 ウィンドウ関数は、OSが呼び出す関数である。 したがって、そのパラメータ並びはOSによって指定されている。 Generic01.dpr の MainWndProc( ) を以下に再掲する。
function MainWndProc(hWindow: HWND; Msg: UINT; WParam: WPARAM; LParam: LPARAM): LRESULT; stdcall; export; var ps: TPaintStruct; begin Result := 0; case Msg of //--------------- WM_PAINT --------------- WM_PAINT: begin BeginPaint(hWindow, ps); MainPaint(hWindow,ps); EndPaint( hWindow, ps ); end; //--------------- WM_DESTROY ------------- WM_DESTROY: MainDestroy(hWindow); //-------- そのほかのメッセージの処理 ------ else begin result := DefWindowProc( hWindow, Msg, wParam, lParam ); exit; end; end; // case end;
これを見ると分かるように、ウィンドウ関数の形式は次のようなものだ。
function WndProc(hWindow: HWND; Msg: UINT; WParam: WPARAM; LParam: LPARAM): LRESULT; stdcall;
多くのAPIと同様に stdcall 呼び出し規約を指定する。 四つのパラメータは、前節で説明した TMsg レコード の最初の四つのフィールド変数に相当している。 つまり、メッセージループの DispatchMessage( ) のパラメータである TMsg 型変数の最初の四つのフィールドが渡されると期待してよい。 ウィンドウ関数に渡されるメッセージは、メッセージループを経由してくるものばかりではない。 ボタンをクリックしたり、メニューをクリックしたときなどは、メッセージループを通らずに直接ウィンドウ関数に(OSを通して)メッセージが届く。また、CreateWindowEx( ) API などの一部のAPIも直接ウィンドウ関数にメッセージを送る。 プログラマは、ウィンドウ関数を呼び出したいときは直接呼び出すことはしない。 そのかわり、 SendMessage( ) APIに上記の四つのパラメータを指定すると、OSがそのメッセージをウィンドウ関数に直接届けてくれる。 この関数は、通常の関数呼び出しと同じようにウィンドウ関数からの処理の結果である戻り値を返してくれると期待できる。 メッセージループを経由してメッセージを送るためには、PostMessage( ) APIがある。 この関数は、メッセージキューにメッセージを送り出す(ポストする)だけですぐに復帰する。
ウィンドウ関数の戻り値は、32ビットの整数である。
type LRESULT = Longint;
戻り値には、メッセージの処理結果を指定することになるが、多くの場合はゼロを指定する。 メッセージによっては、少数ながらゼロ以外の値を指定する必要があるものもある。
Generic01.dpr のウィンドウ関数の構造は、 骨格だけ書くと、
function MainWndProc(hWindow: HWND; Msg: UINT; WParam: WPARAM; LParam: LPARAM): LRESULT; stdcall; export; begin Result := 0; case Msg of //--------------- WM_PAINT --------------- WM_PAINT://WM_PAINTメッセージの処理 //--------------- WM_DESTROY ------------- WM_DESTROY://WM_DESTROYメッセージの処理 //-------- そのほかのメッセージの処理 ------ else begin デフォルトのウィンドウ関数を呼び出す end; end; // case end;
となっている。 ウィンドウ関数には何百種類ものメッセージが届く可能性があるが、 ここでは case 文により二つのメッセージだけを選り分けて処理し、それ以外は DefWindowProc( ) APIにそっくりメッセージを渡して処理してもらっている。 自分で処理した場合は、case 文を抜けたとき戻り値をデフォルトのゼロにして終了する。 戻り値をゼロにしたくないときは、result に値を設定するとよい。
つぎに、このウィンドウ関数からメッセージの処理として呼び出されるイベントハンドラについて調べよう。
procedure MainPaint(hWindow:HWND; pt:TPaintStruct); begin SetBkMode(pt.hdc,TRANSPARENT); TextOut(pt.hdc,10,10,'Hello, World!',13 ); end; procedure MainDestroy(hWindow: HWND); begin PostQuitMessage(0); end;
上の MainPaint( ) 関数は、WM_PAINT メッセージがウィンドウ関数に届くたびに実行される。コードの内容は以降の章に詳述するが、最小化から復帰したときや他のウィンドウの下から出てきたときなどにこのメッセージが届けられる。ウィンドウのクライアント領域にいつでも「Hello,World!」と表示されるのはこのためである。 WM_DESTROY メッセージの処理は必須である。 これはシステムメニューの「閉じる」を選択したときや、「閉じる」ボタンを押したときに送られてくる。 このとき、ユーザーはこのアプリケーションを閉じることを望んでいる。 通常、アプリケーションを終えるための終端処理をここで行うが、Generic01.dpr では、PostQuitMessage( ) APIを呼び出すだけである。
procedure PostQuitMessage(nExitCode: Integer); stdcall;
このAPIは、その名前の通りメッセージキューに WM_QUIT メッセージをポストする。パラメータは、このとき一緒に送られる WParam の値を指定する。 メッセージループの GetMessage( ) APIは、WM_QUIT を受け取ると false を返してループを抜ける。 その後、OSにこの WParam を戻り値にしてプログラムを終了する。 Generic01.dpr では
Halt(Msg.wParam);
が、この部分に相当する。
WM_PAINT と WM_DESTROY 以外のメッセージは DefWindowProc( ) APIによって処理される。 Generic01.dpr によって作成されるウィンドウが普通のウィンドウらしく動作するのは、もっぱら DefWindowProc の処理のおかげである。 DefWindowProc があるおかげで、プログラマは自分で処理したいメッセージだけを case 文でトラップしてコードを書けばよい。
ここまでの説明で Generic01.dpr のコードが理解できたとして、コードの再利用を便利にするために長いコード群で繰り返し使用される部分を関数にまとめてしまうことで、Generic01.dpr を簡略化しよう。以下に手順を示す。
Generic02.dpr の全リストを以下に示す。
program Generic02; uses windows, messages, UtilFunc in 'UtilFunc.pas'; //------------------------------------------------------------ // Event Handlers //------------------------------------------------------------ procedure MainPaint(hWindow:HWND; pt:TPaintStruct); begin SetBkMode(pt.hdc,TRANSPARENT); TextOut(pt.hdc,10,10,'Hello, World!',13 ); end; procedure MainDestroy(hWindow: HWND); begin PostQuitMessage(0); end; //------------------------------------------------------------ // Main Window Procedure //------------------------------------------------------------ function MainWndProc(hWindow: HWND; Msg: UINT; WParam: WPARAM; LParam: LPARAM): LRESULT; stdcall; var ps: TPaintStruct; begin Result := 0; case Msg of //--------------- WM_PAINT --------------- WM_PAINT: begin BeginPaint(hWindow, ps); MainPaint(hWindow,ps); EndPaint( hWindow, ps ); end; //--------------- WM_DESTROY ------------- WM_DESTROY: MainDestroy(hWindow); //-------- そのほかのメッセージの処理 ------ else begin result := DefWindowProc( hWindow, Msg, wParam, lParam ); end; end; // case end; //------------------------------------------------------------ // Main Procedure //------------------------------------------------------------ begin MakeMainWindow(@MainWndProc); Halt(MessageLoopNormal); end.
見れば分かるように、Generic01.dpr と比較していくぶん小さなコードになっている。 これは主に、ウィンドウクラスの登録とウィンドウの作成、およびメッセージループを関数の形に書き直し、UtilFunc.pas に移動したためである。 UtilFunc.pas の全リストを以下に示す。
unit UtilFunc; interface uses windows, messages; // //--------------- window support functions ------------------------ // function GetExeName: string; function MakeMainWindow(WndProc:TFNWndProc): HWND; function MessageLoopNormal: integer; implementation // //--------------- window support functions ------------------------ // function GetLowDir(var Path: string): Boolean; var p,Len: integer; begin result := false; p := Pos('\',Path); if p <> 0 then begin result := true; Len := Length(Path)-p; Path := Copy(Path,p+1,Len); SetLength(Path,Len); end; end; function GetExeName: string; var s,t: string; buf: array[0..MAX_PATH] of Char; Len: integer; begin Len := GetModuleFileName(0,buf,SizeOf(buf)); SetString(s,buf,Len); t := s; while GetLowDir(t) do s := t; Len := Pos('.',s); result := Copy(s,0,Len-1); end; function MakeMainWindow(WndProc:TFNWndProc): HWND; var wc: TWndClass; hWindow: HWND; s: string; begin s := GetExeName; wc.lpszClassName := PChar(s); wc.lpfnWndProc := WndProc; wc.style := CS_VREDRAW or CS_HREDRAW; wc.hInstance := hInstance; wc.hIcon := LoadIcon(0,IDI_APPLICATION); wc.hCursor := LoadCursor(0,IDC_ARROW); wc.hbrBackground := (COLOR_WINDOW+1); wc.lpszMenuName := nil; wc.cbClsExtra := 0; wc.cbWndExtra := 0; RegisterClass( wc ); hWindow := CreateWindowEx(WS_EX_CONTROLPARENT or WS_EX_WINDOWEDGE, PChar(s), PChar(s), WS_VISIBLE or WS_OVERLAPPEDWINDOW, 200,100, 300,200, 0, 0, hInstance, nil); ShowWindow(hWindow,CmdShow); UpDateWindow(hWindow); result := hWindow; end; function MessageLoopNormal: integer; var Msg: TMsg; begin while GetMessage(Msg, 0, 0, 0) do begin TranslateMessage(Msg); DispatchMessage(Msg); end; result := Msg.wParam; end; end.
新たに三つの関数が実装されているが、 MakeMainWindow( ) と MessageLoopNormal については説明の必要はないだろう。 これらのコードは、Generic01.dpr とほとんど同じであり、たんに関数としてまとめただけである。 GetExeName 関数はこの関数が実行されたアプリケーションの名前を string 型で返す関数である。GetModuleFileName( ) APIは、windows.pas で以下のように宣言されている。
function GetModuleFileName(hModule: HINST; lpFilename: PChar; nSize: DWORD): DWORD; stdcall;
hModule パラメータには、名前を必要としているモジュールのハンドルを指定する。 ゼロを指定するとこの関数を呼び出した実行可能ファイルのフルパスを返す。 lpFilename には、指定したモジュールのフルパス付きファイル名を受け取る PChar型のバッファを指定する。 nSize には、lpFilename で指定したバッファの長さを半角字数で指定する。 GetModuleFileName( ) は、バッファにコピーした文字数を返す。 失敗するとゼロを返す。 GetExeName 関数では、MAX_PATH + 1 の長さの Char 配列をバッファに指定して、フルパス名を取得し、それを SetString( ) 関数で string型の変数 s にコピーしている。 MAX_PATH 定数は、windows.pas で以下のように定義されている。
const MAX_PATH = 260;
取得したフルパス付きのファイル名から、補助関数 GetLowDir( ) を使って拡張子のないファイル名を取得している。 GetExeName 関数で取得した string型のファイル名は、MakeMainWindow( ) 関数でウィンドウクラスの名前とウィンドウタイトルを指定するときに使われている。
実行時のイメージは、右図のようになる。ウィンドウのタイトルが自動的に実行ファイル名になっていることが分かるだろう。 以上のように、煩雑なコード群を関数としてまとめ、別のユニットファイルに移すことにより、プロジェクトファイルの大部分はウィンドウ関数のコードだけになってしまい、プログラマはメッセージの処理を記述することだけに専念できるようになった。
これまで説明したように Generic01.dpr のような簡単なプログラムにも、「ハンドル」という言葉がしばしば登場する。 筆者が初めて Win32 のコードベースのプログラミングを学んだときにも、 「ハンドル」という言葉が示す概念にとまどった覚えがある。 Generic01.dpr では、HWND 型の ウィンドウハンドル、HICON 型のアイコンハンドル、HBRUSH 型のブラシハンドル、HCURSOR 型のカーソルハンドルが使われている。 windows.pas には、多くのハンドルの型が定義されている。 主なものを挙げると、
type HWND = Integer; HBITMAP = Integer; HBRUSH = Integer; HENHMETAFILE = Integer; HFONT = Integer; HICON = Integer; HPEN = Integer; HCURSOR = HICON;
などがある。 上の定義から分かるように、これらは単に32ビット整数の言い換えでしかない。 このような定義にどんな意味があるのだろうか。 ハンドルが整数の変数であるということは、任意の整数値を代入したり、四則演算もできることになる。しかし、Generic01.dpr を見てもわかるように、プログラマは、ハンドルに数値を代入することはない。 ハンドルは、なにか windows のリソース(資源)を割り当てるようなAPI関数の戻り値として、代入されている。 ブラシを例にすると、これこれの色とパターンとスタイルを持つブラシが必要なので割り当ててくださいと、 例えば CreateBrushIndirect( ) というAPIを実行すると、windows は「分かりました。割り当てましたのでハンドルを渡します。」という返事とともに、このAPI関数の戻り値としてブラシハンドルを返してくれる。 返してくれる数値は、単なる整数値でありどんな整数変数にも代入できるが、 これをOSが割り当てた「ブラシを識別する番号である」ということをプログラマが容易に認識できるように、HBRUSH 型の変数に代入するというわけである。 したがって、上記のようなコンパイラにとっては無意味とも考えられる型の定義は、もっぱらプログラマへの配慮ためであるということができる。
上記のブラシの例でも分かるように、 windows はあらゆるリソースを割り当てるAPI関数の戻り値としてハンドルを返す。 返されたハンドルは、これらのリソースを識別する番号であり、番号の数値そのものには意味が無い。 これらのハンドルが必要なのは、リソースを操作するAPIが操作の対象を識別するためにハンドルを要求するからである。 APIのパラメータとして渡された整数値を、windows は自分が割り当てた番号が何であるか知っているので適切に処理できるというわけである。 プログラマの観点からすると、ブラシハンドルを記憶するためには、整数型のiとか j とかの変数でもいいわけだが、HBRUSH 型の変数 hBrsh に格納した方が、バグの少ない読みやすいコードになることは明らかだろう。
windows というOSをプログラマの観点からみると、千以上ものAPI関数の集まりである、また、マルチタスクを実現するタスクマネージャである、適切なウィンドウにメッセージを届けるメッセージディスパッチャであるとも言える。これらの見方はどれも一面の真実を衝いているが、ハンドルの概念でも示されたように、あらゆるリソースを管理する整数処理型のデータベースであるという見方もできる。実際、このOSは、特定のハンドルをもつウィンドウが、今どんな位置にどのように表示されているかはもちろん、そのウィンドウはどのような子ウィンドウを持っているか、さらにその子ウィンドウはどんな子ウィンドウをもっているか「知っている」し、インストールされているフォントの全ての属性を「知っている」し、プリンタの仕様も「知っている」し、現在実行中のプロセスの全てのトップレベルウィンドウを列挙する事もできる。 ほとんど全知全能である。 OSのあるべき姿として、かくあるべしというところである。 この強大なOSを相手に、プログラマがOSから供給されるAPI関数を使ってプログラミングすることは、どんな巨大なアプリケーションを開発できたとしても「お釈迦様の手の内から逃れられない孫悟空」の気分をいくらかは味わうことになるだろう。OSの開発会社またはそのCEOに対する、熟練プログラマが持つ屈折した感情の源泉の一部は、「孫悟空」になりきれないところから来ているのだろう。
Delphi は、RAD開発環境のシステムとしては比較的新参者である。 Borland(INPRISE)社には、DOS時代からパスカル言語を供給し育ててきた長い伝統があり、Delphi はその最新バージョンであると考えられる。 DOSやN88-Basicでの開発システムから windows のそれに移行する時期には、言語仕様としてイベント駆動(event-driven)のプログラミングスタイルという言葉が盛んに喧伝された。コードリストの最初から実行して、途中、ユーザからの簡単な入力を受付けて、処理して、終了するという従来のプラグメングスタイルからみると、「イベント駆動」はプログラミングスタイルのまさにパラダイムシフトを伴う大変革であった。本章でつくった Generic01.dpr を見ても分かるように、プログラマが自由にコーディングできる場所はウィンドウをつくることではなく、むしろ、そのウィンドウのウィンドウ関数に送られてきたメッセージをいかにして処理するかという場面である。 したがって、「イベント駆動」は言語仕様ではなく、マルチタスクOSが要請する必然的な開発スタイルであることが分かる。 VCLを使ったRAD開発環境では、このことはもっと顕著に現れ、プログラマはイベントハンドラにコードを書くだけでアプリケーションを完成できる。 「こういう出来事にはこのように対処する」ということをコードで表現してゆくことを繰り返すわけである。 考えてみれば、人間を含む全ての生物を外側から見たときに「外部刺激に応答する機械である」と見なすことと対応しているかもしれない。 下等生物か高等生物かの違いは、刺激を処理する複雑さの程度だけであると考えることもできる。 したがって、人間が使うコンピュータのアプリケーションが「イベント駆動」スタイルでつくられることは必然的な事なのかもしれない。
本章では、 最も簡単なウィンドウアプリケーションである Generic01.dpr を作成し、そのコードを解説した。 Win32 の仕様のもとでウィンドウを作るためには、次のような手順が必要である。
全てのメインウィンドウは、上記の手順をたどって作成されると考えて良い。 ボタンやリストボックス、ツールバーなどのコントロールは、システムが予め上記の1と2を実行済みの「定義済みの子ウィンドウ」である。 また、ダイアログボックスは上記の1と2の手順を簡略化したポップアップウィンドウである。 子ウィンドウやポップアップウィンドウには、5の手順は必要ない。
すべてのウィンドウにはウィンドウ関数というOSから呼び出される関数が付随しており、あらゆるメッセージはウィンドウ関数の中のコードによって処理される。 プログラマが明示的に処理しないメッセージは DefWindowProc( ) に渡して処理することで、ウィンドウの基本的動作の統一性が保たれている。 Generic01.dpr では、二つのメッセージ WM_PAINT と WM_DESTROY だけを処理した。
コードの再利用を便利にするために長いコード群で繰り返し使用される部分を関数にまとめてしまうことで、Generic01.dpr を簡略化し、Generic02.dpr を作成した。これにより、プロジェクトファイルの大部分はウィンドウ関数のコードだけになってしまい、プログラマはメッセージの処理を記述することだけに専念できるようになった。
ここまでのプログラムファイル