Win32 API笔记

简介

Windows编码约定

前缀后缀

Ex后缀,代表功能扩展了。如Creat()和CreatEx()

整数类型

Windows头文件包含了许多变量类型的typedefine,很多都定义在WinDef.h中,例如:

Data type Size Signed?
BYTE 8 bits Unsigned
DWORD 32 bits Unsigned
INT32 32 bits Signed
INT64 64 bits Signed
LONG 32 bits Signed
LONGLONG 64 bits Signed
UINT32 32 bits Unsigned
UINT64 64 bits Unsigned
ULONG 32 bits Unsigned
ULONGLONG 64 bits Unsigned
WORD 16 bits Unsigned

如你所见,有很多替换是重复的,这些是历史遗留造成的。
以上列举的类型都是固定大小,例如DWORD,无论在32位系统还是64位系统上,总是32位宽度。

Boolean类型

Bool类型也是定义在WinDef.h中,是用整数替换的:

#define FALSE    0 
#define TRUE     1

尽管定义了TRUE,但是很多程序都会返回一个非零整数作为“真”,因此使用时你应该这样:

// Right way.
BOOL result = SomeFunctionThatReturnsBoolean();
if (result) 
{ 
    ...
}

而不是这样:

// Wrong!
if (result == TRUE) 
{
    ... 
}

请注意:BOOL是整型,不能和C++中的bool类型通用。

指针类型

Windows定义了很多指针类型,最通用的是P-前缀和LP-前缀,例如LPRECT是一个返回类型的指针,它描述了一个返回值矩阵:

RECT*  rect;  // Pointer to a RECT structure.
LPRECT rect;  // The same
PRECT  rect;  // Also the same.

历史上,P代表指针,LP代表长指针,长指针用来兼容16位程序的,但是现在没有这种区别了,指针就是指针。

Historically, P stands for "pointer" and LP stands for "long pointer". Long pointers (also called far pointers) are a holdover from 16-bit Windows, when they were needed to address memory ranges outside the current segment. The LP prefix was preserved to make it easier to port 16-bit code to 32-bit Windows. Today there is no distinction — a pointer is a pointer.

指针精度

以下数据总是与指针大小相同,在32位机器上是32位,在64位机器上是64位,这些检测是在编译期完成的。
当32位应用运行在64位机器上时,指针仍是4字节,但是64位应用不能运行在32位机器上。

  • DWORD_PTR
  • INT_PTR
  • LONG_PTR
  • ULONG_PTR
  • UINT_PTR

这些类型用于整形强制转化为指针类型时等等。

匈牙利命名法

尽管匈牙利命名法有利有弊,但是MSDN用了大量的匈牙利命名法,所以也要熟悉一下。
略。

字符串处理

Windows原生支持unicode字符串,因为他支持所有的字符类型和语言,
并且Windows更喜欢使用UTF-16,也叫做宽字符,以区别8位的ANSI字符。
VC++支持内置的wchar_t宽字符类型,在WinNT.h 中:

typedef wchar_t WCHAR;

在MSDN样例中,你会看到使用宽字符需要在字符或字符串前加一个L:

wchar_t a = L'a';
wchar_t *str = L"hello";

还有一些其他的字符串相关的替换如下表:

Typedef Definition
CHAR char
PSTR or LPSTR char*
PCSTR or LPCSTR const char*
PWSTR or LPWSTR wchar_t*
PCWSTR or LPCWSTR const wchar_t*

UNICODE和ANSI函数

由于Windows中原生支持UNICODE,因为在Windows中有两个字符串处理的API,一个是ANSI版的,一个是UNICODE版的,
例如设定窗口文本:
SetWindowTextA takes an ANSI string.
SetWindowTextW takes a Unicode string.

但是在内部,ANSI版的API还是会转换成UNICODE版进行输出。
Windows内部也定义了宏UNICODE来自动处理调用哪个函数:

