在CodeBolcks+Windows API下的C++面向对象的编程教程——采用面向对象的方法构建一个Windows GUI项目的主框架
0.前言
我想通过编写一个完整的游戏程序方式引导读者体验程序设计的全过程。我将采用多种方式编写具有相同效果的应用程序,并通过不同方式形成的代码和实现方法的对比来理解程序开发更深层的知识。
了解我编写教程的思路,请参阅体现我最初想法的那篇文章中的“1.编程计划”和“2.已经编写完成的文章(目录)”:
学习编程从游戏开始——编程计划(目录) - lexyao - 博客园
这是一篇专题文章,这篇文章是用来讲解下面这篇文章用到的知识的,我在这篇文章中讲解程序使用的例子就是在下面这篇文章中创建的oTetris项目:
在CodeBolcks+Windows API下的C++面向对象的编程教程——用面向对象的方法改写用向导创建一个Windows GUI项目(oTetris) - lexyao - 博客园
在这篇文章里,我主要讲述以下几个方面的内容:
- 用向导新建一个Windows GUI项目,项目名称为oTetris,项目的主代码文件main.cpp改名为oTetrisMainRaw.cpp
- 复制Microsoft面向对象的方法的构建Windows GUI项目主界面的示例的代码保存在oTetrisMainMic.cpp文件中
- 修改oTetrisMainMic.cpp中的代码生成能够在CodeBlocks的中使用的代码保存在oTetrisMainOOP.cpp文件中
- 优化oTetrisMainOOP.cpp文件中的代码生成最终的面向对象的Windows GUI项目主框架的代码保存在oTetrisMainOur.cpp文件中
- 修改oTetrisMainOur.cpp文件中的代码生成与oTetrisMainRaw.cpp有一致的视觉效果的代码保存在oTetrisMain.cpp文件中
- 结束语
1.用向导新建一个Windows GUI项目,项目名称为oTetris,项目的主代码文件main.cpp改名为oTetrisMainRaw.cpp
在CodeBlocks中点击主菜单[File->New->Files...]用向导新建一个Win32 GUI项目,项目名称为oTetris,项目只有一个代码文件main.cpp,将其改名为oTetrisMainRaw.cpp。
编译运行oTetris项目,出现主窗口界面的截图如下。这篇文章的最终目的是采用面向对象的的编程方法实现同样的效果,实现与这个相同的画面。
以下是向导为我们生成的代码,关于这些代码的详细解释,请点击下面链接查看:
在CodeBolcks+Windows API下的C++编程教程——使用向导新建的Win32 GUI程序代码详解 - lexyao - 博客园
oTetrisMainRaw.cpp文件的完整代码如下:
#if defined(UNICODE) && !defined(_UNICODE) #define _UNICODE #elif defined(_UNICODE) && !defined(UNICODE) #define UNICODE #endif #include <tchar.h> #include <windows.h> /* Declare Windows procedure */ LRESULT CALLBACK WindowProcedure (HWND, UINT, WPARAM, LPARAM); /* Make the class name into a global variable */ TCHAR szClassName[ ] = _T("CodeBlocksWindowsApp"); int WINAPI WinMain (HINSTANCE hThisInstance, HINSTANCE hPrevInstance, LPSTR lpszArgument, int nCmdShow) { HWND hwnd; /* This is the handle for our window */ MSG messages; /* Here messages to the application are saved */ WNDCLASSEX wincl; /* Data structure for the windowclass */ /* The Window structure */ wincl.hInstance = hThisInstance; wincl.lpszClassName = szClassName; wincl.lpfnWndProc = WindowProcedure; /* This function is called by windows */ wincl.style = CS_DBLCLKS; /* Catch double-clicks */ wincl.cbSize = sizeof (WNDCLASSEX); /* Use default icon and mouse-pointer */ wincl.hIcon = LoadIcon (NULL, IDI_APPLICATION); wincl.hIconSm = LoadIcon (NULL, IDI_APPLICATION); wincl.hCursor = LoadCursor (NULL, IDC_ARROW); wincl.lpszMenuName = NULL; /* No menu */ wincl.cbClsExtra = 0; /* No extra bytes after the window class */ wincl.cbWndExtra = 0; /* structure or the window instance */ /* Use Windows's default colour as the background of the window */ wincl.hbrBackground = (HBRUSH) COLOR_BACKGROUND; /* Register the window class, and if it fails quit the program */ if (!RegisterClassEx (&wincl)) return 0; /* The class is registered, let's create the program*/ hwnd = CreateWindowEx ( 0, /* Extended possibilites for variation */ szClassName, /* Classname */ _T("Code::Blocks Template Windows App"), /* Title Text */ WS_OVERLAPPEDWINDOW, /* default window */ CW_USEDEFAULT, /* Windows decides the position */ CW_USEDEFAULT, /* where the window ends up on the screen */ 544, /* The programs width */ 375, /* and height in pixels */ HWND_DESKTOP, /* The window is a child-window to desktop */ NULL, /* No menu */ hThisInstance, /* Program Instance handler */ NULL /* No Window Creation data */ ); /* Make the window visible on the screen */ ShowWindow (hwnd, nCmdShow); /* Run the message loop. It will run until GetMessage() returns 0 */ while (GetMessage (&messages, NULL, 0, 0)) { /* Translate virtual-key messages into character messages */ TranslateMessage(&messages); /* Send message to WindowProcedure */ DispatchMessage(&messages); } /* The program return-value is 0 - The value that PostQuitMessage() gave */ return messages.wParam; } /* This function is called by the Windows function DispatchMessage() */ LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { switch (message) /* handle the messages */ { case WM_DESTROY: PostQuitMessage (0); /* send a WM_QUIT to the message queue */ break; default: /* for messages that we don't deal with */ return DefWindowProc (hwnd, message, wParam, lParam); } return 0; }
2.复制Microsoft面向对象的方法的构建Windows GUI项目主界面的示例的代码保存在oTetrisMainMic.cpp文件中
自己编写一个面向对象的构建Win32 GUI项目的框架有点难,好在网上不乏这类的例子,我们借用一个就行了。
Microsoft有一个名字叫做BaseWindow的示例,就是这方面的一个例子。我先把它的源代码复制过来再做分析、修改。
BaseWindow 示例 - Win32 apps | Microsoft Learn:BaseWindow 示例的首页,由此可以找到相关的页面
Microsoft的资料库中还有两个相关的文件:
Windows Hello World 示例 - Win32 apps | Microsoft Learn:Microsoft的Win32 GUI编程的第一个示例,有着与oTetrisMainRaw.cpp相同的代码。由此可见,C++编程开始的第一步都是一样的。
管理应用程序状态 - Win32 apps | Microsoft Learn:讲解如何将上面的Hello World示例的面向过程的代码改编成BaseWindow 示例的面向对象的代码。
复制BaseWindow 示例的全部代码,保存在oTetrisMainMic.cpp文件中。以下是BaseWindow 示例的全部代码:
#include <windows.h> template <class DERIVED_TYPE> class BaseWindow { public: static LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { DERIVED_TYPE *pThis = NULL; if (uMsg == WM_NCCREATE) { CREATESTRUCT* pCreate = (CREATESTRUCT*)lParam; pThis = (DERIVED_TYPE*)pCreate->lpCreateParams; SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pThis); pThis->m_hwnd = hwnd; } else { pThis = (DERIVED_TYPE*)GetWindowLongPtr(hwnd, GWLP_USERDATA); } if (pThis) { return pThis->HandleMessage(uMsg, wParam, lParam); } else { return DefWindowProc(hwnd, uMsg, wParam, lParam); } } BaseWindow() : m_hwnd(NULL) { } BOOL Create( PCWSTR lpWindowName, DWORD dwStyle, DWORD dwExStyle = 0, int x = CW_USEDEFAULT, int y = CW_USEDEFAULT, int nWidth = CW_USEDEFAULT, int nHeight = CW_USEDEFAULT, HWND hWndParent = 0, HMENU hMenu = 0 ) { WNDCLASS wc = {0}; wc.lpfnWndProc = DERIVED_TYPE::WindowProc; wc.hInstance = GetModuleHandle(NULL); wc.lpszClassName = ClassName(); RegisterClass(&wc); m_hwnd = CreateWindowEx( dwExStyle, ClassName(), lpWindowName, dwStyle, x, y, nWidth, nHeight, hWndParent, hMenu, GetModuleHandle(NULL), this ); return (m_hwnd ? TRUE : FALSE); } HWND Window() const { return m_hwnd; } protected: virtual PCWSTR ClassName() const = 0; virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) = 0; HWND m_hwnd; }; class MainWindow : public BaseWindow<MainWindow> { public: PCWSTR ClassName() const { return L"Sample Window Class"; } LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam); }; int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow) { MainWindow win; if (!win.Create(L"Learn to Program Windows", WS_OVERLAPPEDWINDOW)) { return 0; } ShowWindow(win.Window(), nCmdShow); // Run the message loop. MSG msg = { }; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return 0; } LRESULT MainWindow::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_DESTROY: PostQuitMessage(0); return 0; case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(m_hwnd, &ps); FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1)); EndPaint(m_hwnd, &ps); } return 0; default: return DefWindowProc(m_hwnd, uMsg, wParam, lParam); } return TRUE; }
3.修改oTetrisMainMic.cpp中的代码生成能够在CodeBlocks的中使用的代码保存在oTetrisMainOOP.cpp文件中
虽然都是C++程序开发环境,但是Microsoft的Visual Studio下的C++跟CodeBlocks的C++是不一样的,前者是Microsoft的标准,后者是国际标准,二者不完全兼容。
有些符合Microsoft的标准的C++代码,在CodeBlocks中不支持。这种现象在Microsoft的例子里很常见,以后使用的时候要多多注意。也就是说,Microsoft的C++不是国际标准的C++。
这个问题由来已久,Microsoft试图以自己强大的实力和市场占有率想逼迫国际标准按着它的意愿来,但是几十年的斗争之后,国际标准没有屈服,出现了二者共存的局面。Microsoft虽然在vc++ 2005开始宣布全面支持国际标准,但总是喜欢在自己的范围内添加一些国际标准不支持的东西。
下面我就将从Microsoft下载的oTetrisMainMic.cpp中的BaseWindow示例代码改编成CodeBlocks中可以使用的实现同样功能的代码,改编后的代码保存在oTetrisMainOOP.cpp文件中。
3.1 将oTetrisMainMic.cpp中的代码改编后保存在oTetrisMainOOP.cpp文件中
完整的oTetrisMainOOP.cpp文件文件的内容由以下几部分组成:
- 从复制oTetrisMainRaw.cpp文件最开始几行的编译指令和包含文件的代码
- BaseWindow类的定义的代码
- BaseWindow类的实现的代码
- MainWindow 类的定义的代码
- MainWindow 类的实现的代码
- 程序入口函数wWinMain 的代码
首先,我们复制oTetrisMainRaw.cpp文件最开始几行的编译指令和包含文件的代码作为oTetrisMainOOP.cpp文件的最初的内容,这是CodeBlocks中必须使用的。
#if defined(UNICODE) && !defined(_UNICODE) #define _UNICODE #elif defined(_UNICODE) && !defined(UNICODE) #define UNICODE #endif #include <tchar.h> #include <windows.h>
在oTetrisMainMic.cpp包含了一个名为BaseWindow的基类和它的一个子类MainWindow。
BaseWindow类的定义和实现是混合在一起的,为了便于阅读,我们在改编到oTetrisMainOOP.cpp文件中时将BaseWindow类的定义和实现分开。
以下是BaseWindow类的定义的代码:
template <class DERIVED_TYPE> class BaseWindow { public: static LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); BaseWindow() : m_hwnd(NULL) { } BOOL Create( PCWSTR lpWindowName, DWORD dwStyle, DWORD dwExStyle = 0, int x = CW_USEDEFAULT, int y = CW_USEDEFAULT, int nWidth = CW_USEDEFAULT, int nHeight = CW_USEDEFAULT, HWND hWndParent = 0, HMENU hMenu = 0 ); HWND Window() const { return m_hwnd; } protected: virtual PCWSTR ClassName() const = 0; virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) = 0; HWND m_hwnd; };
以下是BaseWindow类的实现的代码,包括两个函数:
template <class DERIVED_TYPE> LRESULT CALLBACK BaseWindow<DERIVED_TYPE>::WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { DERIVED_TYPE *pThis = NULL; if (uMsg == WM_NCCREATE) { CREATESTRUCT* pCreate = (CREATESTRUCT*)lParam; pThis = (DERIVED_TYPE*)pCreate->lpCreateParams; SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pThis); pThis->m_hwnd = hwnd; } else { pThis = (DERIVED_TYPE*)GetWindowLongPtr(hwnd, GWLP_USERDATA); } if (pThis) { return pThis->HandleMessage(uMsg, wParam, lParam); } else { return DefWindowProc(hwnd, uMsg, wParam, lParam); } } template <class DERIVED_TYPE> BOOL BaseWindow<DERIVED_TYPE>::Create( PCWSTR lpWindowName, DWORD dwStyle, DWORD dwExStyle, int x , int y, int nWidth , int nHeight, HWND hWndParent, HMENU hMenu ) { WNDCLASS wc = {0}; wc.lpfnWndProc = DERIVED_TYPE::WindowProc; wc.hInstance = GetModuleHandle(NULL); wc.lpszClassName = ClassName(); RegisterClass(&wc); m_hwnd = CreateWindowEx( dwExStyle, ClassName(), lpWindowName, dwStyle, x, y, nWidth, nHeight, hWndParent, hMenu, GetModuleHandle(NULL), this ); return (m_hwnd ? TRUE : FALSE); }
MainWindow 类以BaseWindow为基类构建,实现了BaseWindow中定义的两个纯虚函数。
以下是MainWindow 类的定义的代码:
class MainWindow : public BaseWindow<MainWindow> { public: PCWSTR ClassName() const { return L"Sample Window Class"; } LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam); };
以下是MainWindow 类的实现的代码:
LRESULT MainWindow::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_DESTROY: PostQuitMessage(0); return 0; case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(m_hwnd, &ps); FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1)); EndPaint(m_hwnd, &ps); } return 0; default: return DefWindowProc(m_hwnd, uMsg, wParam, lParam); } return TRUE; }
WinMain 函数是用于应用程序入口点的传统名称,每个应用程序是运行都是从WinMain 函数开始的,而Microsoft的应用程序是从wWinMain开始的。按着Microsoft资料中的说法wWinMain可以接受Unicode编码的命令行参数,而WinMain 却不能。
下面是程序入口函数wWinMain 的代码:
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow) { MainWindow win; if (!win.Create(L"Learn to Program Windows", WS_OVERLAPPEDWINDOW)) { return 0; } ShowWindow(win.Window(), nCmdShow); // Run the message loop. MSG msg = { }; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return 0; }
3.2 将oTetrisMainOOP.cpp文件设置成oTetris项目的主文件
现在oTetris项目中有了三个cpp文件:oTetrisMainRawcpp、oTetrisMainMic.cpp、oTetrisMainOOP.cpp。
由于以下原因,这三个文件我们只能保留一个作为oTetris项目的主代码文件:
- 每个cpp文件中都有一个作为程序入口的WinMain或wWinMain函数。这时如果编译运行,会因为定义了多个入口函数而导致编译失败
- 由于这些文件中有Microsoft标准的代码,CodeBlocks不支持,这样就会出现编译错误
首先将oTetrisMainOOP.cpp设置成oTetris项目的主文件。操作方法如下:
- 在CodeBlocks的文件列表中鼠标右击oTetrisMainOOP.cpp文件,选择[Properties...]打开属性设置窗口
- 在属性设置窗口中选择[Build]页面,勾选下图中的四个选项,然后点击[OK]按钮完成设置
用上述方法设置oTetrisMainRawcpp、oTetrisMainMic.cpp文件的属性,四个选项都不要选中。
通过上述设置,虽然三个文件都在项目中,但在编译运行的时候编译器将会只编译oTetrisMainOOP.cpp而忽略oTetrisMainRawcpp、oTetrisMainMic.cpp,这样后边的两个文件中即使有错误也不会影响项目的编译。
3.3 将oTetrisMainOOP.cpp文件中的代码修改成CodeBlocks中可以使用的实现同样功能的代码
编译运行oTetris项目,
编译运行oTetris项目,你会看到第一个错误:
查找原因,这两处错误都与ClassName()有关,查找ClassName()的代码:
class MainWindow : public BaseWindow<MainWindow> { public: PCWSTR ClassName() const { return L"Sample Window Class"; } LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam); };
其中定义字符串的样式跟我以前使用_T()的格式不一样:表示Unicode格式,而_T()由编译指令确定。将L“”格式改为_T("")格式,编译运行,发现还是有问题:
这是字符串格式转换的问题,由于编码格式引起的。
ClassName()定义为PCWSTR ,而编译显示_T()的字符串是char*,也就是PCSTR。这跟我们前面的文章中提到过的支持Unicode编码的问题是一样的。需要加入支持的Unicode编码编译指令:
#if !defined(UNICODE) #define UNICODE #endif
重新编译运行oTetris项目,你会看到编码的问题解决了。不过又出现了另一个问题:
没有定义WinMain。应该还记得,我们复制的Microsoft示例中使用的是wWinMain吧?
看来wWinMain是Microsoft的标准,CodeBlocks不支持。其实这个不是Microsoft标准的问题,而是Unicode支持的问题。还是先暂时使用WinMain吧。
现在我们就把wWinMain改成WinMain就行了。由于二者的参数不一样,我们复制原来的WinMain函数头替换wWinMain的函数头。以下是两者的代码,可以看出差别:
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow)
int WINAPI WinMain (HINSTANCE hThisInstance, HINSTANCE hPrevInstance, LPSTR lpszArgument, int nCmdShow)
主要是参数PWSTR 和LPSTR 的差别。
重新编译运行oTetris项目,你会看到一个空的主窗口界面,与aTetris的主窗口有同样的元素。
不同点有三处:
- 标题的文字不同:这是字符串内容不同,不算是问题
- 界面颜色不同:oTetris构建窗口数据时没有给wc中设置颜色,窗口使用了操作系统默认的颜色
- 界面尺寸大了:oTetris项目使用了默认的窗口宽度和高度,而aTetris指定了具体的数字
由此看出,这三点都不是原则性的问题,但从界面元素来看,二者的效果是一样的。
另外还有一个缺陷:当鼠标从窗口外移动到窗口客户区时,鼠标指针不能恢复正常显示,这说明存在没有处理的消息。比如:当鼠标从左右边框进入客户区时,由于在边框上鼠标指针变成了调整窗口大小的双向箭头指针,进入客户区后还会保持这种指针样式。
4.优化oTetrisMainOOP.cpp文件中的代码生成最终的面向对象的Windows GUI项目主框架的代码保存在oTetrisMainOur.cpp文件中
oTetris改为面向对象的编码复制了Microsoft示例程序中的代码,虽然功能齐全,但从封装和命名的角度来看,还是有些缺陷的。
下面对复制的代码进行修改。修改的原则包括封装的合理性和代码的可读性两个方面。
在修改之前,先复制oTetrisMainOOP.cpp文件中的代码到oTetrisMainOur.cpp中,将oTetrisMainOur.cpp设置为oTetris项目的主代码文件。后面的修改将在oTetrisMainOur.cpp文件中进行,编译运行也是使用这个文件。
4.1 对基类BaseWindow中窗口句柄的优化
窗口句柄的函数:
HWND Window() const { return m_hwnd; }
作为一个HWND函数在命名中没有体现出句柄的特性,所以修改如下,修改后从名字上看更直观了。
HWND WindowHwnd() const { return m_hwnd; }
窗口句柄变量:
protected: HWND m_hwnd;
m_hwnd被定义在protected中,子类的代码中有多处引用m_hwnd。作为友元这样引用是合法的,但作为封装来说就不合理了。已经定义了函数WindowHwnd而弃之不用反而使用变量本身,这是违反封装的原则的。对此修改如下:
- 将m_hwnd定义改到private 下(注:如果将来在子类中有设置这个变量的可能,则应保持在protected中,或者增加一个设置变量的函数)
- 将子类的代码中使用m_hwnd的地方改为WindowHwnd()
LRESULT MainWindow::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_DESTROY: PostQuitMessage(0); return 0; case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(WindowHwnd(), &ps); FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1)); EndPaint(WindowHwnd(), &ps); } return 0; default: return DefWindowProc(WindowHwnd(), uMsg, wParam, lParam); } return TRUE; }
4.2 MainWindow::HandleMessage的修改
在上面这段代码中,还有一个缺陷,也是违背代码的结构性原则的。那就是return的使用问题。
早期的代码中都是这样的,有两个指令收到的指责最多:return和goto。现在goto已经被抛弃了,而一个函数中有多个return的现象还存在。
作为结构化编程的理念,一个函数中最好只有一个出口,特别是要避免从结构语句(循环或多项选择)中直接退出函数。
从出口的理念来说,一般是建议使用break代替中间的return,只保留最后一个return。
从上面这段代码来看,前面有两个return 0。按着这样的结构,将来添加更多的事件之后每个事件的代码结束后都要添加一个return 0。
而最后一个return TRUE是多余的,永远不会被执行。
修改后的代码如下:
LRESULT MainWindow::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_DESTROY: PostQuitMessage(0); break; case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(WindowHwnd(), &ps); FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1)); EndPaint(WindowHwnd(), &ps); } break; default: return DefWindowProc(WindowHwnd(), uMsg, wParam, lParam); } return 0; }
这样保留了两个return,而改成break的地方跳出swich语句之后就会执行最后的一个return 0,避免了在结构语句之内直接退出函数的做法。
另一个return 在swich结构内,保留这一个对于可读性影响不大,而去掉它会增加代码,这样保留下来也是可以的。
还有一个与向导构建的程序不一样的地方,就是WinMain结束时退出程序的时候使用的是return 0,而不是向导的return messages.wParam。
一般情况下,最后在程序结束时返回的这个数值是没有意义的。而当程序作为一个大的团体中的一员的时候,团体中可能有专门的程序通过这个返回值确定程序退出的原因。基于这一点,还是改成向导提供的方式好一些。
4.3 对BaseWindow<DERIVED_TYPE>::Create中注册窗口类的优化
关于注册窗口类有两个问题:
- 注册窗口类使用的是早期版本的WNDCLASS而不是较新版本的WNDCLASSEX
- 基类中设置了WNDCLASS的的少数成员,更多的成员没有机会设置
关于第二个问题,可以通过在添加设置其他成员的代码,但作为一个基类,这样修改是不合理的。除了需要增加新的功能,尽可能避免基类的修改,添加数据就更不合理了。
解决这个问题的方法是在BaseWindow中添加一个纯虚函数WindowClass,放置在RegisterClass之前调用。在BaseWindow 的基类中重载WindowClass函数,在WindowClass中添加设置WNDCLASS其他成员的代码。
以下是修改后的代码,提供了适用于WNDCLASSEX和WNDCLASS的两种版本。
在BaseWindow的protected中天添加两个纯虚函数:
virtual void WindowClass(PWNDCLASS wc) = 0; virtual void WindowClassEx(PWNDCLASSEX wc) = 0;
在BaseWindow<DERIVED_TYPE>::Create的实现代码中做如下修改(红色是添加的代码):
#define USE_EX #if defined(USE_EX) WNDCLASSEX wc = {0}; wc.cbSize = sizeof (WNDCLASSEX); #else WNDCLASS wc = {0}; #endif // defined wc.lpfnWndProc = DERIVED_TYPE::WindowProc; wc.hInstance = GetModuleHandle(NULL); wc.lpszClassName = ClassName(); #if defined(USE_EX) WindowClassEx(&wc); if (!RegisterClassEx(&wc)) return 0; #else WindowClass(&wc); if (!RegisterClass(&wc)) return 0; #endif // defined
如果想使用WNDCLASSEX,则使用#define USE_EX;如果想使用WNDCLASS,则去掉#define USE_EX。
无论是否使用,都要在BaseWindow的子类中重载这两个函数:
class MainWindow : public BaseWindow<MainWindow> { public: ......protected: void WindowClass(PWNDCLASS wc) override{}; void WindowClassEx(PWNDCLASSEX wc) override{}; };
4.4 未能解决的问题
运行oTetris项目,当鼠标从窗口外移动到窗口客户区时,鼠标指针不能恢复正常显示,这说明存在没有处理的消息。比如:当鼠标从左右边框进入客户区时,由于在边框上鼠标指针变成了调整窗口大小的双向箭头指针,进入客户区后还会保持这种指针样式。
审查的代码,与原始的代码做了比较,仍然没有找到问题所在。很遗憾!
4.5 修改后的oTetrisMainOur.cpp文件的完整代码
经过以上oTetrisMainOur.cpp文件可以暂时作为面向对象的Win32 GUI程序框架的最终版本,可以在其他类似项目中使用。oTetrisMainOur.cpp文件完整的代码如下:
#if !defined(UNICODE) #define UNICODE #endif #if defined(UNICODE) && !defined(_UNICODE) #define _UNICODE #elif defined(_UNICODE) && !defined(UNICODE) #define UNICODE #endif #include <tchar.h> #include <windows.h> template <class DERIVED_TYPE> class BaseWindow { public: static LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); BaseWindow() : m_hwnd(NULL) { } BOOL Create( PCWSTR lpWindowName, DWORD dwStyle, DWORD dwExStyle = 0, int x = CW_USEDEFAULT, int y = CW_USEDEFAULT, int nWidth = CW_USEDEFAULT, int nHeight = CW_USEDEFAULT, HWND hWndParent = HWND_DESKTOP, HMENU hMenu = 0 ); HWND WindowHwnd() const { return m_hwnd; } protected: virtual PCWSTR ClassName() const = 0; virtual LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) = 0; virtual void WindowClass(PWNDCLASS wc) = 0; virtual void WindowClassEx(PWNDCLASSEX wc) = 0; private: HWND m_hwnd; }; template <class DERIVED_TYPE> LRESULT CALLBACK BaseWindow<DERIVED_TYPE>::WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { DERIVED_TYPE *pThis = NULL; if (uMsg == WM_NCCREATE) { CREATESTRUCT* pCreate = (CREATESTRUCT*)lParam; pThis = (DERIVED_TYPE*)pCreate->lpCreateParams; SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pThis); pThis->m_hwnd = hwnd; } else { pThis = (DERIVED_TYPE*)GetWindowLongPtr(hwnd, GWLP_USERDATA); } if (pThis) { return pThis->HandleMessage(uMsg, wParam, lParam); } else { return DefWindowProc(hwnd, uMsg, wParam, lParam); } } template <class DERIVED_TYPE> BOOL BaseWindow<DERIVED_TYPE>::Create( PCWSTR lpWindowName, DWORD dwStyle, DWORD dwExStyle, int x , int y, int nWidth , int nHeight, HWND hWndParent, HMENU hMenu ) { #define USE_EX #if defined(USE_EX) WNDCLASSEX wc = {0}; wc.cbSize = sizeof (WNDCLASSEX); #else WNDCLASS wc = {0}; #endif // defined wc.lpfnWndProc = DERIVED_TYPE::WindowProc; wc.hInstance = GetModuleHandle(NULL); wc.lpszClassName = ClassName(); #if defined(USE_EX) WindowClassEx(&wc); if (!RegisterClassEx(&wc)) return 0; #else WindowClass(&wc); if (!RegisterClass(&wc)) return 0; #endif // defined m_hwnd = CreateWindowEx( dwExStyle, ClassName(), lpWindowName, dwStyle, x, y, nWidth, nHeight, hWndParent, hMenu, GetModuleHandle(NULL), this ); return (m_hwnd ? TRUE : FALSE); } class MainWindow : public BaseWindow<MainWindow> { public: PCWSTR ClassName() const { return L"Sample Window Class"; } LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam); protected: void WindowClass(PWNDCLASS wc) override{}; void WindowClassEx(PWNDCLASSEX wc) override{}; }; //int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow) int WINAPI WinMain (HINSTANCE hThisInstance, HINSTANCE hPrevInstance, LPSTR lpszArgument, int nCmdShow) { MainWindow win; if (!win.Create(L"Learn to Program Windows", WS_OVERLAPPEDWINDOW)) { return 0; } ShowWindow(win.WindowHwnd(), nCmdShow); // Run the message loop. MSG msg = { }; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return 0; } LRESULT MainWindow::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) { case WM_DESTROY: PostQuitMessage(0); break; case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(WindowHwnd(), &ps); FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1)); EndPaint(WindowHwnd(), &ps); } break; default: return DefWindowProc(WindowHwnd(), uMsg, wParam, lParam); } return 0; }
5.修改oTetrisMainOur.cpp文件中的代码生成与oTetrisMainRaw.cpp有一致的视觉效果的代码保存在oTetrisMain.cpp文件中
重新编译运行oTetris项目,你会看到一个空的主窗口界面,与aTetris的主窗口有同样的元素。
不同点有三处:
- 标题的文字不同:这是字符串内容不同,不算是问题
- 界面颜色不同:oTetris构建窗口数据时没有给wc中设置颜色,窗口使用了操作系统默认的颜色
- 界面尺寸大了:oTetris项目使用了默认的窗口宽度和高度,而aTetris指定了具体的数字
为了一致性的对比,需要修改代码。
在修改之前,先复制oTetrisMainOur.cpp文件中的代码到oTetrisMain.cpp中,将oTetrisMain.cpp设置为oTetris项目的主代码文件。后面的修改将在oTetrisMain.cpp文件中进行,编译运行也是使用这个文件。前面修改过程中创建的几个oTetrisMain***.cpp文件将作为备份保留,不再作为oTetris项目的文件。
关于标题的文字不同的问题,只需要将以下代码中win.Create(L"Learn to Program Windows", ...)的字符串改成oTetrisMainRaw.cpp中的窗口标题一致的字符串即可。
int WINAPI WinMain (......) { ......if (!win.Create(L"Learn to Program Windows", WS_OVERLAPPEDWINDOW)) ...... }
关于界面尺寸大了的问题,只需要将以下代码中红色的表示窗口尺寸将宽、高的CW_USEDEFAULT改为544、375即可。
template <class DERIVED_TYPE> class BaseWindow { public: ...... BOOL Create( PCWSTR lpWindowName, DWORD dwStyle, DWORD dwExStyle = 0, int x = CW_USEDEFAULT, int y = CW_USEDEFAULT, int nWidth = CW_USEDEFAULT, int nHeight = CW_USEDEFAULT, HWND hWndParent = HWND_DESKTOP, HMENU hMenu = 0 ); ...... };
关于界面颜色不同的问题,可通过添加以下代码解决:
void MainWindow::WindowClass(PWNDCLASS wc) { wc.hbrBackground = (HBRUSH) COLOR_BACKGROUND; } void MainWindow::WindowClassEx(PWNDCLASSEX wc) { wc.hbrBackground = (HBRUSH) COLOR_BACKGROUND; }
重新编译运行oTetris项目与aTetris的主窗口大小、标题都一样了,就是颜色不一样。
设置了同样的颜色,为什么显示的颜色不一样呢?
经过进一步查找之后发现在Microsoft的示例程序中增加了一个WM_PAINT事件处理代码,使用(HBRUSH) (COLOR_WINDOW+1)画刷颜色填充了整个客户区。
去掉这个代码,aTetris与oTetris的窗口就完全一样了。当然,oTetris还没有添加图标和状态栏,这一点不同会在后边解决,不属于代码改写引起的问题。
6.结束语
在这篇文章中,我们通过修改Microsoft的BaseWindiw示例程序构建了我们自己的可以在CodeBlocks中使用的采用面向对象的方法编制的构建Win32 GUI项目的主框架文件。以后可以在这个文件的基础上扩展它的功能。
当然,这个文件中的代码还不够完善,除了有一个已知的缺陷没有找到原因,在功能上也缺少很多。补充更多的功能也是今后编程的工作之一,通过不断添加代码增加功能只是一种形式,根本的目的还是掌握一种编程的方法。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下