窗体初始化

Win32API中最重要的一组API就是窗体的创建,它主要涉及注册窗体类和创建窗体两个过程。这篇笔记我们具体学习Win32API中窗体相关的概念和用法。

基本概念

在具体学习如何创建窗体前,我们需要理解一些基本概念,这里我们以相对简短的语言来描述。当然,初次学习这些概念可能还是比较模糊,稀里糊涂看一遍可能眼花缭乱连“窗”字都不认识了,具体还是要结合后面的例子来理解。

窗体

窗体(窗口)是Windows操作系统中最重要的概念,毕竟操作系统名字都叫Windows(窗体)操作系统。区别于控制台程序,我们打开任何一个图形界面应用程序,大部分都会弹出一个或多个窗体。一个常见的主窗体包含标题栏、菜单栏、状态栏等信息。在Windows中,窗体由一个窗体句柄HWND标识。

窗体还是一个树形结构,也就是说窗体可以包含子窗体。实际上在Win32API中,按钮、输入框等控件都是子窗体,我们也可以通过自定义子窗体的方式实现更多控件,这些子窗体也都有对应的HWND窗体句柄,我们可以使用Visual Studio自带的Spy++工具证明这一点。

不过实际上如果你真的尝试过使用Spy++,你会发现一些软件正如前面所说,是窗体嵌套控件子窗体实现的,主窗体和每个控件都可以找到对应的句柄;而还有许多软件只有主窗体有句柄,这些软件通常采用了自绘的方式绘制按钮等“控件”,因此它们在Windows操作系统内核中没有申请对应的句柄。直接使用Win32API开发的程序、MFC程序、Winform程序大多属于前者,而Qt、Electron、Swing程序通常属于后者。

窗体类 WNDCLASS

什么是窗体类?它可以理解为一个窗体的“原型”,包含了窗体的样式和行为逻辑,具体的一个窗体是窗体类的“实例”。Win32API中窗体类被定义为一个叫WNDCLASS的结构体。具体来说:

  • 窗体类中包含了窗体的各种参数信息和数据结构
  • 创建窗体必须指定窗体类,每个窗体都一定有窗体类
  • 每个窗体类都有一个名称,窗体类使用前必须先注册到操作系统

初始化窗体的步骤

Win32程序中,初始化一个窗体是十分复杂的,但它们通常遵循固定的步骤。

  1. 定义窗体处理函数,用于处理消息
  2. 调用RegisterClass()函数向Windows内核注册WNDCLASS窗体类
  3. 调用CreateWindow()函数创建窗体
  4. 调用ShowWindow()函数显示窗体
  5. 开启消息循环,将消息交给窗体处理函数处理

初始化窗体代码例子

光上面这样描述可能比较模糊,我们还是通过例子的方式进行介绍,下面是一个初始化窗体的例子。

#include <windows.h>

// 声明窗体的处理逻辑函数
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    LPCTSTR appName = TEXT("HelloWin");

    // 注册窗体类
    WNDCLASS wndclass;                                              // 声明WNDCLASS,其为包含窗体许多属性的结构体,具体可参考MSDN
    wndclass.style = CS_HREDRAW | CS_VREDRAW;                       // 客户区移动或调整水平大小时重绘 | 客户区移动或调整垂直大小时重绘
    wndclass.lpfnWndProc = WndProc;                                 // 窗体处理函数的函数指针
    wndclass.cbClsExtra = 0;                                        // 窗体类额外多分配若干字节内存(以用于保存自定义数据),一般传0
    wndclass.cbWndExtra = 0;                                        // 窗体额外多分配若干字节内存(以用于保存自定义数据),一般传0
    wndclass.hInstance = hInstance;                                 // 传入应用程序的实例句柄(从WinMain接收)
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);               // 窗体图标
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);                 // 窗体光标
    wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);   // 用于在窗体中绘制的背景颜色
    wndclass.lpszMenuName = NULL;                                   // 窗体菜单栏
    wndclass.lpszClassName = appName;                               // 窗体类名称

    if (!RegisterClass(&wndclass))                                  // 注册窗体
    {
        MessageBox(NULL, TEXT("This program requires Windows NT!"), appName, MB_ICONERROR);
        return 0;
    }

    // 创建窗体实例
    HWND hwnd;                  // 声明窗体的句柄
    hwnd = CreateWindow(
        appName,                // 窗体类名称
        TEXT("Window程序示例"), // 窗体标题
        WS_OVERLAPPEDWINDOW,    // 窗体风格
        CW_USEDEFAULT,          // 初始X坐标
        CW_USEDEFAULT,          // 初始Y坐标
        CW_USEDEFAULT,          // 初始宽度
        CW_USEDEFAULT,          // 初始高度
        NULL,                   // 父窗体句柄
        NULL,                   // 窗体菜单句柄
        hInstance,              // 应用程序实例句柄
        NULL                    // 创建参数
    );

    // 显示窗体hwnd即窗体句柄,nCmdShow用于指定正常显示、最大化显示,还是最小化显示
    ShowWindow(hwnd, nCmdShow);

    // 立即重绘一次
    UpdateWindow(hwnd);

    // 开启消息循环
    MSG msg;                                // 窗体接收的消息对象
    while (GetMessage(&msg, NULL, 0, 0))    // 循环接收消息,无消息时阻塞等待
    {
        TranslateMessage(&msg);             // 将虚拟键消息转换为字符消息,一般如此固定写法
        DispatchMessage(&msg);              // 将UI线程的消息分派给窗体
    }
    return msg.wParam;
}

