【教程】Windows桌面应用程序入门(一)

· · 个人记录

你是否喜欢编写游戏?

如果是,你是否厌倦了黑色的cmd控制台窗口?

只能输出字符,很难改变字体的大小、颜色,总的来说挺丑的。。

你是否想过,有这么一种东西:

这就是我们今天的主题:Windows桌面应用程序

不过,从名字都可以看的出来,这种程序是要在 Windows 环境下编译运行的,所以如果你是 Linux 使用者(或是想开发跨平台的程序),那还是老老实实用 Qt 吧。(逃

一、创建一个Windows桌面应用程序(API)

就如前面所提到的,Windows 桌面应用程序在 Dev-C++ 上就可以编译运行。当然,如果你想,也可以在别的地方来编写(如我自己通常是用 VS2019 )。

接下来,让我们试试看在 Dev-C++ 上创建一个这样的程序:

  1. 首先在某个地方建立一个新的文件夹,来安置你的文件。

  2. 打开 Dev-C++ ,点击菜单上的“文件\to新建\to项目”。

  3. 点击Windows Application ,然后保存到刚才新建的那个文件内,取个你喜欢的名字。

然后就会出来一个main.cpp,其内容如下:

#include <windows.h>

/* This is where all the input to the window goes to */
LRESULT CALLBACK WndProc(HWND hwnd, UINT Message, WPARAM wParam, LPARAM lParam) {
    switch(Message) {

        /* Upon destruction, tell the main thread to stop */
        case WM_DESTROY: {
            PostQuitMessage(0);
            break;
        }

        /* All other messages (a lot of them) are processed using default procedures */
        default:
            return DefWindowProc(hwnd, Message, wParam, lParam);
    }
    return 0;
}

/* The 'main' function of Win32 GUI programs: this is where execution starts */
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    WNDCLASSEX wc; /* A properties struct of our window */
    HWND hwnd; /* A 'HANDLE', hence the H, or a pointer to our window */
    MSG msg; /* A temporary location for all messages */

    /* zero out the struct and set the stuff we want to modify */
    memset(&wc,0,sizeof(wc));
    wc.cbSize        = sizeof(WNDCLASSEX);
    wc.lpfnWndProc   = WndProc; /* This is where we will send messages to */
    wc.hInstance     = hInstance;
    wc.hCursor       = LoadCursor(NULL, IDC_ARROW);

    /* White, COLOR_WINDOW is just a #define for a system color, try Ctrl+Clicking it */
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
    wc.lpszClassName = "WindowClass";
    wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION); /* Load a standard icon */
    wc.hIconSm       = LoadIcon(NULL, IDI_APPLICATION); /* use the name "A" to use the project icon */

    if(!RegisterClassEx(&wc)) {
        MessageBox(NULL, "Window Registration Failed!","Error!",MB_ICONEXCLAMATION|MB_OK);
        return 0;
    }

    hwnd = CreateWindowEx(WS_EX_CLIENTEDGE,"WindowClass","Caption",WS_VISIBLE|WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, /* x */
        CW_USEDEFAULT, /* y */
        640, /* width */
        480, /* height */
        NULL,NULL,hInstance,NULL);

    if(hwnd == NULL) {
        MessageBox(NULL, "Window Creation Failed!","Error!",MB_ICONEXCLAMATION|MB_OK);
        return 0;
    }

    /*
        This is the heart of our program where all input is processed and 
        sent to WndProc. Note that GetMessage blocks code flow until it receives something, so
        this loop will not produce unreasonably high CPU usage
    */
    while(GetMessage(&msg, NULL, 0, 0) > 0) { /* If no error is received... */
        TranslateMessage(&msg); /* Translate key codes to chars if present */
        DispatchMessage(&msg); /* Send it to WndProc */
    }
    return msg.wParam;
}

尝试保存,然后编译运行,果然出现了一个白色的程序窗口:

恭喜你,虽然这个窗口不能输入任何字符,但这毕竟是你的第一个Windows桌面应用程序!

二、代码分析

当然,在对这个窗口添加更多功能之前,我们需要弄懂,它帮我们创建的程序中,各部分的功能是什么。

