WIN32 API —— 最も単純な Windows ウィンドウ カプセル化クラス [理解しやすい]

1 開発言語の選択

1.1 Win32 プログラムを開発するための言語として C または C++ を選択する

MFC を放棄し、純粋な Win32 API を使用して Windows デスクトップ プログラムを開発することに決めた後でも、C++ を使用するかどうかという言語の選択が残ります。C++ は C のスーパーセットとして、C で実現できるすべての機能を実現できます。実際には、その逆も当てはまります。C 自体は、C++ が超える関数の部分を完成させることもできますが、より多くのコード行が必要になる場合があります。私が理解する限りでは、

  • 大規模なプロジェクトの場合は、純粋な C を使用して構造化する方が安全です。
  • 小規模および中規模のプロジェクトの場合は、C++ の方が便利で高速な場合があります。現在、中小規模のプロジェクトに取り組んでいるため、主な開発言語として C++ を使用することにしました。

1.2 C++ 機能セットの選択

C++ を使用することに決めた後は、もう 1 つの重要な選択があります。それは、C++ 機能セットの選択です。C++ は複雑すぎるため、祖先である C のすべての開発モードをサポートするだけでなく、オブジェクトベース開発 (OB)、オブジェクト指向開発 (OO)、およびテンプレート テクノロジもサポートしています。C++ は真に多用途な言語であると言えますが、その結果、C++ の複雑性も高くなります。異なる開発モデルを使用することは、異なるプログラミング言語を使用することと同じです。私に関する限り、C++ テンプレート プログラミングの経験はまったくありません。過去の経験と教訓、そして C++ の習得に基づいて、私は次のことを決定しました。

  • オブジェクトベースとオブジェクト指向の開発モードを使用し、両方で機能を実現できる場合は、オブジェクトベースの開発モードを使用することをお勧めします。OB に傾いた技術的な観点は、Apple の Object-C の開発経験から来ています。
  • 多重継承を避けるようにしてください。この観点は Java および .Net の開発経験から来ています。
  • データ構造とコンテナーは、C++ 標準テンプレート ライブラリ (STL) を使用し、テンプレート プログラミング自体は複雑ですが、STL を使用するのは非常に簡単です。

2 Windows ウィンドウ オブジェクトのカプセル化クラス

Windows デスクトップ プログラムの場合、ウィンドウとメッセージの概念が核となります。最初にカプセル化する必要があるのはウィンドウです。たとえば、MFC はウィンドウ オブジェクトを CWnd クラスでカプセル化します。最初に MFC を放棄した理由は、MFC が複雑すぎて理解しにくいためでした。そのため、基本的なウィンドウ オブジェクトのカプセル化は可能な限り単純でなければなりません。

2.1 カプセル化の原理

第一の原則は「シンプル」です。Win32API を使用して直接実現できる関数は、決して再パッケージ化されません。たとえば、関数 MoveWindow() はウィンドウの移動に使用できますが、同じ関数を持つ MoveWindow() 関数がクラス内に存在するべきではありません。MFC にはこのような反復的な関数が多数あり、実際には hwnd パラメーターを 1 つ減らすだけで済みますが、呼び出しの追加レイヤーが追加されます。HWND ハンドルはどこにでも表示され、決して隠さないようにしたいだけです。この概念は Windows にとって非常に重要であり、開発者はラッパー クラスを使用するときにこれを無視すべきではないからです。

次に、同じ機能を複数の技術で実現できる場合には、「作業効率」よりも「わかりやすさ」が重視され、わかりやすい技術が優先されます。

2.2 ソースコード

ヘッダー ファイル XqWindow.h

