はじめに
・まだ執筆中なんですが、例によって
公開しながらの加筆、という形をとっています。
・「構文編」から読まれている皆さんは、
他サイトなどをご覧になって、独習を進めておられると思いますが、
本書では、その中でも、特に詰まりそうな箇所について
優先的に書き進めております。
・ある程度理解できた段階でハマる「サブクラス化」についても、
ググっても理解できなかった方でもわかるように
わかりやすいサンプルコードを載せています。
・画面描画については、そのうち書きますが、
msdnでも解説されていますので、そちらをご覧ください。↓
http://msdn.microsoft.com/ja-jp/library/windows/desktop/ff381399(v=vs.85).aspx
DefWindowProc関数 ( メッセージループとウィンドゥプロシージャ )
・さて、あなたのつくえの上には、パソコンがあって、
画面には、Windowsのデスクトップ画面や
ウィンドゥが表示されているとします。
・その手前に置かれたイスには、
あなた(または別のユーザー)が座っていて、マウスを動かしている。
・そしてたまに、キーボードに手をうつして、キーをカタカタ打っている。
・これをもう少しコンピュータ寄りの視点でまとめると、
おおよそ、次のような流れになります。↓
1. ユーザーが入力した情報は、入力機器からWindowsに送られる。
[ユーザー] -(入力)→ [キーボードorマウス] -(入力信号)→ [Windows]
2. Windowsは、アプリケーションプログラムの「メッセージキュー」に、
ウィンドゥメッセージを送信する。
[Windows] - (ウィンドゥメッセージ) → [ メッセージキュー ]
3. アプリケーション (あなたの作ったプログラム) は、
GetMessage関数を、定期的に呼び出して、
「メッセージキュー」に詰め込まれた「ウィンドゥメッセージ」を
1つずつ取り出しては、それに応じた処理を行います。
・このメッセージの取り出しは、通常、
「メッセージループ」といわれる無限ループの中で行います。↓
// 「プロジェクト」を作成した時に、「Windowアプリケーション」を選択した場合は、
// _WinMain関数の最後に、下記のような「メッセージループ」が書かれている。↓
MSG msg; // ウィンドゥメッセージを受け取るMSG構造体。
while ( ::GetMessageW( &msg, NULL, 0, 0 ) > 0 ) // キューがカラなら0、エラー時は-1を返す。
{
::TranslateMessage( &msg ); // 文字キーの入力なら、「WM_CHAR」を送信する。
::DispatchMessageW( &msg ); // 送信先ウィンドゥの「ウィンドゥプロシージャ」を呼ぶ。
}
return msg.wParam; // プログラムを終了する。
・GetMessage関数に渡したMSG構造体には、
送信先のウィンドゥを示す「ハンドル」、(HWND型)
メッセージの種類を示す「メッセージコード」、
クリックされた位置などの「パラメータ」、(メッセージにより異なる)
が格納されます。
・これを、DispatchMessage関数に渡すと、
「ウィンドゥクラス」を登録した時に指定した「ウィンドゥプロシージャ」が呼ばれます。
・たとえば、ボタンがクリックされた時に
ウィンドゥの上に絵を表示させたりする場合は、
ウィンドゥプロシージャの中に、その処理を書いておきます。
・このように、パソコン上でのあらゆる操作は、
Windowsを介して行われます。
・アプリケーションプログラムは、
WindowsAPIの関数を呼び出すことで
コンピューターを間接的に操作します。
// ウィンドゥプロシージャ ↓
LRESULT CALLBACK WindowProc(
HWND h_wnd_, // 送信先ウィンドゥのハンドル
UINT msg_, // メッセージコード
WPARAM wp_, // Wパラメーター
LPARAM lp_ // Lパラメーター
)
{
switch ( msg_ ) // メッセージコードを判定していく。
{
// ------------
case WM_LBOTTONDOWN: // 左クリックなら、
// ここに、左クリックされた時の処理を書く。
break;
// ------------
} // end switch
::DefWindowProcW( h_wnd_, msg_, wp_, lp_ ); // 親ウィンドゥに「通知コード」を送信する。
} // end proc
・メッセージ処理は、体感的には、
入力されたと同時に実行されているかのように見えますが、
厳密には、そうではありません。
・「メッセージループ」は、ものすごい速度で回っているので気付かないんですが、
「メッセージキュー」の中では、多数のメッセージが、常に順番待ちをしていますので、
送信されたからといって、すぐに取り出して処理することはできません。
・ウィンドゥメッセージは、通常、Windowsが送信するものですが、
アプリケーションから送信することもできます。↓
// メッセージキューにメッセージを送信したら、すぐに制御を戻す。↓
PostMessageW( h_wnd_, msg_, wparam_, lparam_ );
// メッセージキューに送信したメッセージが処理されるまで制御を戻さない。↓
SendMessageW( h_wnd_, msg_, wparam_, lparam_ );
・「メッセージキュー」は、「スレッド」ごとに持つことができます。
・「スレッド」というのは、同じプロセス内で、
さらに複数の処理を、同時に実行するための
より細かな実行単位です。( スレッドについては、詳細は後述します )
・上の図では、「プロセスは、同時には動いていない」と書きましたが、
最近は、演算装置(コア)が複数付いた「マルチコアプロセッサ」ですから、
同時に動いています。
・ボタンなどのコントロールは、通常、ウィンドゥの上に表示されますが、
これは、ボタンが、ウィンドゥの子ウィンドゥとして登録されているからです。
・子ウィンドゥでは、クリックなどのイベントが発生すると、
ウィンドゥメッセージ ( WM_ほにゃらら ) が送信されず、
親ウィンドゥに向けて、WM_COMMANDが送信されます。
( ※ SendMessage関数などで送信した場合は、送信される。)
・そんなわけで、子ウィンドゥのイベント処理は、
親ウィンドゥ側のウィンドゥプロシージャの中に
書いていくことになるわけです。
・WM_COMMANDのパラメータには、通知の種類や、
送信者である子ウィンドゥの識別IDなどが格納されています。
・コモンコントロールの場合は、WM_COMMANDではなく、
WM_NOTIFYが送信されます。
CreateWindow関数 ( ウィンドゥを作成する )
・「ウィンドゥ」を、画面上に表示させるには、
次のような手順が必要です。↓
1. 「ウィンドゥクラス」を登録する。( ※いわゆる「クラス」とは別物 )
2. それを指定して、「ウィンドゥ」を作成する。
3. その「ウィンドゥ」を表示させる。
・「プロジェクト」を作成した時に、
「Windowアプリケーション」を選択した場合は、
メインとなるウィンドゥを作成する処理がすでに書いてあるはずですが、
ここではクラス化した場合のサンプルコードで説明していきます。↓
BOOL CForm::Init(
HWND h_parent_, // 親ウィンドゥのハンドル
const wchar_t* p_name_, // ウィンドゥクラスの名前
const wchar_t* p_text_, // タイトルバーに表示される文字列
int x_, int y_, // ウィンドゥの表示位置 (左上端の座標)
int width_, int height_ // ウィンドゥのサイズ
)
{
// ------------------------
// ウィンドゥクラスのスタイルを決めておく。
DWORD c_style = CS_BYTEALIGNWINDOW | CS_BYTEALIGNCLIENT |
// 速度的に有利 (クライアント領域のX座標が8の倍数になるように配置 )
// CS_CLASSDC | // このクラスに属するすべてのウィンドゥで、DCを共有する。
CS_OWNDC | // このクラスに属するウィンドゥには、それぞれ独自のDCを割り当てる。
// CS_PARENTDC | // 子ウィンドゥの場合は、これが必要。
// (子ウィンドゥのクリッピング領域に、親ウィンドゥのクリッピング領域を設定)
// CS_HREDRAW | CS_VREDRAW | // ウィンドウサイズが変化した際に、画面を更新する。
// ( ※ちらつきが発生するので使用しない。
// 代わりに、WM_SIZEではInvalidateRect関数を呼んでいる )
CS_DBLCLKS; // Wクリック時にもメッセージを発生させる。
// CS_NOCLOSE | //「閉じる」を無効にします。
//CS_SAVEBITS // メニューやダイアログなどで使用、無くてもいい。
// ------------------------
// ウィンドゥクラスを登録する。
WNDCLASSEXW wc = {}; // ウィンドゥクラス構造体
wc.hInstance = p_appli->GetInstance(); // アプリケーションインスタンスのハンドラ HINSTANCE
// これは、_tWinMain関数の第一引数のhInstance。関数から取得することもできる。↑
wc.lpszClassName = p_name_; // ウィンドゥクラス名。
wc.lpfnWndProc = WindowProc_Init; // ★ウィンドゥプロシージャ。 (初期化用を格納しておく)
wc.style = c_style; // ウィンドゥクラスのスタイル。 ( 先ほどフラグ定数を詰め込んだ整数値 )
wc.cbSize = sizeof(WNDCLASSEXW); // この構造体のサイズ。 (※必須)
wc.hIcon = NULL; // 大きめのアイコンのハンドル。
wc.hIconSm = NULL; // 小さめアイコンのハンドル。 (タイトルバー)
wc.hCursor = NULL; // マウスポインタのハンドル。
wc.lpszMenuName = NULL; // メニューを付ける場合は、その名前。
wc.cbClsExtra = 0; // ウィンドウクラスごとに固有のデータを格納できる。
wc.cbWndExtra = 0; // ウィンドウごとに固有のデータを格納できる。
wc.hbrBackground = (HBRUSH) COLOR_APPWORKSPACE + 1; // 作業領域の背景色。
// ウィンドゥクラスを登録する。
if ( ::RegisterClassExW( &wc ) == 0 ) return FALSE;
// ------------------------
// ウィンドゥスタイルを決めておく。 ( 後で再設定できるので、適当でいい )
DWORD w_ex_style = WS_EX_CONTROLPARENT | // タブ移動を有効にする。
// WS_EX_APPWINDOW | // 一番上にあるウィンドゥを表示するときに、強制的にタスクバーに含みます。
// WS_EX_ACCEPTFILES | // ウィンドゥにファイルをドロップした際、WM_DROPFILESが送られる。
// WS_EX_LAYERED | // 半透明になる。 ( 不透明度は、SetLayeredWindowAttributes関数で指定 )
// WS_EX_TRANSPARENT | // 透明になる。 ( 下のウィンドウが更新された時にだけ、WM_PAINTを受信 )
WS_EX_TOPMOST | // 常に最前面に表示される。( SetWindowPos関数で変更する)
// WS_EX_TOOLWINDOW | // [X]ボタンだけ表示。
// ( hwnd引数にNULLを指定すると、タスクバーに表示されないトップレベルウィンドゥ扱いになる。
// 一方、hwnd引数に親のhwndを指定すると、 親ウィンドゥと連動して最小化するウィンドゥになる。
// hwnd引数に親のhwndを指定して、さらにWS_CHILDとウィンドゥIDを指定した場合は、
// 親ウィンドゥのクライアント領域内だけで動作する子ウィンドゥになる。)
// WS_EX_CONTEXTHELP | // タイトルバーに[?]ボタン を表示。 ( 子ウィンドゥが WM_HELPを受け取る )
// WS_EX_MDICHILD // MDI 子ウィンドゥを作成する。
// WS_EX_NOPARENTNOTIFY // 子ウィンドゥの場合、作成・破棄時に
// 親ウィンドゥに WM_PARENTNOTIFYを送らない。
WS_EX_COMPOSITED; // ウィンドゥへの描画が、自動的にダブルバッファリングされ、ちらつきがなくなる。
DWORD w_style = WS_OVERLAPPED | // オーバーラップウィンドゥを作成する。
// ( タイトルバーと外枠があり、親ウィンドゥの外へ移動できる。いわゆる「ウィンドゥ」のこと。 )
WS_CAPTION | // タイトルバーを持つウィンドゥを作成する。
WS_SYSMENU|// タイトルバーに、最大化、最小化、閉じるなどのボタンを配置する。
WS_HSCROLL | WS_VSCROLL | // 縦・横のスクロールバーを追加する。
WS_THICKFRAME | // サイズ変更を許可する。 ( リサイズ用の太めの枠が付く )
WS_MINIMIZEBOX | // 最小化ボタンの使用を許可する。
WS_MAXIMIZEBOX | // 最大化ボタンの使用を許可する。
WS_VISIBLE ; // 表示する。
// WS_DISABLED | // ウィンドゥを無効にする。
// WS_MAXIMIZE | WS_MINIMIZE; // 最大or最小表示する。
// WS_POPUP // ポップアップ。( ※WS_CHILD では使えない。ポップアップの親はデスクトップ画面)
// WS_TABSTOP // タブストップを許可する。 ( 子コントロール向け )
// WS_DLGFRAME // 二重境界を持ち、タイトル無し。
// WS_GROUP // コントロールグループの最初のコントロール。(方向キーで移動できる)
// 次に指定したコントロールまでが同じグループとなる。
// ------------------------
// ウィンドゥを作成する。
h_window = ::CreateWindowExW(
w_ex_style, // 拡張ウィンドゥスタイルを指定 ( これは0でもいい )
p_name_, // ウィンドゥクラス名
p_text_, // タイトルバーに表示される文字列
w_style, // ウィンドゥスタイル
x_, // ウィンドゥの左端 (x座標)
y_, // ウィンドゥの上端 (y座標)
width_, // ウィンドゥの横幅
height_, // ウィンドゥの縦幅
h_parent_, // 親ウィンドゥのハンドル HWND
NULL, // メニューのハンドル HMENU
wc.hInstance , // アプリケーションインスタンスのハンドラ HINSTANCE
NULL ); // CREATESTRUCT構造体へのポインタ。( MDIクライアント作成時には必須 )
if ( h_window == NULL ) // 作成に失敗したら、
{
::UnregisterClassW( p_name_, wc.hInstance ); // ウィンドゥクラスを抹消する。
return FALSE;
}
// ------------------------
// ★thisポインタを、格納しておく。( ウィンドゥプロシージャから、メッセージハンドラを呼ぶのに使う )
::SetWindowLongPtrW( h_window, GWLP_USERDATA, (LONG) this );
// ------------------------
// ★ウィンドゥプロシージャを、本番用にすり替える。
::SetWindowLongPtrW( h_window, GWLP_WNDPROC, (LONG) WindowProc );
// ------------------------
::UpdateWindow( h_window ); // ウィンドウを更新する。
::ShowWindow( h_window, SW_SHOWNORMAL ); // ウィンドゥを表示する。
// ------------------------
return TRUE; // 作成に成功した。
}
// 初期化時にだけ使用するウィンドゥプロシージャ。
LRESULT CALLBACK WindowProc_Init(
HWND h_wnd_, UINT msg_, WPARAM wparam_, LPARAM lparam_ )
{
// -----------------
// ウィンドゥが生成されたら、( CreateWindow関数が呼ばれたら )
if ( msg_ == WM_CREATE )
{
// その他のメッセージは、システムに処理してもらう。 ( 親への通知コード送信や標準描画処理 )
::DefWindowProcW( h_wnd_, msg_, wparam_, lparam_ );
return 0; // 0を返すと、CreateWindow関数が成功する。
}
// -----------------
// その他のメッセージは、システムに処理してもらう。 ( 親への通知コード送信や標準描画処理 )
return ::DefWindowProcW( h_wnd_, msg_, wparam_, lparam_ );
// -----------------
}
// ウィンドゥプロシージャ ( すべてのウィンドゥインスタンスで共有する )
LRESULT CALLBACK WindowProc(
HWND h_wnd_, UINT msg_, WPARAM wparam_, LPARAM lparam_ )
{
// -----------------
// 送信先ウィンドゥのハンドルから、コントロールインスタンスへのポインタを取得する。
CControl* p_control = (CControl*) ::GetWindowLongPtrW(
h_wnd_, GWLP_USERDATA );
// -----------------
// インスタンスごとのメッセージ処理を呼び出す。
// 送信先インスタンスのハンドラを呼ぶ。( 処理した場合は、0を返す。 )
return p_control->MessageHandler( h_wnd_, msg_, wparam_, lparam_ );
// -----------------
}
・通常は、ウィンドゥプロシージャの中に、処理を書いていくんですが、
ウィンドゥプロシージャはグローバル関数でないといけないので、
ウィンドゥをクラス化した場合に、メソッドを指定することができません。
・しかし、ウィンドゥプロシージャには、
送信先ウィンドゥのハンドルが渡されますので、
これを使って、ウィンドゥインスタンスのthisポインタを取り出すことができ、
インスタンスごとの処理メソッドを呼ぶことができます。
・ただし、ここでthisポインタを受け取るには、あらかじめ、
ウィンドゥの GWLP_USERDATA に、格納しておく必要があります。
・MessageHandlerメソッドは、CControlクラスのメンバです。
・「仮想関数」として定義されていますので、上書きして使うこともできます。
・CFormクラスは、CControlクラスを派生させたものです。
・CControlクラスというのは、ウィンドゥのハンドルをくるんだクラスで、
すべてのコントロールクラスの親クラスです。
・ウィンドゥのハンドルは、CreateWindow関数の戻り値で、
これがあれば、GetWindowLongPtr関数などから、
そのウィンドゥに関する情報を取得したり、
SetWindowLongPtr関数などから
変更し直したりすることができます。
・ウィンドゥのハンドルは、「HWND型」です。
・Windows は、
ハードディスク上の領域や、
メモリ上の領域など、
すべてを管理しています。
・アプリケーションソフトが、
ハードディスク上のファイルや、
ウィンドゥやコントロールなどを使用するときは、
Windowsからレンタルするような形を取り、
不要になったら返却します。
・アプリケーションソフトは、Windowsから
「システムリソース」 (システム資源) をレンタルするときに、
それらを識別するための「ハンドル」というものを取得します。
・たとえば、メニューコントロールのハンドルは「HMENU型」で、
CreateMenu関数の戻り値として取得することができます。
・WindowsAPIの関数には、
リソースを操作するための関数が多数ありますが、
操作対象となるリソースを指定するために、
第一引数に、この「ハンドル」を渡します。
・まるで部屋を借りるような感覚で
Windowsからウィンドゥを貸してもらい、
料理を注文するような感覚で、
Windowsに位置を移動させてもらうのです。
・そして、使い終わったら、ウィンドゥを返却します。
・アプリケーションソフトは、
こうした指示をWindowsへと送るために、
WindpwsAPIの関数を呼び出しているのです。
同期オブジェクト
・「同期オブジェクト」は、
複数のスレッドが並走している中で、
スレッドを1つずつ動かしたい場合に使用します。
・複数のスレッドが並走している場合は、
同じデータに対して同時に書き込みを行うことで、
データが破損してしまうことがあります。
・「同期オブジェクト」は、複数のスレッドから
同時にアクセスされるデータに対して設定され、
複数のスレットが、同時にアクセスできなくします。
・WaitForSingleObject関数を呼ぶと、
そのスレッドは順番待ちに加わります。
・この時点で、動作中のスレッドがなければ、
このスレッドが動作を開始します。 ( スレッド1のケース )
・そうでない場合は、そのスレッドの処理が
終わるまで待機します。 ( スレッド2のケース )
リソースファイルを編集する
・コントロールやメニューなど、一部のシステムリソースは、
実行ファイルの内部に埋め込むことができます。
・埋め込むリソースの情報は、「リソースファイル」(*.rc) に書きます。
( 「リソーススクリプト」という簡易スクリプト言語で記述します )
・「Express Edition」では、リソースファイルの編集ができません。
( ファイル上で右クリックして、「開く」 ではなく、「コードの表示」を選べば、表示はされます。 )
・編集するには、「ResEdit」というエディタソフトを使用します。↓
http://gurigumi.s349.xrea.com/programming/visualcpp/resedit.html
・インストールした時点では、英語で表示されていますが、
日本語で表示することもできます。↓
[Option]→[Preferences]→[General]→[Language] を、
[Japanese] に切り替える。
・プロジェクト側には、上記のエディタで作成した「リソースファイル」と、
「リソースID」定数を#defineした「ヘッダファイル」を追加します。↓
// 《 dialog_test.dlg 》
MyDialog DIALOG 21, 16, 146, 55
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "タイトル"
FONT 12, "System"
BEGIN
LTEXT "ラベルテキスト1", ID_LABEL1, 47, 6, 82, 8
EDITTEXT ID_EDIT1, 44, 18, 94, 12, ES_AUTOHSCROLL
PUSHBUTTON "OK", ID_OK, 44, 35, 40, 14
PUSHBUTTON "キャンセル", ID_CANCEL, 96, 35, 40, 14
END
// 《 dialog_test.h 》
#define ID_LABEL1 102
#define ID_EDIT1 103
#define ID_OK 104
#define ID_CANCEL 105
// 《 dialog_test.rc 》
#include "test_dialog.h"
#include "test_dialog.dlg"
// 《 dialog_test.cpp 》
#include "test_dialog.h"
int APIENTRY _tWinMain( HINSTANCE h_instance_ , /* 省略 */ )
{
::DialogBox( h_instance_, "MyDialog", NULL, MyDialogProc );
return 0;
}
・ダイアログの場合は、「ダイアログファイル」 (*.dlg) に、リソーススクリプトを書きます。
・この中で、「ID_OK」といった定数が使われていますが、
これは、コントロールリソースのIDで、ソースファイル上でも使用しますので、
ヘッダファイル上で定義しておきます。