Windows API マスターへの道 -- GUI 編
最近、Windows32 API をかじり始めました(1999, 8 月ごろから)。
最初は楽勝☆って考えていましたが、ところがどっこい、こいつは
食べがいがあるぞ!プログラマ初心者には、理解しがたい仕様が満載で、
API 細部に詳しくなければ、どこかでつまづいてバグを挿入しやすくなっている
この仕様には感嘆しています。
最初に買った本が、「Windows98 プログラミング -- C/C++
によるソフトウェア構築」って本です。
分厚い割りに、記述されていない事柄が多すぎて、役に立たなかったです。
使用している用語が不正確だったり、プログラマが知っておくべき重要事項を
書き漏らしていたりして、買って損したと思っています。いわゆる悪本。
さて、約 1 ヶ月の間、この本と奮闘することになります。
他人のソースコードを読んでて、この関数って何するのかな?
このマクロってどういう意味かな?
と思って本を開いても、半分くらいの確率で説明が載っていません。
たとえ載っていたとしても、説明不足で結局、詳細不明だったりします。
あまりにストレスが溜ってきたため、本を買い直すことに決定しました。
買った本は「Win32 プログラムング大全 上/下」の 2 冊です。
Windows 98 の新しい API については説明されていませんが、
かなり詳しく記述されており、この本はとっても美味しいです。
いま、じっくり味わっているところです。
さぁ、これでやっと Windows API マスターへの道が開けたぞ。
以下、Windows API に関する覚え書きです。個々の関数の使い方は、
あなたのお持ちの Windows API マニュアルをご覧ください。
なお、本覚え書きは、特に明示しないかぎりは Windows 95 での
記述です。Windows 98 では異なっている可能性があるので
注意してください。
ときどき項目を追加します。
新しいものほど上にありますので、下から上に向かって読んで行ってください。
あなたも知らない、新事実が満載!
- DOS は OEM 文字セットの文字を使ってファイルの名前を作成する。
ANSI 文字セットを使用する Windows アプリケーションでは、ANSI 文字セット
を使ってファイルの名前を作成する。0x80〜 の文字には機種依存性があり、
両者で同じ名前がマッチしないことがある。(日本語版はどうだか分からない)
- OEM - Original Equipment Manifacture
Intel/Win32 プラットフォームでは、BIOS によって定義されている文字セット
を表す。
- WM_KEYDOWN, WM_KEYUP には、Shift キーに関する情報がないため、`a' と `A' は
区別できません。Shift キーが押されていたかどうかは、GetKeyState(VK_SHIFT)
を呼び出します。GetKeyState() は GetMessage() 関数が呼ばれた時点での
キーに関する情報を返します。今現在のキーの状態を取得するには、
GetAsyncKeyState() を用います。
なお、TranslateMessage() しておけば、WM_KEYDOWN, WM_KEYUP の次の
メッセージは WM_CHAR, WM_DEADCHAR, WM_SYSCHAR, WM_SYSDEADCHAR
のいづれかが取り出されます。WM_CHAR を待ち、wParam を用いれば
`a' なのか `A' なのかを容易に判別できます。文字 `a' 入力すると、
WM_KEYDOWN -> WM_CHAR -> WM_KEYUP というメッセージ列が生成されます。
- デスクトップウィンドウを WM_CLOSE してはなりません。
SendMessage(GetDesktopWindow(), WM_CLOSE, 0, 0); で一発で Windows
を混乱させることができ、
再起動([Ctl+Alt+Del]^2)以外、復旧できなくさせられます。この例からみて
分かるように、ちょっとしたバグがもとで大変なことになってしまうのが、
Windows という環境です。(NT ではどうなるか知りたいので、だれか勇気の
あるひとは試してみて、連絡していただけたら幸いです)
- WM_PAINT 中の再描画から、関数 MessageBox() を用いてエラーメッセージ
をポップアップ表示すると、それを閉じた時に再び WM_PAINT が発生して
エラーがループするということになりかねないので注意しましょう。
- 最近思っていること第3弾:
例外事項が多すぎて、そろそろ記憶があやふやになってきた。数ヵ月後には
ほとんど忘れてしまっているに違いない。
- SelectObject() は Select される前のオブジェクトを返すのが一般的ですが、
リージョンを指定したときだけ例外で、前のオブジェクトを返さずに
リージョンに関するステータスを返します。
(なんて統一性のない API なんだぁ)
- リージョンには常にデバイス座標系が用いられます。
マッピングモードを変更しているときは注意してください。
- NT の GDI 描画命令は、X Lib のようにバッファに蓄積されるので、
すぐには描画されません。以下の条件でこのバッファはフラッシュされます。
- GdiFlush()
- GdiSetBatchLimit() で設定したバッチ制限に達した。
- バッチバッファがいっぱいになった。
- Boolean 値を返さない GDI 関数が呼び出された。
なお、X のデバッグによくお世話になっている XSynchronize() の
ようなことを行うには、GdiSetBatchLimit() でバッチ制限を 1 にします。
- FloodFill, ExtFloodFill ≒ BASIC の PAINT 命令
なお、ディザがかかっているところに用いると、
エッヂ検出が意図した通りに働かず、予期しない結果になるので注意しましょう。
ディザをかけないようにするためには、CreatePen() で PS_SOLID スタイル
のペンを SelectObject() します。ExtCreatePen() だとディザがかかります。
- GetBitmapDimensionEx() で得られるビットマップサイズは、
前もって SetBitmapDimensionEx() で設定しておいたサイズであって、
実際のビットマップサイズではないことに注意しましょう。
SetBitmapDimensionEx() で設定しておかなければ、GetBitmapDimensionEx()
で得られるサイズは 0 になります(意味ねー、何に使うんだよぉ)。
実際のビットマップサイズを得るためには、GetDIBits()
あるいは、GetObject() を用います。
- Windows95 では、StretchBlt() は(SetStretchBltMode() で指定した)
伸縮モードに何を指定していても、必ず STRETCH_DELETESCANS モード
で実行されるというバグがあります。
(Windows98 では直っているはずだが、未確認)
- Win16 時代分かりづらかった不適切なマクロ名は、Win32 になってより
適切な名前に置き換えられました。ところが、Win16 と互換を取るために、
古い名前も依然として残っています。
Win16 | Win32 |
BLACKONWHITE | STRETCH_ANDSCANS |
WHITEONBLACK | STRETCH_ORSCANS |
COLORONCOLOR | STRETCH_DELETESCANS |
HALFTONE | STRETCH_HALFTONE |
- LoadBitmap() は 16 色のビットマップでないとロードできない。
- 矩形を扱ういくつかの関数、例えば、InflateRect() 等、一部の関数は
軸の向きが変わるようなマッピングモードでは正常に動作しない。
- FillRect(), FrameRect(), InvertRect() は、軸の向きが逆になるような
マッピングモードでは動作しない。(何も描画されない)。
Rectangle() は大丈夫なようだ。
只今、私、混乱状態、、、
BOOL Rectangle(HDC hdc, int xUL, int yUL, int xLR, int yLR);
BOOL FillRect(HDC hdc, LPRECT rect, HBRUSH brush);
ありゃ〜統一性ないねぇ。
- 矩形を描画する関数群の中で、なぜか DrawFocusRect()
だけ特殊なので注意しよう。
FrameRect() は他の矩形描画関数と違って、
- 現在していされているペンとブラシは無視され、R2_XORPEN
ラスタオペレーション (ピクセル値の XOR を取る) の点線で描画される。
(よって、もう一度同じところを描画すると消える。再描画には特に注意が
必要)
- MM_TEXT マッピングモード以外では正常に動作しない。なんと、Windows
ドキュメンテーションには、そんなことは記述されていない。みーんな
混乱していることでしょう。
- ROP (Raster OPeration) = X Window の GC Function
HPEN CreatePenIndirect(LOGPEN *);
typedef struct tagLOGPEN {
UINT lopnStyle;
POINT lopnWidth; /* なに!lopnWidth.x のみ使って、lopnWidth.y
* は無視されるそうな。
*/
COLORREF lopnColor;
- CreatePen() と ExtCreatePen()
- CreatePen() では、幅 1 ピクセルよりも太い破線を描画するペンを作成
できない。ExtCreatePen() ではできる。(Windows NT のみ? 95 はできないみたい)
- CreatePen() では、線の端点のキャップスタイルは指定できない(常に円形)。
ExtCreatePen() ではできる。
- CreatePen() は PS_INSIDEFRAME スタイルのみしか、ディザがかからないが、
ExtCreatePen() では他のスタイルもディザがかかる。
- PS_INSIDEFRAME はパスのストロークやリージョンの輪郭には適用されない。
- ExtCreatePen() で、PS_COSMETIC スタイルで幅 1 を指定したペンは、
CreatePen() で幅 0 をしたペンと同じだが、まったく同じというわけでは
ない。ExtCreatePen() の
PS_COSMETIC ペンは、OPAQUE 背景モードをサポートしない。
- ストックオブジェクトを削除してはならない。
(Windows 3.1 より前のバージョンだと、ストックオブジェクトを削除
できたらしい。おぉ)
- そろそろ、全体像が何となくつかめてきた私です。そこで、
最近思っていること第2弾:
・命名規則に統一性がないねぇ。全ての API は大文字から始まるのか?
と思っていたら、マルチメディア系の API って小文字から始まってるし。
どれが Windows API でどれがユーザ定義関数かが分かりづらい。
・SelectObject() や DeleteObject() の別名をいっぱい用意しているのには
笑ってしまった。しかもキャストしてコンパイラが警告を発しないように
なってるし。そんなことすんなら、よろず屋にしなけりゃいいのに。
・可読性をなくすような技がいっぱいありそう。人がプログラムを読んで、
その挙動を把握するのを困難にできるかも?という
M$ の陰謀があるのか?
- DeletePen(1), DeletePen(stdout) のような間違った使い方を
しても、コンパイラは何の警告も出してくれません。Selectほげ() マクロも
同様です。SelectObject(),
DeleteObject() の別名関数(マクロ)を使うときは、用心しましょう。
なお、
i = 1; while(1) DeletePen(i++);
で Windows がクラッシュしてしまいました。ギャー!!
- 一部の GDI 関数(GetClientRect 等)は、常にデバイス座標系が用いられる。
- 論理座標系からデバイス座標系への変換は LPToDP()、その逆変換は
DPtoLP()。
- クライアント座標系からスクリーン座標系への変換は ClientToScreen()、
その逆変換は ScreenToClient()。
- HS_FDIAGONAL ハッチスタイルは "45度上向きハッチ" (左から右に見て)
と定義されているのに、実際使ってみると45度下向きになります。
一方、HS_BDIAGONAL ハッチスタイルは "45度下向きハッチ"
と定義されているのに、実際使ってみると45度上向きになります。
(本当かなぁ?、未確認です)
- SaveDC(), RestoreDC() って便利かも。
- SelectObject() で選択されたままのオブジェクトを DeleteObject() しては
なりません。ただし、リージョンだけは例外で、SelectObject() したまま
DeleteObject() を行わなければなりません。
- CreateCompatibleDC() は 1x1 のモノクロビットマップを作成する。
- システムディスプレイと互換性をもつメモリデバイスコンテキストって?
初めて Windows API を習う人にとって、意味不明な文章だと思うが、
この "互換性" とは、以下の条件をすべて満たすものを言いいます。
- ビットマップ中のビットの 1 ピクセルあたりのビット数が同じ
(X Window でいう Depth のこと)
- ビットプレーンが同じ
- カラー値が同じ (X Window でいう Visual クラスのこと)
なお、Windows のビットマップとは、X Window でいう Pixmap のようなものです。
depth が 1 でなくともビットマップと呼ぶので、X Window プログラマの多くは、
混乱してしまいます。モノクロの場合は、モノクロビットップと呼び
区別します。
- メモリデバイスコンテキスト ≒ XSHM Pixmap
- tips 271ディスプレイ全体のディスプレイ
コンテキスト、あるは、デスクトップウィンドウの取得
- Microsoft のドキュメンテーション、および、それに忠実にしたがって記述されて
いるドキュメンテーションは、
「CS_PARENTDC スタイルは子ウィンドウが親ウィンドウの DC を継承する」
と誤って記述されている。正しくは、「親ウィンドウのクリッピングリージョン
に設定し、親ウィンドウ領域に描画できるようにする」である。決して
親ウィンドウの DC を継承などしやしない。大嘘である。
- GetDC() あるいは BeginPaint() で取得したプライベート
ディスプレイコンテキストは、
解放しなくても正常に動作します。
このプライベートディスプレイコンテキストに ReleaseDC() あるいは EndPaint()
しても、実は何も行われていないのです。ただし、将来、なんらかの問題が発生
するかもしれないので、慣例にしたがって、ReleaseDC() あるいは EndPaint()
しときましょう。
- SCROLLINFO 構造体の nMax と nMin は、SIF_RANGE フラグが
セットされていなければ比較されない。Microsoft ドキュメンテーションの
一部には、SIF_RANGE フラグのことを言及せずに、nMax と nMin と
比較されるという記述のバグがある。
- 各種の GDI 関数に関する Windows のドキュメンテーションは、MM_TEXT
マッピングモードで使われているという暗黙の仮定を行っている
ことが多いです。"論理単位/論理座標" というべきところで
"デバイス単位/デバイス座標" とあったりするので注意しよう。
また、一部の Windows 関数 (例えば InsertRect) は、ある特定の
マッピングモードでは正常動作しません。残念なことに、どの関数が
どのマッピングモードで正常動作するのかしないのか、ということが
これらの Windows ドキュメントに記されていないそうです。
- Windows 95 の座標系では、32bit 座標系の下位 16 bit しか見ておらず、
上位 16 ビットは捨てられています。Windows 98 ではどうであるかは不明。
16 bit あれば十分じゃんと思うかもしれませんが、マッピングモードで
座標系を変えた場合は容易に 16 bit を越えてしまいます。
なお、NT では 32bit フルに使えます。
うーん、これは M$ の陰謀としか思えん。なんで
そんなことするかなぁ。
- WM_PAINT メッセージにはディレイがある。
メッセージキューの全ての処理が終了したのち、複数の WM_PAINT が統合可能
ならば一つにまとめられ、メッセージが処理される。
- ディスプレイコンテキストとはデバイスコンテキストの一部であり、
出力デバイスがディスプレイの時に使う言葉です。
Windows ドキュメンテーションでは、この二つの言葉を正確に使い分けて
いないので、注意しよう(デバイスコンテキストというべきところで
ディスプレイコンテキストと表現しているところがある)。
- オーバーラップウィンドウ = X Window でいう OverrideRedirect
でないウィンドウ。
- アンクラッカは FORWARD_WM_*
- メッセージクラッカ (HANDLE_WM_*) を使おう。Win16 API と Win32 API とでは、
ウィンドウ関数の wParam と lParam の意味が異なります。
例:(WM_COMMAND)
プラットフォーム | wParam | lParam |
LOWORD | HIWORD | LOWORD | HIWORD |
Win16 | コントロール/メニューID | | コントロールハンドル | 通知コード |
Win32 | コントロール/メニューID | 通知コード | コントロールハンドル |
- メッセージループから抜けるには、PostQuitMessage() を呼ぼう。引数は、
GetMessage() の MSG 構造体の wParam フィールドに受け渡されます。
- DefWindowProc() は自分で理解できないメッセージについては、必ず 0 を
返すことが保証されている。
- CreateWindow() => WM_GETMINMAXINFO, WM_NCCREATE, WM_NCCALCSIZE, WM_CREATE
- ShowWindow() => WM_SHOWWINDOW, WM_WINDOWPOSCHANGING, WM_ACTIVATEAPP, WM_NCACTIVATE, WM_GETTEXT, WM_ACTIVATE, WM_SETFORCUS, WM_NCPAINT, WM_GETTEXT, WM_ERASEBKGND, WM_WINDOWPOSCHANGED, WM_SIZE, WM_MOVE
- UpdateWindow() => WM_PAINT
- SendMessage() の返り値は、ウィンドウ関数の返り値です。
- DispatchMessage() の返り値は、ウィンドウ関数の返り値です。
- TranslateMessage()
WM_KEYDOWN, WM_KEYUP が送られたときに、押されたキーに
対応する ASCII 文字コードがあるかどうかを判定し、もしあれば、
対応する WM_CHAR メッセージを生成します。また、Unicode ベースの
アプリケーションであれば、対応する Unicode コードを生成します。
TranslateMessage() は、元の MSG 構造体を全く変更しません。代わりに
WM_KEYDOWN, WM_KEYUP が送られたときに、対応する ASCII あるいは Unicode
があれば、WM_CHAR メッセージをメッセージキューの先頭に置きます。
- WNDCLASSEX 構造体の hbrBackground フィールドにはブラシを指定
するのだが、Windows の定義済のシステムカラーを用いる場合、
そのシンボル値に 1 を加えた値を設定しなければなりません。例:
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
最初設計したときに、システムカラーのインデックス値を 0 から
始めてしまったために、NULL と区別付かなくなったためにそうしたらしい。
マヌケ、、、
- 私もハンガリー記法は嫌いだ。Windows API
もこの記法を忠実に守っていないようです。なぜなら、MSG
構造体やウィンドウ関数定義
見られる
WPARAM wParam
では、`w' プレフィックスなのに、実際は
32 bit unsigned integer だし。あれ?
忠実に守るとドツボにはまるので、大っ嫌いだ!
BOOL foo();
と宣言された Windows API foo() について、
if(foo() == TRUE)
は誤りです。やってはならないスタイルです。
なぜかって?理由は、返り値が BOOL と定義された Windows API 関数の中には、
0, 1 以外の値を返す関数が存在するからです(きったねー!)。
よって、以下のように記述しなければならなりません。
if(foo()) ...
if(!foo()) ...
たとえ、0, 1 しか返さないと分かっている関数であっても、将来の拡張に
よって 0, 1 以外の値を返すかもしれません。
- <windows.h> を include する前に、
#define STRICT
とすると、厳密な型チェックが行われる(MSC)。
- 本書に誤り?発見。
- __stdcall(=WINAPI): 関数が呼び出された側が SP を戻す。
内部シンボル名に "@" + 「引数の合計バイト数」が付加される(Cygwin で確認)。
- __cdecl(デフォルト): 関数の呼び出し側が SP を戻す。
- システムキューとスレッドキュー
システムキューは一つ、スレッドキューはスレッド毎にある。
キーボートやマウス等から発生したハードウェアイベントは
システムキューに入れられる。その後、適切なスレッドキューが
選ばれ、そこに受け渡される。
- さぁ、Windows プログラミング開始だ。最初に思ったこと:
なんで構造体名は全部大文字なんじゃ!タイピングに肩が凝ってしょうがない。
GUI 編があるから、システム編、マルチメディア編、あるいは MFC 編などに続いて
欲しいと思う方もいるかもしれませんが、GUI 編のみです。ごめんなさい。