1.WndProc

首先迎面而来的是一个名叫WndProc的函数:

/* This is where all the input to the window goes to */
LRESULT CALLBACK WndProc(HWND hwnd, UINT Message, WPARAM wParam, LPARAM lParam) {
    switch(Message) {

        /* Upon destruction, tell the main thread to stop */
        case WM_DESTROY: {
            PostQuitMessage(0);
            break;
        }

        /* All other messages (a lot of them) are processed using default procedures */
        default:
            return DefWindowProc(hwnd, Message, wParam, lParam);
    }
    return 0;
}

也许除了一个 switch 语句和 return 之外,你什么也看不懂。(笑哭)别急,待会你就悟了。

想象在记事本程序中,你按下一个按键 “A” ,同时记事本上就会出现一个字符 “A”。系统检测到按键的按下后,会将“按键A按下”的消息传递给WndProc这个函数中,再由这个函数去进行处理,于是,屏幕上就出现了 “A”。

那么我们来看一下生词:

  1. LERSULT CALLBACK

    翻阅一下头文件会发现,这其实是两个宏定义:

    #define LERSULT LONG

    #define CALLBACK __stdcall

    也就是说LRESULT其实是long的意思,即函数的返回值为长整型;至于CALLBACK,是指它是一个回调函数,即这个函数是给系统来调用的。(这与我们前面提到的系统调用WndProc函数相符)

  2. HWND hwnd(重点)

    HWND是指这个窗口的窗口句柄

    对于入门者,你可以把它看做一个指针,指向着一个窗口。(不过和指针有有点区别)如果你需要了解某个窗口的信息,或者对窗口进行修改,窗口句柄是不可或缺的。

  3. UINT Message(重点)

    UINT实际上等同于unsigned int

    这个参数也是非常关键,它代表的是传给你这个窗口的信息。(从名字可以看的出来)

    举个例子:如果你在该窗口中按下了一个按键,那么操作系统马上就调用你的WndProc函数,此时的参数Message的值就是WM_KEYDOWN(即按键按下的消息);如果你在程序中移动了鼠标,此时Message的值就是WM_MOUSEMOVE(即鼠标移动的消息)......

    类似的消息还有很多,具体可以查阅一下这里。

    WndProc函数,实质上就是对不同的Message信息进行不同的操作处理而已

    在函数的内部我们也可以看到:

        switch(Message) {
    
        /* Upon destruction, tell the main thread to stop */
        case WM_DESTROY: {
            PostQuitMessage(0);
            break;
        }
    
        /* All other messages (a lot of them) are processed using default procedures */
        default:
            return DefWindowProc(hwnd, Message, wParam, lParam);

    就有一个对于WM_DESTROY(窗口销毁的消息)的应对措施。

    看到这里你或许已经明白了,那么为什么不试试看呢?在switch语句里面添加一下别的处理方式,如:

        case WM_KEYDOWN:
            MessageBox(hwnd,"You Press a key!","message",MB_OK);
            break;

    编译运行,是你想要的结果吗?

  4. WPARAM wParam, LPARAM lParam

    看到这里或许你有点疑惑,这两个参数又是干什么的呢,前面那些不已经够用了吗?

    还是举个例子:假如你在键盘上按下一个shift键,此时系统马上会调用你的WndProc函数,其中hwnd代表你窗口的句柄,message的值是WM_KEYDOWN,但问题来了,怎么告诉程序你按下的是shift键,而不是别的按键呢?

    没错,这就是剩下这两个参数的用途。

    还是拿上面那个按下shift的例子来说,此时wParam的值就是VK_SHIFT

    关于更多的按键所对应的键码,可以查看这里。

    试试看,在之前你修改过的函数基础上,再进行一些修改:

        case WM_KEYDOWN:
            if (wParam==VK_SHIFT)
                MessageBox(hwnd,"You Press shift key!","message",MB_OK);
            break;

    试试看,是你想要的效果吗?

2.WinMain(或 wWinMain)

WinMain就是整个程序的主函数,和int main()一样。但是和以前的控制台不一样的是,它还需要做一些额外的任务:

一、定义窗口类

在定义变量的部分,我们可以看到有一行WNDCLASSEX wc;

这个WNDCLASSEX又是什么东西呢?你可以把它看做一个结构体(或一个类)。它是用来注册"窗口类"的,也就是说,通过它,我们可以指定创建出来窗口的类型、背景颜色等。

接下来几行,我们就可以看到它是如何指定窗口的样式的:

memset(&wc,0,sizeof(wc));   //先清空一下再修改,防止出错
wc.cbSize        = sizeof(WNDCLASSEX);  //设置结构体所占的内存大小
wc.lpfnWndProc   = WndProc; //设置上文中的“信息处理函数”是哪一个

wc.hInstance     = hInstance; //当你启动一个程序时,操作系统会将这个程序装载到某个内存空间,这个空间的起始地址就是hInstance
wc.hCursor       = LoadCursor(NULL, IDC_ARROW); //设置鼠标光标是什么样式

wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);    //设置背景颜色
wc.lpszClassName = "WindowClass";   //可以形象地理解为:这个窗口在程序内部的代号,注意是程序内部
wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION); //程序的图标
wc.hIconSm       = LoadIcon(NULL, IDI_APPLICATION); //程序的小图标(Sm就是Small的缩写)