// 窗体的消息处理函数,固定参数:接收消息的窗体句柄,标识消息的数字,后两个参数是消息包含的信息
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    HDC hdc;
    PAINTSTRUCT ps;
    RECT rect;

    switch (message)
    {
    case WM_PAINT:
        // 收到WM_PAINT消息,调用GDI绘制文字
        hdc = BeginPaint(hwnd, &ps);
        GetClientRect(hwnd, &rect);
        DrawText(hdc, TEXT("你好,窗体!"), -1, &rect,
            DT_SINGLELINE | DT_CENTER | DT_VCENTER);
        EndPaint(hwnd, &ps);
        return 0;
    case WM_DESTROY:
        // 收到WM_DESTROY,即窗体上点击关闭按钮时,向UI线程发送一个WM_QUIT消息,携带返回值'0',使得应用程序从WinMain退出
        PostQuitMessage(0);
        return 0;
    default:
        // 其它我们不关心的消息,交给默认消息处理函数
        return DefWindowProc(hwnd, message, wParam, lParam);
    }
}

运行结果:

下面我们再具体分析一下代码的执行流程。

首先,程序进入Windows窗体程序的主函数WinMain,然后注册窗口类、创建窗口、显示窗口,最后开启了消息循环。我们实际可以发现,初始化一个窗口的参数是极其复杂的,这些初始化参数分为窗口类WNDCLASS的成员变量和CreateWindow函数参数两个部分,这种封装形式其实是为了更好的进行复用。窗口类WNDCLASS的参数需要用RegisterClass函数进行注册,CreateWindow参数直接传入即可。

通过CreateWindow我们就可以得到新创建的窗口的句柄了,紧接着我们调用了ShowWindowUpdateWindow这两个函数,用于显示窗口以及作为初始化时的重绘。实际上,UpdateWindow并不真的重绘窗口,而是发送一个WM_PAINT消息。

之后我们用while开启了消息循环,当收到WM_QUIT消息时,GetMessage会返回0,程序退出。

WndProc是我们自己定义的窗口处理逻辑,其本质实际上就是处理一些窗口收到的消息:

  • WM_PAINT:窗口重绘时发出,这里我们在窗口上使用GDI绘制文字
  • WM_DESTROY:窗口关闭时发出,这里我们再次发出WM_QUIT消息,指示GetMessage返回,以退出应用程序

注意DefWindowProc,调用这个函数以执行对其它未处理消息的默认处理逻辑,否则你的窗口会有大量消息不能正确响应。

有关Win32的消息机制将在后续章节详细介绍。

窗体类

前面例子中,我们自定义了一个窗体类,它的名字是HelloWin。实际上,Windows中窗体类分为几个种类:

  1. 系统窗体类:Windows预定义的窗体类,包括按钮、输入框等实际上都是通过系统窗体类实现的
  2. 局部窗体类:我们自定义的窗体类,用于配置我们程序内部创建的窗体
  3. 全局窗体类:我们自定义的窗体类,可以跨程序实例使用,不推荐使用,这里仅做了解

局部窗体类和全局窗体类

对于自定义的局部窗体类,我们这里主要关注窗体类结构体的lpszClassNamehInstance属性,它们分别是窗体类名字字符串和程序实例句柄,实际上,这两个参数就唯一标识了一个窗体类。在后续使用CreateWindow()创建窗体实例时,我们也要传窗体类名字字符串和程序实例句柄,窗体实例和窗体类就是通过这两个参数进行关联的。

注:至于全局窗体类,它仅通过窗体类名字就可以标识,因此可以跨程序实例使用。通过指定WNDCLASSstyle属性为CS_GLOBALCLASS即可指定一个窗体类为全局窗体类。然而,很多情况下全局窗体类会造成混乱,它存在潜在的冲突可能,也破坏了程序的封装性,因此微软已经不推荐使用全局窗体类了。

