通用GUI编程技术——Win32 原生编程实战(七)——更多高级控件¶
上一篇文章我们搞定了 TreeView 控件,那种层级结构的展示方式确实很强大。说实话,学到这里你可能已经觉得 Win32 的控件体系相当完善了——按钮、列表、树形视图,常用的 UI 元素基本都有了。但事情没那么简单,实际做项目的时候,你很快会发现还需要一些更"高级"的控件:标签页来分隔不同视图、进度条来显示任务状态、滑块来调节数值、旋转按钮来微调参数。今天我们就把这些剩下的常用控件一网打尽。
为什么还要学更多控件¶
说实话,我刚学 Win32 的时候也觉得控件学得差不多了。直到我开始做实际项目,才发现有些需求用基础控件实现起来特别麻烦。
举个例子,假设你要做一个设置界面,有十几类不同的设置项。用按钮切换视图?太丑了。用多个窗口?管理起来很麻烦。这时候就需要 TabControl——用户熟悉的标签页界面,一个窗口就能容纳大量内容。
再比如,你需要做一个文件复制程序,用户得知道复制进度。用一个编辑框显示百分比?不仅不好看,而且不直观。进度条控件就是为这种场景设计的——它有标准的视觉效果,用户一眼就能看出进度。
还有一个现实原因:现代操作系统和应用程序已经让用户习惯了这些控件。如果你用非标准的方式实现相同的功能,用户会觉得"这个程序很奇怪"。使用系统原生控件,不仅开发效率高,用户体验也更好。
这篇文章会带你学习 TabControl、Progress Bar、Trackbar、UpDown 这些高级控件,每个控件都有完整的示例代码。学完之后,你的 Win32 控件工具箱就更完善了。
环境说明¶
在我们正式开始之前,先明确一下我们这次动手的环境:
- 平台:Windows 10/11(理论上 Windows Vista+ 都行,但谁还用那些老古董)
- 开发工具:Visual Studio 2019 或更高版本
- 编程语言:C++(C++17 或更新)
- 项目类型:桌面应用程序(Win32 项目)
代码假设你已经熟悉前面文章的内容——至少知道怎么创建窗口、WM_NOTIFY 是怎么工作的、怎么处理控件通知。如果这些概念对你来说还比较陌生,建议先去看看前面的笔记。
第一章:TabControl 标签页¶
是什么¶
TabControl(标签页控件)是你每天都在用的东西。打开 Chrome 或者 VS Code,顶部那一排可以点击切换的标签,就是标签页的典型应用。每个标签代表一个独立的内容区域,用户点击不同的标签就能看到不同的内容。
+--------------------------------------------------+
| [ General ] [ Advanced ] [ About ] |
+--------------------------------------------------+
| |
| (这里显示 "General" 标签页的内容) |
| |
| |
+--------------------------------------------------+
当点击 "Advanced" 标签后:
+--------------------------------------------------+
| [ General ] [ Advanced ] [ About ] |
+--------------------------------------------------+
| |
| (这里显示 "Advanced" 标签页的内容) |
| |
| |
+--------------------------------------------------+
长什么样¶
TabControl 本身只显示那排标签,内容区域需要你自己管理。通常的做法是:为每个标签页创建一个子窗口(或对话框),用户切换标签时显示/隐藏对应的子窗口。
用来做什么¶
标签页的主要用途是组织大量相关但不适合同时显示的内容:
- 设置界面的不同分类(通用设置、高级设置、关于等)
- 多标签浏览器/编辑器(Chrome、VS Code)
- 属性页对话框(文件属性、系统属性)
- 向导程序的不同步骤
典型场景¶
| 场景 | 说明 |
|---|---|
| 设置对话框 | 把设置项按类别分成多个标签页 |
| 属性窗口 | 显示对象的不同方面属性 |
| 多文档界面 | 一个窗口内打开多个文档 |
| 选项配置 | 浏览器、编辑器的设置页面 |
WC_TABCONTROL 窗口类¶
创建 TabControl 使用的是 WC_TABCONTROL 窗口类(或者直接用 SYNCTABCONTROL_CLASS,不过前者更常用):
#include <commctrl.h>
#pragma comment(lib, "comctl32.lib")
// 控件 ID
#define ID_TABCONTROL 1001
HWND hTab = CreateWindowEx(
0, // 扩展样式
WC_TABCONTROL, // 窗口类名
L"", // 标题(TabControl 不需要)
WS_CHILD | WS_VISIBLE, // 基本样式
10, 10, // 位置
400, 200, // 大小
hwnd, // 父窗口
(HMENU)ID_TABCONTROL, // 控件 ID
hInstance, // 实例句柄
NULL // 无额外数据
);
⚠️ 注意
创建 TabControl 之前需要初始化通用控件库。这个我们在前面已经讲过,但这里再强调一次:
INITCOMMONCONTROLSEX icex = {0};
icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
icex.dwICC = ICC_TAB_CLASSES;
InitCommonControlsEx(&icex);
如果忘记初始化,TabControl 创建会失败,而且不会有任何错误提示。
TCM_INSERTITEM 添加标签页¶
添加标签页使用 TCM_INSERTITEM 消息,需要一个 TCITEM 结构体:
TCITEM tie = {0};
tie.mask = TCIF_TEXT; // 指明我们有 pszText 字段
tie.pszText = (LPWSTR)L"General"; // 标签文字
tie.cchTextMax = 0; // 不需要,因为 pszText 不是常量
// 在索引 0 的位置插入(或者用 TabCtrl_GetItemCount() 获取当前数量追加到末尾)
SendMessage(hTab, TCM_INSERTITEM, 0, (LPARAM)&tie);
// 添加第二个标签
tie.pszText = (LPWSTR)L"Advanced";
SendMessage(hTab, TCM_INSERTITEM, 1, (LPARAM)&tie);
// 添加第三个标签
tie.pszText = (LPWSTR)L"About";
SendMessage(hTab, TCM_INSERTITEM, 2, (LPARAM)&tie);
TCITEM 结构体的 mask 字段指定哪些字段有效:
| mask 值 | 对应字段 | 说明 |
|---|---|---|
| TCIF_TEXT | pszText | 标签文字 |
| TCIF_IMAGE | iImage | 图像列表索引 |
| TCIF_PARAM | lParam | 应用程序定义的数据 |
| TCIF_STATE | dwState | 标签状态 |
TCN_SELCHANGE 通知¶
当用户点击切换标签时,TabControl 会发送 TCN_SELCHANGE 通知:
case WM_NOTIFY:
{
LPNMHDR pnmh = (LPNMHDR)lParam;
if (pnmh->idFrom == ID_TABCONTROL && pnmh->code == TCN_SELCHANGE)
{
// 获取当前选中的标签索引
int currentIndex = SendMessage(hTab, TCM_GETCURSEL, 0, 0);
// 根据 currentIndex 显示/隐藏对应的内容窗口
OnTabChanged(currentIndex);
}
break;
}
TCITEM 结构详解¶
TCITEM 结构体定义如下:
typedef struct tagTCITEM {
UINT mask; // 指明哪些字段有效
DWORD dwState; // 状态
DWORD dwStateMask;// 状态掩码
LPWSTR pszText; // 标签文字
int cchTextMax; // 文字缓冲区大小
int iImage; // 图像索引
LPARAM lParam; // 应用程序定义数据
} TCITEM, *LPTCITEM;
最常用的字段是 pszText,用于设置或获取标签文字。如果需要获取标签文字,需要设置 cchTextMax 为缓冲区大小:
wchar_t buffer[256];
TCITEM tie = {0};
tie.mask = TCIF_TEXT;
tie.pszText = buffer;
tie.cchTextMax = 256;
// 获取索引 0 的标签文字
SendMessage(hTab, TCM_GETITEM, 0, (LPARAM)&tie);
// 现在 buffer 里就是标签文字
完整示例:简单的标签页¶
下面是一个完整的 TabControl 示例:
#include <windows.h>
#include <commctrl.h>
#pragma comment(lib, "comctl32.lib")
#define ID_TABCONTROL 1001
#define ID_TAB_CONTENT 2000 // 内容窗口 ID 基数
// 标签页内容窗口句柄
HWND g_hTabPages[3] = {NULL};
LRESULT CALLBACK TabPageProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_CREATE:
{
// 获取标签页索引(通过窗口长整形存储)
int index = (int)GetWindowLongPtr(hwnd, GWLP_USERDATA);
// 根据索引创建不同的内容
const wchar_t* texts[] = {
L"这是 General 标签页的内容\n\n可以在这里放置通用设置选项",
L"这是 Advanced 标签页的内容\n\n可以在这里放置高级设置选项",
L"这是 About 标签页的内容\n\n可以在这里放置版本信息"
};
CreateWindowEx(0, L"STATIC", texts[index],
WS_CHILD | WS_VISIBLE,
20, 20, 300, 100,
hwnd, NULL, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
return 0;
}
case WM_DESTROY:
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
void CreateTabPage(HWND hParent, int index, const wchar_t* title, HINSTANCE hInstance)
{
// 注册标签页窗口类(只注册一次)
static BOOL s_classRegistered = FALSE;
if (!s_classRegistered)
{
WNDCLASS wc = {0};
wc.lpfnWndProc = TabPageProc;
wc.hInstance = hInstance;
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.lpszClassName = L"TabPageClass";
RegisterClass(&wc);
s_classRegistered = TRUE;
}
// 创建标签页窗口(初始隐藏)
HWND hPage = CreateWindowEx(
0, L"TabPageClass", L"",
WS_CHILD | WS_CLIPSIBLINGS,
10, 40, 380, 150,
hParent, (HMENU)(ID_TAB_CONTENT + index), hInstance, NULL
);
// 保存索引到窗口用户数据
SetWindowLongPtr(hPage, GWLP_USERDATA, index);
g_hTabPages[index] = hPage;
}
void ShowTabPage(int index)
{
// 隐藏所有标签页
for (int i = 0; i < 3; i++)
{
if (g_hTabPages[i])
ShowWindow(g_hTabPages[i], SW_HIDE);
}
// 显示选中的标签页
if (g_hTabPages[index] && index >= 0 && index < 3)
ShowWindow(g_hTabPages[index], SW_SHOW);
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_CREATE:
{
HINSTANCE hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
// 初始化通用控件
INITCOMMONCONTROLSEX icex = {0};
icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
icex.dwICC = ICC_TAB_CLASSES;
InitCommonControlsEx(&icex);
// 创建 TabControl
HWND hTab = CreateWindowEx(
0, WC_TABCONTROL, L"",
WS_CHILD | WS_VISIBLE,
10, 10, 400, 30,
hwnd, (HMENU)ID_TABCONTROL, hInstance, NULL
);
// 添加标签页
TCITEM tie = {0};
tie.mask = TCIF_TEXT;
tie.pszText = (LPWSTR)L"General";
SendMessage(hTab, TCM_INSERTITEM, 0, (LPARAM)&tie);
tie.pszText = (LPWSTR)L"Advanced";
SendMessage(hTab, TCM_INSERTITEM, 1, (LPARAM)&tie);
tie.pszText = (LPWSTR)L"About";
SendMessage(hTab, TCM_INSERTITEM, 2, (LPARAM)&tie);
// 创建各个标签页的内容窗口
CreateTabPage(hwnd, 0, L"General", hInstance);
CreateTabPage(hwnd, 1, L"Advanced", hInstance);
CreateTabPage(hwnd, 2, L"About", hInstance);
// 显示第一个标签页
ShowTabPage(0);
return 0;
}
case WM_NOTIFY:
{
LPNMHDR pnmh = (LPNMHDR)lParam;
if (pnmh->idFrom == ID_TABCONTROL && pnmh->code == TCN_SELCHANGE)
{
HWND hTab = GetDlgItem(hwnd, ID_TABCONTROL);
int currentIndex = SendMessage(hTab, TCM_GETCURSEL, 0, 0);
ShowTabPage(currentIndex);
}
return 0;
}
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
const wchar_t CLASS_NAME[] = L"TabControlWindow";
WNDCLASS wc = {0};
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
RegisterClass(&wc);
HWND hwnd = CreateWindowEx(
0, CLASS_NAME, L"TabControl 示例",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 450, 250,
NULL, NULL, hInstance, NULL
);
if (hwnd)
{
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
MSG msg = {0};
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return 0;
}
这个示例创建了一个包含三个标签页的窗口,每个标签页显示不同的内容。当用户点击切换标签时,对应的内容窗口会显示出来。
第二章:Progress Bar 进度条¶
是什么¶
Progress Bar(进度条)是一种视觉反馈控件,用来显示某个操作的完成进度。你肯定见过这种控件——安装程序复制文件时、下载文件时、加载游戏时,通常都会有一个从 0% 到 100% 的条形指示器。
未开始时:
[████████████████████████████████████████] 100%
完全填充
进行中:
[████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░] 40%
部分填充
长什么样¶
进度条是一个矩形区域,内部会从左到右填充颜色来表示进度。填充的比例对应操作的完成百分比。
用来做什么¶
进度条的核心用途是让用户知道操作正在进行,以及大概还需要多久。这对用户体验非常重要——如果没有进度提示,用户可能会觉得程序卡死了。
典型场景¶
| 场景 | 说明 |
|---|---|
| 文件复制 | 显示已复制/总字节数 |
| 安装程序 | 显示安装进度 |
| 下载/上传 | 显示已传输/总字节数 |
| 数据处理 | 显示已处理/总记录数 |
| 加载资源 | 游戏或应用程序加载进度 |
PROGRESS_CLASS 窗口类¶
创建进度条使用 PROGRESS_CLASS 窗口类:
#include <commctrl.h>
#pragma comment(lib, "comctl32.lib")
#define ID_PROGRESS 1002
HWND hProgress = CreateWindowEx(
0, // 扩展样式
PROGRESS_CLASS, // 窗口类名
L"", // 标题(进度条不需要)
WS_CHILD | WS_VISIBLE, // 基本样式
10, 10, // 位置
300, 20, // 大小
hwnd, // 父窗口
(HMENU)ID_PROGRESS, // 控件 ID
hInstance, // 实例句柄
NULL // 无额外数据
);
⚠️ 注意
进度条也需要初始化通用控件库,使用 ICC_PROGRESS_CLASS 标志:
INITCOMMONCONTROLSEX icex = {0};
icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
icex.dwICC = ICC_PROGRESS_CLASS;
InitCommonControlsEx(&icex);
PBM_SETRANGE32 设置范围¶
进度条有一个范围(通常是 0-100),你需要在设置进度之前先设置这个范围:
// 设置范围从 0 到 100
SendMessage(hProgress, PBM_SETRANGE32, 0, 100);
// 或者设置为 0 到 1000(更精细的控制)
SendMessage(hProgress, PBM_SETRANGE32, 0, 1000);
PBM_SETRANGE32 消息的 wParam 是范围下限,lParam 是范围上限。都是 32 位有符号整数,所以范围可以很大。
还有一个老版本的 PBM_SETRANGE 消息,但它的参数是 16 位的,范围有限。新代码应该用 PBM_SETRANGE32。
PBM_SETPOS 设置当前位置¶
设置进度条的当前位置使用 PBM_SETPOS 消息:
// 设置进度为 50%
SendMessage(hProgress, PBM_SETPOS, 50, 0);
// 设置进度为 75%
SendMessage(hProgress, PBM_SETPOS, 75, 0);
如果当前范围是 0-100,那么 PBM_SETPOS 的值应该是 0-100 之间的整数。进度条会自动计算并绘制对应的填充比例。
你也可以获取当前位置:
PBS_SMOOTH 平滑样式¶
默认情况下,进度条是"块状"的——它会显示一系列离散的方块。如果你想要平滑连续的进度条,可以加上 PBS_SMOOTH 样式:
HWND hProgress = CreateWindowEx(
0,
PROGRESS_CLASS,
L"",
WS_CHILD | WS_VISIBLE | PBS_SMOOTH, // 注意这里
10, 10, 300, 20,
hwnd, (HMENU)ID_PROGRESS, hInstance, NULL
);
块状进度条 vs 平滑进度条:
完整示例:模拟文件复制进度¶
下面是一个完整的进度条示例,模拟文件复制操作:
#include <windows.h>
#include <commctrl.h>
#pragma comment(lib, "comctl32.lib")
#define ID_PROGRESS 1001
#define ID_START_BUTTON 1002
#define ID_STATUS_TEXT 1003
HWND g_hProgress = NULL;
HWND g_hStatusText = NULL;
VOID CALLBACK CopyProgressCallback(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime)
{
// 获取当前进度
int pos = SendMessage(g_hProgress, PBM_GETPOS, 0, 0);
// 如果已经到 100%,停止定时器
if (pos >= 100)
{
KillTimer(hwnd, idEvent);
SetWindowText(g_hStatusText, L"复制完成!");
EnableWindow(GetDlgItem(hwnd, ID_START_BUTTON), TRUE);
return;
}
// 增加进度
pos += 2; // 每次增加 2%
if (pos > 100) pos = 100;
SendMessage(g_hProgress, PBM_SETPOS, pos, 0);
// 更新状态文字
wchar_t status[64];
swprintf_s(status, 64, L"正在复制... %d%%", pos);
SetWindowText(g_hStatusText, status);
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_CREATE:
{
HINSTANCE hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
// 初始化通用控件
INITCOMMONCONTROLSEX icex = {0};
icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
icex.dwICC = ICC_PROGRESS_CLASS;
InitCommonControlsEx(&icex);
// 创建进度条
g_hProgress = CreateWindowEx(
0, PROGRESS_CLASS, L"",
WS_CHILD | WS_VISIBLE | PBS_SMOOTH,
20, 20, 300, 25,
hwnd, (HMENU)ID_PROGRESS, hInstance, NULL
);
// 设置进度条范围
SendMessage(g_hProgress, PBM_SETRANGE32, 0, 100);
SendMessage(g_hProgress, PBM_SETPOS, 0, 0);
// 创建状态文字
g_hStatusText = CreateWindowEx(
0, L"STATIC", L"点击开始按钮开始复制",
WS_CHILD | WS_VISIBLE,
20, 55, 300, 20,
hwnd, (HMENU)ID_STATUS_TEXT, hInstance, NULL
);
// 创建开始按钮
CreateWindowEx(
0, L"BUTTON", L"开始复制",
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
20, 85, 100, 30,
hwnd, (HMENU)ID_START_BUTTON, hInstance, NULL
);
return 0;
}
case WM_COMMAND:
{
WORD controlId = LOWORD(wParam);
WORD notificationCode = HIWORD(wParam);
if (controlId == ID_START_BUTTON && notificationCode == BN_CLICKED)
{
// 禁用按钮,防止重复点击
EnableWindow((HWND)lParam, FALSE);
// 重置进度
SendMessage(g_hProgress, PBM_SETPOS, 0, 0);
SetWindowText(g_hStatusText, L"正在复制... 0%");
// 启动定时器模拟进度
SetTimer(hwnd, 1, 100, CopyProgressCallback);
}
return 0;
}
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
const wchar_t CLASS_NAME[] = L"ProgressWindow";
WNDCLASS wc = {0};
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
RegisterClass(&wc);
HWND hwnd = CreateWindowEx(
0, CLASS_NAME, L"进度条示例",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 380, 200,
NULL, NULL, hInstance, NULL
);
if (hwnd)
{
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
MSG msg = {0};
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return 0;
}
这个示例创建了一个进度条和一个开始按钮,点击按钮后进度条会自动增加,模拟文件复制操作。
第三章:Trackbar 滑块¶
是什么¶
Trackbar(也叫 Slider,滑块控件)是一个可以让用户拖动滑块来选择数值的控件。你调节系统音量、屏幕亮度时用的就是这种控件。
水平滑块:
0 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100
▲
滑块
带刻度的水平滑块:
0 ━━┯━━━┯━━━┯━━━┯━━━┯━━━┯━━━┯━━━ 100
▲ ▲ ▲ ▲ ▲ ▲ ▲ ▲
刻度
长什么样¶
Trackbar 由一条轨道和一个可移动的滑块组成。用户可以用鼠标拖动滑块,或者点击轨道来快速跳转。轨道两侧可以显示刻度标记,帮助用户估计数值。
用来做什么¶
Trackbar 的主要用途是让用户在一个连续范围内选择数值:
- 音量控制(0-100)
- 亮度调节
- 进度跳转(视频播放器)
- 数值微调(各种设置项)
典型场景¶
| 场景 | 说明 |
|---|---|
| 音量控制 | 系统音量、应用程序音量 |
| 视频播放 | 跳转到指定时间位置 |
| 显示设置 | 亮度、对比度、色彩调节 |
| 游戏设置 | 难度、灵敏度等数值参数 |
| 缩放控制 | 地图、图片的缩放级别 |
TRACKBAR_CLASS 窗口类¶
创建 Trackbar 使用 TRACKBAR_CLASS 窗口类:
#include <commctrl.h>
#pragma comment(lib, "comctl32.lib")
#define ID_TRACKBAR 1003
HWND hTrackbar = CreateWindowEx(
0, // 扩展样式
TRACKBAR_CLASS, // 窗口类名
L"", // 标题(Trackbar 不需要)
WS_CHILD | WS_VISIBLE | TBS_AUTOTICKS, // 样式
10, 10, // 位置
300, 30, // 大小
hwnd, // 父窗口
(HMENU)ID_TRACKBAR, // 控件 ID
hInstance, // 实例句柄
NULL // 无额外数据
);
⚠️ 注意
Trackbar 也需要初始化通用控件库,使用 ICC_BAR_CLASSES 标志:
INITCOMMONCONTROLSEX icex = {0};
icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
icex.dwICC = ICC_BAR_CLASSES;
InitCommonControlsEx(&icex);
TBM_SETRANGE 设置范围¶
设置 Trackbar 的范围使用 TBM_SETRANGE 消息:
// 设置范围从 0 到 100
SendMessage(hTrackbar, TBM_SETRANGE,
(WPARAM)TRUE, // 重绘控件
(LPARAM)MAKELONG(0, 100) // LOWORD=最小值, HIWORD=最大值
);
// 或者分别设置最小值和最大值
SendMessage(hTrackbar, TBM_SETRANGEMIN, TRUE, 0);
SendMessage(hTrackbar, TBM_SETRANGEMAX, TRUE, 100);
TBM_SETRANGE 的 wParam 是 TRUE 表示控件需要重绘,lParam 是一个 LONG 类型,用 MAKELONG 宏把最小值和最大值打包进去。
TBM_SETPOS 设置位置¶
设置滑块位置使用 TBM_SETPOS 消息:
TBM_GETPOS 获取位置¶
获取滑块当前位置使用 TBM_GETPOS 消息:
TBS_AUTOTICKS 自动刻度¶
TBS_AUTOTICKS 样式会让 Trackbar 自动显示刻度。你也可以手动设置刻度间隔:
其他常用样式:
| 样式 | 说明 |
|---|---|
| TBS_HORZ | 水平方向(默认) |
| TBS_VERT | 垂直方向 |
| TBS_AUTOTICKS | 自动显示刻度 |
| TBS_NOTICKS | 不显示刻度 |
| TBS_BOTH | 在两侧都显示刻度 |
| TBS_TOOLTIPS | 支持工具提示 |
| TBS_ENABLESELRANGE | 支持选择范围 |
WM_HSCROLL/WM_VSCROLL 通知¶
Trackbar 通过 WM_HSCROLL(水平)或 WM_VSCROLL(垂直)消息通知父窗口滑块位置的变化:
case WM_HSCROLL:
case WM_VSCROLL:
{
HWND hTrackbar = (HWND)lParam;
// 检查是否是我们的 Trackbar
if (hTrackbar && GetWindowLongPtr(hTrackbar, GWLP_ID) == ID_TRACKBAR)
{
// 获取当前位置
int pos = SendMessage(hTrackbar, TBM_GETPOS, 0, 0);
// 根据通知代码处理
switch (LOWORD(wParam))
{
case TB_THUMBPOSITION:
case TB_THUMBTRACK:
// 用户正在拖动滑块
break;
case TB_LINEUP:
case TB_LINEDOWN:
case TB_PAGEUP:
case TB_PAGEDOWN:
case TB_TOP:
case TB_BOTTOM:
case TB_ENDTRACK:
// 其他通知
break;
}
// 更新显示
OnTrackbarChanged(pos);
}
break;
}
常用的通知代码:
| 通知代码 | 说明 |
|---|---|
| TB_BOTTOM | 滑块移到最小位置 |
| TB_ENDTRACK | 用户释放鼠标 |
| TB_LINEDOWN | 按右箭头键 |
| TB_LINEUP | 按左箭头键 |
| TB_PAGEDOWN | 按 PgDn 或点击滑块右侧 |
| TB_PAGEUP | 按 PgUp 或点击滑块左侧 |
| TB_THUMBPOSITION | 拖动结束后滑块位置(只在 WM_HSCROLL 中) |
| TB_THUMBTRACK | 拖动过程中滑块位置(只在 WM_HSCROLL 中) |
| TB_TOP | 滑块移到最大位置 |
⚠️ 注意
TB_THUMBTRACK 和 TB_THUMBPOSITION 只在 WM_HSCROLL 消息中发送,不在 WM_VSCROLL 中。这是 Windows 的一个历史遗留问题,处理时需要注意。
完整示例:音量调节器¶
下面是一个完整的 Trackbar 示例,实现一个简单的音量调节器:
#include <windows.h>
#include <commctrl.h>
#pragma comment(lib, "comctl32.lib")
#define ID_TRACKBAR 1001
#define ID_VOLUME_TEXT 1002
HWND g_hTrackbar = NULL;
HWND g_hVolumeText = NULL;
void UpdateVolumeText(int volume)
{
wchar_t text[64];
swprintf_s(text, 64, L"音量: %d%%", volume);
SetWindowText(g_hVolumeText, text);
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_CREATE:
{
HINSTANCE hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
// 初始化通用控件
INITCOMMONCONTROLSEX icex = {0};
icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
icex.dwICC = ICC_BAR_CLASSES;
InitCommonControlsEx(&icex);
// 创建音量显示文字
g_hVolumeText = CreateWindowEx(
0, L"STATIC", L"音量: 50%",
WS_CHILD | WS_VISIBLE | SS_CENTER,
20, 20, 260, 25,
hwnd, (HMENU)ID_VOLUME_TEXT, hInstance, NULL
);
// 创建 Trackbar
g_hTrackbar = CreateWindowEx(
0, TRACKBAR_CLASS, L"",
WS_CHILD | WS_VISIBLE | TBS_AUTOTICKS | TBS_BOTH,
20, 55, 260, 40,
hwnd, (HMENU)ID_TRACKBAR, hInstance, NULL
);
// 设置范围 0-100
SendMessage(g_hTrackbar, TBM_SETRANGE, TRUE, MAKELONG(0, 100));
// 设置刻度间隔为 10
SendMessage(g_hTrackbar, TBM_SETTICFREQ, 10, 0);
// 设置初始位置为 50
SendMessage(g_hTrackbar, TBM_SETPOS, TRUE, 50);
return 0;
}
case WM_HSCROLL:
{
HWND hTrackbar = (HWND)lParam;
if (hTrackbar == g_hTrackbar)
{
// 获取当前位置
int pos = SendMessage(hTrackbar, TBM_GETPOS, 0, 0);
// 更新显示
UpdateVolumeText(pos);
// 这里可以调用实际设置音量的 API
// 例如:waveOutSetVolume() 等
}
break;
}
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
const wchar_t CLASS_NAME[] = L"TrackbarWindow";
WNDCLASS wc = {0};
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
RegisterClass(&wc);
HWND hwnd = CreateWindowEx(
0, CLASS_NAME, L"Trackbar 示例 - 音量调节",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 320, 180,
NULL, NULL, hInstance, NULL
);
if (hwnd)
{
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
MSG msg = {0};
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return 0;
}
这个示例创建了一个音量调节器,拖动滑块会实时显示当前音量值。
第四章:UpDown 旋转按钮¶
是什么¶
UpDown 控件(也叫 Spin Button、旋转按钮控件)由两个小箭头按钮组成,一个向上、一个向下。用户点击箭头可以增加或减少数值。
┌─────────────┐
│ 50 │ ┥ <- 伙伴控件(通常是编辑框)
│ │ ┝
└─────────────┘ ┃
┟ <- UpDown 控件
┃
┌─────────────────┐
│ 255 0 0 │ ┥ <- 编辑框显示颜色值
│ │ ┝
└─────────────────┘ ┃ <- UpDown 控件
┟
┃
长什么样¶
UpDown 控件通常紧挨着一个编辑框,两个箭头垂直排列。上箭头增加数值,下箭头减少数值。
用来做什么¶
UpDown 控件的主要用途是微调数值,特别是那些不需要大范围调整、只需要精确微调的场合:
- 颜色选择器中的 RGB 值
- 年月日选择
- 坐标精确调整
- 小范围数值微调
典型场景¶
| 场景 | 说明 |
|---|---|
| 颜色选择 | RGB 值的精确调整 |
| 日期时间 | 年、月、日、时、分、秒选择 |
| 坐标调整 | 精确调整 X/Y 坐标 |
| 数值微调 | 小范围内的数值调整 |
UPDOWN_CLASS 窗口类¶
创建 UpDown 控件使用 UPDOWN_CLASS 窗口类:
#include <commctrl.h>
#pragma comment(lib, "comctl32.lib")
#define ID_UPDOWN 1004
HWND hUpDown = CreateWindowEx(
0, // 扩展样式
UPDOWN_CLASS, // 窗口类名
L"", // 标题(UpDown 不需要)
WS_CHILD | WS_VISIBLE |
UDS_ALIGNRIGHT | // 右对齐伙伴控件
UDS_SETBUDDYINT | // 自动更新伙伴控件的文字
UDS_ARROWKEYS, // 允许用键盘上下键
10, 10, // 位置(通常会自动调整)
20, 30, // 大小(通常会自动调整)
hwnd, // 父窗口
(HMENU)ID_UPDOWN, // 控件 ID
hInstance, // 实例句柄
NULL // 无额外数据
);
⚠️ 注意
UpDown 也需要初始化通用控件库,使用 ICC_UPDOWN_CLASS 标志:
INITCOMMONCONTROLSEX icex = {0};
icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
icex.dwICC = ICC_UPDOWN_CLASS;
InitCommonControlsEx(&icex);
UDM_SETBUDDY 设置伙伴控件¶
UpDown 控件通常需要和一个"伙伴控件"(通常是编辑框)配合使用。设置伙伴控件使用 UDM_SETBUDDY 消息:
// 先创建编辑框
HWND hEdit = CreateWindowEx(
WS_EX_CLIENTEDGE, L"EDIT", L"50",
WS_CHILD | WS_VISIBLE | ES_LEFT | ES_READONLY,
10, 10, 80, 20,
hwnd, NULL, hInstance, NULL
);
// 设置伙伴控件
SendMessage(hUpDown, UDM_SETBUDDY, (WPARAM)hEdit, 0);
设置了伙伴控件后,UpDown 控件会自动调整自己的位置和大小,紧挨着伙伴控件。
UDM_SETRANGE 设置范围¶
设置 UpDown 的数值范围使用 UDM_SETRANGE 消息:
// 设置范围 0-100
SendMessage(hUpDown, UDM_SETRANGE,
0, // 保留,必须为 0
MAKELONG(100, 0) // HIWORD=最大值, LOWORD=最小值
);
注意这里的参数顺序:HIWORD 是最大值,LOWORD 是最小值,和 Trackbar 的 TBM_SETRANGE 相反。
UDM_SETPOS 设置位置¶
设置当前位置使用 UDM_SETPOS 消息:
获取当前位置使用 UDM_GETPOS 消息:
// 获取当前位置
// 返回值:LOWORD=位置, HIWORD=错误码(0 表示成功)
LRESULT result = SendMessage(hUpDown, UDM_GETPOS, 0, 0);
short pos = LOWORD(result);
常用样式¶
| 样式 | 说明 |
|---|---|
| UDS_HORZ | 水平排列箭头(左右而不是上下) |
| UDS_ALIGNLEFT | 左对齐伙伴控件 |
| UDS_ALIGNRIGHT | 右对齐伙伴控件 |
| UDS_SETBUDDYINT | 自动更新伙伴控件的文字 |
| UDS_ARROWKEYS | 允许用键盘上下键调整 |
| UDS_WRAP | 超过最大值时回到最小值 |
| UDS_NOTHOUSANDS | 不显示千位分隔符 |
UDN_DELTAPOS 通知¶
当用户点击箭头改变数值时,UpDown 控件会发送 UDN_DELTAPOS 通知(通过 WM_NOTIFY):
case WM_NOTIFY:
{
LPNMHDR pnmh = (LPNMHDR)lParam;
if (pnmh->code == UDN_DELTAPOS)
{
LPNMUPDOWN pnmud = (LPNMUPDOWN)lParam;
// pnmud->iPos 是当前位置
// pnmud->iDelta 是变化量(+1 或 -1)
// 可以在这里修改变化量
if (pnmud->iPos + pnmud->iDelta > 100)
{
// 阻止超过 100
return TRUE; // 返回 TRUE 表示阻止变化
}
// 或者修改变化量
// pnmud->iDelta *= 10; // 每次变化 10 而不是 1
}
break;
}
NMUPDOWN 结构体定义:
typedef struct tagNMUPDOWN {
NMHDR hdr; // 标准通知头
int iPos; // 当前位置
int iDelta; // 变化量
} NMUPDOWN, *LPNMUPDOWN;
完整示例:颜色值微调器¶
下面是一个完整的 UpDown 示例,实现一个简单的 RGB 颜色值微调器:
#include <windows.h>
#include <commctrl.h>
#pragma comment(lib, "comctl32.lib")
#define ID_EDIT_R 1001
#define ID_UPDOWN_R 2001
#define ID_EDIT_G 1002
#define ID_UPDOWN_G 2002
#define ID_EDIT_B 1003
#define ID_UPDOWN_B 2003
#define ID_COLOR_PREVIEW 1004
HWND g_hEditR = NULL;
HWND g_hEditG = NULL;
HWND g_hEditB = NULL;
HWND g_hColorPreview = NULL;
void UpdateColorPreview()
{
// 获取 RGB 值
int r = GetDlgItemInt(GetParent(g_hEditR), ID_EDIT_R, NULL, FALSE);
int g = GetDlgItemInt(GetParent(g_hEditG), ID_EDIT_G, NULL, FALSE);
int b = GetDlgItemInt(GetParent(g_hEditB), ID_EDIT_B, NULL, FALSE);
// 创建画刷并重绘预览窗口
if (g_hColorPreview)
{
InvalidateRect(g_hColorPreview, NULL, TRUE);
}
}
LRESULT CALLBACK ColorPreviewProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
// 获取 RGB 值
int r = GetDlgItemInt(GetParent(hwnd), ID_EDIT_R, NULL, FALSE);
int g = GetDlgItemInt(GetParent(hwnd), ID_EDIT_G, NULL, FALSE);
int b = GetDlgItemInt(GetParent(hwnd), ID_EDIT_B, NULL, FALSE);
// 创建纯色画刷
HBRUSH hBrush = CreateSolidBrush(RGB(r, g, b));
FillRect(hdc, &ps.rcPaint, hBrush);
DeleteObject(hBrush);
EndPaint(hwnd, &ps);
return 0;
}
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_CREATE:
{
HINSTANCE hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
// 初始化通用控件
INITCOMMONCONTROLSEX icex = {0};
icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
icex.dwICC = ICC_UPDOWN_CLASS;
InitCommonControlsEx(&icex);
// 注册颜色预览窗口类
WNDCLASS wcPreview = {0};
wcPreview.lpfnWndProc = ColorPreviewProc;
wcPreview.hInstance = hInstance;
wcPreview.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wcPreview.lpszClassName = L"ColorPreviewClass";
RegisterClass(&wcPreview);
// 创建标签
CreateWindowEx(0, L"STATIC", L"R:",
WS_CHILD | WS_VISIBLE,
20, 20, 20, 20,
hwnd, NULL, hInstance, NULL);
CreateWindowEx(0, L"STATIC", L"G:",
WS_CHILD | WS_VISIBLE,
20, 50, 20, 20,
hwnd, NULL, hInstance, NULL);
CreateWindowEx(0, L"STATIC", L"B:",
WS_CHILD | WS_VISIBLE,
20, 80, 20, 20,
hwnd, NULL, hInstance, NULL);
// 创建编辑框
g_hEditR = CreateWindowEx(WS_EX_CLIENTEDGE, L"EDIT", L"128",
WS_CHILD | WS_VISIBLE | ES_LEFT | ES_READONLY,
50, 20, 50, 20,
hwnd, (HMENU)ID_EDIT_R, hInstance, NULL);
g_hEditG = CreateWindowEx(WS_EX_CLIENTEDGE, L"EDIT", L"128",
WS_CHILD | WS_VISIBLE | ES_LEFT | ES_READONLY,
50, 50, 50, 20,
hwnd, (HMENU)ID_EDIT_G, hInstance, NULL);
g_hEditB = CreateWindowEx(WS_EX_CLIENTEDGE, L"EDIT", L"128",
WS_CHILD | WS_VISIBLE | ES_LEFT | ES_READONLY,
50, 80, 50, 20,
hwnd, (HMENU)ID_EDIT_B, hInstance, NULL);
// 创建 UpDown 控件
HWND hUpDownR = CreateWindowEx(0, UPDOWN_CLASS, L"",
WS_CHILD | WS_VISIBLE | UDS_ALIGNRIGHT | UDS_SETBUDDYINT | UDS_ARROWKEYS,
0, 0, 0, 0,
hwnd, (HMENU)ID_UPDOWN_R, hInstance, NULL);
SendMessage(hUpDownR, UDM_SETBUDDY, (WPARAM)g_hEditR, 0);
SendMessage(hUpDownR, UDM_SETRANGE, 0, MAKELONG(0, 255));
SendMessage(hUpDownR, UDM_SETPOS, 0, 128);
HWND hUpDownG = CreateWindowEx(0, UPDOWN_CLASS, L"",
WS_CHILD | WS_VISIBLE | UDS_ALIGNRIGHT | UDS_SETBUDDYINT | UDS_ARROWKEYS,
0, 0, 0, 0,
hwnd, (HMENU)ID_UPDOWN_G, hInstance, NULL);
SendMessage(hUpDownG, UDM_SETBUDDY, (WPARAM)g_hEditG, 0);
SendMessage(hUpDownG, UDM_SETRANGE, 0, MAKELONG(0, 255));
SendMessage(hUpDownG, UDM_SETPOS, 0, 128);
HWND hUpDownB = CreateWindowEx(0, UPDOWN_CLASS, L"",
WS_CHILD | WS_VISIBLE | UDS_ALIGNRIGHT | UDS_SETBUDDYINT | UDS_ARROWKEYS,
0, 0, 0, 0,
hwnd, (HMENU)ID_UPDOWN_B, hInstance, NULL);
SendMessage(hUpDownB, UDM_SETBUDDY, (WPARAM)g_hEditB, 0);
SendMessage(hUpDownB, UDM_SETRANGE, 0, MAKELONG(0, 255));
SendMessage(hUpDownB, UDM_SETPOS, 0, 128);
// 创建颜色预览窗口
g_hColorPreview = CreateWindowEx(
WS_EX_CLIENTEDGE, L"ColorPreviewClass", L"",
WS_CHILD | WS_VISIBLE,
120, 20, 100, 80,
hwnd, (HMENU)ID_COLOR_PREVIEW, hInstance, NULL
);
return 0;
}
case WM_NOTIFY:
{
LPNMHDR pnmh = (LPNMHDR)lParam;
if (pnmh->code == UDN_DELTAPOS)
{
// 更新颜色预览
UpdateColorPreview();
}
break;
}
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
const wchar_t CLASS_NAME[] = L"UpDownWindow";
WNDCLASS wc = {0};
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
RegisterClass(&wc);
HWND hwnd = CreateWindowEx(
0, CLASS_NAME, L"UpDown 示例 - RGB 颜色选择器",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 280, 180,
NULL, NULL, hInstance, NULL
);
if (hwnd)
{
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
MSG msg = {0};
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return 0;
}
这个示例创建了一个 RGB 颜色选择器,用户可以通过点击上下箭头微调 RGB 值,右侧会实时显示对应的颜色。
第五章:RichEdit 简介¶
与 Edit Control 的区别¶
我们前面学过的 Edit Control(编辑框控件)功能很基础,只能显示纯文本。而 RichEdit(富编辑框)是一个功能更强大的编辑控件,支持:
- 格式化文本:不同文字可以有不同字体、颜色、大小
- 段落格式:对齐方式、缩进、行距等
- 嵌入对象:图片、OLE 对象等
- 撤销/重做:内置的多级撤销功能
- 搜索替换:强大的文本搜索功能
- URL 检测:自动识别并高亮 URL
- 更多:拼写检查、自动更正等(取决于版本)
普通 Edit Control:
┌─────────────────────────────────────┐
│ 这是纯文本,只有一种字体和颜色 │
└─────────────────────────────────────┘
RichEdit:
┌─────────────────────────────────────┐
│ 这是 粗体 文字,这是 斜体 │
│ 这是红色文字 这是蓝色文字 │
│ ┌───┐ │
│ │图片│ │
│ └───┘ │
└─────────────────────────────────────┘
RICHEDIT_CLASS 窗口类¶
创建 RichEdit 使用 RICHEDIT_CLASS 窗口类(注意:不同版本的 RichEdit 有不同的类名):
| 类名 | 版本 | 说明 |
|---|---|---|
RICHEDIT_CLASS |
1.0 | 最老的版本,功能有限 |
RICHEDIT_CLASS10 |
1.0 | 同上,别名 |
RICHEDIT_CLASS20 |
2.0 | 增强版,支持更多功能 |
RICHEDIT_CLASS30 |
3.0 | 再增强,支持表格等 |
MSFTEDIT_CLASS |
4.1+ | 最新版本(需要 msftedit.dll) |
现代应用程序应该使用 MSFTEDIT_CLASS,它是功能最完整的版本。
LoadLibrary 加载库¶
RichEdit 控件不像其他通用控件那样在 comctl32.dll 里,它有自己的 DLL。使用前需要先加载对应的 DLL:
// 对于 RichEdit 2.0/3.0
LoadLibrary(L"riched20.dll");
// 对于 RichEdit 4.1(MSFTEDIT_CLASS)
LoadLibrary(L"msftedit.dll");
⚠️ 注意
LoadLibrary 调用后应该记得在程序退出时调用 FreeLibrary。而且由于 DLL 是按引用计数加载的,多次调用 LoadLibrary 只会增加引用计数,不会重复加载。
基本用法示例¶
下面是一个使用 RichEdit 4.1 的简单示例:
#include <windows.h>
#include <richedit.h>
#pragma comment(lib, "comctl32.lib")
#define ID_RICHEDIT 1001
// 在 WM_CREATE 或 WinMain 开头加载 DLL
HMODULE hRichEditDll = LoadLibrary(L"msftedit.dll");
// 创建 RichEdit 控件
HWND hRichEdit = CreateWindowEx(
WS_EX_CLIENTEDGE,
MSFTEDIT_CLASS, // 使用 4.1 版本
L"", // 初始文字
WS_CHILD | WS_VISIBLE |
WS_VSCROLL | WS_HSCROLL | // 显示滚动条
ES_MULTILINE | // 多行
ES_AUTOVSCROLL | ES_AUTOHSCROLL |
ES_WANTRETURN, // 回车换行
10, 10, 500, 300,
hwnd, (HMENU)ID_RICHEDIT, hInstance, NULL
);
// 设置文字
SendMessage(hRichEdit, WM_SETTEXT, 0, (LPARAM)L"这是富文本编辑框");
// 程序退出时
if (hRichEditDll)
FreeLibrary(hRichEditDll);
功能简介¶
RichEdit 的功能非常丰富,这里简单介绍一些常用的:
设置文字格式¶
// 设置选中的文字为粗体
CHARFORMAT2 cf = {0};
cf.cbSize = sizeof(CHARFORMAT2);
cf.dwMask = CFM_BOLD;
cf.dwEffects = CFE_BOLD;
SendMessage(hRichEdit, EM_SETCHARFORMAT, SCF_SELECTION, (LPARAM)&cf);
插入图片¶
获取 RTF 文本¶
// 获取 RTF 格式的文本
int len = SendMessage(hRichEdit, WM_GETTEXTLENGTH, 0, 0);
char* buffer = new char[len * 2 + 1]; // RTF 可能比纯文本长
SendMessage(hRichEdit, EM_GETTEXTEX, GETTEXTEX参数, (LPARAM)buffer);
// 使用 buffer...
delete[] buffer;
说实话,RichEdit 是一个非常复杂的控件,完整讲解需要单独一篇甚至几篇文章。如果你需要做复杂的文本编辑功能,建议查阅 MSDN 文档,或者考虑使用现成的富文本编辑库。
第六章:综合实战——系统设置对话框¶
现在我们把今天学的几个控件整合起来,做一个完整的"系统设置"对话框。这个示例会使用 TabControl、Progress Bar、Trackbar 和 UpDown 控件。
#include <windows.h>
#include <commctrl.h>
#pragma comment(lib, "comctl32.lib")
// 控件 ID
#define ID_TABCONTROL 1001
#define ID_TAB_CONTENT_BASE 2000
// 显示设置页
#define ID_BRIGHTNESS_TRACKBAR 3001
#define ID_BRIGHTNESS_TEXT 3002
#define ID_VOLUME_TRACKBAR 3003
#define ID_VOLUME_TEXT 3004
// 性能设置页
#define ID_PROGRESS 4001
#define ID_START_BUTTON 4002
#define ID_CPU_SPIN_EDIT 4003
#define ID_CPU_UPDOWN 4004
// 标签页内容窗口句柄
HWND g_hTabPageDisplay = NULL;
HWND g_hTabPagePerformance = NULL;
// 显示设置页的控件
HWND g_hBrightnessTrackbar = NULL;
HWND g_hBrightnessText = NULL;
HWND g_hVolumeTrackbar = NULL;
HWND g_hVolumeText = NULL;
// 性能设置页的控件
HWND g_hProgress = NULL;
HWND g_hCpuSpinEdit = NULL;
// 更新亮度显示
void UpdateBrightnessText()
{
if (!g_hBrightnessTrackbar || !g_hBrightnessText)
return;
int brightness = SendMessage(g_hBrightnessTrackbar, TBM_GETPOS, 0, 0);
wchar_t text[64];
swprintf_s(text, 64, L"亮度: %d%%", brightness);
SetWindowText(g_hBrightnessText, text);
}
// 更新音量显示
void UpdateVolumeText()
{
if (!g_hVolumeTrackbar || !g_hVolumeText)
return;
int volume = SendMessage(g_hVolumeTrackbar, TBM_GETPOS, 0, 0);
wchar_t text[64];
swprintf_s(text, 64, L"音量: %d%%", volume);
SetWindowText(g_hVolumeText, text);
}
// 显示设置页窗口过程
LRESULT CALLBACK DisplayPageProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_CREATE:
{
HINSTANCE hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
// 初始化通用控件
INITCOMMONCONTROLSEX icex = {0};
icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
icex.dwICC = ICC_BAR_CLASSES;
InitCommonControlsEx(&icex);
// 创建亮度显示
g_hBrightnessText = CreateWindowEx(
0, L"STATIC", L"亮度: 50%",
WS_CHILD | WS_VISIBLE,
20, 20, 200, 20,
hwnd, (HMENU)ID_BRIGHTNESS_TEXT, hInstance, NULL
);
// 创建亮度滑块
g_hBrightnessTrackbar = CreateWindowEx(
0, TRACKBAR_CLASS, L"",
WS_CHILD | WS_VISIBLE | TBS_AUTOTICKS,
20, 45, 300, 30,
hwnd, (HMENU)ID_BRIGHTNESS_TRACKBAR, hInstance, NULL
);
SendMessage(g_hBrightnessTrackbar, TBM_SETRANGE, TRUE, MAKELONG(0, 100));
SendMessage(g_hBrightnessTrackbar, TBM_SETTICFREQ, 10, 0);
SendMessage(g_hBrightnessTrackbar, TBM_SETPOS, TRUE, 50);
// 创建音量显示
g_hVolumeText = CreateWindowEx(
0, L"STATIC", L"音量: 75%",
WS_CHILD | WS_VISIBLE,
20, 90, 200, 20,
hwnd, (HMENU)ID_VOLUME_TEXT, hInstance, NULL
);
// 创建音量滑块
g_hVolumeTrackbar = CreateWindowEx(
0, TRACKBAR_CLASS, L"",
WS_CHILD | WS_VISIBLE | TBS_AUTOTICKS,
20, 115, 300, 30,
hwnd, (HMENU)ID_VOLUME_TRACKBAR, hInstance, NULL
);
SendMessage(g_hVolumeTrackbar, TBM_SETRANGE, TRUE, MAKELONG(0, 100));
SendMessage(g_hVolumeTrackbar, TBM_SETTICFREQ, 10, 0);
SendMessage(g_hVolumeTrackbar, TBM_SETPOS, TRUE, 75);
UpdateVolumeText();
return 0;
}
case WM_HSCROLL:
{
HWND hTrackbar = (HWND)lParam;
if (hTrackbar == g_hBrightnessTrackbar)
UpdateBrightnessText();
else if (hTrackbar == g_hVolumeTrackbar)
UpdateVolumeText();
return 0;
}
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
}
// 性能测试定时器回调
VOID CALLBACK PerformanceTestCallback(HWND hwnd, UINT uMsg, UINT_PTR idEvent, DWORD dwTime)
{
int pos = SendMessage(g_hProgress, PBM_GETPOS, 0, 0);
if (pos >= 100)
{
KillTimer(hwnd, idEvent);
MessageBox(hwnd, L"性能测试完成!", L"提示", MB_OK);
EnableWindow(GetDlgItem(hwnd, ID_START_BUTTON), TRUE);
return;
}
pos += 5;
if (pos > 100) pos = 100;
SendMessage(g_hProgress, PBM_SETPOS, pos, 0);
}
// 性能设置页窗口过程
LRESULT CALLBACK PerformancePageProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_CREATE:
{
HINSTANCE hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
// 初始化通用控件
INITCOMMONCONTROLSEX icex = {0};
icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
icex.dwICC = ICC_BAR_CLASSES | ICC_PROGRESS_CLASS | ICC_UPDOWN_CLASS;
InitCommonControlsEx(&icex);
// CPU 限制标签
CreateWindowEx(
0, L"STATIC", L"CPU 使用限制:",
WS_CHILD | WS_VISIBLE,
20, 20, 100, 20,
hwnd, NULL, hInstance, NULL
);
// CPU 限制编辑框
g_hCpuSpinEdit = CreateWindowEx(
WS_EX_CLIENTEDGE, L"EDIT", L"50",
WS_CHILD | WS_VISIBLE | ES_LEFT | ES_READONLY,
120, 20, 50, 20,
hwnd, (HMENU)ID_CPU_SPIN_EDIT, hInstance, NULL
);
// CPU 限制 UpDown
HWND hUpDown = CreateWindowEx(
0, UPDOWN_CLASS, L"",
WS_CHILD | WS_VISIBLE | UDS_ALIGNRIGHT | UDS_SETBUDDYINT | UDS_ARROWKEYS,
0, 0, 0, 0,
hwnd, (HMENU)ID_CPU_UPDOWN, hInstance, NULL
);
SendMessage(hUpDown, UDM_SETBUDDY, (WPARAM)g_hCpuSpinEdit, 0);
SendMessage(hUpDown, UDM_SETRANGE, 0, MAKELONG(1, 100));
SendMessage(hUpDown, UDM_SETPOS, 0, 50);
// 百分号标签
CreateWindowEx(
0, L"STATIC", L"%",
WS_CHILD | WS_VISIBLE,
175, 20, 20, 20,
hwnd, NULL, hInstance, NULL
);
// 性能测试按钮
CreateWindowEx(
0, L"BUTTON", L"运行性能测试",
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
20, 60, 120, 30,
hwnd, (HMENU)ID_START_BUTTON, hInstance, NULL
);
// 进度条
g_hProgress = CreateWindowEx(
0, PROGRESS_CLASS, L"",
WS_CHILD | WS_VISIBLE | PBS_SMOOTH,
20, 100, 300, 25,
hwnd, (HMENU)ID_PROGRESS, hInstance, NULL
);
SendMessage(g_hProgress, PBM_SETRANGE32, 0, 100);
SendMessage(g_hProgress, PBM_SETPOS, 0, 0);
return 0;
}
case WM_COMMAND:
{
WORD controlId = LOWORD(wParam);
WORD notificationCode = HIWORD(wParam);
if (controlId == ID_START_BUTTON && notificationCode == BN_CLICKED)
{
EnableWindow(GetDlgItem(hwnd, ID_START_BUTTON), FALSE);
SendMessage(g_hProgress, PBM_SETPOS, 0, 0);
SetTimer(hwnd, 1, 200, PerformanceTestCallback);
}
return 0;
}
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
}
// 创建标签页窗口
void CreateTabPage(HWND hParent, int index, WNDPROC wndProc, const wchar_t* title, HINSTANCE hInstance)
{
static BOOL s_classRegistered = FALSE;
if (!s_classRegistered)
{
WNDCLASS wc = {0};
wc.lpfnWndProc = DefWindowProc;
wc.hInstance = hInstance;
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.lpszClassName = L"TabPageClass";
RegisterClass(&wc);
s_classRegistered = TRUE;
}
// 创建带自定义窗口过程的标签页
HWND hPage = CreateWindowEx(
0, L"TabPageClass", L"",
WS_CHILD | WS_CLIPSIBLINGS,
10, 40, 340, 150,
hParent, NULL, hInstance, NULL
);
// 子类化窗口过程
SetWindowLongPtr(hPage, GWLP_WNDPROC, (LONG_PTR)wndProc);
// 发送 WM_CREATE 消息
CREATESTRUCT cs = {0};
cs.hInstance = hInstance;
SendMessage(hPage, WM_CREATE, 0, (LPARAM)&cs);
// 保存到全局变量
if (index == 0)
g_hTabPageDisplay = hPage;
else if (index == 1)
g_hTabPagePerformance = hPage;
}
// 显示指定的标签页
void ShowTabPage(int index)
{
HWND pages[] = { g_hTabPageDisplay, g_hTabPagePerformance };
for (int i = 0; i < 2; i++)
{
if (pages[i])
ShowWindow(pages[i], i == index ? SW_SHOW : SW_HIDE);
}
}
// 主窗口过程
LRESULT CALLBACK WndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_CREATE:
{
HINSTANCE hInstance = ((LPCREATESTRUCT)lParam)->hInstance;
// 初始化通用控件
INITCOMMONCONTROLSEX icex = {0};
icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
icex.dwICC = ICC_TAB_CLASSES;
InitCommonControlsEx(&icex);
// 创建 TabControl
HWND hTab = CreateWindowEx(
0, WC_TABCONTROL, L"",
WS_CHILD | WS_VISIBLE,
10, 10, 360, 30,
hwnd, (HMENU)ID_TABCONTROL, hInstance, NULL
);
// 添加标签页
TCITEM tie = {0};
tie.mask = TCIF_TEXT;
tie.pszText = (LPWSTR)L"显示";
SendMessage(hTab, TCM_INSERTITEM, 0, (LPARAM)&tie);
tie.pszText = (LPWSTR)L"性能";
SendMessage(hTab, TCM_INSERTITEM, 1, (LPARAM)&tie);
// 创建标签页内容
CreateTabPage(hwnd, 0, DisplayPageProc, L"显示", hInstance);
CreateTabPage(hwnd, 1, PerformancePageProc, L"性能", hInstance);
// 显示第一个标签页
ShowTabPage(0);
return 0;
}
case WM_NOTIFY:
{
LPNMHDR pnmh = (LPNMHDR)lParam;
if (pnmh->idFrom == ID_TABCONTROL && pnmh->code == TCN_SELCHANGE)
{
HWND hTab = GetDlgItem(hwnd, ID_TABCONTROL);
int currentIndex = SendMessage(hTab, TCM_GETCURSEL, 0, 0);
ShowTabPage(currentIndex);
}
return 0;
}
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
const wchar_t CLASS_NAME[] = L"SettingsDialog";
WNDCLASS wc = {0};
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
RegisterClass(&wc);
HWND hwnd = CreateWindowEx(
0, CLASS_NAME, L"系统设置",
WS_OVERLAPPEDWINDOW & ~WS_MAXIMIZEBOX,
CW_USEDEFAULT, CW_USEDEFAULT, 400, 260,
NULL, NULL, hInstance, NULL
);
if (hwnd)
{
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
MSG msg = {0};
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return 0;
}
这个综合示例创建了一个带有两个标签页的设置对话框:
- 显示标签页:包含亮度和音量调节滑块
- 性能标签页:包含 CPU 限制微调和性能测试进度条
后续可以做什么¶
到这里,Win32 常用的高级控件就讲完了。你现在应该能够使用 TabControl 组织界面、用 Progress Bar 显示进度、用 Trackbar 和 UpDown 调节数值。加上之前学过的按钮、编辑框、列表框、ListView、TreeView,你的 Win32 控件工具箱已经相当完善了。
但说实话,我们今天只是学习了这些控件的基本用法。每个控件都有更多高级功能和细节,特别是 RichEdit,那是一个可以单独写一本书的复杂控件。如果你需要在实际项目中使用这些控件,建议深入阅读 MSDN 文档。
系列总结¶
到目前为下,我们的 Win32 原生编程系列已经涵盖了:
- 基础概念:开发环境、类型命名、字符串处理、窗口概念、入口点
- 消息循环:窗口创建、消息循环、窗口过程
- 键盘鼠标:键盘消息、鼠标消息、定时器
- 基础控件:按钮、编辑框、列表框、组合框、WM_COMMAND 机制
- WM_NOTIFY:高级控件的消息通知机制
- ListView:列表视图控件
- TreeView:树形视图控件
- 更多高级控件:TabControl、Progress Bar、Trackbar、UpDown、RichEdit(本篇)
掌握了这些内容,你已经具备了开发完整 Win32 GUI 应用程序的能力。
好了,今天的文章就到这里。我们下一篇再见!
相关资源