【C++】Windowsアプリを最初から作ってみる

皆さん、あけましておめでとうございます(もう月末ですが)。

巷は引き続き新型ウイルスが蔓延しており、なかなか余談を許さない状況ではありますが、まぁ私は私にできることを頑張っていきたいと思います。

さて、本日は趣向を変えてWindows APIのお話をしようかなと思います。

とはいえ、私がプログラミングを始めたときには既に.NET FrameworkもVisual Studioも主流の開発環境になっていたこともあり、私自身Windowsの低いレイヤーに詳しいわけではないので誤っている部分も多々あるかと思いますが、ご容赦をお願いします。

Windowsアプリの3つの概念

Win32 APIはなかなかに仕様理解が難解で、全部を把握しようと思うとなかなかに分量も多い(ような気がする)のでここでは最低限Windowsでアプリケーションを起動するために必要な3つの概念をかいつまんでご説明します。

アプリケーション エントリポイント

すべてのアプリケーションにはスタート地点(エントリポイント)が存在します。普通の?CやC++でいえば int main() みたいな関数です。

で、Windowsアプリにおけるエントリポイントはどこかというと、WinMain になります。

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow);

それぞれの引数の解説は省略します。とりあえずあまねくWindowsアプリにはこれが存在します。するはずです。

ウィンドウ プロシージャ

Windowsアプリは、ユーザやプログラムが発生させたイベントを契機として処理を行うイベント駆動型プログラムです。

イベントを処理する専用の関数をイベントハンドラと呼びますが、Windowsアプリにおいて基底レベルのイベント(メッセージ)を処理する関数をウィンドウプロシージャと呼びます。ウィンドウプロシージャはウィンドウクラスを生成する時にコールバックとして設定します。

LRESULT CALLBACK WndProc(HWND hWnd, UINT uMessage, WPARAM wParam, LPARAM lParam);
  • 第1引数:イベントを受け取ったウィンドウハンドル
  • 第2引数:イベント内容(ウィンドウメッセージ)
  • 第3引数:パラメータその1
  • 第4引数:パラメータその2

メッセージ ループ

WinMainは何もしなければ処理が終わり次第即終了します。そこは普通のコンソールアプリと同じです。つまり、何か仕掛けを作らなければ、立ち上がったウィンドウは目的を失って即終了してしまうのです。

でもウィンドウを持ってるアプリがそんな挙動をしては困りますよね。ということで、Windowsアプリはそのメインスレッド(UIスレッド)内でイベント(メッセージ)の無限受け取り待ちをやる必要があります。面倒くさいですがここまで自前で実装するのがWindows APIなのです。

メッセージループのテンプレートが以下です。while句でメッセージを受け取る処理をやります。

while (::GetMessage(&msg, NULL, 0, 0) != 0) {
    ::TranslateMessage(&msg);
    ::DispatchMessage(&msg);
}

典型的にはGetMessageは、アプリが終了するときだけ0以外を返し、それ以外の場合は0を返します。要するにアプリが終了すると共にこのwhileを抜け、WinMainも抜けるという感じになります。

さぁ、Windowsアプリを立ち上げよう

ということで上記に示した通り、まずはエントリポイントとなる WinMain から作り込んでいきます。

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    WNDCLASSEX wcx;
    MSG msg;
    HWMD hWnd;
    LPCWSTR szTitle = TEXT("Sample Application");
    LPCWSTR szWindowClass = TEXT("sampleApp");

    ::ZeroMemory(&wcx, sizeof(WNDCLASSEX));
    // set properties of WNDCLASSEX
    // ...

    if (!::RegisterClassEx(&wcx)) {
        return 0;
    }

    hWnd = ::CreateWindow(
            szWindowClass,
            szTitle,
            WS_OVERLAPPEDWINDOW | WS_VISIBLE,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            NULL,
            NULL,
            hInstance,
            NULL);

    if (!hWnd) {
        return 0;
    }

    // message loop
    while (::GetMessage(&msg, NULL, 0, 0) != 0) {
        ::TranslateMessage(&msg);
        ::DispatchMessage(&msg);
    }

    return (int)msg.wParam;
}

最初にウィンドウクラスを作成します。WNDCLASSEX というやつがそれです。WNDCLASSというもの存在しますが、違いは、謎です(ラーメンズ風)。

「set properties of WNDCLASSEX」というところで構造体の中身をチマチマ突っ込んでいくわけですが、結構分量が多いのでサンプルは末尾に掲載のGitHubをご覧ください。基本的にはウィンドウの初期スタイルとかアイコンとかマウスカーソルとか背景色とかそんなんです。

で、RegisterClassEx でウィンドウクラスを登録。CreateWindow でようやくウィンドウが出てきます。

ウィンドウを表示した後はメッセージループに入ってハンドルを維持します。ちなみに危うくメッセージループを忘れると、一瞬だけウィンドウが表示されるだけのアプリの完成です(笑)

次にウィンドウプロシージャ。

LRESULT CALLBACK WndProc(HWND hWnd, UINT uMessage, WPARAM wParam, LPARAM, lParam) {
    // dispatch with received window message
    switch (uMessage) {
    // windows dispose request
    case WM_DESTROY:
        ::PostQuitMessage(0);
        break;
    default:
        return ::DefWindowProc(hWnd, uMessage, wParam, lParam);
    }
    return 0;
}

ここには第2引数による処理のディスパッチを書きます。とりあえず最低限 WM_DESTROY でウィンドウの破棄だけ行っていればアプリケーションとしての体裁は保てます。なお既定でやってくるウィンドウメッセージだけでもかなりの種類があるので(ウィンドウのサイズが変わったとかフォーカスが当たったとかマウスカーソルが移動したとか)全部のパターンを実装するのは現実的ではなく、面倒くさいと思ったら DefWindowProc しておけばデフォルトの動作をしてくれます。じゃあなんで PostQuitMessage をしてくれなかったんだよっていう。

実行結果

はい。ここまで書いてようやくサラのウィンドウです。でもそういうものなのです。

ソースコード

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です