鳩でもわかるC#管理人はC#の存在とその素晴らしさに気づく前はC++を使っていました。そのときは猫でもわかるプログラミングというサイトをみて学習をしていました。WindowsアプリケーションをつくるためのWindows SDK編ではサンプルプログラムはC言語で書かれているのですが、WndProc関数関数がとにかく長い。短く書けないかと自分で別の書き方をしたりして試行錯誤を繰り返していたのですが、C#を使ったほうが速くアプリケーションが作れることに気づいてC++は完全にご無沙汰状態になっていました。
ところでなぜ「猫でもわかるプログラミング」なんてサイト名になっているのでしょうか?
それは同サイトのイントロダクションに書かれています。いわく
「猫の額」ほどの土地などと言います。猫の額は狭いのです。 額が狭いと言うことは、ゼントーヨー(前頭葉)が小さいと言うことです。 よーするに、賢くないと言うことです。
そんな「猫」でも書けるよう、難しい話は抜きの 実践中心のプログラミングの話です。
ということです。しかし今時の猫は前頭葉が小さいわりに賢いものです。人間がみてもなかなか理解できるものではありません。そこで猫よりも低い知能しか持たない鳩でもわかるように主にC#を扱った鳩でもわかるC#というサイトを立ち上げたのです。
Contents
新たな決意といきなりのハマりどころ
しかし、それでいいのか? やっぱり鳩のままで終わらずせめて猫並みの知能を手に入れるために久しぶりにC++に挑戦してみることにします。MFCなんて便利なものは使わず自分で一から書きます。
ところがブランクがあるからかいきなりハマり道へ。サンプルプログラムを入力してもエラーがでてコンパイルできないのです。
C++の空のプロジェクトを選べばよいと思ったのですが、これが間違い。正しくはWindowsデスクトップウィザードを選ばなければならなかったのです。
Windowsデスクトップウィザードを選択したあとプロジェクト名と場所を設定したあと「作成」ボタンをクリック。すると以下のようなダイアログが出てくるのでコンソールアプリケーションではなくデスクトップアプリケーションを選択して空のプロジェクトにチェックをいれて[OK]ボタンをクリックします。
そして追加メニューでC++ファイルを作成してイントロダクションにあるコードを丸パクリしてコンパイルしようとするとエラーになります(おいっ!)。
イントロダクションが書かれた当時(Mar/15/1997)はこれでも動いたのかもしれませんが、いまや20年以上の年月が流れています。当時のVisual C++と現在のVisual Studioでは仕様も違ってきています。
ここはこう書かなければならないのです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
#include <windows.h> LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // いまではUnicodeが標準になっているのでこの状態でcharを使うとエラーになるではなくWCHARを使う // Unicodeが標準になっているいまではTCHARも非推奨らしい // char szClassNme[] = "ウィンドウクラス・ネーム"; WCHAR szClassNme[] = L"ウィンドウクラス・ネーム"; int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPreInst, LPSTR lpszCmdLine, int nCmdShow) { HWND hWnd; MSG msg; WNDCLASS myProg; if (!hPreInst) { myProg.style = CS_HREDRAW | CS_VREDRAW; myProg.lpfnWndProc = WndProc; myProg.cbClsExtra = 0; myProg.cbWndExtra = 0; myProg.hInstance = hInstance; myProg.hIcon = NULL; myProg.hCursor = LoadCursor(NULL, IDC_ARROW); // HBRUSH型に明示的キャストしないとエラーになる myProg.hbrBackground = GetStockObject(WHITE_BRUSH); myProg.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); myProg.lpszMenuName = NULL; myProg.lpszClassName = szClassNme; if (!RegisterClass(&myProg)) return FALSE; } hWnd = CreateWindow(szClassNme, //"猫でもわかるプログラミング", // UNICODEが定義されているならワイド文字を渡さないといけない L"鳩にはちょっと難しいC++プログラミング", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL); ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (msg.wParam); } LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_DESTROY: PostQuitMessage(0); break; default: return(DefWindowProc(hWnd, msg, wParam, lParam)); } return (0L); } |
クラスを使ってウィンドウを表示させる
ところでC言語とちがってC++ならクラスが使えます。クラスを使わないとグローバル変数ばっかりになって訳が分からなくなったり関数が乱立してわかりにくいコードになってしまいます。そこで以下のように書き直します。
MainWindowクラスを作成してWinMain関数は以下だけにします。
app.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <windows.h> #include "MainWindow.h" int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow) { MainWindow mainWindow(hInstance); MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (msg.wParam); } |
次にMainWindowクラスをどうするかですが、MainWindow.hというファイルをつくって以下のように書きます。
MainWindow.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class MainWindow { HWND m_hWnd = NULL; HINSTANCE m_hInstance = NULL; const WCHAR* m_szClassNme = L"MyClassName1"; const WCHAR* m_szTitle = L"鳩には難しいC++プログラミング"; public: MainWindow(HINSTANCE hInstance); void Create(); static LRESULT CALLBACK WndProcStatic(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp); LRESULT WndProc(UINT msg, WPARAM wp, LPARAM lp); LRESULT OnCreate(WPARAM wp, LPARAM lp); LRESULT OnDestroy(WPARAM wp, LPARAM lp); LRESULT OnPaint(WPARAM wp, LPARAM lp); LRESULT OnCommand(WPARAM wp, LPARAM lp); }; |
コンストラクタ
まずはコンストラクタですが、Createというウィンドウをつくるための関数を呼び出しているだけです。
MainWindow.cpp
1 2 3 4 5 6 7 |
#include <windows.h> #include "MainWindow.h" MainWindow::MainWindow(HINSTANCE hInstance) { Create(); } |
ウィンドウを生成する
ではCreate関数はどうなっているのでしょうか?
まずWNDCLASSに必要なデータをセットしてRegisterClass関数で登録しています。myProg.lpfnWndProc = WndProcStaticとしてウィンドウプロシージャ関数のアドレスを指定しています。これを普通のメンバー関数にしたいのですが、その性格上できません。そこでWndProcStaticはスタティックなメンバ関数にしています。
そのあとWindowsAPI関数のCreateWindow関数を呼び出してウィンドウを生成します。ウィンドウは生成するだけでは表示されず表示するための関数を呼び出さないと表示されません。そこでShowWindow関数を呼び出して表示させています。
問題はCreateWindow関数の最後の引数にthisを渡している点です。これがちょっとしたポイントです。
MainWindow.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
void MainWindow::Create() { WNDCLASS myProg; myProg.style = CS_HREDRAW | CS_VREDRAW; myProg.lpfnWndProc = WndProcStatic; myProg.cbClsExtra = 0; myProg.cbWndExtra = 0; myProg.hInstance = m_hInstance; myProg.hIcon = NULL; myProg.hCursor = LoadCursor(NULL, IDC_ARROW); myProg.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); myProg.lpszMenuName = NULL; myProg.lpszClassName = m_szClassNme; RegisterClass(&myProg); m_hWnd = CreateWindow(m_szClassNme, m_szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 400, 600, NULL, NULL, m_hInstance, this); ShowWindow(m_hWnd, SW_SHOWNORMAL); UpdateWindow(m_hWnd); } |
ウィンドウプロシージャ関数
ウィンドウプロシージャ関数はメッセージを処理する関数です。WM_CREATEはウィンドウが生成されたときに送られてくるメッセージですが、ここでCreateWindow関数の最後の引数で渡したものが生きてきます。
LPCREATESTRUCT(lp)->lpCreateParamsとするとCreateWindow関数の最後の引数で渡したデータ(この場合はMainWindowのインスタンス)を取得できます。そしてこれをSetWindowLongPtr関数をつかってウィンドウ固有のユーザーデータをセットします。それ以降はウィンドウプロシージャ関数が呼び出されるたびにここからMainWindowのインスタンスのアドレスを取得できるようになります。またこのときにウィンドウハンドルをメンバ変数のm_hWndに格納します。
なにがしたいのかというとこれで普通のメンバー関数をウィンドウプロシージャ関数のように使えるようにするためです。最初の行でMainWindowへのポインタを取得できない場合はDefWindowProc関数を呼び出してデフォルトの処理をおこなわせます。ポインタを取得できたらメンバ関数であるWndProc関数を呼び出します。
MainWindow.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
LRESULT CALLBACK MainWindow::WndProcStatic(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) { MainWindow *pMainWindow = (MainWindow *)GetWindowLongPtr(hWnd, GWL_USERDATA); if (msg == WM_CREATE) { LONG_PTR longPtr = (LONG_PTR)LPCREATESTRUCT(lp)->lpCreateParams; SetWindowLongPtr(hWnd, GWLP_USERDATA, longPtr); pMainWindow = reinterpret_cast<MainWindow*>(GetWindowLongPtr(hWnd, GWLP_USERDATA)); pMainWindow->m_hWnd = hWnd; } if (pMainWindow != NULL) return pMainWindow->WndProc(msg, wp, lp); else return DefWindowProc(hWnd, msg, wp, lp); } |
WndProc関数が呼び出せるようになったらここで処理をおこないます。WM_CREATEメッセージが送られてきたらOnCreate関数を、WM_DESTROYメッセージが送られてきたらOnDestroy関数を呼び出します。
MainWindow.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
LRESULT MainWindow::WndProc(UINT msg, WPARAM wp, LPARAM lp) { switch (msg) { case WM_CREATE: return OnCreate(wp, lp); case WM_DESTROY: return OnDestroy(wp, lp); case WM_COMMAND: return OnCommand(wp, lp); case WM_PAINT: return OnPaint(wp, lp); default: return(DefWindowProc(m_hWnd, msg, wp, lp)); } return 0L; } |
メッセージに対応してメンバー関数を呼び出す
ウィンドウが生成されたときの処理をOnCreate関数内に書けば動作させることができます。以下のコードはウィンドウ内にボタンを設置しています。
MainWindow.cpp
1 2 3 4 5 6 7 8 9 10 |
#define ID_BUTTON 10 LRESULT MainWindow::OnCreate(WPARAM wp, LPARAM lp) { CreateWindow( L"BUTTON", L"ボタン", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON, 20, 20, 100, 30, m_hWnd, (HMENU)ID_BUTTON, m_hInstance, NULL); return 0L; } |
ボタンが押されるとWM_COMMANDメッセージが送られてきます。LOWORD(wp)でボタンのIDを取得できるのでOnCommand関数内で処理をします。ボタンが押されたら「ボタンが押されました」というメッセージボックスが表示されます。
1 2 3 4 5 6 7 8 9 10 11 12 |
LRESULT MainWindow::OnCommand(WPARAM wp, LPARAM lp) { switch (LOWORD(wp)) { case ID_BUTTON: MessageBox(m_hWnd, L"ボタンが押されました", L"報告", MB_OK); break; default: break; } return 0L; } |
ウィンドウの再描画がおこなわれるときにはWM_PAINTメッセージが送られてきます。OnPaint関数内になにかを書いていると描画の処理がおこなわれます。いまは特になにもしません。
1 2 3 4 |
LRESULT MainWindow::OnPaint(WPARAM wp, LPARAM lp) { return 0L; } |
ウィンドウが閉じられたらアプリケーションを終了させる
ウィンドウが閉じられたらウィンドウは消えますが、それだけではアプリケーションは終了しません。終了させるための処理をさせなければなりません。WM_DESTROYメッセージが送られてきたらPostQuitMessage関数を実行します。これでWinMain関数のなかのメッセージループから抜け出すことになるのでアプリケーションが終了します。
1 2 3 4 5 |
LRESULT MainWindow::OnDestroy(WPARAM wp, LPARAM lp) { PostQuitMessage(0); return 0L; } |
今回はOnPaint関数内になにも書きませんでしたが、次回はここに何かを書いて描画処理を行ないます。次回はC++でテトリスを作ってみようと考えています。