65.9K
CodeProject 正在变化。 阅读更多。
Home

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

2011年7月20日

公共领域

8分钟阅读

viewsIcon

251695

使用纯 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_INITDIALOGWM_COMMANDWM_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 日:初始版本
© . All rights reserved.