やってみる

アウトプットすべく己を導くためのブログ。その試行錯誤すらたれ流す。

WndProcをclassでラップしてみた

Windowsプログラミングしていると、WndProc関数に実装が集中してしまう。 アプリの機能ごとに分割して実装したい。 方法を考え、実装してみた。

完成図

入手先

GitHub MEGA

問題

WindowsプログラミングはWndProc関数でWidnowsMessageを受信して処理をする形で実装する。 WidnowsMessageはWindowsの全イベントが発生したときの通知である。

ところが、WidnowsMessageは膨大な数が存在する。 あるサイトの一覧から数えると1003個あった。 メッセージを受信するcase文だけでも1つの関数に入れるような数では済まない。

さらに、同じWidnowsMessage内でも、複数のアプリ機能処理を実装することがある。 複数機能のコードが一緒になってしまい、とても見づらい。

WndProc関数はグローバル関数またはstaticなメンバ関数でなければならない。 1プロセスにつき1つでなければならない。

C言語ならグローバル関数。C++ならstaticなメンバ関数。 グローバル関数の場合、状態を保存するために変数はグローバル変数になる。 あらゆる機能の状態を一律グローバル変数として記述することになる。 すると、規模が大きくなるにつれて以下のような状況に陥る。

  • 名前の重複が起こりやすくなる
  • 全機能の変数や関数が一緒くたになって混沌とする
  • まちがいが起こりやすくなる
  • 不具合が作りこまれやすくなる
  • ソースコードを管理できなくなっていく

C++でclassに分割できたら緩和するはず。 しかし、staticなメンバ関数は、staticな変数しか見れない。 結局、グローバル関数と同じく1プロセスに1つ。 そこで、WndProc処理の実装をclassに分けて、それを登録することでアプリ機能ごとに分割して実装できるようにしたい。 そんなフレームワークを作ることを考えた。

LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg) {
        case WM_CREATE:
            ...
        case WM_KEYDOWN:
            ...
        case WM_LBUTTONDOWN:
            ...
        case WM_PAINT:
            ...
        ...
        default:
            return (DefWindowProc(hWnd, uMsg, wParam, lParam));
    }
    return 0L;
}

対策

思いついたのは以下の2つの方法。

  • 複数のグローバル関数に処理を分割する
  • 純粋仮想関数に処理を分割する

複数のグローバル関数に処理を分割する

WndProc内の実装をアプリ機能ごとに分散する。

ただし、classがないから名前が重複する。

ソースコード

主要部分の抜粋。

Program.c

vector<WNDPROC> wndProcs;

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPreInst, LPSTR lpszCmdLine, int nCmdShow)
{
    ...
    wndProcs.push_back(PaintWndProc);
    wndProcs.push_back(KeyboardWndProc);
    ...
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    BOOL* isReturn = false;
    for (int i = 0; i < wndProcs.size(); i++) {
        LRESULT res = wndProcs[i](hWnd, uMsg, wParam, lParam, &isReturn);
        if (isReturn) { return res; }
    }
    return (DefWindowProc(hWnd, uMsg, wParam, lParam));
}

Keyboard.c

LRESULT CALLBACK KeyboardWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg) {
        case WM_KEYUP:
            ...
        case WM_KEYDOWN:
            ...
    }
    return 0L;
}

Paint.c

LRESULT CALLBACK PaintWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg) {
        case WM_PAINT:
            ...
        ...
        default:
            return (DefWindowProc(hWnd, uMsg, wParam, lParam));
    }
    return 0L;
}

問題点

C言語の問題がそのまま当てはまる。 規模が大きくなるとソースコードが管理できなくなっていくだろう。

純粋仮想関数に処理を分割する

C++版。スコープや名前空間の問題点が解決する。

  • WndProcの処理をclassに実装できる
  • 任意の数だけ、任意のclass名で実装できる
  • グローバル変数でなく、classのメンバ変数を持てる

アプリ機能ごとにclassに分ければ、処理も変数もそのclass内だけに収められる。 グローバル関数やグローバル変数で一緒くたにならずに済む。

ソースコード

主要部分の抜粋。

Program.c

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPreInst, LPSTR lpszCmdLine, int nCmdShow)
{
    InitializeWndProc initializeWndProc;
    Window::partWndProcs.push_back(initializeWndProc);

    PaintWndProc paintWndProc;
    Window::partWndProcs.push_back(PaintWndProc);

    KeyboardWndProc keyboardWndProc;
    Window::partWndProcs.push_back(KeyboardWndProc);

    Window::Create(hInstance);
}

Window.h

class Window
{
public:
    static vector<IPartWndProcs*> partWndProcs;
    static void Create(HINSTANCE hInstance);
private:
    static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
};

Window.cpp

vector<IPartWndProcs*> Window::partWndProcs;

LRESULT CALLBACK Window::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    BOOL* isReturn = false;
    for (int i = 0; i < wndProcs.size(); i++) {
        LRESULT res = wndProcs[i](hWnd, uMsg, wParam, lParam, &isReturn);
        if (isReturn) { return res; }
    }
    return (DefWindowProc(hWnd, uMsg, wParam, lParam));
}

