Tutorial 4: Painting with Text

このチュートリアルでは、ウィンドウのクライアントエリアにどうやってテキストを描くかを説明する。 デバイスコンテキストについてもわかるようになるだろう。
   ソース      実行結果  

Theory:

Windows におけるテキストはGUIオブジェクトの一つだ。 個々の文字は、数多くのピクセル(ドット)から成り立っており、 そのピクセルの集まり具合から、ある一つの認識できる文字となる。 そのため、文字を書くではなく、描くと呼ぶのである。 普通、自分の作ったウィンドウのクライアントエリアにテキスト描く(まー、クライアントエリア外でも描けるがそれはまた別の話)。 Windowsの画面上に文字を出力することは、DOSの場合とは全く異なった方法となっている。 DOSでは、80×25 のスクリーン座標系で考えればよかったのだが、 Windowsでは、画面上に複数のプログラムが起動されるので、 お互いがお互いのウィンドウを間違って描いてしまうことのないように、 いくつかのルールを守らなければならない。 Windowsは、それぞれのウィンドウが、 自分のクライアントエリアしか描いてはいけないという描画境界を設けることにしており、 それ以外のエリアは描画できないことになっている。 ただ、ウィンドウのクライアントエリアのサイズは一様ではなく、ユーザはいつでも変更可能だ。 そのため、そのつどクライアントエリアのサイズを変更しないといけない。

クライアントエリアに何かを描く前に、Windowsに描画許可をもらわなければならない。 もはや、DOSのように、Windows画面の絶対座標はいらなくなった。 Windowsに自分のウィンドウのクライアントエリアの描画許可をもらい、 Windowsがそのクライアントエリアのサイズ、フォント、色、その他のGDI属性を決定し、 その後、描画許可要求を出したプログラムにデバイスコンテキスト(以下、デバコン)を返すことになる。 そのデバコンがクライアントエリアを描画する際のパスポートのような役割を持っており、 それを介して描画できるようになる。

デバコンにはカラーとかフォントといった、グラフィック属性値が含まれている。 これらのデフォルト値は自由に変更可能である。 このことは、全てのGDI関数をCALLする際に毎回自分の必要な値に変更しなおすことを 極力しないようにする手助けとなる。
デバコンのことをWindowsから提供されたデフォルト環境のようなものと考えるかもしれないが、 もしお望みなら、いくつかのデフォルト値は後で変更可能である。

描画する際、デバコンへのハンドルを取得しなければならないのだが、 その方法は数種類ある。

デバコンを使い終わった後、一つだけ忘れてはならないことがある。 それは、ある一つのメッセージに対して処理を行っている間にデバコンを解放しなければならないことである。 つまり、一つのメッセージに対してデバコンを扱っている間に、 他のデバコンを取得してはならず、 もし取得したければ、解放してから、また別のデバコンを取得しなければならない。

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関数を呼べば、自動的に有効になる。 BeginPaint関数とEndPaint関数との間では、 クライアントエリアを描画するためのどんなGDI関数でも呼び出し可能である。 それらの関数のほぼ全ては、デバコンのハンドルを引数に持つ

Content:

では、クライアントエリアの中央に、 "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

Analysis:

プログラムの大半はチュートリアル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 ends

lefttopは矩形の左上の、 rightbottomは右下の座標値となる。 一点だけ注意しないといけないことがあり、座標原点が左上にあるということだ。 つまり、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 ps

WM_PAINTメッセージに対する処理は、描画したいウィンドウのハンドルと まだ初期化されていないPAINTSTRUCT構造体を引数にしてBeginPaint関数をCALLする。 関数が成功すれば、eaxレジスタにデバコンへのハンドルがSETされる。 次に、GetClientRect関数をCALLしクライアントエリアの座標系を取得し、 その取得した rect 構造体を DrawText 関数の引数の一つとして渡すことになる。 DrawText関数の宣言は以下の通りだ。

DrawText proto hdc:HDC, lpString:DWORD, nCount:DWORD, lpRect:DWORD, uFormat:DWORD

DrawText関数は文字列を出力する高レベルのAPI関数で、 文字をラップしたり、センタリングしたりと本当に複雑な処理を行っている。 そのため、描画したい文字列にだけ集中できるようになる。 これと対を成す、TextOut関数については次のチュートリアルで調査するつもりだ。 DrawText関数は矩形領域にぴったりフィットするように文字列を調整し、 現在設定されているフォントやカラー、背景色で文字列を描く。 デバコンへの文字列の書き込みが終わったら、文字列の高さが返ってくる。 このケースではピクセルである。 では、引数について詳しく見ていこう

クライアントエリアの描画が終了したら、 EndPaint関数をCALLし、デバコンへのハンドルを解放しなければならない。 これで終了だ。
さて、今回の要点を以下にまとめよう。

  1. WM_PAINTメッセージに応答して、BeginPaint関数、EndPaint関数を呼び出す
  2. その BeginPaint関数とEndPaint関数の間で、クライアントエリアに対して描画する
  3. 他のメッセージに対応して、クライアントエリアを描画したい場合は2通りの方法がある
    • GetDC関数とReleaseDC関数をペアにして呼び出し、これらの関数の間で描画できる
    • InvalidateRect関数、もしくは UpdateWindow関数を呼び出しクライアントエリア全体を無効領域にして、Windowsに強制的にWM_PAINTメッセージを送信させ、WM_PAINT メッセージに対して処理を行う

[戻る]