笔者在日常开发中经常需要将某个窗口置顶显示,例如查看文档时希望它始终浮在编辑窗口之上。虽然网上有不少现成工具,但自己动手实现一个既能加深对Win32 API的理解,又能按需定制。本文基于SetWindowPos、SetForegroundWindow、RegisterHotKey等API,实现了一个支持快捷键和下拉列表两种方式的置顶窗口小工具,核心代码仅35KB,完整项目已上传GitHub。
核心功能:置顶与前台激活
置顶窗口的关键是SetWindowPos函数,将窗口的Z序设置为HWND_TOPMOST即可实现置顶,设置为HWND_NOTOPMOST取消置顶。但直接调用会有一个问题:如果目标窗口处于最小化或未激活状态,仅置顶不会使它显示在前台。需要先将窗口带到前台,再置顶,才能达到“窗口浮在最上方且可见”的效果。
SetForegroundWindow可以激活窗口,但文档明确提到该函数受到系统限制——只有当调用线程与目标窗口的线程属于同一个输入队列时才能成功。因此必须使用AttachThreadInput将当前线程与目标窗口所在的线程绑定,然后再调用SetForegroundWindow。处理完成后立即解除绑定,避免影响其他操作。
具体实现代码如下(C++ Win32):- void topwin(HWND hWnd)
- {
- if (hWnd == nullptr) {
- hWnd = GetForegroundWindow();
- }
- DWORD currentId = GetCurrentThreadId();
- DWORD topId = GetWindowThreadProcessId(GetForegroundWindow(), NULL);
- AttachThreadInput(currentId, topId, TRUE);
-
- ShowWindow(hWnd, SW_SHOWNORMAL);
- SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE);
- SetForegroundWindow(hWnd);
-
- AttachThreadInput(currentId, topId, FALSE);
- }
复制代码 注意:SetWindowPos的SWP_NOSIZE | SWP_NOMOVE标志表示不改变窗口大小和位置。
快捷键方式:CTRL+SHIFT+T置顶 / CTRL+SHIFT+C取消
使用RegisterHotKey注册全局快捷键,ID分别设为1和2。在消息循环中捕获WM_HOTKEY消息,根据wParam判断是哪个快捷键触发。这里有一个细节:WM_HOTKEY处理需要放在DispatchMessage之后,否则打包的exe可能无法正常运行。
- int topHotkeyId = 1;
- int untopHotkeyId = 2;
- if (!RegisterHotKey(NULL, topHotkeyId, MOD_CONTROL | MOD_SHIFT, 'T')) return 1;
- if (!RegisterHotKey(NULL, untopHotkeyId, MOD_CONTROL | MOD_SHIFT, 'C')) return 1;
- while (GetMessage(&msg, nullptr, 0, 0)) {
- if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) {
- TranslateMessage(&msg);
- DispatchMessage(&msg);
- if (msg.message == WM_HOTKEY) {
- HWND hWnd = GetForegroundWindow();
- if (msg.wParam == topHotkeyId) topwin(nullptr);
- if (msg.wParam == untopHotkeyId) {
- SetWindowPos(hWnd, HWND_NOTOPMOST, 0, 0, 100, 100, SWP_NOMOVE | SWP_NOSIZE);
- }
- }
- }
- }
复制代码 注意:快捷键方式需要先点击目标窗口使其获得焦点,再按下快捷键才能正确操作。
下拉列表方式:枚举窗口并选择置顶
主窗口创建一个下拉框(ComboBox),通过EnumWindows遍历所有顶层窗口,过滤条件为:窗口可见且不是桌面图标窗口(即正常显示状态)。获取每个窗口的标题添加到下拉框中,同时将HWND保存到vector中,以便后续根据选中索引获取窗口句柄。
- std::vector<HWND> windows;
- bool IsWindowOnDesktop(HWND hwnd) {
- WINDOWPLACEMENT placement = { sizeof(WINDOWPLACEMENT) };
- if (GetWindowPlacement(hwnd, &placement)) {
- return placement.showCmd == SW_SHOWNORMAL;
- }
- return false;
- }
- BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) {
- if (IsWindowVisible(hwnd) && !IsWindowOnDesktop(hwnd)) {
- int titleLength = GetWindowTextLength(hwnd);
- if (titleLength > 0) {
- std::wstring windowTitle(titleLength + 1, L'\0');
- GetWindowText(hwnd, &windowTitle[0], titleLength + 1);
- SendMessageW(hComboBox, CB_ADDSTRING, 0, reinterpret_cast<LPARAM>(windowTitle.c_str()));
- windows.push_back(hwnd);
- }
- }
- return TRUE;
- }
- EnumWindows(EnumWindowsProc, 0);
复制代码 下拉框控件创建时设置唯一标识MENU_ID(29),在WM_COMMAND消息中检测CBN_SELCHANGE事件,获取当前选中项索引,然后调用topwin(windows.at(selectedIndex))完成置顶。
- #define MENU_ID 29
- HWND hComboBox;
- hComboBox = CreateWindowW(L"ComboBox", L"", CBS_DROPDOWN | WS_CHILD | WS_VISIBLE,
- 10, 10, 360, 200, hWnd, (HMENU)MENU_ID, nullptr, nullptr);
- SendMessageW(hComboBox, CB_SETCURSEL, 0, 0);
- case WM_COMMAND:
- if (HIWORD(wParam) == CBN_SELCHANGE) {
- LRESULT selectedIndex = SendMessageW(GetDlgItem(hWnd, MENU_ID), CB_GETCURSEL, 0, 0);
- topwin(windows.at(selectedIndex));
- }
- break;
复制代码 下拉列表方式未内置取消置顶,可结合快捷键CTRL+SHIFT+C实现。
总结与扩展
本文实现了一个轻量级的置顶窗口小工具,核心逻辑围绕SetWindowPos与线程输入绑定展开。目前功能已满足日常使用,但仍有优化空间:例如增加取消置顶的下拉选项、支持右键菜单、保存置顶状态等。读者可以基于此代码自行扩展,如有疑问欢迎交流。 |