このチュートリアルでは、ウィンドウのクライアントエリアにどうやってテキストを描くかを説明する。 デバイスコンテキストについてもわかるようになるだろう。
ソース | 実行結果 |
|
Windows におけるテキストはGUIオブジェクトの一つだ。 個々の文字は、数多くのピクセル(ドット)から成り立っており、 そのピクセルの集まり具合から、ある一つの認識できる文字となる。 そのため、文字を書くではなく、描くと呼ぶのである。 普通、自分の作ったウィンドウのクライアントエリアにテキスト描く(まー、クライアントエリア外でも描けるがそれはまた別の話)。 Windowsの画面上に文字を出力することは、DOSの場合とは全く異なった方法となっている。 DOSでは、80×25 のスクリーン座標系で考えればよかったのだが、 Windowsでは、画面上に複数のプログラムが起動されるので、 お互いがお互いのウィンドウを間違って描いてしまうことのないように、 いくつかのルールを守らなければならない。 Windowsは、それぞれのウィンドウが、 自分のクライアントエリアしか描いてはいけないという描画境界を設けることにしており、 それ以外のエリアは描画できないことになっている。 ただ、ウィンドウのクライアントエリアのサイズは一様ではなく、ユーザはいつでも変更可能だ。 そのため、そのつどクライアントエリアのサイズを変更しないといけない。
クライアントエリアに何かを描く前に、Windowsに描画許可をもらわなければならない。 もはや、DOSのように、Windows画面の絶対座標はいらなくなった。 Windowsに自分のウィンドウのクライアントエリアの描画許可をもらい、 Windowsがそのクライアントエリアのサイズ、フォント、色、その他のGDI属性を決定し、 その後、描画許可要求を出したプログラムにデバイスコンテキスト(以下、デバコン)を返すことになる。 そのデバコンがクライアントエリアを描画する際のパスポートのような役割を持っており、 それを介して描画できるようになる。
デバコンにはカラーとかフォントといった、グラフィック属性値が含まれている。 これらのデフォルト値は自由に変更可能である。 このことは、全てのGDI関数をCALLする際に毎回自分の必要な値に変更しなおすことを 極力しないようにする手助けとなる。
デバコンのことをWindowsから提供されたデフォルト環境のようなものと考えるかもしれないが、 もしお望みなら、いくつかのデフォルト値は後で変更可能である。描画する際、デバコンへのハンドルを取得しなければならないのだが、 その方法は数種類ある。
- WM_PAINT メッセージに対しては BeginPaint 関数をCALLする
- WM_PAINT 以外のメッセージに対しては GetDC 関数をCALLする
- 独自のデバコンを作るには CreateDC 関数をCALLする
デバコンを使い終わった後、一つだけ忘れてはならないことがある。 それは、ある一つのメッセージに対して処理を行っている間にデバコンを解放しなければならないことである。 つまり、一つのメッセージに対してデバコンを扱っている間に、 他のデバコンを取得してはならず、 もし取得したければ、解放してから、また別のデバコンを取得しなければならない。
Windowsは、クライアントエリアの再描画を示唆するための WM_PAINT というメッセージを そのクライアントエリアを所有するウィンドウに送信する。 Windowsはクライアントエリアの内容はセーブしない。 代わりに、(あるウィンドウが他のウィンドウに重ねられ、その重なったウィンドウが移動したりというような) クライアントエリアの再描画が要求が発生した時、 Windows はそのウィンドウのメッセージキューに WM_PAINT メッセージをプッシュする。 これはつまり、クライアントエリアを実際に描画する責任はそのウィンドウにあるということであり、 ウィンドウプロシージャのWM_PAINTメッセージを処理するセクションでどのようにクライアントエリアを 再描画するかを記述しなければならない。
もう一つ、注意しないといけないところがある。それは、無効矩形だ。 Windowsは、再描画する必要のあるクライアントエリアの最小となるような矩形領域を、無効矩形と定義している。 Windowsが、クライアントエリアの不正矩形を認識したら、そのウィンドウにWM_PAINTメッセージを送ることになっている。 WM_PAINTメッセージを送られたウィンドウは、 描画構造体を受け取り、その構造体には不正矩形の座標値が入っている。 不正矩形を有効なものとするため、WM_PAINTメッセージに対しては BeginPaint関数をCALLする。 もし、WM_PAINTメッセージを処理しないのなら、 せめて、DefWindowProc関数か、ValidateRect関数を呼ばなければならない。 さもないと、WM_PAINTメッセージが再三送られてくることになる(つまり、メッセージオーバーフローで落ちる)。
以下は、WM_PAINTメッセージに対する処理の概要だ。
- BeginPaint関数でデバコンのハンドルを取得
- クライアントエリアを描画
- EndPaint関数でデバコンのハンドルを解放
明示的に不正矩形を有効にする必要がないことに注意せよ。 BeginPaint関数を呼べば、自動的に有効になる。 BeginPaint関数とEndPaint関数との間では、 クライアントエリアを描画するためのどんなGDI関数でも呼び出し可能である。 それらの関数のほぼ全ては、デバコンのハンドルを引数に持つ
|
では、クライアントエリアの中央に、 "Win32 assembly is great and easy!"というテキスト文字列を表示させよう。
.386 .model flat,stdcall option casemap:none WinMain proto :DWORD,:DWORD,:DWORD,:DWORD include \masm32\include\windows.inc include \masm32\include\user32.inc includelib \masm32\lib\user32.lib include \masm32\include\kernel32.inc includelib \masm32\lib\kernel32.lib .DATA ClassName db "SimpleWinClass",0 AppName db "Our First Window",0 OurText db "Win32 assembly is great and easy!",0 .DATA? hInstance HINSTANCE ? CommandLine LPSTR ? .CODE start: invoke GetModuleHandle, NULL mov hInstance,eax invoke GetCommandLine mov CommandLine,eax invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT invoke ExitProcess,eax WinMain proc hInst:HINSTANCE, hPrevInst:HINSTANCE, CmdLine:LPSTR, CmdShow:DWORD LOCAL wc:WNDCLASSEX LOCAL msg:MSG LOCAL hwnd:HWND mov wc.cbSize,SIZEOF WNDCLASSEX mov wc.style, CS_HREDRAW or CS_VREDRAW mov wc.lpfnWndProc, OFFSET WndProc mov wc.cbClsExtra,NULL mov wc.cbWndExtra,NULL push hInst pop wc.hInstance mov wc.hbrBackground,COLOR_WINDOW+1 mov wc.lpszMenuName,NULL mov wc.lpszClassName,OFFSET ClassName invoke LoadIcon,NULL,IDI_APPLICATION mov wc.hIcon,eax mov wc.hIconSm,eax invoke LoadCursor,NULL,IDC_ARROW mov wc.hCursor,eax invoke RegisterClassEx, addr wc invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\ WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\ CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\ hInst,NULL mov hwnd,eax invoke ShowWindow, hwnd,SW_SHOWNORMAL invoke UpdateWindow, hwnd .WHILE TRUE invoke GetMessage, ADDR msg,NULL,0,0 .BREAK .IF (!eax) invoke TranslateMessage, ADDR msg invoke DispatchMessage, ADDR msg .ENDW mov eax,msg.wParam ret WinMain endp WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM LOCAL hdc:HDC LOCAL ps:PAINTSTRUCT LOCAL rect:RECT .IF uMsg==WM_DESTROY invoke PostQuitMessage,NULL .ELSEIF uMsg==WM_PAINT invoke BeginPaint,hWnd, ADDR ps mov hdc,eax invoke GetClientRect,hWnd, ADDR rect invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, \ DT_SINGLELINE or DT_CENTER or DT_VCENTER invoke EndPaint,hWnd, ADDR ps .ELSE invoke DefWindowProc,hWnd,uMsg,wParam,lParam ret .ENDIF xor eax, eax ret WndProc endp end start
|
プログラムの大半はチュートリアル3とほとんど同じなので、重要な変更点だけを説明しよう。
LOCAL hdc:HDC LOCAL ps:PAINTSTRUCT LOCAL rect:RECTこれらは、WM_PAINTメッセージに対する処理を行うところで使用するGDI関数で使用するローカル変数だ。 hdcは、BeginPaint関数の戻り値であるデバコンへのハンドルを、 psは、PAINTSTRUCT構造体を格納する。 通常、この構造体の中の変数は使用しない。 BeginPaint関数にそのまま引渡し、Windowsが適切な値をSETする。 そして、クライアントエリアの描画が終われば、その ps をEndPaint関数に渡して終了となる。 rectはRECT構造体で以下のように定義されている。
RECT Struct left LONG ? top LONG ? right LONG ? bottom LONG ? RECT endsleftとtopは矩形の左上の、 rightとbottomは右下の座標値となる。 一点だけ注意しないといけないことがあり、座標原点が左上にあるということだ。 つまり、y=10 は y=0 より下に位置することになる。
invoke BeginPaint,hWnd, ADDR ps mov hdc,eax invoke GetClientRect,hWnd, ADDR rect invoke DrawText, hdc,ADDR OurText,-1, ADDR rect, DT_SINGLELINE or DT_CENTER or DT_VCENTER invoke EndPaint,hWnd, ADDR psWM_PAINTメッセージに対する処理は、描画したいウィンドウのハンドルと まだ初期化されていないPAINTSTRUCT構造体を引数にしてBeginPaint関数をCALLする。 関数が成功すれば、eaxレジスタにデバコンへのハンドルがSETされる。 次に、GetClientRect関数をCALLしクライアントエリアの座標系を取得し、 その取得した rect 構造体を DrawText 関数の引数の一つとして渡すことになる。 DrawText関数の宣言は以下の通りだ。
DrawText proto hdc:HDC, lpString:DWORD, nCount:DWORD, lpRect:DWORD, uFormat:DWORDDrawText関数は文字列を出力する高レベルのAPI関数で、 文字をラップしたり、センタリングしたりと本当に複雑な処理を行っている。 そのため、描画したい文字列にだけ集中できるようになる。 これと対を成す、TextOut関数については次のチュートリアルで調査するつもりだ。 DrawText関数は矩形領域にぴったりフィットするように文字列を調整し、 現在設定されているフォントやカラー、背景色で文字列を描く。 デバコンへの文字列の書き込みが終わったら、文字列の高さが返ってくる。 このケースではピクセルである。 では、引数について詳しく見ていこう
- hdc
デバコンのハンドル- lpString
矩形領域に表示したい文字列へのポインタ。NULL文字で終了する文字列でない場合は、引数nCountに文字数を記述する。- nCount
出力する文字数。NULL文字で終了する文字なら、nCountは -1 でなければならない。そうでないなら、表示したい文字数。- lpRect
RECT構造体へのポインタで、その矩形領域に文字列を描画する。これはクリッピング領域でもあるということに注意せよ。つまり、この領域以外には描画しない。- uFormat
文字列をどのように表示するかを指定する。以下の3つの値を使用でき、"or" 演算子で結合することも可能だDT_SINGLELINE : 一行に表示する DT_CENTER : 左右の中心に表示する DT_VCENTER : 上下の中心に表示する(DT_SINGLELINEも指定しないといけない)クライアントエリアの描画が終了したら、 EndPaint関数をCALLし、デバコンへのハンドルを解放しなければならない。 これで終了だ。
さて、今回の要点を以下にまとめよう。
- WM_PAINTメッセージに応答して、BeginPaint関数、EndPaint関数を呼び出す
- その BeginPaint関数とEndPaint関数の間で、クライアントエリアに対して描画する
- 他のメッセージに対応して、クライアントエリアを描画したい場合は2通りの方法がある
- GetDC関数とReleaseDC関数をペアにして呼び出し、これらの関数の間で描画できる
- InvalidateRect関数、もしくは UpdateWindow関数を呼び出しクライアントエリア全体を無効領域にして、Windowsに強制的にWM_PAINTメッセージを送信させ、WM_PAINT メッセージに対して処理を行う