基于对话框的 Win32 C 程序,循序渐进






4.84/5 (56投票s)
使用纯 Win32 C 代码编写基于对话框的程序。
1. 引言
编写纯 Win32 程序时,通常会看到教程演示如何使用“原生”窗口,通过填充 WNDCLASSEX
结构,调用 RegisterClassEx
,然后调用 CreateWindowEx
。Charles Petzold 的经典著作 Programming Windows 详细解释了这一点——我必须说,这是任何 Win32 程序员的必备书籍。
但有时你不需要从头开始创建一个全新的窗口,一个简单的对话框就能满足你的需求。
在本文中,我将从头开始,一步步讨论如何将对话框作为程序的主窗口。使用任何资源编辑器都可以快速创建对话框资源——包含标签、编辑框和按钮。我将使用 Visual Studio 2008,但步骤对于其他 Visual Studio 版本,甚至其他 IDE 来说都应该相似。
我将使用纯 Win32 C 代码,以尽可能简单的方式进行:不使用 MFC,不使用 ATL,不使用 WTL,或其他任何库。我还将使用 TCHAR
函数(声明在tchar.h 中,更多信息 在此),使代码在 ANSI 和 Unicode 下具有可移植性,并且只使用兼容 x86 和 x64 的函数。
1.1. 程序结构
我们的程序将由三个文件组成:
- C 源文件——我们将实际编写的源代码,也是本文的中心主题;
- RC 资源脚本——描述对话框资源,可以由 Visual Studio 或任何资源编辑器轻松创建,甚至手工编写,然后用资源编译器进行编译;以及
- H 资源头文件——只是 RC 文件中用于标识资源的宏常量,通常与 RC 脚本一起自动创建。
2. 对话框
在编写 C 源代码之前,我们将创建一个空项目并为其添加一个对话框资源。这样做时,会创建一个资源脚本,其中包含对话框代码。让我们开始一个新项目:
在左侧树状图中选择“Visual C++”和“Win32”,然后选择“Win32 项目”,并为其命名。注意保存的目录。然后单击“确定”。
现在选择“Windows 应用程序”和“空项目”。创建空项目时,Visual Studio 不会为我们创建任何文件,这很重要,因为我们要创建一个纯 Win32 程序,不包含额外的库。然后,单击“完成”。
现在,让我们添加对话框。在解决方案资源管理器窗口中——如果看不到,请在“视图”菜单中启用它——右键单击项目名称,然后选择“添加”、“资源”。
在这里,您可以看到一些资源项,它们的脚本可以由 Visual Studio 自动生成。我们只使用对话框,所以选择“Dialog”并单击“新建”。
完成后,您应该会在资源编辑器中看到您的对话框,您可以通过鼠标拖放来添加控件——如编辑框、按钮和标签——并快速地定位和排列它们——这比使用“原生窗口”应用程序要快得多,因为在原生窗口应用程序中,您必须直接处理代码。我的对话框看起来是这样的:
此时,我们有了资源脚本和资源头文件,可以在解决方案资源管理器中看到它们。现在是时候编写源代码来让这个对话框“活”起来了。
3. 源代码
让我们向项目添加一个空源文件。在解决方案资源管理器中,右键单击“源文件”文件夹,然后选择“添加”、“新建项”。然后为文件命名,例如“main.c”。
在 Visual Studio 中,默认情况下,源文件将根据文件扩展名进行编译:C 文件编译为纯 C;CPP、CXX(以及其他一些)编译为 C++。这里我们将编写 C 代码,但也可以将其编译为 C++,因此文件扩展名可以是上述任何一种。特别是,我使用了 C 扩展名,以清楚地表明这是一个纯 C 程序。
我们的 C 源文件将只包含两个函数:
WinMain
——程序入口点,其中包含主程序循环;以及DialogProc
——对话框过程,它将处理对话框消息。
让我们开始编写正常的 Win32 入口点函数(的 TCHAR
版本):
#include <Windows.h>
#include <tchar.h>
/* usually, the resource editor creates this file to us: */
#include "resource.h"
int _tWinMain(HINSTANCE hInst, HINSTANCE h0, LPCTSTR lpCmdLine, int nCmdShow)
{
return 0;
}
4. 对话框创建和消息循环
对话框将在 WinMain
函数中使用 CreateDialogParam
函数(而不是 CreateWindowEx
)创建,并且不需要窗口类注册。然后,我们通过调用 ShowWindow
使其可见。
HWND hDlg;
hDlg = CreateDialogParam(hInst, MAKEINTRESOURCE(IDD_DIALOG1), 0, DialogProc, 0);
ShowWindow(hDlg, nCmdShow);
IDD_DIALOG1
是我们对话框的资源标识符,在resource.h 中声明。DialogProc
是我们的对话框过程,它将处理所有对话框消息——稍后我将展示它。
然后是主程序消息循环。它是任何 Win32 程序的“心脏”——可以将其视为操作系统和您的程序之间的桥梁。它也存在于常见的“原生窗口”程序中,尽管略有不同。在这里,消息循环专门用于将对话框作为主窗口处理。
BOOL ret;
MSG msg;
while((ret = GetMessage(&msg, 0, 0, 0)) != 0) {
if(ret == -1) /* error found */
return -1;
if(!IsDialogMessage(hDlg, &msg)) {
TranslateMessage(&msg); /* translate virtual-key messages */
DispatchMessage(&msg); /* send it to dialog procedure */
}
}
如果消息属于对话框,IsDialogMessage
函数会立即将其转发给我们的对话框过程。否则,消息将进入常规处理。有关消息循环的更多信息可以在此处找到。
有可能绕过此程序循环(不编写它),正如 Iczelion 在他的精彩 Win32 汇编文章系列的第 10 课中所解释的那样。但是,这样做的话,我们的控制就少了:我们无法在循环中进行任何验证,例如加速键处理。所以,让我们在代码中保留循环。
4.1. 启用视觉样式
为了获得 Windows XP 引入的通用控件 6 视觉样式,您不仅必须调用 InitCommonControls
(声明在CommCtrl.h 中),还必须将 manifest XML 文件嵌入到您的代码中。幸运的是,Visual C++ 编译器有一个方便的技巧,我从Raymond Chen 的博客中学到的。只需在代码中添加以下内容:
#pragma comment(linker, \
"\"/manifestdependency:type='Win32' "\
"name='Microsoft.Windows.Common-Controls' "\
"version='6.0.0.0' "\
"processorArchitecture='*' "\
"publicKeyToken='6595b64144ccf1df' "\
"language='*'\"")
这将自动生成并嵌入 XML manifest 文件,您将再也不用担心它了。
要调用 InitCommonControls
,您必须将程序静态链接到ComCtl32.lib,这也可以通过 #pragma comment
指令来实现。
#pragma comment(lib, "ComCtl32.lib")
4.2. 我们的 WinMain
到目前为止,这是我们的完整 WinMain
函数(尚未包含对话框过程):
#include <Windows.h>
#include <CommCtrl.h>
#include <tchar.h>
#include "resource.h"
#pragma comment(linker, \
"\"/manifestdependency:type='Win32' "\
"name='Microsoft.Windows.Common-Controls' "\
"version='6.0.0.0' "\
"processorArchitecture='*' "\
"publicKeyToken='6595b64144ccf1df' "\
"language='*'\"")
#pragma comment(lib, "ComCtl32.lib")
int WINAPI _tWinMain(HINSTANCE hInst, HINSTANCE h0, LPTSTR lpCmdLine, int nCmdShow)
{
HWND hDlg;
MSG msg;
BOOL ret;
InitCommonControls();
hDlg = CreateDialogParam(hInst, MAKEINTRESOURCE(IDD_DIALOG1), 0, DialogProc, 0);
ShowWindow(hDlg, nCmdShow);
while((ret = GetMessage(&msg, 0, 0, 0)) != 0) {
if(ret == -1)
return -1;
if(!IsDialogMessage(hDlg, &msg)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return 0;
}
5. 对话框过程
对话框过程负责处理所有程序消息,响应所有事件。它这样开始:
INT_PTR CALLBACK DialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
return FALSE;
}
Windows 会为每个程序消息调用此函数。如果我们返回 FALSE
,则意味着 Windows 可以对消息执行默认处理,因为我们对此不感兴趣;如果我们返回 TRUE
,我们告诉 Windows 我们实际上已经处理了该消息。这与通过WndProc
进行“原生”窗口消息处理略有不同,在原生窗口消息处理中,您会返回对 DefWindowProc
函数的调用。在这里,我们不要调用 DefWindowProc
,只需返回 FALSE
。
因此,我们只会编写我们感兴趣的消息的处理。在最常用的消息中,我们有 WM_INITDIALOG
、WM_COMMAND
和 WM_SIZE
。但要构建一个最小的功能程序,我们只需要两个:
switch(uMsg)
{
case WM_CLOSE: /* there are more things to go here, */
return TRUE; /* just continue reading on... */
case WM_DESTROY:
return TRUE;
}
请注意,使用对话框时,我们不需要处理 WM_PAINT
消息。
5.1. 最小消息处理
WM_CLOSE
在窗口关闭之前被调用。如果您想询问用户是否真的想关闭程序,可以在这里进行检查。要关闭窗口,我们调用 DestroyWindow
——如果我们不调用它,窗口将不会关闭。
因此,这里是消息处理,也提示了用户。如果您不需要提示用户,只需省略 MessageBox
检查并直接调用 DestroyWindow
。并且不要忘记在这里返回 TRUE
,无论您是否关闭窗口。
case WM_CLOSE:
if(MessageBox(hDlg,
TEXT("Close the window?"), TEXT("Close"),
MB_ICONQUESTION | MB_YESNO) == IDYES)
{
DestroyWindow(hDlg);
}
return TRUE;
最后,我们必须处理 WM_DESTROY
消息,告知 Windows 我们要退出主程序线程。我们通过调用 PostQuitMessage
函数来做到这一点。
case WM_DESTROY:
PostQuitMessage(0);
return TRUE;
WM_DESTROY
消息也是释放程序已分配但仍待释放的资源的最佳位置——这是最终清理时间。但请记住,在调用 PostQuitMessage
之前进行清理。
5.2. 按 ESC 键关闭
对话框的一个有趣特性是,它们可以轻松编程为在用户按下 ESC 键时关闭,并且当对话框是主窗口时也可以实现。要做到这一点,我们必须处理 WM_COMMAND
消息并等待 IDCANCEL
标识符,它出现在 WPARAM
参数的低字中,这就是我们所做的:
case WM_COMMAND:
switch(LOWORD(wParam))
{
case IDCANCEL:
SendMessage(hDlg, WM_CLOSE, 0, 0);
return TRUE;
}
break;
IDCANCEL
标识符在WinUser.h 中声明,它包含在Windows.h 中,因此始终可用。
请注意,在处理 IDCANCEL
时,我们向对话框窗口发送了一个 WM_CLOSE
消息,这会导致对话框过程再次被调用,并处理我们之前编写的 WM_CLOSE
消息,因此用户将被提示是否要关闭窗口(因为我们就是这样编写的)。
6. 最终程序
这是我们的最终程序。它是功能最小化的基于对话框的 Win32 程序的 C 源代码,具有正确启用的消息循环和视觉样式,随时可用。您可以将其保留作为任何基于对话框的 Win32 程序的骨架。
#include <Windows.h>
#include <CommCtrl.h>
#include <tchar.h>
#include "resource.h"
#pragma comment(linker, \
"\"/manifestdependency:type='Win32' "\
"name='Microsoft.Windows.Common-Controls' "\
"version='6.0.0.0' "\
"processorArchitecture='*' "\
"publicKeyToken='6595b64144ccf1df' "\
"language='*'\"")
#pragma comment(lib, "ComCtl32.lib")
INT_PTR CALLBACK DialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch(uMsg)
{
case WM_COMMAND:
switch(LOWORD(wParam))
{
case IDCANCEL:
SendMessage(hDlg, WM_CLOSE, 0, 0);
return TRUE;
}
break;
case WM_CLOSE:
if(MessageBox(hDlg, TEXT("Close the program?"), TEXT("Close"),
MB_ICONQUESTION | MB_YESNO) == IDYES)
{
DestroyWindow(hDlg);
}
return TRUE;
case WM_DESTROY:
PostQuitMessage(0);
return TRUE;
}
return FALSE;
}
int WINAPI _tWinMain(HINSTANCE hInst, HINSTANCE h0, LPTSTR lpCmdLine, int nCmdShow)
{
HWND hDlg;
MSG msg;
BOOL ret;
InitCommonControls();
hDlg = CreateDialogParam(hInst, MAKEINTRESOURCE(IDD_DIALOG1), 0, DialogProc, 0);
ShowWindow(hDlg, nCmdShow);
while((ret = GetMessage(&msg, 0, 0, 0)) != 0) {
if(ret == -1)
return -1;
if(!IsDialogMessage(hDlg, &msg)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return 0;
}
7. 进一步的组织
通常,当您的程序变得复杂时,您最终会有一个包含大量代码的庞大 DialogProc
,因此维护起来非常困难。一种有用的方法是使用函数调用来处理每个消息——著名的子程序。这特别有用,因为它隔离了代码的逻辑,您可以清楚地看到程序的每个部分,从而产生响应事件的概念。例如,我们的程序可以有两个专用函数:
void onCancel(HWND hDlg)
{
SendMessage(hDlg, WM_CLOSE, 0, 0);
}
void onClose(HWND hDlg)
{
if(MessageBox(hDlg, TEXT("Close the program?"), TEXT("Close"),
MB_ICONQUESTION | MB_YESNO) == IDYES)
{
DestroyWindow(hDlg);
}
}
这将允许我们重写 DialogProc
,如下所示:
INT_PTR CALLBACK DialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch(uMsg) /* more compact, each "case" through a single line, easier on the eyes */
{
case WM_COMMAND:
switch(LOWORD(wParam))
{
case IDCANCEL: onCancel(hDlg); return TRUE; /* call subroutine */
}
break;
case WM_CLOSE: onClose(hDlg); return TRUE; /* call subroutine */
case WM_DESTROY: PostQuitMessage(0); return TRUE;
}
return FALSE;
}
历史
- 2011 年 7 月 20 日:初始版本