[cpp

  1. #プラグマワンス
  2. #include <ベクター>
  3. クラスXqWindow
  4. {
  5. 公共:
  6. XqWindow(HINSTANCE hInst);
  7. ~XqWindow();
  8. プライベート:
  9. HWND hWnd; // セキュリティを確保するため、外部読み取り専用
  10. HINSTANCE hインスタンス;
  11. 公共:
  12. // ウィンドウオブジェクトハンドルを返す
  13. HWND GetHandle();
  14. // メッセージ処理。後続のデフォルト処理が必要な場合は 0 を返す必要があり、メッセージの後続の処理が停止された場合は 1 を返す必要があります。
  15. virtual int HandleMessage(HWND hWnd、UINT メッセージ、WPARAM wParam、LPARAM lParam);
  16. プライベート:
  17. // 元のウィンドウプロシージャ
  18. static LRESULT CALLBACK WndProc(HWND hWnd、UINT メッセージ、WPARAM wParam、LPARAM lParam);
  19. プライベート:
  20. // 登録されたクラスコレクション
  21. 静的 std::vector<void*> registeredClassArray;
  22. 公共:
  23. // ウィンドウを作成する
  24. void Create();
  25. };

実装ファイル XqWindow.cpp

[cpp

  1. #include “stdafx.h”
  2. #include “XqWindow.h”
  3. std::vector<void*> XqWindow::registeredClassArray;
  4. // ウィンドウを作成する
  5. void XqWindow::Create()
  6. {
  7. wchar_t szクラス名[32];
  8. wchar_t szTitle[128];
  9. void* _vPtr = *((void**)this);
  10. ::wsprintf(szClassName, L“%p”, _vPtr);
  11. std::vector<void*>::iterator それ;
  12. for (it = registeredClassArray.begin(); it != registeredClassArray.end(); it++) // オブジェクトのクラスが登録されているかどうかを判定します
  13. {
  14. if ((*it) == _vPtr)
  15. 壊す;
  16. }
  17. if (it == registeredClassArray.end()) // 登録されていない場合は登録します
  18. {
  19. //ウィンドウクラスを登録する
  20. WNDCLASSEX wcex;
  21. wcex.cbSize = sizeof(WNDCLASSEX);
  22. wcex.style = CS_HREDRAW | CS_VREDRAW;
  23. wcex.lpfnWndProc = XqWindow::WndProc;
  24. wcex.cbClsExtra = 0;
  25. wcex.cbWndExtra = 0;
  26. wcex.hInstance = this->hInstance;
  27. wcex.hIcon = NULL;
  28. wcex.hCursor = ::LoadCursor(NULL, IDC_ARROW);
  29. wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
  30. wcex.lpszメニュー名 = NULL;
  31. wcex.lpszクラス名 = szクラス名;
  32. wcex.hIconSm = NULL;
  33. if (0 != ::RegisterClassEx(&wcex)) // 正常に登録されたクラスをリンク リストに追加します
  34. {
  35. registeredClassArray.push_back(_vPtr);
  36. }
  37. }
  38. // ウィンドウを作成する
  39. if (this->hWnd == NULL)
  40. {
  41. ::wsprintf(szTitle, L"ウィンドウ クラス名 (C++ 仮想テーブル ポインター): %p", _vPtr);
  42. HWND hwnd = ::CreateWindow(szClassName,
  43. szタイトル、
  44. WS_OVERLAPPEDWINDOW、
  45. 0、0、800、600、
  46. ヌル、
  47. ヌル、
  48. hインスタンス、
  49. (LPVOID)これ
  50. );
  51. if (hwnd == NULL)
  52. {
  53. this->hWnd = NULL;
  54. wchar_t メッセージ[100];
  55. ::wsprintf(msg, L"CreateWindow() が失敗しました: %ld", ::GetLastError());
  56. ::MessageBox(NULL, msg, L"エラー", MB_OK);
  57. 戻る;
  58. }
  59. }
  60. }
  61. XqWindow::XqWindow(HINSTANCE hInst)
  62. {
  63. this->hWnd = NULL;
  64. this->hInstance = hInst;
  65. }
  66. XqWindow::~XqWindow()
  67. {
  68. if ( this->hWnd!=NULL && ::IsWindow(this->hWnd) ) // C++ オブジェクトが破棄される前にウィンドウ オブジェクトを破棄します
  69. {
  70. ::DestroyWindow(this->hWnd); // システムに hWnd を破棄し、WM_DESTROY を wndproc に送信するように指示します。
  71. }
  72. }
  73. HWND XqWindow::GetHandle()
  74. {
  75. これを返す -> hWnd;
  76. }
  77. // メッセージ処理。後続のデフォルト処理が必要な場合は 0 を返す必要があり、メッセージの後続の処理が停止された場合は 1 を返す必要があります。
  78. int XqWindow::HandleMessage(HWND hWnd、UINT メッセージ、WPARAM wParam、LPARAM lParam)
  79. {
  80. 0を返します。
  81. }
  82. // 元のウィンドウプロシージャ
  83. LRESULT コールバック XqWindow::WndProc(HWND hWnd、UINT メッセージ、WPARAM wParam、LPARAM lParam)
  84. {
  85. XqWindow* pObj = NULL;
  86. if (message == WM_CREATE) // このメッセージを受信すると、ウィンドウ オブジェクト ハンドルを C++ オブジェクト メンバーに割り当て、同時に C++ オブジェクト アドレスをウィンドウ オブジェクト メンバーに割り当てます
  87. {
  88. pObj = (XqWindow*)(((LPCREATESTRUCT)lParam)->lpCreateParams);
  89. pObj->hWnd = hWnd; // ここで HWND を取得します。CreateWindow() はまだ戻っていません。
  90. ::SetWindowLong(hWnd, GWL_USERDATA, (LONG)pObj); // USERDATA を通じて HWND を C++ オブジェクトに関連付けます
  91. }
  92. pObj = ( XqWindow * )::GetWindowLong ( hWnd , GWL_USERDATA ) ;
  93. スイッチ(メッセージ)
  94. {
  95. WM_CREATEの場合:
  96. pObj->HandleMessage(hWnd, メッセージ, wParam, lParam);
  97. 壊す;
  98. WM_DESTROY の場合:
  99. if (pObj != NULL) // この時点で、ウィンドウ オブジェクトは破棄されており、hWnd=NULL を設定することで C++ オブジェクトに通知されます。
  100. {
  101. pObj->hWnd = NULL;
  102. }
  103. 壊す;
  104. デフォルト:
  105. pObj = ( XqWindow * )::GetWindowLong ( hWnd , GWL_USERDATA ) ;
  106. if (pObj != NULL)
  107. {
  108. if (pObj->HandleMessage(hWnd, message, wParam, lParam) == 0) // サブクラスのメッセージ処理仮想関数を呼び出します
  109. {
  110. return DefWindowProc(hWnd, メッセージ, wParam, lParam);
  111. }
  112. }
  113. それ以外
  114. {
  115. return DefWindowProc(hWnd, メッセージ, wParam, lParam);
  116. }
  117. 壊す;
  118. }
  119. 0を返します。
  120. }

2.3 使用例

基本的な使用法は、TestWindow クラスを作成し、XqWindow から継承して、仮想関数 HandleMessage() を再実行することです。すべての業務処理コードは HandleMessage() で呼び出す必要があります。この関数はメンバー関数であるため、これを直接使用して TestWindow クラス オブジェクトのメンバーを参照できます。コードの例は次のとおりです。

テストウィンドウ.h

[cpp

  1. #プラグマワンス
  2. #include “XqWindow.h”
  3. クラス TestWindow :
  4. パブリック XqWindow
  5. {
  6. 公共:
  7. テストウィンドウ(HINSTANCE);
  8. ~TestWindow();
  9. 保護されています:
  10. int HandleMessage(HWND hWnd、UINT メッセージ、WPARAM wParam、LPARAM lParam);
  11. プライベート:
  12. // ビジネスデータ部分
  13. int rectWidth;
  14. int 長方形の高さ;
  15. };

テストウィンドウ.cpp

[cpp

#include "stdafx.h" 
#include "TestWindow.h"

TestWindow::TestWindow(HINSTANCE hInst) :XqWindow(hInst) 
{  
	rectWidth = 300;  rectHeight = 200; 
}

TestWindow::~TestWindow() 
{

} 

int TestWindow::HandleMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) 
{  
	PAINTSTRUCT ps;  
	HDC hdc;  
	switch (message)  
	{  
		case WM_PAINT:   
			 hdc = ::BeginPaint(hWnd, &ps);   
			 ::Rectangle(hdc, 0, 0, this->rectWidth, this->rectHeight);   
			 ::EndPaint(hWnd, &ps);   
			 return 1;  
		default:   
		break;  
	}  
	return 0; 
}

呼び出し部分:

[cpp

  1. pTest = 新しい TestWindow(theApp.m_hInstance);
  2. pTest->Create();
  3. ::ShowWindow(pTest->GetHandle(), SW_SHOW);
  4. ::UpdateWindow(pTest->GetHandle());

実行結果:

2.4 技術的なポイント

この XqWindow クラスは、ウィンドウ オブジェクトの最小のパッケージを作成し、主にメッセージ処理関数と C++ オブジェクトの関連付けを実現します。メモリのレイアウトは次のとおりです。

説明する必要があるいくつかの点:

(1) C++ クラスとウィンドウクラスは 1 対 1 に対応します。VC++ はデフォルトでは RTTI を有効にしないため、コードの互換性と操作効率を考慮すると、RTTI を有効にすることはお勧めできません。RTTI サポートがなければ、実行時に同じクラスのすべてのインスタンスを他のクラスのインスタンスからどのように区別できますか? 毛織物? ここでは C++ 仮想テーブル ポインターを使用します。仮想関数を持つ各クラスは独自の独立した仮想テーブルを持ち、この仮想テーブル ポインターは各インスタンスに格納されます。同じクラスの異なるインスタンスは vtable を共有するため、オブジェクトが属する C++ クラスを区別する機会が得られます。もちろん、この手法は仮想関数を持つクラスでのみ使用でき、仮想関数を持たないクラスのオブジェクトには仮想テーブルは存在しません。この例の場合、XqWindow クラスには HandleMessage 仮想関数があるため、このクラスを継承する他のすべてのサブクラスと孫クラスは独自の仮想テーブルを持ちます。

RegisterClass() の前に、まず現在の C++ オブジェクトが属するクラスの仮想テーブル ポインタが vptrAraay リンク リストに存在するかどうかを判断します。存在しない場合は、ウィンドウ クラスを登録し、仮想テーブル ポインタを vptrArray リンク リストに格納します。存在する場合は、仮想テーブル ポインタに対応するウィンドウ クラスを直接使用します。

オブジェクト仮想テーブル ポインター値を取得する操作は、XqWindow::XqWindow() コンストラクターでは実行できないことに注意してください。これは、この関数が実行されるとき、C++ オブジェクトの仮想テーブル ポインター メンバーが point に設定されていないためです。派生クラスのアドレスの仮想テーブルにコピーします (サブクラスのコンストラクターがまだ呼び出されないため)。したがって、仮想テーブル ポインター値は、オブジェクトの構築が完了した後に取得する必要があります。そのため、XqWindow() コンストラクターで Create() を呼び出すことができません。(呼び出しを簡素化するために、XqWindow() に Create() を入れたことがあります。その結果、すべてのオブジェクトに対して同じ仮想テーブル ポインターが得られました。)

(2) C++ オブジェクトとウィンドウ オブジェクトの関係。C++ オブジェクトが作成された後、Create() を呼び出すことが、それをウィンドウ オブジェクトにバインドする唯一の方法です。古いウィンドウが破棄されるまで、C++ オブジェクトは新しいウィンドウを作成できなくなり、Create() を複数回呼び出しても無駄になります。

C++ オブジェクトの有効期間は、対応するウィンドウの有効期間よりも長くなります。そうでない場合、ウィンドウ プロセスで C++ オブジェクトを使用すると、メモリへの不正なアクセスが発生します。これら 2 種類のオブジェクトの存続順序は次のとおりです。 C++ オブジェクトの誕生 - Create() を呼び出してウィンドウ オブジェクトを生成 - 何らかの理由でウィンドウ オブジェクトの破棄 - C++ オブジェクトの破棄。

C++ オブジェクトがウィンドウ オブジェクトよりも前に破棄されないようにするには、XqWindow クラスのデストラクターで、最初に DestroyWindow() を通じてウィンドウ オブジェクトを破棄します。ウィンドウ オブジェクトが破棄されると、C++ オブジェクトの hWnd も NULL に設定され、C++ オブジェクト ウィンドウの破棄が通知されます。

もっとわかりやすく言えば、C++ オブジェクトとウィンドウ オブジェクトは一夫一婦制であり、夫婦関係は死別のみで離婚はできません。C++ オブジェクトの寿命は長く、ウィンドウ オブジェクトの寿命は短いです。ウィンドウ オブジェクトが終了した後でのみ、C++ オブジェクトは新しいウィンドウを再生成できます。そして、C++ オブジェクトが消滅する前に、ウィンドウ オブジェクトも強制終了されて一緒に埋められる必要があります。

(3) C++ オブジェクトとウィンドウ オブジェクト間の相互参照。C++ オブジェクトはメンバー変数 hWnd を通じてウィンドウ オブジェクトを参照し、ウィンドウ オブジェクトは GWL_USERDATA 追加データ ブロックを通じて C++ オブジェクトを指します。さらに、WM_CRATE メッセージを適時にキャプチャして HandleMessage で処理するために、C++ メンバー hWnd の割り当ては CreateWindow() の後ではなく、元のウィンドウ プロシージャ関数が WM_CREAT メッセージを処理するときに行われます。これは主に CreateWindow() 原則に関連しています。

CreateWindow()

{

HWND hwnd = malloc(..);

ウィンドウオブジェクトを初期化します。

WndProc(hwnd, WM_CRATE, ..); // この時点でウィンドウは作成されています

その他の操作。

hwndを返します。

}

DestroyWindow() の原理も同様です。

ウィンドウを破棄(hwnd)

{

ウィンドウオブジェクトのクリーンアップ。

WndProc(hwnd, WM_DESTROY, ..); // この時点ではウィンドウは表示されません

その他の操作。

無料(ハードウェア);

}

2.5 問題がある

XqWindow クラスは正常に動作しますが、いくつかの問題があります。

(1) Window オブジェクトは USERDATA によって C++ オブジェクトを参照しているため、他のコードが SetWindowLong(hwnd, GWL_USERDATA, xxx) を通じてこのデータ ブロックを変更すると、プログラムがクラッシュします。この被害を防ぐにはさらなる研究が必要です。

(2) C++ オブジェクトの仮想テーブル ポインタを使用しますが、このポインタの特定のメモリ レイアウトに関する明確な標準はありません。将来、VC++ コンパイラが仮想テーブル ポインタの格納場所を変更すると、プログラムに問題が発生します。 。ただし、バイナリ互換性を考慮すると、VC++ がそのような変更を行う可能性は低いです。

おすすめ

転載: blog.csdn.net/onebound_linda/article/details/130922852