C++でウィンドウプロシージャやウィンドウ関連操作APIを(C++の)クラスにラップしてみる

More than 3 years have passed since last update.


C++でウィンドウ関連APIなどをクラスでラップしてみる


はじめに(本稿の要点)

ウィンドウプロシージャやウィンドウ関連のWin32API(いわゆるWindowsSDKのAPI直叩き)をC++のクラス化でラップできないかどうか、という問題をQiitaにて記事化してみたものである。

どうしてもAPI叩きたいが、クラス化(ラップ)もして自作のライブラリっぽくしたいけども、やり方がワカランチン……という感じの時の1つの指針になればいいかな(自分に対しても)。

今更な感じもするしそんなことしなくてもと思うけど、どうしてもそれがやりたい人向け。

この手の情報は限られている上、サンプルまで全部載せてるウェブページがなかなかないようなので、自分の備忘録としてもここにて投稿してみる次第。(おそらく自分もコレをみないと同じものを書けません……)

※なお本稿において、Qiitaにて記事を投稿するのは初めてであるし、C++やWin32APIなどの知識もかなり独学であり、各所のウェブページを拾い漁った挙句自分なりに解釈した部分で書いてることが大半であると思うのでそこら辺は何卒ご容赦頂きたい。

※Win32API関数呼び出しの作法や引数など、色々と妥当性から外れてたりしてたらすみません。


対象環境など

・Windows SDK(Win32API)

・言語=C++ ※『nullptr』などの予約語を使わなければC++11未満でも行けるはず

・VC++ or MinGW GCC(g++) or clang++ ※多分。VC++2015とMinGW/g++で確認済み


C++においてウィンドウプロシージャとするクラスのメソッドやWINAPI関数などをクラスにラップしてまとめる際の前提条件


  1. WNDCLASS(WNDCLASSEX)のlpfnWndProcに渡す自分で定義するウィンドウプロシージャ関数においてのアドレス値はもちろん静的であり、動的に生成されたオブジェクトのメソッドのアドレスは渡せないはずである。

  2. どうにかしてウィンドウプロシージャとなるメソッドの静的なアドレスを渡したい ※これができれば目的は達成できたも同然


LRESULT CALLBACK (HWND, UINT, WPARAM, LPARAM)な関数をC++のクラスに取り込むための問題点と解決法


  • 該当メソッドの宣言に「static」を付ければ、静的になるんじゃない?

  • そしたらメソッドのアドレスを構造体にセットできますよね?

  • だけども、そしたら色々と問題が発生します(クラスを継承した場合とかに対象の関数とか)


思案完了。実際にC++でコーディングして動かしてみる

というわけでクラス定義してやってみるわけなのだが、つまずくポイントとして

・ウィンドウプロシージャをクラスメソッド(静的)にしちゃったら、困るよね?

という辺だろうか。

それを解決するために幾つかの”トリック”で騙されてみよう。

・void SetPointer(HWND)

・static LRESULT CALLBACK BaseWndProc(HWND,UINT,WPARAM,LPARAM)

・virtual LRESULT LocalWndProc(HWND,UINT,WPARAM,LPARAM);

※ここで、いい名前が思いつかないのと、昔に資料をあさった際該当する情報が載っているウェブページでこんな名前だったのでこの名前(SetPointer)を採用している。


似非だとしてもプログラマの端くれならば、拳(サンプルソースコードとコメント)で語ってやるっ!(ああすみませんすみません・・・このノリ)


Window.hpp

#pragma once

#include <windows.h>
#include <windowsx.h>
#include <tchar.h>
namespace wcl {
const int MAX_LOADSTRING = 100; //ウィンドウクラスネームとアプリケーションタイトルの最大文字数です

class Window {
public:
Window();
virtual ~Window();
public:
bool Create(HINSTANCE hInstance, TCHAR class_name[MAX_LOADSTRING], TCHAR app_name[MAX_LOADSTRING], int width, int height);
bool IsCreated(); //ウィンドウが生成(生存)されているか確認
HWND GetHWND() { return hWnd; }
bool UpdateWindow();
bool ShowWindow(int nCmdShow);
void SetTitle(TCHAR title[MAX_LOADSTRING]);
WPARAM GetMessageLoop(UINT wMsgFilterMin, UINT wMsgFilterMax); //汎用かつ定番のメッセージループ
WPARAM PeekMessageLoop(UINT wMsgFilterMin, UINT wMsgFilterMax, UINT wRemoveMSG); //特定用途メッセージループ

public: //カスタマイズするメソッド
virtual void InnerPeekMessage();//PeekMessageLoopで呼ばれる
virtual LRESULT LocalWndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp); //デフォルトではDefWndProcを呼ぶかWM_DESTORYを処理するぐらい

private:
static LRESULT CALLBACK BaseWndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp); //ラップするためのトリックな関数
void SetPointer(HWND hWnd); //windowオブジェクト(thisポインタ)とウィンドウハンドルを関連付けるためのトリックな関数

