前言
最近更新网易云发现任务栏按钮中除了播放相关的按钮,多了一个喜欢的按钮:
之前我一直以为网易云任务栏的按钮只是 Windows 为音乐软件专门提供的,于是我又看了一眼系统自带的播放器,发现并没有爱心按钮:
这时我就想会不会是 Windows 提供了相关接口可以让用户自定义,一搜发现还真有,ITaskbarList3接口提供了自定义任务栏按钮的方法,于是就有了下面这个 demo 的实现:
在实现的过程中也遇到了很多问题:
- 由于自定义缩略图,导致悬浮在缩略图上无法查看原有的预览窗口内容。
- 使用 WIN + TAB 切换窗口时,显示的预览图是缩略图无法查看原有的预览窗口内容。
不过,经过搜索发现网易云的开发者已经分享过相关的思路(文末的参考文献),就是没有相应的编码实现,之后我就按照自己的理解实现了相关的功能,相关效果见下图,本文涉及到的完整代码已上传到GitHub。
使用 WIN + TAB 正常显示原窗口信息:
鼠标悬浮缩略图上正常显示原窗口信息:
自定义按钮
首先是自定义按钮的实现,我们先添加四个按钮,使用ITaskbarList3
接口即可:
#include <shobjidl.h>
#define BTN_COUNT 4
// 任务栏按钮
THUMBBUTTON btns[BTN_COUNT];
// 任务栏对象
ITaskbarList3* pTaskbar;
// 初始化 COM
CoInitialize(NULL);
CoCreateInstance(CLSID_TaskbarList, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pTaskbar));
WCHAR tips[BTN_COUNT][4] = { L"上一首", L"暂停", L"下一首", L"喜欢" };
int icons[BTN_COUNT] = { IDI_PREVIOUS, IDI_PAUSE, IDI_NEXT, IDI_UNLIKE };
for (int i = 0; i < BTN_COUNT; i++)
{
btns[i].dwMask = THB_BITMAP | THB_ICON | THB_FLAGS | THB_TOOLTIP;
btns[i].iId = 1000 + i;
btns[i].iBitmap = i;
btns[i].hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(icons[i]));
btns[i].dwFlags = THBF_ENABLED;
wcscpy_s(btns[i].szTip, tips[i]);
}
pTaskbar->ThumbBarAddButtons(hWnd, BTN_COUNT, btns);
// 释放资源
pTaskbar->Release();
CoUninitialize();
然后针对对应的按钮,设置相应的点击事件,这里的DwmSetIconicThumbnail
用于设置缩略图,留到下面再具体说明,1000 ~ 1003
对应上文中设置的按钮的iId
:
#define BG_COUNT 3
// 当前下标
int bgIndex = 0;
// 背景图
WCHAR bgImgs[3][8] = { L"bg1.bmp", L"bg2.bmp", L"bg3.bmp" };
// 控制暂停/播放切换
bool play = true;
// 控制喜欢/取消喜欢切换
bool unlike = true;
case WM_COMMAND:
{
int wmId = LOWORD(wParam);
// 分析菜单选择:
switch (wmId)
{
case 1000:
bgIndex = (bgIndex + BG_COUNT - 1) % BG_COUNT;
DwmSetIconicThumbnail(hWnd, LoadImageAndConvertToHBITMAP(bgImgs[bgIndex]), 0);
break;
case 1001:
if (play) {
btns[1].hIcon = LoadIcon(hInst, MAKEINTRESOURCE(IDI_PAUSE));
wcscpy_s(btns[1].szTip, L"播放");
}
else {
btns[1].hIcon = LoadIcon(hInst, MAKEINTRESOURCE(IDI_PLAY));
wcscpy_s(btns[1].szTip, L"暂停");
}
play = !play;
// 更新按钮显示
pTaskbar->ThumbBarUpdateButtons(hWnd, BTN_COUNT, btns);
break;
case 1002:
bgIndex = (bgIndex + 1) % BG_COUNT;
DwmSetIconicThumbnail(hWnd, LoadImageAndConvertToHBITMAP(bgImgs[bgIndex]), 0);
break;
case 1003:
if (unlike) {
btns[3].hIcon = LoadIcon(hInst, MAKEINTRESOURCE(IDI_LIKE));
wcscpy_s(btns[3].szTip, L"取消喜欢");
}
else {
btns[3].hIcon = LoadIcon(hInst, MAKEINTRESOURCE(IDI_UNLIKE));
wcscpy_s(btns[3].szTip, L"喜欢");
}
unlike = !unlike;
// 更新按钮显示
pTaskbar->ThumbBarUpdateButtons(hWnd, BTN_COUNT, btns);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
以上两步实现了以下效果:
自定义缩略图
自定义缩略图需要使用到DwmSetIconicThumbnail接口,同时还需要注意缩略图的格式必须为bmp
,这里使用GDI
进行加载:
#include <gdiplus.h>
#pragma comment(lib, "gdiplus.lib")
// 开启自定义背景
BOOL enableBg = TRUE;
// 初始化 GDI+
ULONG_PTR gdiplusToken;
// 是否初始化 GDI+
bool initGDI = false;
void InitializeGDIPlus() {
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, nullptr);
}
void ShutdownGDIPlus() {
Gdiplus::GdiplusShutdown(gdiplusToken);
}
// 加载图像文件并返回 HBITMAP
HBITMAP LoadImageAndConvertToHBITMAP(const WCHAR* filePath) {
if (!initGDI) {
InitializeGDIPlus();
initGDI = true;
}
Gdiplus::Bitmap bitmap(filePath);
if (bitmap.GetLastStatus() != Gdiplus::Ok) {
return nullptr;
}
HBITMAP hBitmap = nullptr;
Gdiplus::Color color;
bitmap.GetHBITMAP(color, &hBitmap);
return hBitmap;
}
case WM_CREATE:
// 开启自定义缩略图
DwmSetWindowAttribute(hWnd, DWMWA_HAS_ICONIC_BITMAP, &enableBg, sizeof(BOOL));
DwmSetWindowAttribute(hWnd, DWMWA_FORCE_ICONIC_REPRESENTATION, &enableBg, sizeof(BOOL));
DwmInvalidateIconicBitmaps(hWnd);
break;
case WM_DWMSENDICONICTHUMBNAIL:
// 设置缩略图
DwmSetIconicThumbnail(hWnd, LoadImageAndConvertToHBITMAP(bgImgs[bgIndex]), 0);
break;
GdiplusStartup
不能在 main 中调用,原因参考官方文档。
以上步骤就实现了我们的基本功能:
细节优化
通过上述操作,我们已经完成了自定义按钮和缩略图的功能,但是通过 WIN + TAB 会发现显示的窗口也变成光秃秃的缩略图:
同时悬浮在缩略图上显示的窗口也不正常:
强迫症表示受不了!
于是就有了下面的优化(思路参考参考文献的文章):
- 创建一个临时窗口用于正常显示以上两个界面,并把该窗口设置为隐藏。
- 通过
ITaskbarList3
接口的RegisterTab
和SetTabOrder
方法将隐藏窗口和原窗口设置成组。
具体实现如下:
// 临时窗口
HWND tmp;
tmp = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
0, 0, 0, 0, nullptr, nullptr, hInstance, nullptr);
// SW_HIDE 隐藏窗口
ShowWindow(tmp, SW_HIDE);
// 注册成组
pTaskbar->RegisterTab(tmp, hWnd);
pTaskbar->SetTabOrder(tmp, hWnd);
UpdateWindow(tmp);
// 发送 WM_DWMSENDICONICTHUMBNAIL 信息避免第一次缩略图显示异常
SendMessage(tmp, WM_DWMSENDICONICTHUMBNAIL, (WPARAM)tmp, 0);
case WM_CREATE:
// 开启自定义缩略图
DwmSetWindowAttribute(hWnd, DWMWA_HAS_ICONIC_BITMAP, &enableBg, sizeof(BOOL));
break;
case WM_DWMSENDICONICTHUMBNAIL:
// 需要重新设置按钮, 否则无法正常显示
pTaskbar->ThumbBarAddButtons(hWnd, BTN_COUNT, btns);
pTaskbar->ThumbBarUpdateButtons(hWnd, BTN_COUNT, btns);
DwmSetWindowAttribute(hWnd, DWMWA_FORCE_ICONIC_REPRESENTATION, &enableBg, sizeof(BOOL));
DwmInvalidateIconicBitmaps(hWnd);
DwmSetIconicThumbnail(hWnd, LoadImageAndConvertToHBITMAP(bgImgs[bgIndex]), 0);
break;
以上步骤就可以解决 WIN+TAB 的显示问题了,如下图所示:
但是仍然无法处理悬浮在缩略图上显示异常的问题,这是由于原窗口自定义了缩略图后未定义实时预览图,导致原窗口无法正常显示,也就导致了临时窗口的预览图无法显示,解决方法如下:
// 设置实时预览图
void SetWindowLivePreview(HWND hwnd, HBITMAP hBitmap) {
// 不显示原窗口的预览图, 这里设置负坐标
POINT ptOffset;
ptOffset.x = -1000;
ptOffset.y = -2000;
DwmSetIconicLivePreviewBitmap(hwnd, hBitmap, &ptOffset, 0);
}
case WM_DWMSENDICONICLIVEPREVIEWBITMAP:
SetWindowLivePreview(hWnd, LoadImageAndConvertToHBITMAP(bgImgs[bgIndex]));
break;
通过以上设置就可以发现实时预览图也显示正常了:
总结
本文简单讲解了如何在 Windows 下实现任务栏自定义按钮和缩略图,由于个人水平有限,示例代码可能存在一些问题,欢迎一起交流讨论。
参考文献
- 一个体验好的Windows 任务栏缩略图开发心得