优化 Windows 缩略图工具栏

468 阅读6分钟

Windows缩略图

1. 缩略图设置

DwmSetWindowAttribute

当我们需要自定义设置窗口的缩略图和预览时,可以使用 DwmSetWindowAttribute 函数来操作窗口的属性。这个函数是与 桌面窗口管理器 (DWM)  相关的,允许我们对窗口的非客户端呈现属性进行设置。

看一下这个函数的关键参数:

  • hwnd:这是要为其设置属性值的窗口的句柄。
  • dwAttribute:描述要设置的值的标志,指定为 DWMWINDOWATTRIBUTE 枚举的值。这个参数指定了我们要设置的具体属性。
  • pvAttribute:这是一个指向包含属性值的对象的指针。属性的类型取决于 dwAttribute 参数的值。
  • cbAttribute:通过 pvAttribute 参数设置的属性值的大小(以字节为单位)。同样,属性的类型和大小也取决于 dwAttribute 参数的值。 这个函数的返回值是一个 HRESULT,如果操作成功,它将返回 S_OK。
    BOOL fForceIconic = TRUE;
    BOOL fHasIconicBitmap = TRUE;

    DwmSetWindowAttribute(
        hwnd,
        DWMWA_FORCE_ICONIC_REPRESENTATION,
        &fForceIconic,
        sizeof(fForceIconic));

    DwmSetWindowAttribute(
        hwnd,
        DWMWA_HAS_ICONIC_BITMAP,
        &fHasIconicBitmap,
        sizeof(fHasIconicBitmap));

通过调用 DwmSetWindowAttribute,设置窗口的属性(DWMWA_FORCE_ICONIC_REPRESENTATION 和 DWMWA_HAS_ICONIC_BITMAP),设置完才能自定义设置窗口缩略图和预览,从而实现自定义的缩略图和预览功能(更多关于控制非客户端区域呈现的信息,可以参考代码示例)。

DwmSetIconicBitmap

应用程序通常在收到其窗口的WM_DWMSENDICONICTHUMBNAIL消息后调用 DwmSetIconicThumbnail 函数来设置窗口的缩略图,以便在任务栏、Alt+Tab 切换和其他窗口管理器中显示。

  • 缩略图尺寸限制:缩略图的尺寸不应超过 WM_DWMSENDICONICTHUMBNAIL 消息中指定的最大 x 坐标和 y 坐标(通常情况下是 120x120 )。
  • 颜色深度要求:缩略图必须具有 32 位颜色深度。
    HBITMAP hbm;
    //……
    DwmSetIconicThumbnail(hwnd, hbm, 0);

WM_DWMSENDICONICTHUMBNAIL

在窗口处理函数中响应 WM_DWMSENDICONICTHUMBNAIL 消息,然后调用DwmSetIconicBitmap就能够设置窗口缩略图啦。

2. 预览设置

DwmSetIconicLivePreviewBitmap

调用DwmSetIconicLivePreviewBitmap函数设置窗口的实时预览图。如果函数成功,则返回 S_OK ,否则返回错误值。

    HBITMAP hbm;
    //……
    DwmSetIconicLivePreviewBitmap(hwndIcon, hbm, &ptOffset, 0);

WM_DWMSENDICONICLIVEPREVIEWBITMAP

当鼠标指针移到任务栏的窗口缩略图上或在 ALT+TAB 窗口中提供缩略图焦点时,就会在窗口处理函数中响应WM_DWMSENDICONICLIVEPREVIEWBITMAP 消息,然后调用DwmSetIconicLivePreviewBitmap设置窗口预览。

3. 缩略图按钮设置

当我们在 Windows 应用程序中创建自定义任务栏缩略图按钮时,需要使用两个关键函数:ThumbBarAddButtons 和 ThumbBarUpdateButtons

ThumbBarAddButtons

  • 用于向缩略图工具栏中添加按钮。
  • 它接受一个 THUMBBUTTON 数组作为参数,其中包含要添加的按钮的信息。数组的个数有限制,必须小于 7 个。如果超过这个限制,多余的按钮会被截断,因此需要将关键的按钮放在左边。

ThumbBarUpdateButtons

用于更新缩略图工具栏中的按钮。虽然我们无法直接添加或删除单个按钮,但可以通过这个函数来显示、隐藏或更改按钮的状态。

Alt + Tab 程序切换预览图

1. 新的问题出现啦

给main窗口设置了自定义缩略图后,遇到一个新的问题。按下 Alt + Tab 切换程序时呈现的窗口缩略图变成了我们自定义的缩略图。虽然也不影响使用,但从视觉上来说,就是有些不太好看😥😥。开始寻找解决方案!

2. SetTabOrder 和 RegisterTab

  • 创建隐藏窗口:首先创建一个用于显示任务栏缩略图和按钮的隐藏窗口(hwndIcon),并将其设置为隐藏状态。这个窗口将用于作为任务栏预览图的显示目标。
  • 注册为同一组:调用 ITaskbarList3 接口的 RegisterTab 方法,将隐藏窗口(hwndIcon)和需要显示在任务栏上的窗口(hwnd)注册为同一组。这样做可以确保它们被视为同一个应用程序的不同部分,而不是两个独立的应用程序。如果不隐藏的话,仍是显示两个任务栏图标。
  • 调整窗口顺序:利用 SetTabOrder 方法调整注册窗口的顺序。这样设置后,任务栏预览图将显示该组内第一个窗口的设置的预览图,而不是其他窗口的预览图。
    ITaskbarList3* pTaskbarList;
    HRESULT hr = CoCreateInstance(CLSID_TaskbarList, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pTaskbarList));
    if (SUCCEEDED(hr))
    {
        hr = pTaskbarList->HrInit();
        if (SUCCEEDED(hr))
        {
            pTaskbarList->RegisterTab(hwndIcon, hwnd);
            pTaskbarList->SetTabOrder(hwndIcon, hwnd);
        }
        else
        {
            // …… 
        }
    }

