简介
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.
然而现在TEXT
和TCHAR
都很少用了,因为现在都是用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程序的入口点,有两个版本WinMain
和wWinMain
,两者区别是从命令行传入的是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是分发信息的。
例如,假设用户按下了左键,会发生以下事件:
- 操作系统将 WM_LBUTTONDOWN 放入消息队列中;
- 你的程序调用GetMessage函数;
- GetMessage函数获取WM_LBUTTONDOWN信息并填充MSG结构体;
- 你的程序调用 TranslateMessage 和 DispatchMessage 函数;
- DispatchMessage内部,操作系统调用你的窗口处理函数;
- 你的窗口处理函数内部,可以处理或忽视信息。
接下来进入消息循环,如果你想离开,可以调用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-