#ifdef UNICODE
#define SetWindowText  SetWindowTextW
#else
#define SetWindowText  SetWindowTextA
#endif

TCHARs

当应用程序需要同时支持 Windows NT 以及 Windows 95、Windows 98 和 Windows Me 时,根据目标平台,为 ANSI 或 Unicode 字符串编译相同的代码非常有用。为此,Windows SDK 提供将字符串映射到 Unicode 或 ANSI 的宏,具体取决于平台。

Macro Unicode ANSI
TCHAR wchar_t char
TEXT("x") L"x" "x"

例如下面的代码:

SetWindowText(TEXT("My Application"));

是为了解决下面的情况:

SetWindowTextW(L"My Application"); // Unicode function with wide-character string.

SetWindowTextA("My Application");  // ANSI function.

然而现在TEXTTCHAR都很少用了,因为现在都是用unicode,

在微软C运行库中,也有类似的一系列定义,例如_tcslen

#ifdef _UNICODE
#define _tcslen     wcslen
#else
#define _tcslen     strlen
#endif

什么是窗口

什么是窗口

显然,Windows的核心内容就是窗口,
你认为的窗口可能是这样的:

窗口类型会被应用窗口(application window)或主窗口(main window)调用,
典型的,他有一个框架,包含标题栏,最大化最小化按钮和其他的标准UI元素,
框架被叫做窗口的非客户端区域,因为框架是被操作系统调用的,
框架内的区域叫做客户端区域,这部分是由你的程序管理的。

另一种窗口类型是这样的:

如果你是Windows编程的新手,你可能会很吃惊,像按钮这样的控件竟然也是窗口。
UI控件和应用窗口的主要区别是,UI控件不能够独立存在,UI控件依赖于应用窗口,当移动应用窗口时,如你所愿,UI控件也会一起移动。
还有,控件和应用窗口之间也可以通信,例如,应用窗口会收到来自按钮的单击事件。

因此,当谈到窗口的时候,不要简单的想到是一个应用窗口,而是要把它想成一个联合:

  • 占用屏幕的一部分
  • 给定时刻可见或不可见
  • 知道怎样描画自己
  • 响应用户或操作系统的事件

父窗口和所属窗口

在控件案例中,控件是应用窗口的子窗口,应用窗口是控件的父窗口。
父窗口给子窗口提供坐标系,以及影响子窗口的显示等。

另一种关系例如应用窗口和模态对话框,应用窗口是属主窗口,模态对话框是所属窗口,
所属窗口总是出现在属主窗口前面,当属主窗口最小化时,所属窗口也会最小化。

应用窗口拥有对话框窗口,对话框是两个控件的父窗口,下图显示了这种关系:

窗口句柄

窗口是一个对象,拥有代码和数据,但他不是C++的类,
程序应用窗口通过句柄handle,句柄是一个不透明类型,本质上句柄只是一个操作系统用来识别窗口的整数,
你也可以把窗口和句柄想象成一个表,通过句柄来索引出窗口(但是内部是否是这样操作并不重要),
窗口句柄的数据类型是HWND,通常发音为"aitch-wind."
窗口句柄通常由窗口创建函数返回: CreateWindow CreateWindowEx

想要操作一个窗口,可以使用HWND值来操作,例如要移动窗口,可以这样:

BOOL MoveWindow(HWND hWnd, int X, int Y, int nWidth, int nHeight, BOOL bRepaint);

需要注意的是,hwnd不是指针,所以不能*hwnd来使用。

屏幕和窗口坐标

坐标是由设备无关像素测量的,后续会介绍。
有屏幕坐标,窗口坐标和客户端坐标。

WinMain

WinMain是Windows程序的入口点,有两个版本WinMainwWinMain,两者区别是从命令行传入的是ANSI字符还是Unicode字符,一般都用wWinMain,下面是wWinMain函数:

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow);