此外,我们还需要关注窗体类的style属性,它指定了窗体类的一些基本表现行为。例子中,我们使用的CS_HREDRAW | CS_VREDRAW指定了客户区在移动或调整水平垂直大小时进行重绘,这是一个比较常用的主窗体属性组合设置,除此之外还有很多其它可用的属性,具体可以参考MSDN中的相关章节。

使用系统窗体类

系统窗体类是Windows系统内部预定义的,我们可以直接使用。下面例子代码中,我们使用系统窗体类BUTTON创建了一个按钮。

HWND buttonHwnd = CreateWindow(TEXT("BUTTON"), TEXT("Click Me"),
    WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON,
    50, 50, 100, 30,
    hwnd, NULL, hInstance, NULL
);

有关其它系统窗体类和其属性设置,可以参考MSDN文档。

窗体实例

创建窗体简而言之就是调用CreateWindow函数,该函数有很多参数,其中最重要的就是窗体类名字字符串和程序实例句柄,这在前面已经介绍过了,通过这两个参数窗体才能关联到一个窗体类上,此外,还传入了窗体的标题、光标、位置、大小等信息。

窗体风格

CreateWindow函数创建窗体时有一个dwStyle参数,它指定了窗体的风格。Win32API中定义了大量的选项供我们创建各种风格的窗体,例如是否显示边框、是否显示标题栏、是否显示水平或垂直滚动条等,其中我们使用的WS_OVERLAPPEDWINDOW其实是一个组合属性,它的实际值等效于WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME |WS_MINIMIZEBOX | WS_MAXIMIZEBOX

我们这里就不对窗体风格逐一列举了,具体参考MSDN文档即可。

子窗体

窗体还有一个重要的使用方式就是子窗体,前面我们介绍的按钮其实就是一个子窗体,它在使用CreateWindow创建时传入了hWndParent参数,即父窗体的句柄,此时创建的窗体就是子窗体。

HWND childHwnd1 = CreateWindow(TEXT("MyChildWindClass"), TEXT("Child1"),
    WS_OVERLAPPEDWINDOW | WS_CHILD | WS_VISIBLE,
    0, 0, 100, 100,
    hwnd, NULL, hInstance, NULL
);

运行结果如下,它类似于一个MDI的效果。

这里我们给子窗体设置了一个WS_CHILD风格,它表示该窗体是子窗体,子窗体会被限制在父窗体内,此外要注意具有此样式的窗体不能有菜单栏。另外我们还设置了WS_VISIBLE风格,子窗体和主窗体不同,显示子窗体需要设置WS_VISIBLE窗体风格,而主窗体是通过调用ShowWindow函数来显示的。

多窗体程序

下面代码中,我们创建了两个窗体,只有当两个窗体都关闭时,程序才结束。

#include <windows.h>

ATOM RegisterWndClass(HINSTANCE hInstance, WNDPROC winProc, LPCTSTR className);

LRESULT CALLBACK WndProc1(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK WndProc2(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    LPCTSTR className1 = TEXT("wnd01");
    LPCTSTR className2 = TEXT("wnd02");
    RegisterWndClass(hInstance, WndProc1, className1);
    RegisterWndClass(hInstance, WndProc2, className2);
    HWND hWnd1 = CreateWindow(
                     className1,
                     TEXT("窗口01"),
                     WS_OVERLAPPEDWINDOW,
                     CW_USEDEFAULT,
                     CW_USEDEFAULT,
                     CW_USEDEFAULT,
                     CW_USEDEFAULT,
                     NULL,
                     NULL,
                     hInstance,
                     NULL
                 );
    HWND hWnd2 = CreateWindow(
                     className2,
                     TEXT("窗口02"),
                     WS_OVERLAPPEDWINDOW,
                     CW_USEDEFAULT,
                     CW_USEDEFAULT,
                     CW_USEDEFAULT,
                     CW_USEDEFAULT,
                     NULL,
                     NULL,
                     hInstance,
                     NULL
                 );
    ShowWindow(hWnd1, nCmdShow);
    ShowWindow(hWnd2, nCmdShow);
    UpdateWindow(hWnd1);
    UpdateWindow(hWnd2);
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))
    {
        if (!IsWindow(hWnd1) && !IsWindow(hWnd2))
        {
            PostQuitMessage(0);
        }
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return 0;
}

ATOM RegisterWndClass(HINSTANCE hInstance, WNDPROC wndProc, LPCTSTR className)
{
    WNDCLASS wndclass;
    wndclass.style = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc = wndProc;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hInstance = hInstance;
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = className;
    return RegisterClass(&wndclass);
}