当一切都设置好的时候,我们就可以注册窗口类:

if(!RegisterClassEx(&wc)) {
        MessageBox(NULL, "Window Registration Failed!","Error!",MB_ICONEXCLAMATION|MB_OK);
        return 0;
}

需要注意的是,窗口类注册好之后,不代表窗口就会显示出来,因为正如我们前面提到过的,他只是用来给你的窗口提供一个“格式”。当一个窗口就要创建的时候,它可以选择它的格式是哪个窗口类,就像浏览器的应用主题一样:你的电脑上有三个主题,不代表这三个主题都会显示出来,只有当主题“套”在浏览器上面,才会显示出效果。

二、创建窗口

设置好窗口类这个“应用主题”之后,我们就可以创建窗口了。

hwnd = CreateWindowEx(
        WS_EX_CLIENTEDGE,   //窗口的扩展风格,具体见百度
        "WindowClass",  //所选择窗口类的代号。好像在前面哪里见过它?
        "Caption",  //窗口的标题(就是显示在左上角的那一行字)
        WS_VISIBLE|WS_OVERLAPPEDWINDOW,     //窗口的风格,具体见百度
        CW_USEDEFAULT,  //窗口相对显示屏左上角的x坐标
        CW_USEDEFAULT,  //窗口相对显示屏右上角的y坐标
        640,    //窗口的宽度
        480,    //窗口的高度
        NULL,   //父窗口的句柄,好比树状数据结构里一个节点的上级一样(子窗口同理)
        NULL,   //菜单的句柄,或子窗口的句柄
        hInstance,  //应用程序的实例句柄。好像在前面哪里见过它?
        NULL    //指向窗口的创建数据
        );

if(hwnd == NULL) {
        MessageBox(NULL, "Window Creation Failed!","Error!",MB_ICONEXCLAMATION|MB_OK);
        return 0;
    }   //处理创建失败的特殊情况

三、运行“消息处理机制”

到这里,就是让WndProc派上用场的时候了。

应用程序会不断地收集信息,并将解析好的信息全部甩给WndProc函数,交给它来处理。如果出现错误,就退出程序。

由此可见,其实WinMain虽然是程序的主函数,但当你真正编写游戏的时候,几乎不需要对它做任何改动。

    while(GetMessage(&msg, NULL, 0, 0) > 0) { //如果没有出错的话
        TranslateMessage(&msg); //解析收到的消息
        DispatchMessage(&msg); //然后全部甩给WndProc去处理
    }
    return msg.wParam; //退出时给系统返回一个数值,让系统知道是正常退出还是别的情况

到这里,整个代码就结束了。

三、结语

今天,我们学习了:

不过,你是不是想问:开头的图形效果呢?又是怎么做出来的?其实,并不需要什么高科技,它仍然是和WndProc函数息息相关。

敬请期待:Windows桌面应用程序入门(二)