Windows编程之创建第一个应用

https://blog.csdn.net/majalis_C/article/details/88920674

https://blog.csdn.net/majalis_C/article/details/88827871

https://blog.csdn.net/majalis_C/article/details/88921745

https://blog.csdn.net/majalis_C/article/details/88927392

有了以上几篇文章的基础,就可以开始写第一个Windows应用了。

王叔叔告诉我们,做事先定一个小目标,比如赚他一个亿。在赚一个亿之前,你需要知道如何写一个空白的窗口。

就像这样。

 

我们先来贴实现它的完整代码,然后再逐个解释。

以下是实现这个窗口程序的完整代码。在贴代码之前呢,要插一句。我们现在用的编译器版本大多很高,会有很多的集成环境供人选择。例如MFC。但是要写win32程序,需要建立一个空项目。visual studio建立空项目的方法是点击新建,然后选择windows桌面向导,在弹出的对话框中勾选空项目,就可以建立一个完完全全什么都没有的空项目。在源文件的文件夹下新建.cpp文件,键入以下代码,执行。

#ifndef UNICODE
#define UNICODE
#endif 

#include <windows.h>

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow)
{
    // Register the window class.
    const wchar_t CLASS_NAME[]  = L"Sample Window Class";
    
    WNDCLASS wc = { };

    wc.lpfnWndProc   = WindowProc;
    wc.hInstance     = hInstance;
    wc.lpszClassName = CLASS_NAME;

    RegisterClass(&wc);

    // Create the window.

    HWND hwnd = CreateWindowEx(
        0,                              // Optional window styles.
        CLASS_NAME,                     // Window class
        L"Learn to Program Windows",    // Window text
        WS_OVERLAPPEDWINDOW,            // Window style

        // Size and position
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,

        NULL,       // Parent window    
        NULL,       // Menu
        hInstance,  // Instance handle
        NULL        // Additional application data
        );

    if (hwnd == NULL)
    {
        return 0;
    }

    ShowWindow(hwnd, nCmdShow);

    // Run the message loop.

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

    return 0;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;

    case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hwnd, &ps);
            FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));
            EndPaint(hwnd, &ps);
    }
    return 0;

}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

其中wWinMain函数是程序的入口函数,程序一开始,它注册了一些有关于窗口行为的信息。其中最重要的是处理函数,在这里被命名为WindowProc,它定义了窗口的行为,包括窗口的显示,以及如何同内部控件交互等等。

接下来,程序创建了一个窗口,并且获得一个唯一标识窗口的handle。

当窗口创建成功,程序进入了一个循环体中,程序一直在循环体中,直到用户关闭窗口退出应用。

需要注意的是,程序并非明显地调用WindowPro函数。窗口通过一系列的Message(消息)来和程序进行通信。循环体中的代码驱动着这一过程。每当调用DispatchMessage函数的时候,它都会间接导致Windows对每个Message调用一次WindowPro函数。

我们先来看看程序是如何创建一个窗口的。

创建窗口之前,需要了解window class。window class定义了大多数窗口的都有的行为。例如,在一组button中,当用户点击button时,每个按钮都有相似的行为。当然了,每个button的行为都不是完全一致的。

每个窗口都必须关联一个window class,即使你的程序只为该类创建一个实例。很重要的一点是要理解window class不是C++意义上的类。它是操作系统内部使用的一个数据结构。在程序运行时向操作系统注册window class。

要注册一个window class,首先要填写一个WNDCLASS结构。

// Register the window class.
    const wchar_t CLASS_NAME[]  = L"Sample Window Class";
    
    WNDCLASS wc = { };

    wc.lpfnWndProc   = WindowProc;
    wc.hInstance     = hInstance;
    wc.lpszClassName = CLASS_NAME;

程序中所示的三个结构成员是必须要赋值的。class name是当前进程的本地名称,所以在当前进程中必须唯一。标准的windows控件也有类,在使用这些控件的时候,其控件名称不可与类名相同。例如,Button类型控件的名称不能为 'Button'。

向操作系统中注册窗口类的时候,需要将上诉窗口结构体的地址传递给RegisterClass函数。该函数将窗口类注册到操作系统。

RegisterClass(&wc);

做了以上工作,就可以开始创建一个窗口了。

可以调用CreateWindowEx函数来创建一个窗口实例。

HWND hwnd = CreateWindowEx(
        0,                              // Optional window styles.
        CLASS_NAME,                     // Window class
        L"Learn to Program Windows",    // Window text
        WS_OVERLAPPEDWINDOW,            // Window style

        // Size and position
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,

        NULL,       // Parent window    
        NULL,       // Menu
        hInstance,  // Instance handle
        NULL        // Additional application data
        );

    if (hwnd == NULL)
    {
        return 0;
    }

我们来大致说一下,CreateWindowEx函数中的各个参数是干什么用的。

第一个参数可以让你为窗口制定一些可选行为。当该值为0时表示默认设置。