void Window::Create(HINSTANCE hInstance)
{
    // RegisterClassEx(); // Window::WndProcを登録する
    // CreateWindowEx();
    // ShowWindow();
    // UpdateWindow();
}

IPartWndProc.h

class IPartWndProc
{
public:
    virtual LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL* pIsReturn) = 0;
};

InitializeWndProc.h

class InitializeWndProc : public IPartWndProc
{
public:
    LRESULT CALLBACK PartWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL* pIsReturn);
};

InitializeWndProc.cpp

LRESULT CALLBACK InitializeWndProc::PartWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL* pIsReturn)
{
    switch (uMsg) {
        case WM_CREATE:
            ...
        ...
    }
    return (0L);
}

PaintWndProc.h

#include "IPartWndProc.h"

class PaintWndProc : public IPartWndProc
{
public:
    LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL* pIsReturn);
};

PaintWndProc.cpp

LRESULT CALLBACK PaintWndProc::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL* pIsReturn)
{
    switch (uMsg) {
        case WM_PAINT:
            ...
        ...
    }
    return 0L;
}

KeyboardWndProc.h

#include "IPartWndProc.h"

class KeyboardWndProc : public IPartWndProc
{
public:
    LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL* pIsReturn);
};

KeyboardWndProc.cpp

LRESULT CALLBACK KeyboardWndProc::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL* pIsReturn)
{
    switch (uMsg) {
        case WM_KEYUP:
            ...
        case WM_KEYDOWN:
            ...
    }
    return 0L;
}

プロジェクト構造

プロジェクトを3つに分ける。 少し細かく分けすぎかもしれないが、試しにやってみる。

WndProcをclassでラップする部分はアプリの機能ではない。 WindowsプログラミングをC++でラップしたFrameworkである。 静的ライブラリとして独立させたい。

アプリ機能ごとにWndProcを実装した部分がそのアプリの主要部分である。 実装だけに集中できるよう別プロジェクトに分離させる。 これも静的ライブラリ。

メイン関数やメインループ部分はほぼ定型処理。 分割したアプリ機能の取り込みになる。 これも分割させたい。

ディレクトリ構成

  • WndProcClass201607260715
    • WndProcClass
    • Window201607260715
    • WndProcClass201607260715
    • WndProcClass201607260715.sln
    • Debug
    • Release

ソリューション構成

  • Solution
    • WndProcClass201607260715
      • Program.cpp
    • Window201607260715
      • InitializeWndProc.h
      • InitializeWndProc.cpp
      • DrawWndProc.h
      • DrawWndProc.cpp
      • KeyboardWndProc.h
      • KeyboardWndProc.cpp
    • WndProcClass
      • IPartWndProc.h
      • Window.h
      • Window.cpp

プロジェクトの違い

1つのソリューション内に3つのプロジェクトをつくる。

プロジェクト名 プロジェクト種類 内容
WndProcClass201607260715 Windowsアプリケーション メイン関数とメインループ部分
Window201607260715 スタティック ライブラリ WndProc実装部分
WndProcClass スタティック ライブラリ WndProcのclass化Framework部分

.EXEが.LIBを参照する形で実装した。

参照設定

基本、前回と同じ。

ただ、相対パスでもOKらしいので、そうした。 むしろ、こうしないとダウンロードした環境ではリンクエラーになるかもしれない。

一応、プロジェクト依存関係と参照設定の画像を貼っておく。

3プロジェクト

3プロジェクト

ソースファイル一覧

ソースファイル一覧

ビルド順序

ビルド順序

プロジェクト依存関係

プロジェクト依存関係1 プロジェクト依存関係2 プロジェクト依存関係3

参照設定

WndProcClass201607260715

Mainの追加インクルードDir

Mainの追加ライブラリDir

Mainの依存ファイル名

Window201607260715

Windowの追加インクルードDir

ファイルサイズ

気になったのはファイルサイズ。

libファイルがすごく重たい。 これまで全部exeに入れて、せいぜい5~20KBくらいだったのに。

しかも、DebugとReleaseで全然ちがう。 これまでexeだけだったが、Debugのほうが重かった。 でも、libはReleaseのほうが重い。

なぜ?デバッグ情報をなくしたReleaseのほうが軽いと思うのだけど。 気になって調べるもわからず。 とりあえずメモ。DebugとReleaseの数値はKB単位。

ファイル名 Debug Release
WndProcClass201607260715.exe 70 14
Window201607260715.lib 160 2149
WndProcClass.lib 150 762

所感

C++Windowsプログラミング、どちらも少しずつ見えてきた気がする。

これまではほとんどコピペだけな感じだったが、少しずつ自分で考えて書けている感じがする。 C#より面倒だが、C#より楽しい。 C#よりWindowsの機能を生かせるし、実装の工夫もできるから楽しいのかも。

C#でも同じことはできるだろうけど、マーシャリングがつまらない。 他人が作ったフレームワークに合わせて実装するのもつまらない。 マーシャリングなしで全部できるならC#一択かもしれなかったが。

C++C#より、未開の地を自分の手で開拓しているような感じがして楽しい。 やったらやった分だけ手ごたえがありそう。

C++C#とは比べ物にならないほど無駄に面倒で難解。可読性も悪い。罠も多い。 でも、しばらくはC#よりC++をやってみようかな。