创建一个Win32窗口

原创
2022-12-06
4673
15

1. GUI入口函数

GUI应用程序的入口函数是 WinMain , 这是一个自定义的入口函数。WinMain函数采用的是windows标准调用方式。下面是简单的windows程序:

#include <windows.h>
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPreInstance, LPSTR lpCmdLine, int nCmdShow)
{
    ::MessageBox(nullptr, TEXT("Hello, Win32 Application"), TEXT("First Win32 Window"), MB_OK);
    return 0;
}

系统传给WinMain函数几个参数定义如下:

  • hInstance: 制定当前模块的实例句柄。在win32下,模块的实例句柄和模块句柄是一样的,只是说法不同。所以可以通过下面语句获取当前执行模块的实例句柄。
hInsatnce = (HINSTANCE)GetModuleHandle(nullptr);

GetModuleHandle 函数的唯一参数是模块名称,函数返回这个模块句柄。如果传递nullptr的话函数返回可执行文件所在模块的模块句柄。

  • lpCmdLine 是命令行参数。其值是由CreateProcess函数的第二个参数指定。
  • nCmdShow 指定窗口初始化的显示方式。

当Windows向程序发送消息时,他调用程序的一个函数,这个函数的参数精确的描述了Windows发送的消息。在程序中称为窗口函数或消息处理函数。它是一个自定义的回掉函数,原型如下:

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
hwnd 标识了消息的到达窗口
uMsg 参数时一个被命名的常量(消息ID)

下面是一个简单示例

#include <windows.h>
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPreInstance, LPSTR lpCmdLine, int nCmdShow)
{
    HWND hWnd = ::FindWindow(nullptr, TEXT("无标题 - 记事本"));
    if (hWnd != nullptr)
        ::SendMessage(hWnd, WM_CLOSE, 0, 0);
    return 0;
}

FindWindow 函数会查找窗口类名和窗口标题与指定字符串相匹配的窗口。返回找到的窗口句柄; SendMessage 用于向窗口中发送消息。


2. 创建窗口

下面是创建一个窗口的完整代码

#include <windows.h>
#include <iostream>

LRESULT CALLBACK MainWindProc(HWND, UINT, WPARAM, LPARAM);

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPreInstance, LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASSEX wndClass;
    wndClass.cbSize = sizeof(wndClass);
    wndClass.style = CS_HREDRAW | CS_VREDRAW;
    wndClass.lpfnWndProc = MainWindProc;
    wndClass.cbClsExtra = 0;
    wndClass.cbWndExtra = 0;
    wndClass.hInstance = hInstance;
    wndClass.hIcon = ::LoadIcon(nullptr, IDI_APPLICATION);
    wndClass.hCursor = ::LoadCursor(nullptr, IDC_ARROW);
    wndClass.hbrBackground = (HBRUSH)::GetStockObject(WHITE_BRUSH);
    wndClass.lpszMenuName = nullptr;
    wndClass.lpszClassName = TEXT("MainWClass");
    wndClass.hIconSm = nullptr;

    // 注册窗口类
    ::RegisterClassEx(&wndClass);
    
    // 创建窗口
    HWND hwnd = ::CreateWindowEx(\
            0, \
            TEXT("MainWClass"), \
            TEXT("My First Window"), \
            WS_OVERLAPPEDWINDOW, \
            CW_USEDEFAULT, \
            CW_USEDEFAULT, \
            CW_USEDEFAULT, \
            CW_USEDEFAULT, \
            nullptr, \
            nullptr, \
            hInstance, \
            nullptr);

    if (hwnd == nullptr)
    {
        std::cout << "Create Window Error!" << std::endl;
        return -1;
    }

    // 显示窗口
    ::ShowWindow(hwnd, nCmdShow);
    // 刷新窗口客户区
    ::UpdateWindow(hwnd);

    MSG msg;
    while (::GetMessage(&msg, nullptr, 0, 0))
    {
        ::TranslateMessage(&msg);
        ::DispatchMessage(&msg);
    }

    return msg.wParam;
}

LRESULT CALLBACK MainWindProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_PAINT:
    {
        PAINTSTRUCT ps;
        HDC hdc = ::BeginPaint(hwnd, &ps);
        std::wstring str = L"This is Test Text!";
        ::TextOut(hdc, 0, 0, str.c_str(), str.size());
        ::EndPaint(hwnd, &ps);
        return 0;
    }
    case WM_DESTROY:
        ::PostQuitMessage(0);
        return 0;
    }

    return ::DefWindowProc(hwnd, message, wParam, lParam);
}