这四个参数分别是:
hInstance,叫做句柄实例,当程序加载到内存,操作系统根据句柄实例来识别是哪个exe,有一些程序需要句柄实例作为参数,例如加载图标和位图。
hPrevInstance,没有意义,用于16位窗口,现在总是0.
pCmdLine,包含了命令行参数,这里是Unicode字符串。
nCmdShow,标志位,用于指示应用窗口是否可以最大化,最小化或正常显示。

下面是一个空的WinMain函数:

INT WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    PSTR lpCmdLine, INT nCmdShow)
{
    return 0;
}

第一个窗口程序

概览

先实现一个空窗口,

代码如下:

#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 hPrevInstance, 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);
}

先概览注释内容,下面我们来介绍详细内容。

创建窗口

每个窗口都要绑定一个窗口类,一定要明白,窗口类和C++类是不一样的!
要使用窗口类,首先需要填充柄注册窗口类,先来填充窗口类:

// 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;

一定要设置以下成员:

  • lpfnWndProc,是一个函数指针,用于处理窗口事件,后面会详细介绍。
  • hInstance,句柄实例,由wWinMain传递进来。
  • lpszClassName,类名,用于标示窗口类。用于当前进程的本地名称。

其他的成员暂不介绍。
下面我们来注册这个窗口类:

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;
}
  • 第一个参数设定了窗口行为,例如透明,NULL是默认行为。
  • 第二个参数是窗口类名,就是我们刚刚创建的窗口类,注意该函数是为了实例化窗口类。
  • 第三个参数设定了标题栏名称。
  • 第四个参数是一系列标识位,通过或运算,WS_OVERLAPPEDWINDOW 给出了窗口标题栏,边界,菜单,最大化最小化按钮。
  • 第五个参数是窗口尺寸和位置。
  • 第六个参数是窗口的父窗口。
  • 第七个参数是窗口菜单,本例不使用菜单,因此是NULL
  • 第八个参数是实例化句柄,由wWinMain传入。
  • 最后一个参数用于传递数据。

CreateWindowEx函数调用成功返回窗口句柄,失败返回0,
创建完窗口实例后,我们来显示窗口:

ShowWindow(hwnd, nCmdShow);

hwnd是CreateWindowEx的返回值,nCmdShow用于最大化最小化窗口。

下面是这小结的完整代码,注意此时还没有实现窗口处理函数WindowProc,WindowProc还只是个前置声明。

// 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);

窗口消息

所有GUI应用都要响应来自用户和操作系统的事件:

  • 来自用户的事件包括,鼠标点击,键盘输入,触屏等。
  • 来自操作系统的事件包括,任何应用程序本身以外的事件,如插入一个新设备,或进入低电模式等。

信息是用数字来表示的,例如:

#define WM_LBUTTONDOWN    0x0201

有些信息还可以绑定数据,如: WM_LBUTTONDOWN ,包含了鼠标点击的的x坐标和y坐标。

消息循环

对于每一个创建窗口的线程,操作系统都会为它建立一个消息队列,包含所有由该线程创建窗口的信息,
想要获取信息,可以通过GetMessage进行访问:

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

第一个参数是信息队列,其余参数是用来筛选信息的。

传递信息通过两个函数:

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

TranslateMessage和键盘输入有关,用于处理将键盘输入转化为字符串;
DispatchMessage是分发信息的。

例如,假设用户按下了左键,会发生以下事件:

  1. 操作系统将 WM_LBUTTONDOWN 放入消息队列中;
  2. 你的程序调用GetMessage函数;
  3. GetMessage函数获取WM_LBUTTONDOWN信息并填充MSG结构体;
  4. 你的程序调用 TranslateMessage 和 DispatchMessage 函数;
  5. DispatchMessage内部,操作系统调用你的窗口处理函数;
  6. 你的窗口处理函数内部,可以处理或忽视信息。

接下来进入消息循环,如果你想离开,可以调用PostQuitMessage函数:

PostQuitMessage(0);