LRESULT CALLBACK WndProc1(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch(message)
    {
    case WM_LBUTTONDOWN:
        MessageBox(hwnd, TEXT("窗口1被点击了"), TEXT("提示"), MB_OK);
        return 0;
    default:
        return DefWindowProc(hwnd, message, wParam, lParam);
    }
}

LRESULT CALLBACK WndProc2(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch(message)
    {
    case WM_LBUTTONDOWN:
        MessageBox(hwnd, TEXT("窗口2被点击了"), TEXT("提示"), MB_OK);
        return 0;
    default:
        return DefWindowProc(hwnd, message, wParam, lParam);
    }
}

代码中,我们并没有在单个窗体中通过发送WM_QUIT来结束消息循环,因为这里我们要求两个窗体均关闭才结束程序。我们这里是在消息循环中,判断两个窗体是否都已关闭销毁,如果两个窗体都不存在,才发送WM_QUIT结束消息循环,进而结束程序。

实现模态窗体

win32api中,并没有直接实现模态窗体的接口,我们需要手动来实现,创建模态窗体基本步骤如下:

  1. 使用EnableWindow()将原窗体禁用(禁用后不再接收用户输入)
  2. 使用CreateWindow()创建模态窗体
  3. 在当前UI线程开启新的消息循环

模态窗体关闭时,步骤如下:

  1. 模态窗体通过处理WM_DESTROY,结束新创建的消息循环,回到之前的消息循环
  2. 使用EnableWindow()将原窗体恢复
  3. 使用SetForegroundWindow()使焦点回到原窗体

下面例子代码中,在“窗体1”上点击鼠标,会打开模态窗体“窗体2”。模态窗体打开时,原窗体不能接收用户输入,在其上点击鼠标,会看到“窗体2”的闪动提示。关闭“窗体2”,“窗体1”恢复。

#include <windows.h>

ATOM RegisterWndClass(HINSTANCE hInstance, WNDPROC winProc, LPCTSTR className);

LRESULT CALLBACK WndProc1(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
LRESULT CALLBACK WndProc2(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    LPCTSTR className1 = TEXT("wnd01");
    RegisterWndClass(hInstance, WndProc1, className1);
    HWND hWnd1 = CreateWindow(
                     className1,
                     TEXT("窗体01"),
                     WS_OVERLAPPEDWINDOW,
                     CW_USEDEFAULT,
                     CW_USEDEFAULT,
                     CW_USEDEFAULT,
                     CW_USEDEFAULT,
                     NULL,
                     NULL,
                     hInstance,
                     NULL
                 );
    ShowWindow(hWnd1, nCmdShow);
    UpdateWindow(hWnd1);

    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return 0;
}

ATOM RegisterWndClass(HINSTANCE hInstance, WNDPROC wndProc, LPCTSTR className)
{
    WNDCLASS wndclass;
    wndclass.style = CS_HREDRAW | CS_VREDRAW;
    wndclass.lpfnWndProc = wndProc;
    wndclass.cbClsExtra = 0;
    wndclass.cbWndExtra = 0;
    wndclass.hInstance = hInstance;
    wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
    wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wndclass.lpszMenuName = NULL;
    wndclass.lpszClassName = className;
    return RegisterClass(&wndclass);
}

LRESULT CALLBACK WndProc1(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch(message)
    {
    case WM_LBUTTONDOWN:
    {
        EnableWindow(hwnd, FALSE);
        HINSTANCE hInstance = GetModuleHandle(NULL);
        LPCTSTR className2 = TEXT("wnd02");
        RegisterWndClass(hInstance, WndProc2, className2);
        HWND hWnd2 = CreateWindow(
                         className2,
                         TEXT("窗体02"),
                         WS_OVERLAPPEDWINDOW,
                         CW_USEDEFAULT,
                         CW_USEDEFAULT,
                         CW_USEDEFAULT,
                         CW_USEDEFAULT,
                         hwnd,
                         NULL,
                         hInstance,
                         NULL
                     );
        ShowWindow(hWnd2, SW_SHOW);
        UpdateWindow(hWnd2);
        MSG msg;
        while (GetMessage(&msg, NULL, 0, 0))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        EnableWindow(hwnd, TRUE);
        SetForegroundWindow(hwnd);
        return 0;
    }
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    default:
        return DefWindowProc(hwnd, message, wParam, lParam);
    }
}

LRESULT CALLBACK WndProc2(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch(message)
    {
    case WM_LBUTTONDOWN:
        MessageBox(hwnd, TEXT("窗体2被点击了"), TEXT("提示"), MB_OK);
        return 0;
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    default:
        return DefWindowProc(hwnd, message, wParam, lParam);
    }
}

关于模态窗体的实现可能需要我们对Windows的消息机制有更进一步的理解,这将在下一章节介绍。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。