Windows | 模仿网易云任务栏实现自定义按钮及缩略图

前言

最近更新网易云发现任务栏按钮中除了播放相关的按钮,多了一个喜欢的按钮:

Windows | 模仿网易云任务栏实现自定义按钮及缩略图_第1张图片

之前我一直以为网易云任务栏的按钮只是 Windows 为音乐软件专门提供的,于是我又看了一眼系统自带的播放器,发现并没有爱心按钮:

Windows | 模仿网易云任务栏实现自定义按钮及缩略图_第2张图片

这时我就想会不会是 Windows 提供了相关接口可以让用户自定义,一搜发现还真有,ITaskbarList3接口提供了自定义任务栏按钮的方法,于是就有了下面这个 demo 的实现:

Windows | 模仿网易云任务栏实现自定义按钮及缩略图_第3张图片

在实现的过程中也遇到了很多问题:

  • 由于自定义缩略图,导致悬浮在缩略图上无法查看原有的预览窗口内容。
  • 使用 WIN + TAB 切换窗口时,显示的预览图是缩略图无法查看原有的预览窗口内容。

不过,经过搜索发现网易云的开发者已经分享过相关的思路(文末的参考文献),就是没有相应的编码实现,之后我就按照自己的理解实现了相关的功能,相关效果见下图,本文涉及到的完整代码已上传到GitHub。

使用 WIN + TAB 正常显示原窗口信息:

Windows | 模仿网易云任务栏实现自定义按钮及缩略图_第4张图片

鼠标悬浮缩略图上正常显示原窗口信息:

Windows | 模仿网易云任务栏实现自定义按钮及缩略图_第5张图片

自定义按钮

首先是自定义按钮的实现,我们先添加四个按钮,使用ITaskbarList3接口即可:

#include 

#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;

以上两步实现了以下效果:

Windows | 模仿网易云任务栏实现自定义按钮及缩略图_第6张图片

自定义缩略图

自定义缩略图需要使用到DwmSetIconicThumbnail接口,同时还需要注意缩略图的格式必须为bmp,这里使用GDI进行加载:

#include 
#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 中调用,原因参考官方文档。

Windows | 模仿网易云任务栏实现自定义按钮及缩略图_第7张图片

以上步骤就实现了我们的基本功能:

Windows | 模仿网易云任务栏实现自定义按钮及缩略图_第8张图片

细节优化

通过上述操作,我们已经完成了自定义按钮和缩略图的功能,但是通过 WIN + TAB 会发现显示的窗口也变成光秃秃的缩略图:

Windows | 模仿网易云任务栏实现自定义按钮及缩略图_第9张图片

同时悬浮在缩略图上显示的窗口也不正常:

Windows | 模仿网易云任务栏实现自定义按钮及缩略图_第10张图片

强迫症表示受不了!

于是就有了下面的优化(思路参考参考文献的文章):

  1. 创建一个临时窗口用于正常显示以上两个界面,并把该窗口设置为隐藏。
  2. 通过ITaskbarList3接口的RegisterTabSetTabOrder方法将隐藏窗口和原窗口设置成组。

具体实现如下:

// 临时窗口
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 的显示问题了,如下图所示:

Windows | 模仿网易云任务栏实现自定义按钮及缩略图_第11张图片

但是仍然无法处理悬浮在缩略图上显示异常的问题,这是由于原窗口自定义了缩略图后未定义实时预览图,导致原窗口无法正常显示,也就导致了临时窗口的预览图无法显示,解决方法如下:

// 设置实时预览图
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 | 模仿网易云任务栏实现自定义按钮及缩略图_第12张图片

总结

本文简单讲解了如何在 Windows 下实现任务栏自定义按钮和缩略图,由于个人水平有限,示例代码可能存在一些问题,欢迎一起交流讨论。

参考文献

  • 一个体验好的Windows 任务栏缩略图开发心得

你可能感兴趣的:(C,windows,c++)