PostQuitMessage函数会向消息队列中放入WM_QUIT消息,他会触发GetMessage函数返回0,并退出消息循环。

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

Posted Messages 和 Sent Messages

这两个概念很模糊:

  • Posting意思是将消息放入消息队列中
  • Sending意思是消息不放入队列,操作系统直接调用窗口处理程序

窗口处理程序

窗口处理程序如下:

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

有四个参数:

  • hwnd是窗口句柄
  • uMsg是消息代码,例如 WM_SIZE 代表调整窗口尺寸
  • wParam和lParam包含了额外信息,具体含义依赖于消息代码而不同

LRESULT是返回值类型,这个值依赖于错误码,
CALLBACK是函数的调用公约。

典型的窗口处理函数是一个switch结构:

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

    // etc
}

其他数据如lPararm和wParam都是整型指针(32位和64位)。

例如WM_SIZE信息状态如下:

  • wParam是一个标志位,表明是否最大化,最小化或尺寸可调。
  • lParam包含了窗口的心宽度和高度。

因此这个窗口处理函数看起来是这样:

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
    case WM_SIZE:
        {
            int width = LOWORD(lParam);  // Macro to get the low-order word.
            int height = HIWORD(lParam); // Macro to get the high-order word.

            // Respond to the message:
            OnSize(hwnd, (UINT)wParam, width, height);
        }
        break;
    }
}

void OnSize(HWND hwnd, UINT flag, int width, int height)
{
    // Handle resizing
}

LOWORD和HIWORD宏从lParam获取16位的宽度和高度。

默认信息处理

如果你不想处理信息,也可以使用默认处理函数:

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

避免阻塞

当执行窗口处理程序时,他会阻塞其他信息如窗口创建等,
例如假设你在窗口处理程序中处理一个TCP连接,那么在TCP完成请求之前你的UI都不会响应,这期间的鼠标操作键盘输入等都不会处理。

为了避免阻塞,你可以:

  • 创建新线程
  • 使用线程池
  • 使用异步I/O调用
  • 使用异步处理调用

描画窗口

在创建窗口之后,现在你想在内部添加点东西,在Windows术语中,这叫窗口描画,你可以理解为窗口是一个画布,等着你去填充。

有时你需要更新整个窗口,有时你只需要更新部分窗口,那么这时会发送一个WM_PAINT消息,
描画一部分窗口叫做区域更新。

初始化时一定会描绘整个窗口,因此,你至少会收到一个WM_PAINT消息。

你只需要针对客户端区域,框架由操作系统描画,
现在假设你移动了另一个窗口遮盖了前一个窗口,这时你也会收到一个WM_PAINT消息。

如果你拖长窗口,也会收到WM_PAINT消息。

下面是描画一个矩形,并填充颜色:

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;
}

描画内容在开始描画函数和结束描画函数之间,
在你的描画代码中,你有两个基本选项:

  • 描画整个客户端区域,不管更新区域有多大,任何在更新区域外部的内容都会被操作系统裁剪掉;
  • 通过只描画更新区域来进行优化。

FillRect的第二个参数指描画整个区域。

关闭窗口

当你要关闭窗口时(如点击关闭按钮,或者按ALT+F4按键),窗口会接收到一个WM_CLOSE消息,你可以增加一次提示消息:

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

此时消息函数显示了一个模态对话框,包含OK按钮和离开按钮,如果用户点击了OK按钮,就执行销毁函数,否则就不执行。

当窗口要销毁时,在销毁前,会收到一个 WM_DESTROY 消息,你可以调用PostQuitMessage函数来响应它:

case WM_DESTROY:
    PostQuitMessage(0);
    return 0;

这样消息循环就会退出。

窗口关闭流程如下:

管理应用状态

在通常程序中,你可以使用全局变量来追踪程序在函数间的调用,但是在大型程序中,大量的全局变量会难以维护。
Windows提供了一种方式,可以用来追踪API调用信息。