protected:
volatile HWND hWnd = nullptr;
};
};



Window.cpp

#include "Window.hpp"

namespace wcl {
//コンストラクタ
Window::Window() {

}
//デストラクタ
Window::~Window() {

}

bool Window::Create(HINSTANCE hInstance, TCHAR class_name[MAX_LOADSTRING], TCHAR app_name[MAX_LOADSTRING], int width, int height) {
WNDCLASSEX wcex;
ZeroMemory(&wcex,sizeof(wcex));

wcex.cbSize = sizeof(WNDCLASSEX); //この構造体自体のサイズなので。
wcex.cbClsExtra = 0; //拡張用?とりあえず使わないので0
wcex.cbWndExtra = 0; //拡張用?とりあえず使わないので0
wcex.lpfnWndProc = Window::BaseWndProc;//このウィンドウクラスのプロシージャを指定。このソースコードではこうする
wcex.hInstance = hInstance;
wcex.lpszClassName = class_name;
wcex.lpszMenuName = nullptr; //メニューの名前。使わないならとりあえずNULLでOK
wcex.style = CS_VREDRAW | CS_HREDRAW;
wcex.hCursor = ::LoadCursor(hInstance, IDC_ARROW); //カーソルアイコン
wcex.hIcon = ::LoadIcon(hInstance, IDI_APPLICATION); //プログラムアイコン
wcex.hIconSm = ::LoadIcon(hInstance, IDI_APPLICATION); //プログラムアイコン(小)::タイトルバーに使われるやつ?
wcex.hbrBackground = (HBRUSH)::GetStockBrush(BLACK_BRUSH); //クライアント領域の塗りつぶし色(ブラシ)
//↑とかは特に、メソッドの引数を受け取ってカスタマイズできるようにするのが望ましいかな
//ATOMへWNDCLASSEX登録
if (!::RegisterClassEx(&wcex)) {
return false;
}
//ウィンドウ生成
HWND ret = ::CreateWindow(
class_name,
app_name,
WS_OVERLAPPEDWINDOW, //ウィンドウスタイル。とりあえずデフォルトな感じで
CW_USEDEFAULT, CW_USEDEFAULT, //初期位置。適当にやってくれる。
width, height, //ウィンドウサイズ
nullptr, //親ウィンドウのハンドル。特にないんで今回はNULL
nullptr, //メニューハンドル。特にないので今回はNULL
hInstance,
this //トリックの肝。CreateParameterに設定
);
//ウィンドウ生成に失敗?
if (ret == nullptr) {
return false;
}
//成功したのでtrueを返して抜ける
return true;
}

bool Window::IsCreated()
{
if (this->hWnd == nullptr) {
return false;
}
else {
return true;
}
}

bool Window::UpdateWindow() {
if (this->hWnd == nullptr) {
return false;
}
::UpdateWindow(this->hWnd);
return true;
}

bool Window::ShowWindow(int nCmdShow) {
if (this->hWnd == nullptr) {
return false;
}
::ShowWindow(this->hWnd, nCmdShow);
return true;
}

void Window::SetTitle(TCHAR title[MAX_LOADSTRING]) {
::SetWindowText(hWnd, title);
}

WPARAM Window::GetMessageLoop(UINT wMsgFilterMin, UINT wMsgFilterMax) {
MSG msg;
while (::GetMessage(&msg, this->hWnd, wMsgFilterMin, wMsgFilterMax) > 0) { //「-1」が返ってくるかもしれないのでこうする
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
this->hWnd = nullptr; //ココに記述しておいてみる
return msg.wParam;
}
WPARAM Window::PeekMessageLoop(UINT wMsgFilterMin, UINT wMsgFilterMax, UINT wRemoveMSG) {
MSG msg;
do {
//既定の処理はプロシージャなどに任せて……
if (::PeekMessage(&msg, this->hWnd, wMsgFilterMin, wMsgFilterMax, PM_REMOVE)) {
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
//そうでない処理は自前でする
else {
//自前の処理であるInnerPeekMessage()を呼び出し
this->InnerPeekMessage();
}
} while (msg.message != WM_QUIT);//イベントドリブン処理ループを抜けるまで実行
this->hWnd = nullptr; //ココに記述しておいてみる
return msg.wParam;
}

void Window::InnerPeekMessage() {
//TODO:InnerPeekMessage()
}

LRESULT Window::LocalWndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) {
case(WM_DESTROY) :
::PostQuitMessage(0);
break;
default:
return ::DefWindowProc(hWnd, msg, wp, lp);
}
return 0;
}

LRESULT CALLBACK Window::BaseWndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {
//Window::SetPointerでセットしたthisポインタを取得
Window* window = (Window*)(::GetWindowLongPtr(hWnd, GWLP_USERDATA));
//取得に失敗してる場合
if (window == nullptr) {
//おそらくWM_CREATEの最中なので
if (msg == WM_CREATE) {
//CreateWindowのパラメータから取得する
window = (Window*)(((LPCREATESTRUCT)lp)->lpCreateParams);
}
//↑の処理で取得できてるはずなんだけど、一応チェックしてから
if (window != nullptr) {
//windowオブジェクトとウィンドウハンドルを関連付ける
window->SetPointer(hWnd);
}
}
//無事取得できててる
if (window != nullptr) {
//そのオブジェクトによるウィンドウプロシージャ実装を使うというか振り分ける
return window->LocalWndProc(hWnd, msg, wp, lp);
}
//よくわからんけど、それでも取得できたりしてない場合(以下のようにするよりも例外作って投げるのがよさそうだがw
else {
return ::DefWindowProc(hWnd, msg, wp, lp);
}
}

void Window::SetPointer(HWND hWnd) {
::SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
this->hWnd = hWnd;
}
};


