在CodeBolcks+Windows API下的C++编程教程——给你的项目中添加状态栏(通用公共控件)
0.前言
我想通过编写一个完整的游戏程序方式引导读者体验程序设计的全过程。我将采用多种方式编写具有相同效果的应用程序,并通过不同方式形成的代码和实现方法的对比来理解程序开发更深层的知识。
了解我编写教程的思路,请参阅体现我最初想法的那篇文章中的“1.编程计划”和“2.已经编写完成的文章(目录)”:
学习编程从游戏开始——编程计划(目录) - lexyao - 博客园
这是一篇专题文章,这篇文章是用来讲解下面这篇文章用到的知识的,我在这篇文章中讲解程序使用的例子就是在下面这篇文章中创建的aTetris项目:
在CodeBolcks+Windows API下的C++编程教程——用向导创建一个Windows GUI项目(aTetris) - lexyao - 博客园
在这篇文章里,我主要讲述以下几个方面的内容:
- 什么是通用公共控件
- Microsoft关于介绍状态栏的有关文章
- 从微软获得的一个使用状态栏的示例
- 给我们的Win32 GUI程序(aTetris)添加一个状态栏
- 向状态栏添加文字
- 添加状态栏的代码的改造
- 探索状态栏停靠问题的尝试
- 结束语
1.什么是通用公共控件
Microsoft的Windows操作系统中常用的控件中有一部分属于各种应用程序经常用得到的被称作通用公共控件。
公共控件放在Comctl32.dll中,这是 Windows 操作系统随附的 DLL。 与其他控件窗口一样,公共控件是一个子窗口,应用程序将该子窗口与另一个窗口结合使用,以实现与用户的交互。
了解更多微软通用公共控件的知识,请打开以下链接阅读,这里我就不再花时间复制内容了。
关于常用控件 - Win32 apps | Microsoft Learn
控件库 - Win32 apps | Microsoft Learn
(CommCtrl.h) 的窗口类 - Win32 apps | Microsoft Learn
使用这些公共控件有一个共同的特点:
- 在创建之前需要使用InitCommonControls函数确保已加载公共控件 DLL
- 在应用程序中使用CreateWindowEx创建控件的示例
- 创建后获得一个HWND类型的称作手柄的数值,以后使用这个手柄与创建的控件交流
- 创建公共控件后你可以直接使用它预置的功能
状态栏是这些公共控件中的一个。
2.Microsoft关于介绍状态栏的有关文章
关于状态栏,在Microsoft的资料库中有大量的介绍,我没有他们说得好,就不在这里多啰嗦了。下面是Microsoft资料库中关于状态栏的文章,阅读后对你理解状态栏大有好处:
3.从微软获得的一个使用状态栏的示例
关于如何在我们的应用程序中创建一个状态栏,Microsoft资料库中给出了一个示例,下面是文章的链接。为了便于写下面的文章,我复制了Microsoft提供的代码:
如何创建状态栏 - Win32 apps | Microsoft Learn
//描述:
//创建状态栏并将其划分为指定数量的部分。
//参数:
//hwndParent - 状态栏的父窗口。
//idStatus - 状态栏的子窗口标识符。
//hinst - 应用程序实例的句柄。
//cParts - 要将状态栏划分为的零件数。
//返回:
//状态栏的手柄。
//
HWND DoCreateStatusBar(HWND hwndParent, int idStatus, HINSTANCE hinst, int cParts) { HWND hwndStatus; RECT rcClient; HLOCAL hloc; PINT paParts; int i, nWidth; // 确保已加载公共控件 DLL。 InitCommonControls(); // 创建状态栏。 hwndStatus = CreateWindowEx( 0, // 无扩展样式 STATUSCLASSNAME, // 状态栏类的名称 (PCTSTR) NULL, // 首次创建时无文本 SBARS_SIZEGRIP | // 包括一个尺寸调整手柄 WS_CHILD | WS_VISIBLE, // 创建可见的子窗口 0, 0, 0, 0, // 忽略大小和位置 hwndParent, // 父窗口的手柄 (HMENU) idStatus, // 子窗口标识符 hinst, // 应用程序实例的手柄 NULL); // 无窗口创建数据 // 获取父窗口的工作区的坐标。 GetClientRect(hwndParent, &rcClient); // 分配一个数组来保存右边缘坐标。 hloc = LocalAlloc(LHND, sizeof(int) * cParts); paParts = (PINT) LocalLock(hloc); // 计算每个零件的右边缘坐标,然后将坐标复制到数组中。 nWidth = rcClient.right / cParts; int rightEdge = nWidth; for (i = 0; i < cParts; i++) { paParts[i] = rightEdge; rightEdge += nWidth; } // 指示状态栏创建窗口部分。 SendMessage(hwndStatus, SB_SETPARTS, (WPARAM) cParts, (LPARAM)paParts); // 释放数组,然后返回。 LocalUnlock(hloc); LocalFree(hloc); return hwndStatus; }
4.给我们的Win32 GUI程序(aTetris)添加一个状态栏
看完了Microsoft的资料,下面回到我们的项目aTetris,讨论给我们的aTetris项目主窗口添加状态栏的问题。
第一步、为创建状态栏的代码创建一个C++代码文件(.cpp)
在Code Blocks中通过主菜单[File->New->Files…]为我们的aTetris项目添加一个新的[C/C++ source]文件,文件名为aTetrisCommCtrls.cpp。
将上面Microsoft的示例中的代码复制到aTetrisCommCtrls.cpp文件中。除了复制代码,还要在最前面为它添加需要包含的头文件:
#include <windows.h>
#include <commctrl.h>
第二步、为了引用aTetrisCommCtrls.cpp文件中的函数创建一个头文件(.h)
为了在我们的程序中使用代码中的DoCreateStatusBar函数需要将这个函数的定义添加到一个头文件中。
为了便于管理,我们为项目添加一个新的头文件,通过主菜单[File->New->Files…]为我们的aTetris项目添加一个新的[C/C++ header]文件,文件名为aTetrisCommCtrls.h。
在aTetrisCommCtrls.h文件中添加以下内容:
#ifndef ATETRISCOMMCTRLS_H_INCLUDED #define ATETRISCOMMCTRLS_H_INCLUDED #include <tchar.h> #include <commctrl.h> HWND DoCreateStatusBar(HWND hwndParent, int idStatus, HINSTANCE hinst, int cParts);#endif // ATETRISCOMMCTRLS_H_INCLUDED
第三步、包含头文件aTetrisCommCtrls.h
在aTetrisMain.cpp和aTetrisCommCtrls.cpp文件中添加以下代码:
#include "aTetrisCommCtrls.h"
第四步、添加调用创建状态栏函数的代码。
aTetrisMain.cpp中的WinMain函数中使用CreateWindowEx创建主窗口后会给窗口过程函数WindowProcedure发送一个WM_CREATE消息。在程序主窗口显示之前需要添加到主窗口中的控件都是在收到WM_CREATE消息后添加的。
我们添加响应WM_CREATE消息的代码,在其中创建我们的状态栏。在aTetrisMain.cpp中添加以下代码:
LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { HWND hwndStatus; switch (message) /* handle the messages */ { case WM_CREATE: hwndStatus = DoCreateStatusBar(hwnd,NULL,GetModuleHandle(NULL),2); break;...... } return 0; }
编译运行aTetris,成功显示了主界面窗口,并且在窗口的下面添加了一个分成两栏的状态栏。尽管风格与xTetris不一样,但起码说已经添加上了。
5.向状态栏添加文字
Windows API向控件发送文字都是通过消息实现的,就像上面的代码中将状态栏分成两格也是通过发送消息实现的。
发送消息使用的函数是SendMessage。
其实在WindowsAPI中并没有SendMessage这个函数,这只是一个别名。真正的函数是SendMessageA和SendMessageW。对于数字参数,这两个函数没有差别,而对于字符串就不同了。
详细的解释你可以百度一下会找到答案,这里我就不详细讲了。
发送文字通常是使用的消息是WM_SETTEXT,当状态栏只有一个分格的时候可以使用这个消息,当有多个分格的时候,就要用到另一个消息SETTEXT。
下面是在aTetrisCommCtrls.cpp文件中添加的向状态栏添加文字的函数的代码:
HRESULT SetStatusTextA(HWND hwndStatus, LPSTR pszStatusText, int iParts) { return SendMessageA(hwndStatus, SB_SETTEXT, iParts, (LPARAM)(LPSTR)(pszStatusText)); } HRESULT SetStatusTextW(HWND hwndStatus, LPWSTR pszStatusText, int iParts) { return SendMessage(hwndStatus, SB_SETTEXT, iParts, (LPARAM)(LPWSTR)(pszStatusText)); }
下面是在aTetrisCommCtrls.h文件中添加的向状态栏添加文字的函数的代码:
HRESULT SetStatusTextA(HWND hwndStatus, LPSTR pszStatusText, int iParts); HRESULT SetStatusTextW(HWND hwndStatus, LPWSTR pszStatusText, int iParts); #define SetStatusText __MINGW_NAME_AW(SetStatusText)
这里采用的两种版本的SetStatusText函数,一个是SetStatusTextA,一个是SetStatusTextW。使用宏__MINGW_NAME_AW给这两个函数定义了一个别名。
这是编写程序的一种惯例,就像SendMessage一样有两种版本和一个别名,名字后面带有字母W的支持宽字符,也就是说支持Unicode。在使用的时候使用别名,而具体选择哪一个由编译器去选择。
在aTetrisMain.cpp中添加使用SetStatusText的代码:
case WM_CREATE: hwndStatus=DoCreateStatusBar2(hwnd,NULL,GetModuleHandle(NULL),2); SetStatusText(hwndStatus,_T("欢迎使用多彩俄罗斯方块!"),0); break;
添加了代码之后编译运行aTetris项目,应该就会在状态栏的第一格中显示出“欢迎使用多彩俄罗斯方块!”,但我们看到的结果却不是这样,汉字变成了乱码。
我将代码中的汉字替换成英文的“SetStatusText”,编译运行后显示的结果是正常的。这说明编译器选择了SetStatusTextA,而不是我们期望的SetStatusTextW。
为什么这样呢?原因是没有定义Unicode。
我们将aTetrisMain.cpp文件前面的几行编译指令添加到aTetrisCommCtrls.h文件中,编译运行只显示了一个S,再添加到aTetrisCommCtrls.cpp文件中,编译运行发现结果正常了,无论英文还是汉字,都能正常显示。这说明要想支持Unicode,必须再每一个文件中都要添加支持Unicode的编译指令。
#if !defined(UNICODE) #define UNICODE #endif #if defined(UNICODE) && !defined(_UNICODE) #define _UNICODE #elif defined(_UNICODE) && !defined(UNICODE) #define UNICODE #endif
除了在文件的头部添加支持Unicode的编译指令,还有一个办法是在编译设置中添加命令行参数来达到让编译器支持Unicode的目的,但这样做可能会因为更换了不同的编译环境而导致同样的文件编译后产生不同的结果。这不是我们想要看到的结果。要想达到一致性,就只能选择在文件的头部添加编译指令了。
6.添加状态栏的代码的改造
从添加到aTetrisCommCtrls.cpp文件中的DoCreateStatusBar函数的代码可以看出,其中包含了创建状态栏和将状态栏分成多个分格两部分的代码,而且是固定的。这在编程中不是一个好的习惯。
选择我们把代码拆分开来,按功能分成两个独立的函数,再编写一个应用这两个函数达到DoCreateStatusBar效果的函数,这就是代码中的DoCreateStatusBar2。
以下是拆分后aTetrisCommCtrls.cpp文件中的代码:
HWND DoCreateStatusBar2(HWND hwndParent, int idStatus, HINSTANCE hinst, int cParts) { HWND hwndStatus; RECT rcClient; HLOCAL hloc; PINT paParts; int i, nWidth; hwndStatus = CreateStatusBar(hwndParent,idStatus,hinst); // Get the coordinates of the parent window's client area. GetClientRect(hwndParent, &rcClient); // Allocate an array for holding the right edge coordinates. hloc = LocalAlloc(LHND, sizeof(int) * cParts); paParts = (PINT) LocalLock(hloc); // Calculate the right edge coordinate for each part, and // copy the coordinates to the array. nWidth = rcClient.right / cParts; int rightEdge = nWidth; for (i = 0; i < cParts-1; i++) { //paParts[i] = -10; paParts[i] = rightEdge; rightEdge += nWidth; } paParts[i] = -1; // Tell the status bar to create the window parts. SetStatusParts(hwndStatus, cParts, paParts); // Free the array, and return. LocalUnlock(hloc); LocalFree(hloc); return hwndStatus; } HWND CreateStatusBar(HWND hwndParent, int idStatus, HINSTANCE hinstApp,DWORD dwStyle) { HWND hwndStatus; // Ensure that the common control DLL is loaded. InitCommonControls(); // Create the status bar. return CreateWindowEx( 0, // no extended styles STATUSCLASSNAME, // name of status bar class (PCTSTR) NULL, // no text when first created dwStyle | // includes a sizing grip WS_CHILD | WS_VISIBLE, // creates a visible child window 0, 0, 0, 0, // ignores size and position hwndParent, // handle to parent window (HMENU) idStatus, // child window identifier hinstApp, // handle to application instance NULL); // no window creation data } HRESULT SetStatusParts(HWND hwndStatus, int iParts, const int* aPartWidth) { return SendMessage(hwndStatus, SB_SETPARTS, (WPARAM)iParts, (LPARAM)aPartWidth); }
以下是拆分后aTetrisCommCtrls.h文件中的添加的函数定义:
HWND DoCreateStatusBar2(HWND hwndParent, int idStatus, HINSTANCE hinst, int cParts); HWND CreateStatusBar(HWND hwndParent, int idStatus, HINSTANCE hinstApp,DWORD dwStyle = SBARS_SIZEGRIP); HRESULT SetStatusParts(HWND hwndStatus, int iParts, const int* aPartWidth);
这样,调用CreateStatusBar创建状态栏,调用SetStatusParts设置状态栏的分格。DoCreateStatusBar2算是使用这两个函数的例子。
将aTetrisCommCtrls.cpp文件中使用DoCreateStatusBar的代码改为DoCreateStatusBar2,编译运行后发现这两个函数的效果是一样的。
7.探索状态栏停靠问题的尝试
本来以为写道这里就已经完成了,可是意外的情况发生了。尝试调整窗口大小的时候,发现状态栏并没有像预想的那样随着窗口大小的变化保持停靠在窗口的底部,而是赖在原地一动不动。
这种情况在以前我编写程序的时候遇到过,不过不记得是什么时候、什么情况下、怎么解决的了。
按着Microsoft文档里的说法,当窗口调整大小的时候,主窗口会向状态栏发送一个WM_SIZE消息,而状态栏会自动调整自己的位置和尺寸,无论窗口怎么变化,状态栏都能保持停靠的窗口的最底部,而且保持宽度正好适合窗口的客户区宽度。现在看到的是状态栏没有达到预期的效果。
尝试用鼠标拖动状态栏右端的手柄调整窗口大小,发现当鼠标点下去的时候状态栏发生了动作,停靠到窗口底部了。当窗口调整变小的时候,状态栏到了窗口之外,看不到了。
问题发生在哪里呢?
尝试在aTetrisMain.cpp中的窗口过程函数WindowProcedure中添加代码将WM_SIZE消息发送给状态条,结果还是一样。再次尝试使用MoveWindow和SetWindowPos函数,也是没有反应。
百度一下问题,发现有人遇到了同样的问题。有许多热心人给他提出建议,也没有得到解决。
CStatusBarCtrl状态栏停靠位置的疑问?-CSDN社区
他使用的是Microsoft的CStatusBarCtrl,我用的是Windows的API,结局是一样的。有没有朋友帮我找到解决办法呢?很期待!
8.结束语
公共控件在Windows API编程中起着重要的作用。Microsoft定义了多种公共控件,程序员采用了相似的方法在自己编写的程序中使用这些公共控件。
我们通过使用状态栏的过程领略了使用公共控件的过程和方法,为使用更多的公共控件打下了基础。
这篇文章初步介绍了以下内容:
- Microsoft公共控件及相关资料
- 在自己的应用程序中使用状态栏
- 在头文件中添加函数的定义
- 使自己的应用程序支持Unicode的方法