CLASS_NAME是window class的名称,其定义了你创建的窗口类型(button,listview 或其他)。

对于不同的窗口类型,window text的使用方式是不一样的。如果你创建的window有标题栏,那么该值显示在标题栏当中。

Window style是一组定义window外观的标志,他们定义了窗口是否有标题栏,边界线,系统菜单等特性。

对于位置和大小,CW_USEDEFAULT 意为使用默认的位置和大小。

接下来的参数为新创建的window指定parent window或是owner window。

紧随其后的参数为窗口定义菜单,本例中并未给窗口定义菜单,故填NULL。

hInstance是程序实例handle,开头给出的文章中有介绍,当程序装入内存后,操作系统用它来唯一标识一个exe。

最后一个参数是指向*void的任意数据的指针,可以用此值来传递一个数据结构体给window procedure。

CreateWindowEx返回一个handle给新创建的window。如果创建window失败,它将返回0值。将window的handle当做参数传递给ShowWindow函数中,来使得window可见。

ShowWindow(hwnd, nCmdShow);

参数hwnd是CreateWindowEx返回的window的handle,nCmdShow在最大化或最小化时使用。操作系统通过wWinMain函数将以上两个参数的值传递给程序。

到这里为止,便是完整的创建了一个window。

但我们仅仅是创建了一个空白的window,没有任何内容,也没有任何交互性。在实际应用中,window是可以对操作系统或用户发出的事件进行响应的。window通过消息来传递事件以及响应事件。

下面我们就看看窗口的消息机制如何提供交互性。

GUI应用程序必须响应来自用户和操作系统的事件。事件可能在程序运行过程中的任何时候发生。那么怎么构建一个无法预先预测执行流程的程序响应结构呢?

windows采用消息传递模型来解决这一问题。操作系统通过传递消息来同window进行通信。一个消息(message)是指标识特定事件的数字代码。

例如鼠标的点击事件:

#define WM_LBUTTONDOWN    0x0201

一些消息含有与其相关联的数据,例如鼠标的点击消息含有点击位置的 x 坐标和 y 坐标。

操作系统通过window procedure函数来传递消息。就是本例中的windowpro函数。在注册window class时曾用到过。

一个应用在执行过程中将接收到很多的消息。另外,一个应用可以有多个窗口,每个窗口都有其自己的windowpro函数。那么程序是如何接收这么多的消息并且正确地做出响应的呢?

应用程序利用一个循环体来检索消息并将其分派到正确的窗口。

每个创建了窗口的线程,操作系统都为其创建一个窗口消息队列。此队列中包含了该线程所创建的所有窗口的消息。队列是隐藏的,无法显式地操作,但是可以通过GetMessage方法来从队列中取出一个消息。

MSG msg;
GetMessage(&msg, NULL, 0, 0);

该方法获取队列头的一个消息,如果队列是空的,那么该方法便阻塞,直到有消息进入队列。GetMessage函数的第一个参数是MSG结构体的地址,如果该函数执行成功,则将获取的消息信息写入MSG结构体变量中。消息中包含目标窗口及消息数字编码等信息。其他的三个参数可以让你筛选要获得的消息,不过在几乎所有的情况下,他们都被设置为0。

虽然MSG结构体中包含了消息的所有信息,但你往往不需要去直接访问其中的内容,而是将其传递给两个函数。

TranslateMessage(&msg); 
DispatchMessage(&msg);

TranslateMessage函数同键盘输入的操作有关,无需关注它的内部细节,需要牢记的是,必须在DispatchMessage函数之前调用它。DispatchMessage告诉操作系统调用目标窗口的windows procedure。

举个栗子,假设用户点击了鼠标的左键,这一举动导致了一连串的事件:

1,操作系统将WM_LBUTTONDOWN消息放入消息队列。

2,程序调用GetMessage函数。

3,GetMessage函数获取WM_LBUTTONDOWN消息并将其写入MSG结构体中。

4,程序调用TranslateMessage函数和DispatchMessage函数。

5,在DispatchMessage函数中,操作系统调用目标窗口的window procedure函数。

6,Window procedure选择响应消息事件或者是忽略。

当window procedure执行完,其返回DispatchMessage函数。循环继续,程序进行下一次消息的处理。通常,GetMessage函数返回的是一个非0值,也就是说循环体的判断条件可能一直为真,其会一直循环下去。如果想要退出应用程序并且跳出循环体,调用PostQuitMessage函数。

PostQuitMessage(0);

PostQuitMessage向消息队列中添加一个WM_QUIT消息。WM_QUIT是一个特殊的消息,它使得GetMessage函数返回0值,以此让程序退出循环。window procedure从来都不接收WM_QUIT消息,没有必要将其写入window procedure函数中。

说了半天消息机制,该说说消息的处理过程了。大多数的消息都是在window procedure中处理的。

那么我们如何来实现window procedure呢?

以上提到过,DispatchMessage方法调用目标窗口的window procedure来响应消息。window procedure有如下声明结构。

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