3. WM_SETFOCUS

现在我们有两个窗口,当点击缩略图时,需要确保主窗口获得焦点。

    // ……
    case WM_SETFOCUS:
        if (hwnd) {
            if (IsIconic(hwnd)) {
                ShowWindow(hwnd, SW_RESTORE);
            }
            SetFocus(hwnd);
        }
        break;
     // ……

代码及运行结果

1. 准备工作

    // ……
    // hwnd :主窗口句柄;
    // hwndIcon :用于设置缩略图的隐藏窗口的句柄;
    HINSTANCE hInst = GetModuleHandle (0);
    if (ThumbnailWindow(hInst))
    {
        TaskbarHelper(hwndIcon, hwnd);
        if(!Config(hwndIcon)){
            // ……
        }
    }
    // ……
//创建隐藏窗口
BOOL ThumbnailWindow(HINSTANCE hInstance)
{
    TCHAR IconClass[] = TEXT("IconWindow");
    TCHAR IconTitle[] = TEXT("MusicFree");

    WNDCLASSEXW wcex;

    wcex.cbSize = sizeof(WNDCLASSEX);

    wcex.style = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc = ThumnailWndProc; //隐藏窗口的窗口处理函数
    wcex.cbClsExtra = 0;
    wcex.cbWndExtra = 0;
    wcex.hInstance = hInstance;
    wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
    wcex.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
    wcex.lpszMenuName = NULL;
    wcex.lpszClassName = szIconClass;
    wcex.hIconSm = NULL;

    if(!RegisterClassExW(&wcex)){
        // ……
        return FALSE;
    }

    hwndIcon = CreateWindowW(szIconClass, szIconTitle, WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
    if (!hwndIcon) {
        // ……
        return FALSE;
    }

    ShowWindow(hwndIcon, SW_HIDE);
    UpdateWindow(hwndIcon);

    return TRUE;
}
// 将 hwndIcon 和 hwnd 注册为同一组
void TaskbarHelper(HWND hwndIcon, HWND hwnd)
{
    ITaskbarList3* pTaskbarList;
    HRESULT hr = CoCreateInstance(CLSID_TaskbarList, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pTaskbarList));
    if (SUCCEEDED(hr))
    {
        hr = pTaskbarList->HrInit();
        if (SUCCEEDED(hr))
        {
            pTaskbarList->RegisterTab(hwndIcon, hwnd);
            pTaskbarList->SetTabOrder(hwndIcon, hwnd);
        }
        else
        {
        }
    }
}
// 设置窗口的 DWMWA_FORCE_ICONIC_REPRESENTATION 和 DWMWA_HAS_ICONIC_BITMAP 属性
BOOL Config(HWND hwndIcon){
    BOOL fForceIconic = TRUE;
    BOOL fHasIconicBitmap = TRUE;
    HRESULT hr = E_FAIL;

    hr = DwmSetWindowAttribute(
        hwndIcon,
        DWMWA_FORCE_ICONIC_REPRESENTATION,
        &fForceIconic,
        sizeof(fForceIconic));
    if(hr != S_OK)
    {
        return FALSE;
    }

    hr = DwmSetWindowAttribute(
        hwndIcon,
        DWMWA_HAS_ICONIC_BITMAP,
        &fHasIconicBitmap,
        sizeof(fHasIconicBitmap));
    if(hr != S_OK)
    {
        return FALSE;
    }

    return TRUE;
}

2. 设置缩略图

    // ……
    HBITMAP hbm = CreateDIB(nWidth, nHeight, pByte);
    if (hbm)
    {
        hr = DwmSetIconicThumbnail(hwndIcon , hbm, 0);
        if(hr != S_OK)
        {
            // ……
        }
    }

02_01_缩略图.png

3. 设置预览

    // 在隐藏窗口(hwndIcon)的窗口处理函数中
    // ……
    case WM_DWMSENDICONICLIVEPREVIEWBITMAP:
            OnSendLivePreviewBitmap();
            break;
    // ……
    // 在 OnSendLivePreviewBitmap 函数中
    // ……
    HBITMAP hbm = _CreateDIB(rcClient.right - rcClient.left, rcClient.bottom - rcClient.top);
    // 在创建位图时,需要将位图的BGRA参数都设置为1,这样就能够表现为透明(显示出主窗口的预览)。此处设置为黑透明(0,0,0,0)和白透明(255,255,255,0)都不能达到想要的效果
    // PS:在部分移动端机型的webview中,黑透明和白透明的表现可能不一致
    if (hbm)
    {
        hr = DwmSetIconicLivePreviewBitmap(hwndIcon, hbm, &ptOffset, 0);
        if(hr != S_OK)
        {
            // ……
        }
        DeleteObject(hbm);
    }
    // ……

02_02_预览.png

4. Alt + Tab

02_03_Alt+Tab.png

总结

emmm……Windows缩略图看似小小的功能,却蕴含着许多巧妙的技巧和窍门,希望大家都能为自己的应用程序增添更加丰富和个性化的用户体验吧~🚀🌟

PS:在设置预览创建位图时,需要将位图的BGRA参数都设置为1,这样就能够表现为透明(显示出主窗口的预览)。此处设置为黑透明(0,0,0,0)和白透明(255,255,255,0)都不能达到想要的效果😭😭

PPS:在部分移动端机型的webview中,黑透明和白透明的表现可能不一致🤯

仅作个人纪录,感谢所有写过相关文章的其他作者!!

参考: