Windows 编程/窗口创建
在 Windows 操作系统中,大多数用户可交互的界面对象被称为“窗口”。每个窗口都与一个特定的类相关联,一旦该类向系统注册,就可以创建该类的窗口。
要注册一个窗口类,你需要填写 WNDCLASS 结构中的数据字段,并需要将该结构传递给系统。但是,首先,你需要为你的类提供一个名称,以便 Windows(系统)可以识别它。习惯上将窗口类名定义为全局变量
LPTSTR szClassName = TEXT("My Class");
你可以随意命名它,这只是一个例子。
获得类名后,就可以开始填充 WNDCLASS 结构。WNDCLASS 被定义如下
typedef struct {
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
} WNDCLASS, *PWNDCLASS;
有关此结构的更多信息,请参见此 Microsoft 开发人员网络文章。
请注意,最后一个数据字段是指向名为“lpszClassName”的字符串的指针?这里是指向你刚刚定义的类名。名为“hInstance”的字段是你在其中提供程序的实例句柄的地方。我们将把其余字段分成几个不同的类别。
WNDCLASS 结构中有许多不同的数据类型以字母“h”开头。正如我们从匈牙利表示法讨论中记住的那样,如果一个变量以“h”开头,则该变量本身保存一个 HANDLE 对象。
- HICON hIcon
- 这是指向程序将使用的图标的句柄,该图标位于左上角和任务栏中。我们将在稍后讨论图标。但是,在我们下面的示例中,我们将为此项使用默认值。
- HCURSOR hCursor
- 这是指向窗口将使用的标准鼠标指针的句柄。在我们的示例中,我们也将为此使用默认值。
- HBRUSH hbrBackground
- 这是指向窗口背景的画刷(画刷本质上是一种颜色)的句柄。以下是 Windows 提供的默认颜色列表(这些颜色会根据计算机上启用的“主题”而改变)
COLOR_ACTIVEBORDER COLOR_ACTIVECAPTION COLOR_APPWORKSPACE COLOR_BACKGROUND COLOR_BTNFACE COLOR_BTNSHADOW COLOR_BTNTEXT COLOR_CAPTIONTEXT COLOR_GRAYTEXT COLOR_HIGHLIGHT COLOR_HIGHLIGHTTEXT COLOR_INACTIVEBORDER COLOR_INACTIVECAPTION COLOR_MENU COLOR_MENUTEXT COLOR_SCROLLBAR COLOR_WINDOW COLOR_WINDOWFRAME COLOR_WINDOWTEXT
由于软件问题,必须将任何这些值的 1 添加到任何这些值中才能使其成为有效的画刷。
另一个值得一提的值是“lpszMenuName”变量。lpszMenuName 指向一个字符串,该字符串包含程序菜单栏的名称。如果你的程序没有菜单,你可以将其设置为 NULL。
WNDCLASS 结构中还有 2 个“额外”数据成员,允许程序员指定要为类分配的额外空间量(以字节为单位)(cbClsExtra)和要为每个特定窗口实例分配的额外空间量(cbWndExtra)。如果你想知道,前缀“cb”代表“字节数”。
int cbClsExtra;
int cbWndExtra;
如果你不知道如何使用这些成员,或者不想使用它们,你可以将它们都保留为 0。我们将在稍后更详细地讨论这些成员。
WNDCLASS 中有 2 个字段专门用于处理窗口的运行方式。第一个是“style”字段,它本质上是一组位标志,将确定系统可以对类采取的一些操作。这些标志可以使用|运算符进行按位或运算(使用|运算符)以将多个标志组合到“style”字段中。MSDN WNDCLASS 文档包含更多信息。
下一个(也是最重要的)WNDCLASS 成员是 lpfnWndProc 成员。该成员指向一个 WNDPROC 函数,该函数将控制窗口,并将处理所有窗口的消息。
在初始化 WNDCLASS 结构的字段后,你需要将你的类注册到系统。这可以通过将指向 WNDCLASS 结构的指针传递给 RegisterClass 函数来完成。如果 RegisterClass 函数返回零值,则注册失败,并且系统无法注册新的窗口类。
窗口通常使用“CreateWindow”函数创建,尽管还有一些其他函数也很有用。一旦 WNDCLASS 注册成功,你就可以通过将类名(还记得我们定义的那个全局字符串吗?)传递给 CreateWindow 函数来告诉系统从该类创建一个窗口。
HWND CreateWindow(
LPCTSTR lpClassName,
LPCTSTR lpWindowName,
DWORD dwStyle,
int x,
int y,
int nWidth,
int nHeight,
HWND hWndParent,
HMENU hMenu,
HINSTANCE hInstance,
LPVOID lpParam
);
(有关更多信息,请参见此 MSDN 文章。)
第一个参数“lpClassName”与我们的窗口类关联的字符串。参数“lpWindowName”是将在窗口标题栏中显示的标题(如果窗口有标题栏)。
“dwStyle”是一个包含多个按位或运算标志的字段,这些标志将控制窗口创建。
参数“x”和“y”指定窗口左上角在屏幕上的坐标。如果“x”和“y”都为零,则窗口将出现在屏幕的左上角。“nWidth”和“nHeight”分别指定窗口的宽度和高度(以像素为单位)。
有 3 个 HANDLE 值需要传递给 CreateWindow:hWndParent、hMenu 和 hInstance。hwndParent 是指向父窗口的句柄。如果你的窗口没有父窗口,或者你不想让你的窗口相互关联,可以将其设置为 NULL。hMenu 是指向菜单的句柄,hInstance 是指向程序实例值的句柄。
要将值传递给新窗口,可以在 CreateWindow 的 lpParam 值中传递一个通用 LPVOID 指针(一个 32 位值)。通常,通过这种方法传递参数比将所有变量都设置为全局变量更好。如果要将多个参数传递给新窗口,则应将所有值放入结构体中,并将指向该结构体的指针传递给窗口。我们将在稍后更详细地讨论这一点。
最后,我们将展示此过程的一个简单示例。该程序将在屏幕上显示一个简单的窗口,但窗口不会执行任何操作。该程序是一个精简的程序,它包含了使任何 Windows 程序执行任何操作所需的大部分框架。除此之外,可以轻松地向程序添加更多功能。
#include <windows.h>
LPSTR szClassName = "MyClass";
HINSTANCE hInstance;
LRESULT CALLBACK MyWndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrevInstance, LPSTR szCmdLine, int iCmdShow)
{
WNDCLASS wnd;
MSG msg;
HWND hwnd;
hInstance = hInst;
wnd.style = CS_HREDRAW | CS_VREDRAW; //we will explain this later
wnd.lpfnWndProc = MyWndProc;
wnd.cbClsExtra = 0;
wnd.cbWndExtra = 0;
wnd.hInstance = hInstance;
wnd.hIcon = LoadIcon(NULL, IDI_APPLICATION); //default icon
wnd.hCursor = LoadCursor(NULL, IDC_ARROW); //default arrow mouse cursor
wnd.hbrBackground = (HBRUSH)(COLOR_BACKGROUND+1);
wnd.lpszMenuName = NULL; //no menu
wnd.lpszClassName = szClassName;
if(!RegisterClass(&wnd)) //register the WNDCLASS
{
MessageBox(NULL, "This Program Requires Windows NT",
"Error", MB_OK);
return 0;
}
hwnd = CreateWindow(szClassName,
"Window Title",
WS_OVERLAPPEDWINDOW, //basic window style
CW_USEDEFAULT,
CW_USEDEFAULT, //set starting point to default value
CW_USEDEFAULT,
CW_USEDEFAULT, //set all the dimensions to default value
NULL, //no parent window
NULL, //no menu
hInstance,
NULL); //no parameters to pass
ShowWindow(hwnd, iCmdShow); //display the window on the screen
UpdateWindow(hwnd); //make sure the window is updated correctly
while(GetMessage(&msg, NULL, 0, 0)) //message loop
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
LRESULT CALLBACK MyWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch(msg)
{
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
随着每一代的更新,Win32 API 的功能越来越强大,尽管微软一直致力于保持 API 与旧版本 Windows 的向后兼容性。为了添加更多功能,微软需要添加新的函数和结构,以利用新特性。WNDCLASS 结构的扩展版本称为“WNDCLASSEX”结构,它具有更多字段,并允许更多选项。要注册 WNDCLASSEX 结构,必须使用 RegisterClassEx 函数。
此外,还有一个具有扩展功能的 CreateWindow 函数版本:CreateWindowEx。要了解有关这些扩展的更多信息,可以在 MSDN 上搜索。
对话框是特殊类型的窗口,其创建和管理方式与其他窗口不同。要创建对话框,我们将使用 CreateDialog、DialogBox 或 DialogBoxParam 函数。我们将在后面讨论这些函数。可以通过定义 WNDCLASS 并调用 CreateWindow 来创建对话框,但 Windows 已经将所有定义存储在内部,并提供了一些易于使用的工具。有关完整讨论,请参阅:对话框。
Windows 系统中已经定义并存储了许多窗口类。这些类包括按钮和编辑框等元素,手动定义这些元素需要花费太多工作。以下是部分预制窗口类型的列表:
- 按钮
- 按钮窗口可以包含从按钮到复选框和单选按钮的一切。按钮窗口的“标题”是按钮上显示的文本。
- 滚动条
- 滚动条窗口是滑块控件,通常用于较大的窗口边缘,用于控制滚动。滚动条类型也可以用作滑块控件。
- MDICLIENT
- 这种客户端类型支持 多文档界面 (MDI) 应用程序。我们将在后面的章节中讨论 MDI 应用程序。
- 静态
- 静态窗口是简单的文本显示窗口。静态窗口很少接受用户输入。但是,如果需要,可以修改静态窗口使其看起来像超链接。
- 列表框、组合框
- 列表框窗口是下拉列表框,其中可以填充用户可以选择的不同选项。组合框窗口类似于列表框,但它可以包含复杂项目。
- 编辑、富文本编辑
- 编辑窗口允许使用光标输入文本。基本的编辑窗口还允许进行复制粘贴操作,尽管需要自己提供处理这些选项的代码。富文本编辑控件允许文本编辑和格式化。可以将编辑控件视为 Notepad.exe,将富文本编辑控件视为 WordPad.exe。
窗口或对话框中可以包含多种不同的菜单。最常见(也是最重要的)菜单之一是显示在窗口或对话框顶部的下拉菜单栏。此外,许多程序在鼠标右键单击窗口时会提供菜单。窗口顶部的栏称为“菜单栏”,我们将在下面首先讨论它。有关在资源脚本中创建菜单的信息,请参阅本书附录中的 资源脚本参考页面。
在资源脚本中创建菜单是最简单直观的方法。假设我们要创建一个包含一些常见标题的菜单:“文件”、“编辑”、“视图”和“帮助”。这些是大多数程序都有的常见菜单项,也是大多数用户熟悉的菜单项。
创建菜单时,建议使用常见的名称和公认的顺序创建菜单,以便计算机用户知道在哪里找到这些菜单项。 |
我们在资源脚本中创建一个项目来定义这些菜单项。我们将通过数字标识符“IDM_MY_MENU”来表示我们的资源。
IDM_MY_MENU MENU DISCARDABLE BEGIN POPUP "File" POPUP "Edit" POPUP "View" POPUP "Help" END
关键字 POPUP 表示一个菜单,当您单击它时会打开。但是,假设我们不希望“帮助”菜单项弹出,而是希望单击“帮助”一词,然后立即打开帮助窗口。我们可以这样更改它:
IDM_MY_MENU MENU DISCARDABLE BEGIN POPUP "File" POPUP "Edit" POPUP "View" MENUITEM "Help" END
MENUITEM 指示符表明,当我们单击“帮助”时,不会打开另一个菜单,而会向程序发送一个命令。
现在,我们不希望菜单为空,因此我们将在“文件”和“编辑”菜单中填充一些常见命令,使用与上面相同的 MENUITEM 关键字:
IDM_MY_MENU MENU DISCARDABLE BEGIN POPUP "File" BEGIN MENUITEM "Open" MENUITEM "Save" MENUITEM "Close" END POPUP "Edit" BEGIN MENUITEM "Cut" MENUITEM "Copy" MENUITEM "Paste" END POPUP "View" MENUITEM "Help" END
现在,在“视图”类别中,我们希望再创建一个弹出菜单,显示“工具栏”。当我们将鼠标悬停在“工具栏”命令上时,将在右侧打开一个子菜单,其中包含所有选择项:
IDM_MY_MENU MENU DISCARDABLE BEGIN POPUP "File" BEGIN MENUITEM "Open" MENUITEM "Save" MENUITEM "Close" END POPUP "Edit" BEGIN MENUITEM "Cut" MENUITEM "Copy" MENUITEM "Paste" END POPUP "View" BEGIN POPUP "Toolbars" BEGIN MENUITEM "Standard" MENUITEM "Custom" END END MENUITEM "Help" END
这起初比较容易,只是现在我们需要提供一种方法来将我们的菜单与程序连接起来。为此,我们必须为每个 MENUITEM 分配一个命令标识符,我们可以在头文件中定义它。通常,使用“IDC_”前缀来命名这些命令资源,然后加上简短的文本来说明它是什么。例如,对于“文件 > 打开”命令,我们将使用名为“IDC_FILE_OPEN”的 ID。我们将在后面的资源头文件中定义所有这些 ID 标签。以下是包含所有 ID 的菜单:
IDM_MY_MENU MENU DISCARDABLE BEGIN POPUP "File" BEGIN MENUITEM "Open", IDC_FILE_OPEN MENUITEM "Save", IDC_FILE_SAVE MENUITEM "Close", IDC_FILE_CLOSE END POPUP "Edit" BEGIN MENUITEM "Cut", IDC_EDIT_CUT MENUITEM "Copy", IDC_EDIT_COPY MENUITEM "Paste", IDC_EDIT_PASTE END POPUP "View" BEGIN POPUP "Toolbars" BEGIN MENUITEM "Standard", IDC_VIEW_STANDARD MENUITEM "Custom", IDC_VIEW_CUSTOM END END MENUITEM "Help", IDC_HELP END
当我们单击窗口中的一个条目时,消息循环将收到一个 WM_COMMAND 消息,消息中的 WPARAM 参数包含标识符。
我们将在头文件中定义所有标识符,使其在任意范围内成为数值,并且不会与其他输入源(加速器表、按钮等)的命令标识符重叠。
//resource.h
#define IDC_FILE_OPEN 200
#define IDC_FILE_SAVE 201
#define IDC_FILE_CLOSE 202
#define IDC_EDIT_COPY 203
#define IDC_EDIT_CUT 204
#define IDC_EDIT_PASTE 205
#define IDC_VIEW_STANDARD 206
#define IDC_VIEW_CUSTOM 207
#define IDC_HELP 208
然后,我们将此资源头文件包含到主程序代码文件和资源脚本中。当我们想要将菜单加载到程序中时,我们需要创建一个指向菜单的句柄,即 HMENU。HMENU 数据项的大小和形状与其他句柄类型相同,只是它们专门用于指向菜单。
当我们启动程序时,通常是在 WinMain 函数中,我们将使用 HMENU 数据项使用 LoadMenu 函数获取指向此菜单的句柄:
HMENU hmenu;
hmenu = LoadMenu(hInst, MAKEINTRESOURCE(IDM_MY_MENU));
我们将在下面的另一部分讨论如何使用此句柄来使菜单显示出来。
要将菜单与窗口类关联,我们需要将菜单的名称包含在 WNDCLASS 结构中。请记住 WNDCLASS 结构:
typedef struct {
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
} WNDCLASS, *PWNDCLASS;
它有一个名为“lpszMenuName”的数据字段。我们将在这里包含菜单的 ID:
WNDCLASS wnd;
wnd.lpszMenuName = MAKEINTRESOURCE(IDM_MY_MENU);
请记住,我们需要使用 MAKEINTRESOURCE 关键字将数字标识符(IDM_MY_MENU)转换为适当的字符串指针。
接下来,在将菜单与窗口类关联后,我们需要获取指向菜单的句柄:
HMENU hmenu;
hmenu = LoadMenu(hInst, MAKEINTRESOURCE(IDM_MY_MENU));
在获得指向菜单的 HMENU 句柄后,我们可以将其提供给 CreateWindow 函数,以便在创建窗口时创建菜单:
HWND CreateWindow(
LPCTSTR lpClassName,
LPCTSTR lpWindowName,
DWORD dwStyle,
int x,
int y,
int nWidth,
int nHeight,
HWND hWndParent,
HMENU hmenu,
HINSTANCE hInstance,
LPVOID lpParam
);
我们将 HMENU 句柄传递给 CreateWindow 函数调用的 hMenu 参数。这是一个简单的示例:
HWND hwnd;
hwnd = CreateWindow(szClassName, "Menu Test Window!",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT,
NULL,
hmenu,
hInstance,
0);
快速回顾一下,请注意我们正在为所有位置和大小属性使用默认值。我们将新窗口定义为 WS_OVERLAPPEDWINDOW,这是一种常见且普通的窗口类型。此外,窗口的标题栏将显示“菜单测试窗口!”。我们还需要传递 HINSTANCE 参数,它是倒数第二个参数。