hwnd参数是window 的 handle。

uMsg是消息数字代码,例如WM_SIZE标识window要调整尺寸。

wParam和lParam包含于消息有关的其他数据,其意义同特定的消息有关。

LRESULT是一个整型数值,其值包含程序对特定消息的响应。其含义由传入的消息数字代码决定。

经典的window procedure是一个switch语句,case语句下用来处理特定的消息。

switch (uMsg)
{
case WM_SIZE: // Handle window resizing
    
// etc

}

其他的消息相关信息在wParam和lParam参数中,两个参数都是指针大小的整数。

如果没有处理特定的消息,则默认调用DefWindowPro函数。其定义如下 

return DefWindowProc(hwnd, uMsg, wParam, lParam);

我们提到过,每个线程都有一个消息队列,那么在多线程的情况下,就要避免窗口程序阻塞。

当window procedure执行时,其阻塞了一个队列中的其他窗口的消息事件。因此,要避免在window procedure函数中处理耗时长的任务。否则,窗口将长时间处于无响应状态,直到该耗时任务执行完毕。在处理耗时任务是,可以创建一个线程去专门做这件事。

可以利用以下几种方式中的一种来处理。

1,创建一个新的线程。

2,使用线程池。

3,使用异步I/O调用。

4,使用异步过程调用。

那么,到现在,你已经创建了一个窗口,并且可以通过消息与其进行通信。还缺点儿什么?窗口是不是太空了,似乎就像一个空白的画布一样,等着你向里面绘制内容。

有时候,程序会启动窗口绘制程序来改变窗口的外观。在一些时候,操作系统会提醒你必须重新绘制窗口的特定区域。当此事发生时,操作系统发送一个WM_PAINT消息给窗口,需要绘制的位置称为update region。

当窗口第一次显示的时候,全部的client区域都需要被绘制。因此窗口至少会收到一次WM_PAINT信息。

我们只需要绘制client区域,边框的区域,包括标题栏,操作系统会帮我们绘制。绘制完成后,需要清除update region,该动作告诉操作系统,除非有改变,否则不需要发送WM_PAINT消息。

现在,我们假设用户将另一个窗口移到了现有窗口之上,使得现有窗口部分被遮挡。当窗口移开,被遮挡的部分重新显示,则该区域为update region,此时窗口将收到另一个WM_PAINT消息。

在开头给出的全部代码中,我们绘制client区域的方式很简单,只是为其填充了固定的颜色。但是它也能体现很多的概念。我们再来看看那段代码。

switch (uMsg)
    {case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hdc = BeginPaint(hwnd, &ps);

		// All painting occurs here, between BeginPaint and EndPaint.

        FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));

        EndPaint(hwnd, &ps);
    }
    return 0;
}

调用BeginPaint函数来进行绘制的开始,该函数用重绘请求的信息来填写PAINTSTURCT结构。当前的绘制区域(update region)是由PAINTSTRUCT中的rcPaint成员提供的。

update region相对于client区域定义。

在你的处理绘制的代码中,有两种情况。一是绘制整个client区域。另一个是仅绘制特定的update region。

以下代码用单一颜色绘制了特定的update region。

FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));

COLOR_WINDOW是系统默认的背景颜色。第二个参数给出了绘制区域的坐标。

当绘制完成后,调用EndPaint函数,其清除update region,表明已经完成了绘制。

到这里,我们介绍了对窗口的基本操作,包括窗口如何创建,如何响应事件,如何绘制。

在最后,介绍窗口的关闭。

当用户关闭窗口时,该行为将触发WM_CLOSE消息。在处理此消息时,可以做一些关闭窗口之前需要做的操作。

如果想要关闭窗口,调用DestroyWindow函数。如果不想关闭,则可以直接返回0值,操作系统将忽略这一消息,不销毁窗口。

case WM_CLOSE:
    if (MessageBox(hwnd, L"Really quit?", L"My application", MB_OKCANCEL) == IDOK)
    {
        DestroyWindow(hwnd);
    }
    // Else: User canceled. Do nothing.
    return 0;

还记得前面介绍的DefWindowPro函数吗,它会执行一些消息的默认操作。如果忽略了在switch中处理WM_CLOSE消息,DefWindowPro函数会默认调用DestroyWindow函数来对窗口进行关闭。

当窗口被销毁的时候,会收到一个WM_DESTROY消息。这个消息在窗口从屏幕上移除之后,窗口销毁之前(特别是子窗口被销毁之前)发送。

case WM_DESTROY:
        PostQuitMessage(0);
        return 0;

 

在你的程序中,通常需要响应WM_DESTROY消息,在处理该消息时调用PostQuitMessage函数。

下图说明了窗口关闭到销毁的过程。

好了,到现在为止,一个窗口的从生到死就简单介绍完了。拿着这些基础去写代码吧!

原文链接:https://docs.microsoft.com/zh-cn/windows/desktop/LearnWin32/learn-to-program-for-windows

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值