在CodeBolcks+Windows API下的C++面向对象的编程教程——给你的项目中添加状态栏(通用公共控件)
0.前言
我想通过编写一个完整的游戏程序方式引导读者体验程序设计的全过程。我将采用多种方式编写具有相同效果的应用程序,并通过不同方式形成的代码和实现方法的对比来理解程序开发更深层的知识。
了解我编写教程的思路,请参阅体现我最初想法的那篇文章中的“1.编程计划”和“2.已经编写完成的文章(目录)”:
学习编程从游戏开始——编程计划(目录) - lexyao - 博客园
这是一篇专题文章,这篇文章是用来讲解下面这篇文章用到的知识的,我在这篇文章中讲解程序使用的例子就是在下面这篇文章中创建的oTetris项目:
在CodeBolcks+Windows API下的C++面向对象的编程教程——用面向对象的方法改写用向导创建一个Windows GUI项目(oTetris) - lexyao - 博客园
在这篇文章里,我主要讲述以下几个方面的内容:
- 按aTetris项目给oTetris项目添加状态栏
- 用面向对象的方法改造aTetris项目状态栏单元的代码
- 采用面向对象的方法添加给oTetris项目添加状态栏
- oTetris和aTetris项目添加状态栏的代码对比
- 结束语
1.按aTetris项目给oTetris项目添加状态栏
在Win32 GUI项目中,要给应用程序添加状态栏,需要使用Window API函数,还要使用Microsoft的通用公共控件。
作在aTetris项目中已经给项目添加了状态栏,以下是介绍给aTetris项目添加状态栏的文章的链接:
在CodeBolcks+Windows API下的C++编程教程——给你的项目中添加头文件和菜单 - lexyao - 博客园
oTetris项目可以直接使用aTetris项目添加状态栏的代码给自己添加状态栏。具体操作包括以下步骤:
- 将aTetris项目添加状态栏的文件aTetrisCommCtrls.h和aTetrisCommCtrls.cpp添加到oTetris项目
- 在oTetrisMain.cpp文件的头部添加包含头文件的代码:#include "aTetrisCommCtrls.h"
- 复制aTetrisMain.cpp文件中添加状态栏的代码到oTetrisMain.cpp文件中
以下是aTetrisMain.cpp文件中添加状态栏的代码,是在窗口过程函数WindowProcedure 中的:
LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { HWND hwndStatus; switch (message) /* handle the messages */ { case WM_CREATE: hwndStatus=DoCreateStatusBar2(hwnd,NULL,GetModuleHandle(NULL),2); SetStatusText(hwndStatus,_T("欢迎使用多彩俄罗斯方块!"),0);//SetStatusText SetStatusText(hwndStatus,_T("aTetris"),1); break; ...... }
在oTetrisMain.cpp文件中,窗口过程函数是基类中的BaseWindow<DERIVED_TYPE>::WindowProc函数,它将消息传递给了子类的MainWindow::HandleMessage函数。真正的消息处理实在子类的MainWindow::HandleMessage函数中进行的。所以要给oTetris项目添加状态栏,只需要将aTetris项目WindowProcedure 函数中的代码复制到oTetris项目的MainWindow::HandleMessage函数中就行了。
以下是aTetrisMain.cpp文件中添加状态栏的代码,是在窗口过程函数MainWindow::HandleMessage中的:
LRESULT MainWindow::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) /* 处理消息 handle the messages */ { HWND hwndStatus; case WM_CREATE: hwndStatus=DoCreateStatusBar2(WindowHwnd(),NULL,GetModuleHandle(NULL),2); SetStatusText(hwndStatus,_T("欢迎使用多彩俄罗斯方块!"),0);//SetStatusText SetStatusText(hwndStatus,_T("oTetris"),1); break; ...... }
添加以上代码后,编译运行oTetris项目,出现的界面与aTetris项目的界面完全一样,就连状态栏停靠问题的缺陷也是一样的。
已经给oTetris项目添加的状态栏,事情到这里就应该结束了,可是我觉得还有话要说:
aTetris是面向过程的,而oTetris是面向对象的。既然是面向对象,就应该用面向对象的方法解决问题。也就是说,应该考虑采用面向对象的方法给oTetris项目添加状态栏。下面我们就来探讨这个问题的答案。
2.用面向对象的方法改造aTetris项目状态栏单元的代码
在Windows中,应用程序的主窗口称为顶层窗口,作为公共控件的状态栏、按钮等统称为子窗口。虽然有不同的特性,但都是窗口,都是使用CreateWindowEx函数创建的,都是使用句柄交流。
既然如此,主窗口可以使用的基类,子窗口也可以参照执行。
现在我们就参照主窗口的代码构建状态栏。
2.1 构建状态栏的基类CommWindow
第一步、将借用的aTetris项目的两个文件aTetrisCommCtrls.h和aTetrisCommCtrls.cpp从oTetris项目中移除。
第二步、在oTetris项目中新建两个文件:oTetrisCommCtrls.h和oTetrisCommCtrls.cpp。
第三步、复制aTetrisMain.cpp中定义BaseWindow的代码复制到oTetrisCommCtrls.h文件中。
第四步、修改oTetrisCommCtrls.h文件中BaseWindow类定义:
- 将类的名称由BaseWindow改为CommWindow
- 修改Create的参数,将创建状态栏必须用到的参数前移,不是必须的则放在后面并设置默认值
- 修改HandleMessage函数由纯虚函数改为虚函数,返回值为{return DefWindowProc(hwnd, uMsg, wParam, lParam);},这样在子类中可以不再必须重写这个函数
- 删除纯虚函数WindowClassEx
修改后oTetrisCommCtrls.h文件的代码如下:
#ifndef OTETRISCOMMCTRLS_H_INCLUDED #define OTETRISCOMMCTRLS_H_INCLUDED #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> #include <commctrl.h> #include "oTetrisCommCtrls.h" template <class DERIVED_TYPE> class CommWindow { public: static LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); CommWindow() : m_hwnd(NULL) { } BOOL Create( HWND hWndParent, HMENU idCtrl, PCWSTR lpWndText, DWORD dwStyle, DWORD dwExStyle, int x = CW_USEDEFAULT, int y = CW_USEDEFAULT, int nWidth = CW_USEDEFAULT, int nHeight = CW_USEDEFAULT ); inline HWND WindowHwnd() const { return m_hwnd; } protected: virtual PCWSTR ClassName() const = 0; virtual LRESULT HandleMessage(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam){return DefWindowProc(hwnd, uMsg, wParam, lParam);}; private: HWND m_hwnd; };#endif // OTETRISCOMMCTRLS_H_INCLUDED
第五步、复制aTetrisMain.cpp中定义BaseWindow实现Create和WindowProc的代码复制到oTetrisCommCtrls.cpp文件中。
第六步、修改oTetrisCommCtrls.cpp文件中BaseWindow类实现的代码:
- 将类的名称由BaseWindow改为CommWindow
- 按头文件重点定义修改Create的参数,同时修改调用CreateWindowEx函数的单数
- 去掉定义和注册出窗口结构的代码,使用InitCommonControls()代替
修改后oTetrisCommCtrls.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> //#include "aTetris.h" #include "oTetrisCommCtrls.h" template <class DERIVED_TYPE> BOOL CommWindow<DERIVED_TYPE>::Create( HWND hwndParent, HMENU idCtrl, PCWSTR lpWndText, DWORD dwStyle, DWORD dwExStyle, int x, int y, int nWidth, int nHeight ) { // Ensure that the common control DLL is loaded. InitCommonControls(); // Create the status bar. m_hwnd = CreateWindowEx( dwExStyle, // no extended styles ClassName(), // name of status bar class (PCTSTR)lpWndText, // no text when first created dwStyle | // includes a sizing grip WS_CHILD | WS_VISIBLE, // creates a visible child window x, y, nWidth, nHeight, // ignores size and position hwndParent, // handle to parent window (HMENU) idCtrl, // child window identifier GetModuleHandle(NULL), // handle to application instance this); // no window creation data return (m_hwnd ? TRUE : FALSE); } template <class DERIVED_TYPE> LRESULT CALLBACK CommWindow<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(hwnd, uMsg, wParam, lParam); } else { return DefWindowProc(hwnd, uMsg, wParam, lParam); } }
2.2 构建状态栏的类cxStatusBar
第一步、复制aTetrisMain.cpp中定义MainWindow的代码复制到oTetrisCommCtrls.h文件中。
第二步、修改oTetrisCommCtrls.h文件中MainWindow类定义:
- 将类的名称由MainWindow改为cxStatusBar
- 修改ClassName的返回值修改为STATUSCLASSNAME
- 去掉重写两个函数的代码:HandleMessage、WindowClassEx
- 添加Create函数,函数的参数改为创建状态栏需要的参数,去掉状态栏不需要的参数
- 添加两个函数的定义:SetStatusParts和SetStatusText
修改后oTetrisCommCtrls.h文件中定义cxStatusBar类的代码如下:
class cxStatusBar : public CommWindow<cxStatusBar> { public: PCWSTR ClassName() const { return STATUSCLASSNAME; } BOOL Create( HWND hWndParent, HMENU idCtrl = NULL, DWORD dwStyle = SBARS_SIZEGRIP, DWORD dwExStyle = 0 HRESULT SetStatusParts(int iParts, int* aPartWidth = NULL); HRESULT SetStatusText(PCWSTR pszStatusText, int iParts); ); };
第三步、在oTetrisCommCtrls.cpp文件中cxStatusBar类新增加的三个函数的实现的代码。其中SetStatusParts可以指定分格宽度,也可以不用指定。当不指定分格宽度时,将按分格数平均分配分格宽度。
修改后oTetrisCommCtrls.cpp文件增加的代码如下:
BOOL cxStatusBar::Create(HWND hWndParent, HMENU idCtrl, DWORD dwStyle, DWORD dwExStyle) { return CommWindow::Create(hWndParent, idCtrl, (PCWSTR)NULL, dwStyle, dwExStyle); //return cxWindowBase<cxStatusBar>::Create(hWndParent,idCtrl,(PCWSTR)NULL,dwStyle,dwExStyle); } HRESULT cxStatusBar::SetStatusParts(int iParts, int* aPartWidth) { HWND hwndParent; HWND hwndStatus; RECT rcClient; HLOCAL hloc; //PINT paParts; int i, nWidth; HRESULT hResult; hwndStatus = this->WindowHwnd(); if(!aPartWidth) { // 获得主窗口客户区坐标 hwndParent = GetParent(this->WindowHwnd()); GetClientRect(hwndParent, &rcClient); // 为设置状态栏分格右边界坐标分配内存 hloc = LocalAlloc(LHND, sizeof(int) * iParts); aPartWidth = (PINT) LocalLock(hloc); // 计算状态栏分格右边界坐标 nWidth = rcClient.right / iParts; int rightEdge = nWidth; for(i = 0; i < iParts - 1; i++) { aPartWidth[i] = rightEdge; rightEdge += nWidth; } //最后一格设置为-1可以自动扩展到主主窗口右边界 aPartWidth[i] = -1; } hResult = SendMessage(this->WindowHwnd(), SB_SETPARTS, (WPARAM)iParts, (LPARAM)aPartWidth); // 释放内存 LocalUnlock(hloc); LocalFree(hloc); return hResult; } HRESULT cxStatusBar::SetStatusText(PCWSTR pszStatusText, int iParts) { return SendMessage(this->WindowHwnd(), SB_SETTEXT, iParts, (LPARAM)(pszStatusText)); }
3.采用面向对象的方法添加给oTetris项目添加状态栏
使用上面编写的cxStatusBar类可以使用以下代码给oTetris项目的主窗口添加状态栏。根据设置分格宽度的方式可以有以下两种代码。
指定分格宽度的方式添加状态栏:
LRESULT MainWindow::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) /* 处理消息 handle the messages */ { cxStatusBar* stBar; int paParts[2]; case WM_CREATE: stBar = new cxStatusBar(); stBar->Create(WindowHwnd()); paParts[0]= 300; paParts[1]= -1; stBar->SetStatusParts(2,paParts); stBar->SetStatusText(_T("欢迎使用多彩俄罗斯方块!"),0); stBar->SetStatusText(_T("oTetris"),1); break; ...... } return 0; }
平均分配分格宽度的方式添加状态栏:
LRESULT MainWindow::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) { switch (uMsg) /* 处理消息 handle the messages */ { cxStatusBar* stBar; case WM_CREATE: stBar = new cxStatusBar(); stBar->Create(WindowHwnd()); stBar->SetStatusParts(2); stBar->SetStatusText(_T("欢迎使用多彩俄罗斯方块!"),0); stBar->SetStatusText(_T("oTetris"),1); break; ...... } return 0; }
4.oTetris和aTetris项目添加状态栏的代码对比
从上面实现添加状态栏的代码来看,似乎采用面向过程的方法更简单一些,而面向对象的方法有些复杂。
当我们需要再向窗口添加其他的通用共公共控件的时候,那时你会发现,还是采用面向对象的方法简单:基类CommWindow的代码可以不用改变,只是为其他的公共控件编写一个与cxStatusBar相似的类就行了。
5.结束语
在这一篇文章中,我们采用了面向过程的方式和面向对象的方式向oTetris项目的主窗口添加了状态栏。
通过这一篇文章我们了解了面向对象的方法,在今后添加更多的控件的时候会看到使用面向对象的方法的好处。
可能大家已经看到了一个问题:主窗口和状态栏都是窗口,为什么要编写两个基类呢?能不能使用同一个基类呢?
答案是肯定的:能。
我准备在下一篇文章里回答这个问题:重新编写一个基类,让主窗口和状态栏使用同一个基类创建。