使用 C++ 和 Win32 API 为Windows电脑生成桌面应用
二次整理自网络教程。
0 前言
由于需要用到windows窗口,参考以下文档:
使用 Win32
API 生成桌面Windows应用 - Win32 apps
进入其中的:Win32
和 C++ 入门 - Win32 apps
都是机翻的,得看英文原版。
安装Windows SDK即可。注意其不支持硬件驱动程序开发。
匈牙利表示法虽然仍在文档中存在,但已经不再使用。
1 第一个窗口程序
窗口简介
窗口之间有两种关系,拥有关系和亲子关系:
窗口句柄的数据类型是
HWND,通过将其作为参数的函数来操作它。句柄不是指针。
原点始终在左上角:
每个窗口程序都包含一个名为 WinMain 或
wWinMain 的入口点函数。
1
| int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pCmdLine, int nCmdShow);
|
- hInstance
称为“实例句柄”或“模块句柄”。操作系统使用此值在内存中加载可执行文件时标识可执行文件
(EXE) 。 某些Windows函数需要实例句柄,例如加载图标或位图。
- hPrevInstance 没有意义。 它在 16
位Windows中使用,但现在始终为零。
- pCmdLine 包含作为 Unicode 字符串的命令行参数。
- nCmdShow
是一个标志,指示主应用程序窗口是最小化、最大化还是正常显示。
WinMain 函数与 wWinMain
相同,但命令行参数作为 ANSI 字符串传递。
WINAPI 是调用约定。
调用约定定义函数如何从调用方接收参数。
例如,它定义参数在堆栈上显示的顺序。
程序示例
创建一个窗口。
代码来自microsoft/Windows-classic-samples中的Samples/Win7Samples/begin/LearnWin32/HelloWorld/cpp。此后的程序示例均位于LearnWin32中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| #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) { const wchar_t CLASS_NAME[] = L"Sample Window Class"; WNDCLASS wc = { };
wc.lpfnWndProc = WindowProc; wc.hInstance = hInstance; wc.lpszClassName = CLASS_NAME; RegisterClass(&wc);
HWND hwnd = CreateWindowEx( 0, CLASS_NAME, L"Learn to Program Windows", WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, NULL, hInstance, NULL );
if (hwnd == NULL) { return 0; }
ShowWindow(hwnd, nCmdShow);
MSG msg = { }; while (GetMessage(&msg, NULL, 0, 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); }
|
关于消息和消息队列
- Win32 apps
关闭窗口的消息流程:
管理应用程序状态
- Win32 apps
2 使用COM
COM简介
COM 是
二进制标准,而不是语言标准:它定义应用程序和软件组件之间的二进制接口。
作为二进制标准,COM 是语言中立的,尽管它自然映射到某些 C++ 构造。
COM接口的概念与C#、Java中的接口和C#中的纯虚类类似。使用名为“接口定义语言”
(IDL) 的语言定义 COM 接口。 IDL 文件由 IDL 编译器处理,该编译器生成 C++
头文件。
初始化
使用 COM 的任何Windows程序都必须通过调用 CoInitializeEx
函数来初始化 COM 库。 使用 COM 接口的每个线程都必须单独调用此函数。
对于 CoInitializeEx 的每个成功调用,必须在线程退出之前调用 CoUninitialize
。 此函数不采用任何参数,并且没有返回值。
1 2
| HRESULT CoInitializeEx(LPVOID pvReserved, DWORD dwCoInit); CoUninitialize();
|
CoInitializeEx第一个参数是保留的,必须为 NULL。
第二个参数指定程序将使用的线程模型。
COINIT_APARTMENTTHREADED |
单元线程。 |
COINIT_MULTITHREADED |
多线程。 |
如果使用单元线程,你应该保证:
- 你将从单个线程访问每个 COM 对象;不会在多个线程之间共享 COM
接口指针。
- 线程将有一个消息循环。
COM 方法和函数返回 HRESULT
类型的值要指示成功或失败。 HRESULT 是一个 32
位整数,高阶位表示成功或失败, 0表示成功,1 表示失败。少量 COM
方法不返回 HRESULT 值。 例如, AddRef
和 Release
方法返回无符号长值。 Windows SDK 标头提供两个宏,SUCCEEDED
宏和 FAILED
宏,检查 COM 方法是否成功。
创建对象
CoCreateInstance
函数提供用于创建对象的泛型机制。两个 COM
对象可以实现相同的接口,一个对象可以实现两个或多个接口。
因此,创建对象的泛型函数需要两段信息:
在 COM 中,通过为其分配 128
位数字来标识对象或接口,该数字称为全局唯一标识符 (GUID) 。GUID
以有效唯一的方式生成。 GUID
是解决如何在没有中央注册机构的情况下创建唯一标识符的问题。 GUID 有时称为
通用唯一标识符 (UUID) 。 在 COM 之前,它们用于 DCE/RPC
(分布式计算环境/远程过程调用) 。 存在多个用于创建新 GUID
的算法,并非所有算法都严格保证唯一性,但意外创建同一 GUID
值的概率极小,实际上为零。
程序示例
创建一个“打开”对话框。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| #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;
hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_ALL, IID_IFileOpenDialog, reinterpret_cast<void**>(&pFileOpen)); if (SUCCEEDED(hr)) { hr = pFileOpen->Show(NULL);
if (SUCCEEDED(hr)) { IShellItem *pItem; hr = pFileOpen->GetResult(&pItem); if (SUCCEEDED(hr)) { PWSTR pszFilePath; hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath);
if (SUCCEEDED(hr)) { MessageBoxW(NULL, pszFilePath, L"File Path", MB_OK); CoTaskMemFree(pszFilePath); } pItem->Release(); } } pFileOpen->Release(); } CoUninitialize(); } return 0; }
|
管理对象的生存期
每个 COM 接口都必须直接或间接地从名为 IUnknown
的接口继承。 此接口提供所有 COM
对象必须支持的一些基线功能,它定义了三种方法:
为了释放资源,C++ 使用自动析构函数,而 C# 和 Java 则使用垃圾回收。COM
使用称为 引用计数的方法。
每个 COM 对象都维护内部计数。 这称为引用计数。
引用计数跟踪对象当前处于活动状态的引用数。
当引用数降至零时,对象将删除自身。 最后一部分值得重复:对象删除自身。
程序永远不会显式删除对象。
1
| HRESULT QueryInterface(REFIID riid, void **ppvObject);
|
查询某个组件是否支持某个特定的接口。第一个参数标识客户所需的接口;第二个参数是指向接口的指针。
COM
中的内存分配 - Win32 apps