使ってみる場合のサンプルソースコード。


Main.cpp

#include <windows.h>

#include <windowsx.h>
#include <tchar.h>
#include "Window.hpp"
/*
class MyWindow : public wcl::Window{
public:
virtual LRESULT LocalWndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp){
swtich(msg){
case(WM_PAINT):{
//TODO:描画処理
break;
}
case(WM_DESTROY) :
::PostQuitMessage(0);
break;
default:
return ::DefWindowProc(hWnd, msg, wp, lp);
}
return 0;
}
};
//*/

//いわゆるWinMain関数
int WINAPI _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdline, int nCmdShow) {
UNREFERENCED_PARAMETER(hPrevInstance); //おまじない?
UNREFERENCED_PARAMETER(lpCmdline);
//さーて試してみるか
wcl::Window wnd;
// MyWindow wnd;
TCHAR class_name[wcl::MAX_LOADSTRING] = _T("__WIN32_API_WRAPPPER_CLASS_LIBRARY__");//なんでもいいけど、他のプログラムと被りそうにない文字列
TCHAR app_name[wcl::MAX_LOADSTRING] = _T("hogefoobar"); //何でもいい
if (!wnd.Create(hInstance, class_name, app_name, 800, 600)) {
MessageBox(nullptr, _T("Call to Window::Create() is Failed!!"), _T("ErrorInfo"), MB_OK | MB_ICONERROR);
return 1;
}
if (!wnd.IsCreated()) {
MessageBox(nullptr, _T("Window::IsCreated() is False!!"), _T("ErrorInfo"), MB_OK | MB_ICONERROR);
return 1;
}
//ウィンドウ可視化
wnd.ShowWindow(nCmdShow);
wnd.UpdateWindow();
//ウィンドウタイトルの変更
wnd.SetTitle(_T("WIN32API-WrapperClassLibrary")); //せっかく書いたので使ってみる。生成時に設定した文字列ではなくこちらがタイトルバーに表示されればおk この時点で呼んでいいのかわからないけども・・・。
//イベントドリブン開始
WPARAM wp = wnd.GetMessageLoop(0, 0);
//イベントドリブン終了。WinMain関数も抜ける。
return wp;
}



最後に

あとは、これをベースに改善実装したいならWindowsクラスに色々書き足してAPIなどをラップしていって、それっぽくすればいいように思います。

ある程度仕様と実装ができあがったら、それを継承してカスタマイズして再利用ですね。

JavaっぽくListenerクラス(インターフェイス)だとか、後は大きく仕様と実装を変更してOnPaint()とか、OnClosing()、OnCreate()だとか書いてLocalWndProcは変更しないというのも面白いと思います。

一応、目的は達成したのだが1つ実装に問題があって、それはLocalWndProcとメンバーのhWndの扱いである。

LocalWndProcはオーバーライドする前提でコーディングしたのだが、メンバーのhWndをnullptrにするタイミングがよくわからなくて、LocalWndProc内に書いてしまっているので、本当はBaseWndProc内で書いておきたいのとそもそも少し別の使い方をかければよかったのだが。

ちょっと疲れてしまったので当記事としてはこれで締めることにする・・・なんとかしたいけど。

※今回のようなコーディングにおいて良い解決法がある場合、コメントにて教えてくださると幸いです。よく考えて机上トレースすればすぐにわかりそうですが。


※暫定ですが、とりあえず修正しました=>修正箇所:LocalWndProc,GetMessageLoop,PeekMessageLoop

あと、多分スレッドセーフとかいうやつじゃないと思うので、もしマルチスレッドで利用する際はそれなりの修正が必要だと思われます。