上面的代码是创建一个窗口程序的一般步骤,具体流程如下:

  1. 注册窗口类(RegisterClassEx
  2. 创建窗口(CreateWindowEx
  3. 在桌面显示窗口(ShowWindow
  4. 更新窗口客户端(UpdateWindow
  5. 进入消息处理和获取循环。首先获取消息(GetMessage),如果有消息到达,则消息分派给回调函数进行出路 (DispatchMessage)。如果消息是MW_QUIT, 则GetMessage函数返回False,整体消息循环结束。 具体的处理过程在MainWndProc函数中进行。

2.1 注册窗口类

注册窗口类使用函数RegisterClassEx,一个窗口类定义了窗口的一些主要属性,如图标、光标、背景色和负责处理消息的窗口函数等。这些属性定义在WNDCLASSEX结构中。

typedef struct _WNDCLASSEX{
    UINT        cbSize;             // WNDCLASSEX结构的大小
    UINT        style;              // 从窗口派生的窗口具有的风格
    WNDPROC     lpfnWndProc;        // 消息处理函数指针
    int         cbClsExtra;         // 指定紧跟在窗口类结构后的附加字节数
    int         cbWndExtra;         // 指定紧跟在窗口示例后的附加字节数
    HINSTANCE   hInstance;          // 本模块的实例句柄
    HICON       hIcon;              // 窗口左上角图标的句柄
    HCURSOR     hCursor;            // 光标的句柄
    HBRUSH      hbrBackground;      // 背景画刷的句柄
    LPCSTR      lpszMenuName;       // 菜单名
    LPCSTR      lpszClassName;      // 该窗口类的名称
    HICON       hIconSm;            // 小图标句柄
}WNDCLASSEX;
(1) 指定窗口类的风格
wndClass.style = CS_HREDRAW | CS_VREDRAW;	// 指定如果大小改变就重画

前缀CS_意为class style,在WINUSER.H中定义了全部可选样式。

#define CS_VREDRAW          0x0001
#define CS_HREDRAW          0x0002
#define CS_DBLCLKS          0x0008
#define CS_OWNDC            0x0020
#define CS_CLASSDC          0x0040
#define CS_PARENTDC         0x0080
#define CS_NOCLOSE          0x0200
#define CS_SAVEBITS         0x0800
#define CS_BYTEALIGNCLIENT  0x1000
#define CS_BYTEALIGNWINDOW  0x2000
#define CS_GLOBALCLASS      0x4000
(2)指定窗口处理函数地址
wndClass.lpfnWndProc = MainWindProc;

WNDCLASSEX 结构成员lpfnWndProc 指定了基于此类窗口的窗口函数。当窗口收到消息时Windows即自动调用这个函数通知应用程序。

(3) 本程序的实例句柄传给hInstance成员
wndClass.hInstance = hInstance;
(4) 设置图标和光标
wndClass.hIcon = ::LoadIcon(nullptr, IDI_APPLICATION);
wndClass.hCursor = ::LoadCursor(nullptr, IDC_ARROW);

LoadIcon 函数装载了一个预定义图标,命名为 IDI_APPLICATION
LoadCursor 函数装载了一个预定义的光标,命名为 IDC_ARROW

(5)指定窗口重画客户区画刷
wndClass.hbrBackground = (HBRUSH)::GetStockObject(WHITE_BRUSH);	// 使用白色画刷
(6)指定窗口类名称
wndClass.hbrBackground = (HBRUSH)::GetStockObject(WHITE_BRUSH);	// 使用白色画刷

填充完WNDCLASSEX 结构,就可以进行注册了。RegisterClassEx 函数调用失败将返回0

::RegisterClassEx(&wndClass);

2.2 创建窗口

要创建窗口,使用函数 CreateWindowEx

HWND hwnd = ::CreateWindowEx(\
    0, \                                        // dwExStyle, 扩展样式
    TEXT("MainWClass"), \             // 类名
    TEXT("My First Window"), \        // 标题
    WS_OVERLAPPEDWINDOW, \                      // 窗口风格
    CW_USEDEFAULT, \                            // X, 初始X坐标
    CW_USEDEFAULT, \                            // Y, 初始Y坐标
    CW_USEDEFAULT, \                            // nWidth, 宽度
    CW_USEDEFAULT, \                            // nHeight, 高度
    nullptr, \                                  // hWndParent, 父窗口句柄
    nullptr, \                                  // hMenu,菜单句柄
    hInstance, \                                // hInstance, 程序实例句柄
    nullptr);                                   // lpParam, 用户数据

函数调用成功将返回窗口句柄,失败返回nullptr。
第四个参数dwStyle的值是 WS_OVERLAPPEDWINDOW,即重叠窗口。由他指定的窗口有标题栏、系统菜单、可以改变大小的边框,以及最大化、最小化和关闭按钮。这是一个标准的窗口样式。下面是一些常见风格定于:

  • WS_BORDER 创建一个单边框窗口
  • WS_CAPTION 创建一个有标题框的窗口
  • WS_CHID 创建一个子窗口。这个风格不能与WS_POPVP合用
  • WS_DISABLED 创建一个初始状态为禁止的子窗口。
  • WS_DLGFRAME 创建一个带对话框边框风格的窗口,这种风格的窗口不能带标题条
  • WS_HSCROLL 创建一个有水平滚动条的窗口
  • WS_VSCROLL 创建一个有垂直滚动条的窗口
  • WS_ICONIC 创建一个初始状态为最小化状态的窗口,与WS_MINIMIZE风格相同
  • WS_MAXIMIZE 创建一个具有最大化按钮的窗口。
  • WS_OVERLAPPED 产生一个层叠的窗口。
  • WS_OVERLAPPEDWINDOW 创建一个具有WS_OVERLAPPEDWS_CAPTIONWS_SYSMENUWS_THICKFRAMEWS_MINIMZEBOXWS_MAXMIZEBOX 风格的层叠窗口。
  • WS_POPUP 创建一个弹出式窗口
  • WS_SIZEBOX 创建一个可调边框的窗口。
  • WS_SYSMENU 创建一个在标题条上带有窗口菜单的窗口。
  • WS_THICKFRAME 创建一个具有可调边框的窗口
  • WS_VISIBLE 创建一个初始状态为可见的窗口

2.3 在桌面显示窗口
::ShowWindow(hwnd, nCmdShow);

ShowWindow 函数用于指定窗口的显示状态。nCmdShow 取值可以是 SW_SHOWSW_HIDESW_MINIMIZE


2.4 更新窗口客户区
::UpdateWindow(hwnd);

如果指定窗口的更新区域不为空的话,UpdateWindow 函数通过向这个窗口发送一个 WM_PAINT 消息更新它的客户区。当窗口显示在屏幕上时,窗口的客户区被 WNDCLASSEX 中指定的刷子擦去,调用 UpdateWindow 函数将促使客户区重画,以显示其内容。


2.5 进入无限的消息循环

Windows为每一个线程维护一个消息队列,每当有一个输入发生,Windows就把用户输入翻译成消息放在消息队列中。GetMessage 函数可以从消息队列中取一个消息填充MSG结构。
如果消息队列中没有消息,这个函数会一直等下去,直到有消息进入消息队列为止。

typedef struct tagMSG {
    HWND        hwnd;       // 消息要发向的窗口句柄
    UINT        message;    // 消息标识符,以WM_开头的预定义值(Windows Message)
    WPARAM      wParam;     // 消息的参数之一
    LPARAM      lParam;     // 消息的参数之二
    DWORD       time;       // 消息放入消息队列的时间
    POINT       pt;         // 这是一个POINT数据结构,表示消息放入消息队列时鼠标的位置
} MSG,

GetMessage 函数从消息队列中取得的消息如果不是WM_QUIT , 则返回非零值。一个WM_QUIT 消息会使GetMessage 函数返回0,从而消息循环结束。

::TranslateMessage(&msg);

此调用把键盘输入翻译成可调用的消息。

::DispatchMessage(&msg);

DispatchMessage 函数分发一个消息到对应窗口的窗口函数。 MainWndProc 处理消息后把控制权交给Windows,此时 DispatchMessage 函数仍然继续工作,当他返回时,消息队列从调用 GetMessage 函数开始进入下一轮。


3. 处理消息代码

所有消息不做处理的消息都必须返回一个名为 DefWindowProc的函数让Windows做默认处理, 从DefWindowProc 函数返回的值也必须从消息处理函数返回。

每当客户区变为无效,消息处理函数就会收到一个新的WM_PAINT 消息。
WM_DESTORY 是窗口必须处理的一个消息,当用户关闭窗口时,消息处理函数就会收到一 个 WM_DESTORY消息。当接收到这个消息的时候,说明窗口正在销毁。

::PostQuitMessage(0);

PostQuitMessage函数会会向消息队列中插入一个 WM_QUIT 消息。 GetMessage 函数如果从消息队列中获得到消息时 WM_QUIT ,它将返回0。从而退出消息循环。如果不使用函数 PostQuitMessage 发送 WM_QUIT 消息,则界面虽然被销毁了,但是消息循环还在继续,程序还没有结束。

不会飞的纸飞机
扫一扫二维码,了解我的更多动态。

下一篇文章:Windows中的GDI