C++でGUIを作りたい
C++でGUIアプリケーションを開発したい。C++を扱ったことがある方なら、そう思ったことは一度はあるのではないでしょうか? この記事を読めば、その方法を知る事が出来ます。
そんな経験が無い? なら質問を変えます。ゲームを起動したときに「レンダリングエンジンを選択してください。DirectX描画 GDI描画」のような選択肢を見かけた事はあるのではないでしょうか? この記事を読めば、その意味が理解できます。
そんな経験もない? ……と、ともかく、この記事ではC++でGUIアプリケーションを作る方法について、紹介しようと思います。先に言っておきますと、この記事はWindows向けです。Mac向けのアプリを開発したいなら、ブラウザバックして下さい。
WindowsAPIとは?
Windowsのシステムコール用のAPIです。要は、Windowsのシステムに何か働きかける事が出来るのですが、その機能の一つに「Windowの作成」があるのです。
Windowsシステムに対し「これから新しいGUIアプリケーションを作るよ」「Windowはこのくらいのサイズね」「DPIはこんな風にして」などなどお願いするイメージでしょうか。
利点は(Windowsで開発している限り)最初から搭載されている機能ですので、ライブラリの追加などをしなくてもいい点が挙げられます。C言語・C++の開発環境に標準装備されているという事です。
欠点は「難しい」「デザインが古い」という事です。
難しさの一例を示しましょう。例えばウィンドウを作る関数CreateWindow
には引数が11個あります。また、CreateWindow
は「ウィンドウの作成」「ボタンの作成」「チェックボックスの作成」などなど多数の機能を持っています。「なんでだよ!」と叫びたくなるAPIなのです。
※注意:CreateWindow
は正確には関数ではなくマクロです。しかし、この記事では「関数」と表記する事にします。
また、ここで「古い」と言ったのは、デザインが一昔前の物になってしまうという意味です。以下の写真をご覧ください。ボタンやテキストボックス、その他のコントロールを配置してみましたが、「モダンなデザイン」とは到底言えないでしょう。
「え、でもモダンなデザインも作れるんでしょう?」と思ったあなた。残念、基本的には無理です。モダンでフラットなボタンを実装したければ、自前で「四角形を描画」「その中にテキストを描画」「マウスがクリックされた時、クリック場所が範囲内かどうか」などを実装する必要があります。
「いやいや。それを簡単にするライブラリとか使えばいいじゃん」という意見が出るかもしれませんが、そうするくらいならいっそ、WindowsAPI以外のライブラリを使うべきでしょう。
WindowsAPIを使ってみる
※注意
gccでコンパイルする時は「g++ source.cpp -mwindows」のように-mwindowsオプションを付けましょう。またVisual Studioなら「構成プロパティー→リンカー→システム」を開き、サブシステムを「Windows (/SUBSYSTEM:WINDOWS)」にします。
#include <Windows.h>
int WINAPI WinMain(HINSTANCE hCurInst, HINSTANCE hPrevInst, LPSTR lpsCmdLine, int nCmdShow) {
MessageBoxA(NULL, "Hello Windows API", "Message", MB_OK);
return 0;
}
いきなり訳の分からないコードが出てきましたね。慣れ親しんだmain関数すら登場しません。これは一体何なのでしょう?
察しの良い方なら「このWinMain
っていうのがmain関数の変わりなのかな?」と気付くかもしれません。そう、これがWindowsAPIにおけるmain関数なのです。
つまり、このコードは次のコードとほぼ同値と言えます。
#include <Windows.h>
int main(){
MessageBoxA(NULL, "Hello Windows API", "Message", MB_OK);
return 0;
}
一気にすっきりしましたね。「WindowsAPIではmain関数がちょっと豪華になっている」とだけ、ここでは理解しておいてください。
さて、続いての話題。MessageBoxA(NULL, "Hello Windows API", "Message", MB_OK);
の意味を解説していきます。
メッセージボックスとは次のような物です。
MessageBoxA
関数はこれを作成する関数です。
第一引数はHWND
型=Handle of Windowです。このメッセージボックスを出しているオーナーウィンドウを指定します。今回はオーナーが存在しないのでNULLを指定しています。
第二引数はconst char*
型で、メッセージボックスに表示される文字列を指定します。今回は"Hello Windows API"
と指定しています。
第三引数もconst char*
型で、メッセージボックスの上部に表示される文字列を指定します。今回は"Message"
と指定しています。
第四引数はunsigned int
型で、メッセージボックスの種類を指定します。MB_OK
は「Message Box OK」の略でOKとだけ表示されたメッセージボックスが作成されます。他にMB_OKCANCEL
、MB_YESNO
、MB_YESNOCANCEL
などを指定できます。
もう完全にこのプログラムを理解できるようになりましたね。
「ちょっと待って下さい! さっきネットを調べてたら、MessageBoxA
関数以外にMessageBox
関数とMessageBoxW
関数っていうのも見つけたのですけど、これはなにですか?」
あ、話すのを忘れていました。解説しましょう。Windows APIの関数には「A」と「W」と「無印」がある場合があります。AはANSI、Wはワイド文字、無印はコンパイル環境に応じて自動選択を意味します。
一文字(char
型)は何バイトでしょうか? 1バイトですよね。つまり最大255種類の文字種を保存できます。アルファベットと記号くらいなら255種類もあれば十分ですが、グローバル化しつつあるこの世界で255種類の文字しか格納出来ないというのは明らかに不十分です。
そこで作られたのがUnicodeと呼ばれる文字セットです。これは2バイトの文字ですので、255✕255種類の文字を格納できます。C言語、C++ではこれはwchar_t
型になります。wchar_t
型の文字列はL"Hello"
のようにクオーテーションの前にLをつける必要があります。
wchar_t character=L'H';
wchar_t text[6]=L"Hello";
さて、MessageBoxA
の第2、第3引数はconst char*
型でした。これではアルファベットと平仮名とハングル文字が混ざったような文字は表示できません。そこで、ワイド文字も表示できる関数も用意されており、それがMessageBoxW
関数です。
MessageBoxA(NULL, "Hello Windows API", "Message", MB_OK);
MessageBoxW(NULL, L"Hello Windows API", L"Message", MB_OK);
残るはMessageBox
関数。これはAとWを自動で選択してくれます。と言うと「良いじゃん」と思うかもしれませんが、この「自動で」というのは「コンパイル時の設定に応じて」という意味です。「引数に応じて自動選択」ではないです。
つまり、コンパイル時の設定がUnicodeなのにMessageBox(NULL, "Hello", ......);
と書くとエラーになります。逆にコンパイル時の設定がASNIなのにMessageBox(NULL, L"Hello", ......);
と書くとエラーになります。つまり、エラーの温床です。
この問題を解決するには"Hello"
をコンパイル時の設定に応じて自動的にLにしたりLを外したりする機能が必要になります。これがTEXT
マクロです。
MessageBox(NULL, TEXT("Hello Windows API"), TEXT("Message"), MB_OK);
このように書くことで、コンパイル時の設定に応じたプログラムとなります。
今後、この記事では「A」を中心に使うことにします。
もう質問は無いですか? なら次に……。
「ちょっと待って下さい! 今ネットを調べたらMessageBoxWの引数がこんなふうに書いてました」
int MessageBoxW(
HWND hWnd,
LPCWSTR lpText,
LPCWSTR lpCaption,
UINT uType
);
「第2、3引数がconst wchar_t*
型ではないですが、これはどういうことなのでしょう?」
良い質問ですね! これは裏でconst wchar_t*
として扱われます。つまり、typedefされています。WindowsAPIには様々な「大文字の略称」があります。例えば、第4引数のunsigned int
はUINT
と略されていますね。
LPCWSTR
について、これがなんの略なのか説明します。
1文字めのLは「これはlong
型です」という意味です。wchar_t*
はポインタ変数であり、中身はメモリのアドレスです。アドレスは整数であり、内部的にはlong
型なのです。
2文字目のPはこれがポインタ型であることを意味します。最初のLと合わせて「これはポインタ(内部的にはlong
型)です」となりますね。
3文字目のCはこれがconst
型であることを意味します。
4〜7文字目のWSTRはこれがワイド文字列であることを意味します。
何もしないウィンドウ
アプリケーションの登録
この記事の最初に「Windows APIはWindowsシステムに命令するAPI」という話をしました。アプリケーションの作成も、システムに「今からGUIアプリを作るよ」とお願いする必要があります。このお願いを忘れると、バツボタンを押しても消えない、クリックしてもフォーカスが当らない、そもそも表示すらされないアプリケーションが出来上がってしまいます。
「今からGUIアプリを作るよ」とお願いする関数がRegisterClassExA
関数です。引数は一つ、WNDCLASSEXA
構造体へのポインタです。
WNDCLASSEXA
構造体とはRegisterClassExA
に関する情報を含んでいる構造体で、以下のような要素を含みます。
- cbSize
- style
- lpfnWndProc
- cbClsExtra
- cbWndExtra
- hInstance
- hIcon
- hCursor
- hbrBackground
- lpszMenuName
- lpszClassName
- hIconSm
多い多い! これ、毎回一つずつ設定しないといけないの?
いいえ。ぶっちゃけ、コピペでOKです。とはいえ、コピペでは駄目な物もあり、それがlpfnWndProc
とlpszClassName
です。
lpfnWndProc
はウィンドウプロシージャーのポインタです。ウィンドウプロシージャーとは、ウィンドウの動作を決定する関数です。これは後ほど解説します。
lpszClassName
は今から作るアプリケーションのID的なものです。アプリの名前、というと語弊があるので「アプリケーション固有のID」と理解しましょう。
以下、「固有のID」と「ウィンドウプロシージャーのポインタ」から、アプリケーションの登録を行うオリジナル関数です。「色々設定してるなあ」くらいの認識で大丈夫です。
#include <Windows.h>
BOOL RegisterApp(const char* windowID, WNDPROC windowProc) {
WNDCLASSEXA wc;
wc.cbSize = sizeof(WNDCLASSEXA);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = windowProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = GetModuleHandleA(NULL);
wc.hIcon = (HICON)LoadIconA(NULL, IDI_APPLICATION);
wc.hCursor = (HCURSOR)LoadCursorA(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wc.lpszMenuName = NULL;
wc.lpszClassName = windowID;
wc.hIconSm = (HICON)LoadImageA(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_DEFAULTSIZE | LR_SHARED);
return (RegisterClassExA(&wc));
}
ウィンドウの作成
いよいよウィンドウの作成です。これにはCreateWindowA
関数を用います。
void CreateWindowA(
アプリケーションのID(RegisterAppで指定した文字列) ,
Windowのタイトル ,
Windowスタイル ,
Windowの左上のX座標 ,
Windowの左上のY座標 ,
Windowの幅 ,
Windowの高さ ,
親ウィンドウ。今回はNULL ,
メニューの指定。今回はNULL ,
hInstance(基本的にhCurInstを使う) ,
lpParam(基本的にNULL)
);
引数が11個あります。「どうせまた、ほとんど無視で良いんだろ?」と思いきや、そうはいきません。残念ながら、ほぼ全てが重要です。
第1引数はアプリケーションのIDで、RegisterApp
で使った文字列です。ここを間違えるとバグるので注意!
第2引数はウィンドウのタイトルです。ウィンドウの一番上に表示される文字列ですね。
第3引数はウィンドウスタイルです。よく使うのは2つ、WS_OVERLAPPEDWINDOW
とWS_POPUP
です。前者は「×ボタンや最小化ボタンが付いた、普通のウィンドウ」を意味し、後者は「×ボタンも最小化ボタンもない、真っ白な四角形のウィンドウ」です。
また、WS_CHILD
も使います。これは、ウィンドウがあるウィンドウの子ウィンドウである時に指定します。子ウィンドウとは、ウィンドウの中のウィンドウです。いったん今は気にしなくて大丈夫です。先の二文だけで「ウィンドウ」という単語が6回も出てきましたね……。
第4~7引数はウィンドウの場所や大きさを指定します。左上のX,Y座標と幅、高さの順番で指定します。
第8引数は親ウィンドウのハンドルです。子ウィンドウの項目で話すので、今はNULLでOKです。
第9引数はメニューバーの指定ですが、これも今はNULLでOKです。
第10引数はINSTANCEハンドルを指定します。WinMain関数の引数「hCurInst」を書けばOKです。
第11引数はNULLです。
CreateWindowA
関数の返り値は「HWND
型」です。これはウィンドウの情報が入った物でHandle of Windowの略です。
Windowの表示、アップデート、削除
ShowWindow(hWnd, SW_SHOW);
UpdateWindow(hWnd);
DestroyWindow(hWnd);
ウィンドウの表示、アップデート(再描画)にはそれぞれShowWindow
、UpdateWindow
関数を使います。どちらも第一引数はHWND
です。ShowWindow
の第二引数は表示のさせ方(表示させる/させない。敢えて表示させない、最大化した状態で表示など)を指定します。基本はSW_SHOWで良いと思います。
最後にDestroyWindow
ですが、これでウィンドウを破棄します。
上記をまとめると
もう一息です。
まずRegisterApp
ですが、第一引数(アプリケーション固有のID)を"MyFirstApp"
とし、第二引数(ウィンドウプロシージャ)はWindowsが用意しているデフォルトのウィンドウプロシージャ「DefWindowProcA
」を利用しました。
#include <Windows.h>
BOOL RegisterApp(const char* windowID, WNDPROC windowProc) {
WNDCLASSEXA wc;
wc.cbSize = sizeof(WNDCLASSEXA);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = windowProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = GetModuleHandleA(NULL);
wc.hIcon = (HICON)LoadIconA(NULL, IDI_APPLICATION);
wc.hCursor = (HCURSOR)LoadCursorA(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wc.lpszMenuName = NULL;
wc.lpszClassName = windowID;
wc.hIconSm = (HICON)LoadImageA(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_DEFAULTSIZE | LR_SHARED);
return (RegisterClassExA(&wc));
}
int WINAPI WinMain(HINSTANCE hCurInst, HINSTANCE hPrevInst, LPSTR lpsCmdLine, int nCmdShow) {
HWND hWnd;
if (!RegisterApp("MyFirstApp", DefWindowProcA)) return FALSE;
hWnd = CreateWindowA(
"MyFirstApp",
"Title",
WS_OVERLAPPEDWINDOW,
0, 0, 500, 300,
NULL, NULL, hCurInst, NULL);
ShowWindow(hWnd, SW_SHOW);
UpdateWindow(hWnd);
MessageBoxA(NULL, "Click OK to destroy window", "message", MB_OK);
DestroyWindow(hWnd);
return 0;
}
ウィンドウとメッセージボックスが表示されます。
OKを押すと、Windowが閉じます。
「すみません、このプログラムのMessageBoxA(NULL, "Click OK to destroy window", "message", MB_OK);
の第一引数NULLはhWndじゃないんですか?」
ああ、これはウィンドウを触れるようにしたいからです。写真ではTitleとかかれたウィンドウにフォーカスがあたっていますね。この状態では、最小化ボタンや最大化ボタンを押せるほか、ウィンドウを移動させたりウィンドウサイズを変更させたりできます。
しかし、MessageBoxA(hWnd……
と書いてしまうと、メッセージボックスにフォーカスがあたってしまい、親ウィンドウを触れる事が出来なくなってしまいます。
是非、実際に試してみてください。
ウィンドウプロシージャー
先ほどのプログラムではきちんと説明しなかったウィンドウプロシージャについて説明しようと思います。いきなりプログラムをお示ししますね。
#include <Windows.h>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
BOOL RegisterApp(const char* windowID, WNDPROC windowProc) {
WNDCLASSEXA wc;
wc.cbSize = sizeof(WNDCLASSEXA);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = windowProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = GetModuleHandleA(NULL);
wc.hIcon = (HICON)LoadIconA(NULL, IDI_APPLICATION);
wc.hCursor = (HCURSOR)LoadCursorA(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wc.lpszMenuName = NULL;
wc.lpszClassName = windowID;
wc.hIconSm = (HICON)LoadImageA(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_DEFAULTSIZE | LR_SHARED);
return (RegisterClassExA(&wc));
}
int WINAPI WinMain(HINSTANCE hCurInst, HINSTANCE hPrevInst, LPSTR lpsCmdLine, int nCmdShow) {
HWND hWnd;
if (!RegisterApp("MyFirstApp", WndProc)) return FALSE;
hWnd = CreateWindowA(
"MyFirstApp",
"Title",
WS_OVERLAPPEDWINDOW,
0, 0, 500, 300,
NULL, NULL, hCurInst, NULL);
ShowWindow(hWnd, SW_SHOW);
UpdateWindow(hWnd);
MSG msg;
BOOL value;
while (TRUE) {
value = GetMessageA(&msg, NULL, 0, 0);
if (value == 0) break;
if (value == -1)return 1;
TranslateMessage(&msg);
DispatchMessageA(&msg);
}
return 0;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) {
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProcA(hWnd, msg, wp, lp);
}
変わった点はたった三つ。
- ウィンドウプロシージャ「
WndProc
という関数」を定義し、それをRegisterApp関数の第二引数に指定した。 -
MessageBoxA(NULL, "Click OK to destroy window", "message", MB_O);
を消して、代わりにwhileループを置いた。 -
DestroyWindow(hWnd);
を消した。
DestroyWindowは自動で行ってくれる
まずは一言で済む「DestroyWindow(hWnd);
を消した」について説明しましょう。実は「ウィンドウの破棄はウィンドウの×ボタンを押した時にシステムが自動で行ってくれます」。よって自前で書く必要はないです!
と書くと、「じゃあさっきは?」と言う疑問が浮かぶでしょう。これには理由があります。先ほどのプログラムは「メッセージボックスのOKを押した」という「外部的な要因」でウィンドウを破棄したかったのです。だからわざわざ自前で書いたのです。
ウィンドウプロシージャは社畜だ
次に「ウィンドウプロシージャ『WndProc
という関数』を定義し、それをRegisterApp関数の第二引数に指定した」について説明しましょうか。
先ほどから時々話しているウィンドウプロシージャですが、難しく考える必要はなく、「ただの関数」です。
返り値はLRESULT
です。これは内部的にはlongと同義です。CALLBACK
と指定していますが、これはおまじないとでも思っておいてください。
引数は四つ。HWND hWnd, UINT msg, WPARAM wp, LPARAM lp
です。
hWnd
に格納されているのは、自分のウィンドウハンドルです。
msg
は自然数ですが、そこに「ウィンドウに今起きている事」が入っています。「msg==WM_CREATE
」なら今まさにウィンドウが作成されたことを意味します。「msg==WM_PAINT
」ならウィンドウが描画されなければいけないことを意味します。「msg==WM_MOUSEMOVE
」ならマウスカーソルが動いたことを意味します。「msg==WM_DESTROY」なら、ウィンドウがもう消えてしまった事を意味します。
wp
やlp
はその他の情報が格納されています。どちらも内部的にはlong
型です。
そう、このウィンドウプロシージャには「そのウィンドウに起きたありとあらゆる情報が流れてきます」。そのすべてに対して、ウィンドウプロシージャは「瞬時に処理を行って、システムに終わったと伝えないといけない」です。
イメージとしてはこうです。
社長「WndProc君、今君の上でマウスカーソルが動いたよ。どうする? 何かする? 返事は0.01秒以内に教えてね」
WndProc社畜「は、はい! 教えて頂きありがとうございます!」
こんな感じです。さて、WndProc社畜はありとあらゆる事の報告を受けますが、「これは僕では分からない。部長の判断を仰ごう」という事もあります。そんな時に使うのがDefWindowProcA
です。これはなんかいい感じに処理を行ってくれる関数となっています。
社長「WndProc君、○○が起こったよ。どうする? 何かする? 返事は0.01秒以内に教えてね」
WndProc社畜「は、はい! 教えて頂きありがとうございます! ……僕じゃわからないな。DefWindowProcA部長に任せよう!」
DefWindowProcA「しゃーないな。ま、俺に任せとき」
ちなみに、WndProc社畜の返事が遅れると……。アプリが「応答なし」になります。
その上で、改めて今回のウィンドウプロシージャを見てみましょう。
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) {
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProcA(hWnd, msg, wp, lp);
}
今回行っていることは一つだけです。
WM_DESTROY、つまり「×ボタンが押され、自動的にDestroyWindowが行われた。もうウィンドウは存在しない」という時にPostQuitMessage(0)
を行って、returnしています。PostQuitMessage(0)
とは何なのか。これは社長に対して、「僕らの会社、もう潰れました」と報告する行為なのです。社長は激務の末に会社がつぶれた事さえも気づけなくなっています。そんな彼の目を覚ましてあげましょう。
社長「次の業務ハ……ツギの業ムは……ツギノギョウムハ……」
WndProc社畜「あの、社長……」
社長「ナンダ。ワタシハ、ツギノギョウムヲオコナワナイト……」
WndProc社畜「社長! 目を覚ましてください! 僕らの会社はつぶれたんです! もう仕事はしなくていいんです!」
社長「……は! 俺は一体何を!」
ところで、社長と呼称していましたが、これの正式名称は何なのか。その名前は「メッセージループ」、最後のトピックです。
メッセージループ
最後に話すのが「メッセージループ」についてです。先ほどまで社長と呼称していた存在についてですね。
MSG msg;
BOOL value;
while (TRUE) {
value = GetMessageA(&msg, NULL, 0, 0);
if (value == 0) break;
if (value == -1)return 1;
TranslateMessage(&msg);
DispatchMessageA(&msg);
}
MSG
型の変数msgとBOOL
型の変数valueを定義しています。
MSG
型については良く知らなくてOKです。そんなのがあるという事だけ。
BOOL
型の変数valueは重要です。ますはBOOL型とは内部で何として扱われると思いますか?
「はい。booleanですよね? trueかfalseが保存されます」
正解……と言いたいですが、実は不正解です。
「え? 違うのですか?」
はい。これは内部的にはlong
型です。
「は?(ブチギレ)」
怒らないでください。仕様です。
C言語におけるBOOL型はTRUE=1とFALSE=0以外にも、色々な整数を格納できます。だって中身はlong
だもの。
とは言え、valueには基本的に1か0が保存されます。0が格納されていたら「PostQuitMessageが呼び出された」事を意味します。そう、先ほどの話がここに繋がってくるのです。PostQuitMessageは「社長に対して、『僕らの会社、もう潰れました』と報告する行為」と説明しましたね。そして、確かにif (value == 0) break;
つまりvalueが0ならループを抜けてプログラムを終了しなさいと書かれています。
さて、次にif (value == -1)return 1;
ですが、これは単純に「予想外のエラーが起きたらプログラムを強制終了しなさい」と言う意味です。どういう時に起こるんですかね? 僕もピンときません。
これで説明は終わりです。
お疲れさまでした。
「待ってください! value = GetMessageA(&msg, NULL, 0, 0);
とかTranslateMessage(&msg);
とかDispatchMessageA(&msg);
についてまだ聞いてません!」
ごめんなさい、良い説明が思いつきませんでした。まあ、なんやかんややっていると思っておいてください。
コントロールの配置
次に作るのは以下のアプリケーションです。上の方で一度お見せした写真ですね。
ボタンとラジオボタン、プルダウン(コンボボックス)とエディットコントロールを含むアプリケーションですね。
まずは、このボタンやプルダウンをよく見てみてください。よーく見てくださいね。これ、小さなWindowに見えてきませんか?
「見えませんが……?」
見えますよね。
「えっと」
見えますよね!
「はい、すみません」
そう。実はこれらは親ウィンドウの中に配置された子ウィンドウなのです。そして、これらは「CreateWindwA
」を使って作成されます。
以下の例をご覧ください。
HWND button;
button = CreateWindowA(
"BUTTON",
"OK",
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
0, 0, 100, 30,
hWnd, (HMENU)1, ((LPCREATESTRUCT)(lp))->hInstance, NULL);
これはボタンを作成するコードです。先ほどウィンドウを作る時に「固有のID」としていたCreateWindowA
関数の第1引数を、ボタン作成時は「"BUTTON"
」とします。
第2引数はボタンに書かれる文字です。今回はOKと設定しました。
第3引数は、WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON
です。さあ、知らない物が出てきました。WS_CHILD
はこれが子ウィンドウである事を示します。次にWS_VISIBLE
はこのボタンが見える事を意味します。まあ「ふーん」くらいの認識で良いです。BS_PUSHBUTTON
はこれが「押すボタン」であることを意味します。ラジオボタンではないという事ですね。このような引数のパターンは無数にあり、全て覚えるのは無理だと思うので、毎回調べても良いと思います。
第4~7引数は座標です。左上の座標と幅と高さですね。
第8引数は親ウィンドウのウィンドウハンドルです。hWndを指定します。
第9引数はこのボタンのIDを整数で指定します。このIDは後程使います。また、HMENU型にキャストしないといけません。
第10引数はINSTANCEハンドルを指定するのですが……。これは上記の様に書きます。理由を考えてはいけません。
第11引数はNULLです。
同様にラジオボタンやプルダウン(コンボボックス)、エディットボックスもCreateWindowAを使います。
さて、ボタンが押されたり、エディットが編集されたり、コンボボックスが選択されたりした時、ウィンドウプロシージャに「WM_COMMAND」というメッセージが送られてきます。
「え? 全部同じメッセージが送られてくるのですか?」
そうです。
「じゃあ、どうやって区別するのですか?」
それこそが先ほど後回しにしたCreateWindowA
の第9引数、この子ウィンドウのIDです。さあ、難しくなってきますが付いて来てください!
ウィンドウプロシージャにはmsg以外に「wp」と「lp」という変数がありましたよね。このうち「wp」に「どのボタンが押されたか」に関する情報が保存されているのです。
wpの下位ビット(二進数で見た時に、下16桁)にIDが格納されています。つまりwp & 0xffff
です。これを行うマクロ「LOWORD(wp)」をここでは使います。
「つまり、先ほどのCreateWindowA(略, (HMENU)1, 略);
で作られたボタンを押すと、LOWORD(wp)==1
になるという理解で良いですか?」
その通りです。
では、早速ウィンドウプロシージャを加筆します。
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {
static HWND button, radio1, radio2, combo, edittext;
static const char* comboitem[] = { "Apple", "Peach", "Banana" };
switch (msg) {
case WM_CREATE:
button = CreateWindowA(
"BUTTON",
"OK",
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
0, 0, 100, 30,
hWnd, (HMENU)1, ((LPCREATESTRUCT)(lp))->hInstance, NULL);
radio1 = CreateWindowA(
"BUTTON",
"Option1",
WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON,
0, 40, 100, 30,
hWnd, (HMENU)2, ((LPCREATESTRUCT)(lp))->hInstance, NULL);
radio2 = CreateWindowA(
"BUTTON",
"Option2",
WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON,
120, 40, 100, 30,
hWnd, (HMENU)3, ((LPCREATESTRUCT)(lp))->hInstance, NULL);
combo = CreateWindowA(
"COMBOBOX", NULL,
WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST,
0, 80, 100, 300,
hWnd, (HMENU)4, ((LPCREATESTRUCT)(lp))->hInstance, NULL);
for (int i = 0; i < 3; i++) {
SendMessageA(combo, CB_ADDSTRING, 0, (LPARAM)comboitem[i]);
}
edittext = CreateWindowA(
"EDIT",
"This is edit control",
WS_CHILD | WS_VISIBLE | WS_BORDER | ES_LEFT,
0, 120, 300, 30,
hWnd, (HMENU)5, ((LPCREATESTRUCT)(lp))->hInstance, NULL);
return 0;
case WM_COMMAND:
switch (LOWORD(wp))
{
case 1:
onButtonCommand(hWnd, HIWORD(wp));//後述
break;
case 2:
onRadio1Command(hWnd, HIWORD(wp));//後述
break;
case 3:
onRadio2Command(hWnd, HIWORD(wp));//後述
break;
case 4:
onComboCommand(hWnd, combo, HIWORD(wp));//後述
break;
case 5:
onEditCommand(hWnd, edittext, HIWORD(wp));//後述
break;
}
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProcA(hWnd, msg, wp, lp);
}
ウィンドウが作られた時(msg==WM_CREATE)、ボタンなどを作成しています。
注意すべきはこのコードでしょうか?
for (int i = 0; i < 3; i++) {
SendMessageA(combo, CB_ADDSTRING, 0, (LPARAM)comboitem[i]);
}
SendMessageA
は本来ウィンドウのウィンドウプロシージャにメッセージを送信する関数です。これをコンボボックスに対して行う事で、アイテムを追加したりその他の事を行う事が出来ます。
ここでは三つのアイテムApple, Peach, Bananaを追加しました。
次はWM_COMMANDについてです。繰り返し話しますが、LOWORD(wp)はCreateWindowAの第9引数で指定した値となります。
case WM_COMMAND:
switch (LOWORD(wp)){
case 1:
onButtonCommand(hWnd, HIWORD(wp));//後述
break;
……略……
case 5:
onEditCommand(hWnd, edittext, HIWORD(wp));//後述
break;
}
return 0;
ボタンを押すと「onButtonCommand(hWnd, HIWORD(wp));
」が呼び出されますし、エディットボックスを編集すると「onEditCommand(hWnd, edittext, HIWORD(wp));
」が呼び出されます。
「HIWORD(wp)っていうのはなんですか?」
HIWORDはLOWORDの逆、上位WORDを取得します。(wp >> 16)と同様です。
ここに「ボタン・ラジオボタン・コンボボックス・エディットボックス」などに起こった出来事が格納されています。
さあ、この物語はクライマックスです。最後にonButtonCommandなどの中身を記載します。
void onButtonCommand(HWND hWnd, WORD code) {
if (code == BN_CLICKED) {
MessageBoxA(hWnd, "Button is clicked!", "onButtonCommand", MB_OK);
}
}
void onRadio1Command(HWND hWnd, WORD code) {
if (code == BN_CLICKED) {
MessageBoxA(hWnd, "Option1 is clicked!", "onRadio1Command", MB_OK);
}
}
void onRadio2Command(HWND hWnd, WORD code) {
if (code == BN_CLICKED) {
MessageBoxA(hWnd, "Option2 is clicked!", "onRadio2Command", MB_OK);
}
}
void onComboCommand(HWND hWnd, HWND combo, WORD code) {
if (code == CBN_SELCHANGE) {
MessageBoxA(hWnd, "Selection Changed!", "onComboCommand", MB_OK);
int cursel = SendMessageA(combo, CB_GETCURSEL, 0, 0);//Current Selection?
if (cursel == 0) {
MessageBoxA(hWnd, "Apple is selected", "onComboCommand", MB_OK);
}
else if (cursel == 1) {
MessageBoxA(hWnd, "Peach is selected", "onComboCommand", MB_OK);
}
else if (cursel == 2) {
MessageBoxA(hWnd, "Banana is selected", "onComboCommand", MB_OK);
}
}
}
void onEditCommand(HWND hWnd, HWND edit, WORD code) {
if (code == EN_CHANGE) {
char text[256];
GetWindowTextA(edit, text, 255);
if (lstrlenA(text) != 0) {
SetWindowTextA(hWnd, text);
}
}
}
「例えばボタンがクリックされた時、code=HIWORD(wp)がBN_CLICKEDになる、という理解であっていますか?」
そうです。コンボボックスの選択肢が変わった時は「CBN_SELCHANGE」ですね。これはおそらく「Selection Change」の略だと思われます。
コンボボックスでは、SendMessageA(combo, CB_GETCURSEL, 0, 0)
とメッセージを送る事で、現在、何番目のアイテムが選ばれているかを取得できます。CB_GETCURSELは「ComboBox(コンボボックス) Get(取得) Current(現在の) Selection(選択)」の略だと思われます。
テキストボックスの中身が編集された時は、「EN_CHANGE」です。ここで、テキストボックスの中身を取得するにはGetWindowTextA
関数を使用します。
第一引数はテキストボックスのハンドル。第二引数は文字列を格納する為のchar*
で第三引数はchar*
に格納できるサイズです。ここではマックス255文字としていますね。
取得した文字列はlstrlenA
関数を使って文字の長さを取得しています。そして0文字でなかった場合、親ウィンドウのタイトルを変更します。
タイトルの変更にはSetWindowTextAが使用されます。
「正直、難しすぎて、理解が追い付きませんでした……。これ、実用的に使われるのですか?」
正直に言いますとNOです。はっきり言って、これを理解し、使いこなせるようになるよりも、既存のライブラリをインストール、勉強した方がはるかに為になります。
最後のメッセージです。
WindowsAPIは複雑で難解です。ボタンが押されたことを確認するだけでも一苦労します。これではあまりにも使い勝手が悪いから、様々なライブラリが開発されたのです。ライブラリを使う時は『こういうのが背景にあるのか』と思って、ライブラリ作成者に感謝しながら使いましょう。