当CreateWindowEx函数被调用时,他会发送以下两个消息给你的窗口处理程序:

  • WM_NCCREATE
  • WM_CREATE

CreateWindowEx函数的最后一个参数可以传递任意类型的指针,你可以传递一个结构体指针,里面包含你想要描述该窗口的信息,这样当收到以上两个消息时,就可以取出这个结构体。
首先定义一个结构体,结构体内容略:

// Define a structure to hold some state information.

struct StateInfo {
    // ... (struct members not shown)
};

然后将结构体指针传递给CreateWindowEx:

StateInfo *pState = new (std::nothrow) StateInfo;

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

// Initialize the structure members (not shown).

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
    pState      // Additional application data
    );

当收到 WM_NCCREATE 和 WM_CREATE 消息时,lParam参数会包含一个CREATESTRUCT结构体,该结构体里包含了你传递的指针。

首先强制转换lPatam指针:

CREATESTRUCT *pCreate = reinterpret_cast<CREATESTRUCT*>(lParam);

然后再强制转换lpCreateParams成员:

pState = reinterpret_cast<StateInfo*>(pCreate->lpCreateParams);

接下来调用SetWindowLongPtr函数保存结构体数据:

SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pState);

最后我们可以使用GetWindowLongPtr来获取结构体:

LONG_PTR ptr = GetWindowLongPtr(hwnd, GWLP_USERDATA);
StateInfo *pState = reinterpret_cast<StateInfo*>(ptr);

现在你可以向下边这样写程序了:

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    StateInfo *pState;
    if (uMsg == WM_CREATE)
    {
        CREATESTRUCT *pCreate = reinterpret_cast<CREATESTRUCT*>(lParam);
        pState = reinterpret_cast<StateInfo*>(pCreate->lpCreateParams);
        SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pState);
    }
    else
    {
        pState = GetAppState(hwnd);
    }

    switch (uMsg)
    {


    // Remainder of the window procedure not shown ...

    }
    return TRUE;
}

面向对象方法

TODO.

使用Windows COM组件

Component Object Model(COM)组件是一个二进制标准,而不是语言标准,很多基于Windows的程序都依赖于COM组件,例如:

  • Graphics (Direct2D)
  • Text (DirectWrite)
  • The Windows Shell
  • The Ribbon control
  • UI animation

COM组件的三个主要目标是:

  • 分离接口和实现
  • 管理对象的生命周期
  • 在运行时发现对象的能力?(Discovering the capabilities of an object at run time.)

COM接口是什么?

接口是一个抽象类,用于调用和实现分离。

初始化COM库

使用COM之前,一定要调用初始化函数CoInitializeEx函数,每个使用COM的线程都必须单独调用此函数:

HRESULT CoInitializeEx(LPVOID pvReserved, DWORD dwCoInit);

第一个参数一定要是NUll;
第二个参数指定了线程模型:单线程或者多线程。
如果你指定了单线程,你就要保证以下两点:

  • 你要保证只在一个线程中使用COM,而不会在多线程中使用COM;
  • 线程要有消息循环。

如果不能保证以上两点,就得使用多线程。
将标志位设置成以下flgs:

Flag Description
COINIT_\APARTMENTTHREADED Apartment threaded.
COINIT_MULTITHREADED Multithreaded.

通常来说创建一个窗口的线程应该使用COINIT_APARTMENTTHREADED标志位,其他线程使用COINIT_MULTITHREADED 标志位。

下面是初始化COM的样例:

HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);

反初始化COM库

在线程退出前,一定要反初始化COM库:

CoUninitialize();

COM 中的错误代码

COM函数的返回值是一个32位整形,
Windows SDK中定义了两个宏来处理调用是否成功:TRUE和FALSE。
下面是检测COInitializeEx函数的处理:

HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | 
    COINIT_DISABLE_OLE1DDE);

if (SUCCEEDED(hr))
{
    // The function succeeded.
}
else
{
    // Handle the error.
}

但是有时反向处理更容易些:

HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | 
    COINIT_DISABLE_OLE1DDE);

if (FAILED(hr))
{
    // Handle the error.
}
else
{
    // The function succeeded.
}

创建一个COM对象

创建COM对象有两种方式:

  • 模块提供了创建对象的方法
  • 通用创建方法CoCreateInstance

例如在之前的Shape图形库中,该库提供了创建对象的方法:

// Not an actual Windows function. 
// 不是真的Windows函数,虚构的。

HRESULT CreateShape(IDrawable** ppShape);

这样你就能创建一个新Shape对象了:

IDrawable *pShape;

HRESULT hr = CreateShape(&pShape);
if (SUCCEEDED(hr))
{
    // Use the Shape object.
}
else
{
    // An error occurred.
}

上述的ppShape是指向指针的指针,
由于CreateShape的返回值用于判断调用是否成功,因此他有两种出参方式,引用出参或者指针出参,COM选择使用指针出参,
又由于我们想要返回的是一个接口类型的指针,
因此这里会出现指向指针的指针,
第一个指针作为出参,第二个指针作为返回值。

通用创建对象方式

众所周知,C++中一个类可以继承自多个接口,
因此,要创建一个COM对象,需要两个元素,
一个是要创建的对象,一个是要实现的接口。

在COM中,对象或者接口都是由一个128位的数字表示的,叫做globally unique identifier (GUID),有时也叫universally unique identifiers (UUIDs).

例如Shape库声明了两个GUID常量

extern const GUID CLSID_Shape;
extern const GUID IID_IDrawable;

CLSID前缀是类ID,IID前缀是接口标识符ID(interface identifier)

给出这些值,就能创建一个Shape实例了:

IDrawable *pShape;
hr = CoCreateInstance(CLSID_Shape, NULL, CLSCTX_INPROC_SERVER, IID_Drawable,
     reinterpret_cast<void**>(&pShape));

if (SUCCEEDED(hr))
{
    // Use the Shape object.
}
else
{
    // An error occurred.
}

样例:打开对话框

Shape样例我们使用的虚拟的库,下面我们做一个真正的Windows程序,一个打开对话框:

这个例子中我们使用的是Common Item Dialog对象,这个对象实现了IFileOpenDialog接口,她声明在Shobjdl.h中:

#include <windows.h>
#include <shobjidl.h> 

int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow)
{
    HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | 
        COINIT_DISABLE_OLE1DDE);
    if (SUCCEEDED(hr))
    {
        IFileOpenDialog *pFileOpen;

        // Create the FileOpenDialog object.
        hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_ALL, 
                IID_IFileOpenDialog, reinterpret_cast<void**>(&pFileOpen));

        if (SUCCEEDED(hr))
        {
            // Show the Open dialog box.
            hr = pFileOpen->Show(NULL);

            // Get the file name from the dialog box.
            if (SUCCEEDED(hr))
            {
                IShellItem *pItem;
                hr = pFileOpen->GetResult(&pItem);
                if (SUCCEEDED(hr))
                {
                    PWSTR pszFilePath;
                    hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath);

                    // Display the file name to the user.
                    if (SUCCEEDED(hr))
                    {
                        MessageBoxW(NULL, pszFilePath, L"File Path", MB_OK);
                        CoTaskMemFree(pszFilePath);
                    }
                    pItem->Release();
                }
            }
            pFileOpen->Release();
        }
        CoUninitialize();
    }
    return 0;
}

管理COM对象的生命周期

COM提供了引用计数来自动删除对象,
当然这里说的自动删除是指如果这个COM指针被共享,COM才删除过期的对象。
这和shared_ptr应该是一样的。

查询接口

就是安全转换的一种方式。
TODO.

Windows控件

参考链接:
https://docs.microsoft.com/en-us/windows/win32/learnwin32/what-is-a-window-

posted @ 2021-02-14 17:46  多弗朗强哥  阅读(336)  评论(0编辑  收藏  举报