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

前言

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

image-20231123151125974

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

image-20231123151504786

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

动画

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

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

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

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

image-20231123153136728

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

image-20231123153228763

自定义按钮

首先是自定义按钮的实现,我们先添加四个按钮,使用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 中调用,原因参考官方文档。

image-20231124090806930

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

image-20231124091144682

细节优化

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

image-20231124091312916

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

image-20231124091424258

强迫症表示受不了!

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

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

image-20231124093118748

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

// 设置实时预览图
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;

通过以上设置就可以发现实时预览图也显示正常了:

image-20231124093046537

总结

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

参考文献

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/184045.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于5G+物联网+SaaS+AI的农业大数据综合解决方案:PPT全文44页,附下载

关键词&#xff1a;智慧农业大数据&#xff0c;5G智慧农业&#xff0c;物联网智慧农业&#xff0c;SaaS智慧农业&#xff0c;AI智慧农业&#xff0c;智慧农业大数据平台 一、智慧农业大数据建设背景 1、应对全球人口快速增长带来的粮食生产压力&#xff0c;未来的粮食生产力必…

【C++】:多态

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本期来给大家解读一下有关多态的知识点&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; C 语 言 专 栏&#xff1a;C语言&#xff1a;从入门到精通 数据结…

Python Opencv实践 - 全景图片拼接stitcher

做一个全景图片切片的程序Spliter 由于手里没有切割好的全景图片资源&#xff0c;因此首先写了一个切片的程序spliter。 如果有现成的切割好的待拼接的切片文件&#xff0c;则不需要使用spliter。 对于全景图片的拼接&#xff0c;需要注意一点&#xff0c;各个切片图片之间要有…

什么是机器学习

前言 机器学习&#xff08;Machine Learning, ML&#xff09;是一个总称&#xff0c;用于解决由各位程序员自己基于 if-else 等规则开发算法而导致成本过高的问题&#xff0c;想要通过帮助机器 「发现」 它们 「自己」 解决问题的算法来解决 &#xff0c;而不需要程序员将所有…

影响语音芯片识别率的因素概述

语音芯片识别率是指芯片对人类语音信号的识别能力。在实际应用中&#xff0c;语音芯片识别率的高低直接影响了用户对芯片的体验和满意度。因此&#xff0c;提高语音芯片识别率是当前语音技术领域的重要任务之一。 1.、语音芯片的硬件设计&#xff1a;设计良好的芯片可以更好地…

竹云参编《公共数据授权运营平台技术要求》团体标准正式发布

2023年11月23日&#xff0c;第二届全球数字贸易博览会“数据要素治理与市场化论坛”于杭州成功召开&#xff0c;国家数据局党组书记、局长刘烈宏&#xff0c;浙江省委常委、常务副省长徐文光出席会议并致辞。会上&#xff0c;国家工业信息安全发展研究中心发布并解读了我国首部…

大厂前沿技术导航

百度Geek说 - 知乎 腾讯技术 - 知乎 美团技术团队

Java 项目中常用注解汇总!! (自整理)

Spring框架的注解 PostMapping("/getDetails") post请求 映射到接口 RequestBody 用来接收HTTP请求体中参数 GetMapping("/getDetails") get请求 映射到接口 RequestParam 用来接收URL中的查询参数 PutMappi…

Tomcat 配置

1&#xff1a; 打开 2&#xff1a;选择版本号&#xff0c;我这边是 1.7 3&#xff1a;添加 web 4: 添加jar包 5&#xff1a;添加 6&#xff1a;添加 Tomcat

逆矩阵相关性质与例题

1.方阵的行列式&#xff1a;就是将方阵中的每一个元素转换至行列式中。 1.性质一&#xff1a;转置方阵的行列式等于转置前的行列式。&#xff08;对标性质&#xff1a;行列式与它的转置行列式相等&#xff09; 2.性质二&#xff1a;|ka||a|*k的n次方&#xff0c;n为方阵阶数。 …

平台工程时代的 Kubernetes 揭秘:2023年生产状况报告深度剖析

Kubernetes 在生产环境中的复杂性已经成为常态&#xff0c;在2023年这个平台工程盛行的时代&#xff0c;容器管理的最大亮点可能在于其灵活性&#xff0c;然而在运维政策和治理等方面仍然存在诸多挑战。八年过去了&#xff0c;在生产环境中使用 Kubernetes 仍然需要面临许多挑战…

目前比较好用的护眼台灯,小学生适合的护眼台灯推荐

随着技术的发展&#xff0c;灯光早已成为每家每户都需要的东西。但是灯光不好可能会对眼睛造成伤害是很多人没有注意到的。现在随着护眼灯产品越来越多&#xff0c;市场上台灯的选择越来越多样化&#xff0c;如何选择一个对眼睛无伤害、无辐射的台灯成为许多家长首先要考虑的问…

mysql:修改密码的几种方式

背景 当我们 brew install mysql 新安装 mysql 的时候&#xff0c;是没有密码的&#xff0c;我们可以直接通过 mysql -u root 连接上。但是密码还是要设置的&#xff0c;一是为了安全&#xff0c;二是有些数据库软件如 Sequel 连接都是必须要密码的&#xff0c;接下来我们来看…

自监督LIGHTLY SSL教程

Lightly SSL 是一个用于自监督学习的计算机视觉框架。 github链接&#xff1a;GitHub - lightly-ai/lightly: A python library for self-supervised learning on images. Documentation&#xff1a;Documentation — lightly 1.4.20 documentation 以下内容主要来自Documen…

ElasticSearch 7 SQL 详解

平时使用Elasticsearch的时候,会在Kibana中使用Query DSL来查询数据.每次要用到Query DSL时都基本忘光了,需要重新在回顾一遍,最近发现Elasticsearch已经支持SQL查询了(6.3版本以后),整理了下一些用法. 简介 Elasticsearch SQL是一个X-Pack组件,它允许针对Elasticsearch实时执…

Zabbix-Liunx服务器内存使用率测试

要在Python 2.7中运行内存消耗脚本并安装psutil&#xff0c;您需要先安装pip。以下是完整的步骤&#xff0c;包括如何在Python 2.7环境中安装pip&#xff0c;然后安装psutil&#xff0c;以及最后如何运行内存消耗脚本。 步骤1: 安装pip 在Python 2.7中安装pip&#xff1a; 首先…

2016年8月15日 Go生态洞察:Go 1.7版本发布

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

JVM内存模型及调优

本文将为大家详细介绍JVM内存模型及如何对JVM内存进行调优。我们将分为以下几个部分进行讲解&#xff1a; JVM内存模型概述JVM内存区域及作用JVM内存调优方法实战案例与优化技巧 一、JVM内存模型概述 在深入了解JVM内存模型之前&#xff0c;我们需要先了解一下Java内存模型&am…

通俗理解词向量模型,预训练模型,Transfomer,Bert和GPT的发展脉络和如何实践

最近研究GPT&#xff0c;深入的从transfomer的原理和代码看来一下&#xff0c;现在把学习的资料和自己的理解整理一下。 这个文章写的很通俗易懂&#xff0c;把transformer的来龙去脉&#xff0c;还举例了很多不错的例子。 Transformer通俗笔记&#xff1a;从Word2Vec、Seq2S…

2016年8月18日 Go生态洞察:Go 1.7版本二进制文件缩小

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…