TortoiseSVN Logo

列表控件背景图片

发布于 2007 年 1 月 21 日

虽然 TortoiseSVN 的主要重点在于易用性,但有时我喜欢添加一些实际上并没有增加价值,但只是看起来不错的东西。

上周我想要实现的就是这样:一些看起来不错但不会打扰用户的东西。Windows 资源管理器会在右下角显示一个稍微可见的图像,具体取决于当前文件夹中显示的文件。该图像几乎不可见,就像水印一样。我想在我们的主对话框文件列表中显示这样的水印图像。

The watermark in Windows explorer

完成此操作的显而易见的步骤是使用 SetBkImage 方法,该方法属于 CListCtrl 类,因为那是我们用来在对话框中显示文件列表的控件。所以我像这样调用了该方法

HBITMAP hbm = LoadImage(hResource,
                        MAKEINTRESOURCE(IDB_BACKGROUND),
                        IMAGE_BITMAP,
                        128, 128,
                        LR_DEFAULTCOLOR);

m_ListCtrl.SetBkImage(hbm, FALSE, 100, 100);

但当然,这根本不起作用。没有显示背景图像。单步调试 SetBkImage 的代码表明它只是 LVM_SETBKIMAGE 消息的包装器。阅读关于该消息的 MSDN 文档后,发现 SetBkImage 的文档中完全缺少的内容:LVBKIMAGE 结构的参数 hbm “目前未使用”。太棒了。然后我尝试使用另一个选项

TCHAR szBuffer[MAX_PATH];
VERIFY(::GetModuleFileName(hResource, szBuffer, MAX_PATH));

CString sPath;
sPath.Format(_T("res://%s/#%d/#%d"),
             szBuffer, RT_BITMAP,
             IDB_BACKGROUND);

m_ListCtrl.SetBkImage(sPath, FALSE, 100, 100);

而这实际上奏效了。但是,一方面,图像被实心绘制,图像的所有透明像素都绘制成黑色,并且当控件的内容滚动时,图像没有停留在右下角。我既无法使图像使用 alpha 通道正确绘制,也无法使图像停留在右下角,即使在滚动事件处理程序中设置了图像位置也是如此。显然,我走错了路。

SetBkImage the obvious way

接下来,我尝试直接在列表控件的 NM_CUSTOMDRAW 处理程序中绘制图像。这效果很好,直到我稍微快一点滚动文件列表。这导致水印图像出现一些难看的“残留物”。事实证明,列表控件在滚动时并不总是完全重绘其背景,这实际上对性能来说是一件好事,但对我以及我想做的事情来说当然是件坏事。
顺便提一下:CDRF_NOTIFYPOSTERASE 未在列表控件中使用。

The watermark drawn in the NM_CUSTOMDRAW handler

但肯定有一种方法,因为 Microsoft 在资源管理器中就是这样做的,当然,前提是他们没有使用一些他们喜欢保密的未公开功能。
有时阅读 SDK 中的头文件很有用。在文件 commctrl.h 中,我找到了以下用于 LVBKIMAGE 的定义

#if (_WIN32_WINNT >= 0x0501)
#define LVBKIF_FLAG_TILEOFFSET  0x00000100
#define LVBKIF_TYPE_WATERMARK   0x10000000
#define LVBKIF_FLAG_ALPHABLEND  0x20000000
#endif

但在这三个定义中,只有第一个被记录在案。嗯,也不完全是。在 MSDN 中搜索 LVBKIF_TYPE_WATERMARK 后,找到了 此页面。在这里,这些定义被记录在案。找到了!或者我是这么认为的。

TCHAR szBuffer[MAX_PATH];
VERIFY(::GetModuleFileName(hResource, szBuffer, MAX_PATH));

CString sPath;
sPath.Format(_T("res://%s/#%d/#%d"),
             szBuffer, RT_BITMAP,
             IDB_BACKGROUND);

LVBKIMAGE lv = {0};
lv.ulFlags = LVBKIF_TYPE_WATERMARK|LVBKIF_FLAG_ALPHABLEND;

lv.pszImage = sPath;
lv.xOffsetPercent = 100;

lv.yOffsetPercent = 100;
m_ListCtrl.SetBkImage(&lv);

不,仍然不起作用。也许我使用的位图没有真正的 alpha 通道?删除 LVBKIF_FLAG_ALPHABLEND 标志也没有帮助。出于绝望,我尝试了以下方法

HBITMAP hbm = LoadImage(hResource,
                        MAKEINTRESOURCE(IDB_BACKGROUND),
                        IMAGE_BITMAP,
                        128, 128,
                        LR_DEFAULTCOLOR);

LVBKIMAGE lv = {0};
lv.ulFlags = LVBKIF_TYPE_WATERMARK;

lv.hbm = hbm;
lv.xOffsetPercent = 100;

lv.yOffsetPercent = 100;
SetBkImage(&lv);

而这奏效了!难以置信。即使文档说 LVBKIMAGE 结构的 hbm 成员“目前未使用”,但如果设置了 LVBKIF_TYPE_WATERMARK 标志,它显然会被使用(并且必须使用)。图像显示在右下角,并且即使在滚动文件列表时也保持在那里,没有任何 UI 故障。但是(总有一个“但是”),图像没有使用其 alpha 通道显示。在应该透明的地方,它被绘制成黑色。但这就是 LVBKIF_FLAG_ALPHABLEND 标志的用途。

HBITMAP hbm = LoadImage(hResource,
                        MAKEINTRESOURCE(IDB_BACKGROUND),
                        IMAGE_BITMAP,
                        128, 128,
                        LR_DEFAULTCOLOR);

LVBKIMAGE lv = {0};
lv.ulFlags = LVBKIF_TYPE_WATERMARK|LVBKIF_FLAG_ALPHABLEND;

lv.hbm = hbm;
lv.xOffsetPercent = 100;

lv.yOffsetPercent = 100;
SetBkImage(&lv);

或者至少我是这么认为的。添加 LVBKIF_FLAG_ALPHABLEND 标志后,位图消失了。我尝试了不同的位图,使用不同的图像编辑器创建了带有 alpha 通道的位图,但没有任何效果。我甚至从资源管理器使用的 shell.dll 中提取了位图。即使是那些也不起作用!

但在如此接近目标时放弃?我可不会 :)
简单的解决方案是直接使用带有白色背景的位图。这在大多数用户没有更改默认系统颜色的系统上看起来不错。但是有些用户实际上会更改它们,有些用户甚至使用红色或其他彩色背景。在这些系统上,背景图像会看起来非常难看。所以这不是一个真正的解决方案。

我最终想出的办法是将图像 alpha 混合到空白位图中,其中背景设置为用户设置的系统背景。

bool CSVNStatusListCtrl::SetBackgroundImage(UINT nID)
{
    SetTextBkColor(CLR_NONE);
    COLORREF bkColor = GetBkColor();

    // create a bitmap from the icon
    HICON hIcon = (HICON)LoadImage(AfxGetResourceHandle(),
                                   MAKEINTRESOURCE(nID), IMAGE_ICON,
                                   128, 128, LR_DEFAULTCOLOR);

    if (!hIcon)
        return false;

    RECT rect = {0};

    rect.right = 128;
    rect.bottom = 128;

    HBITMAP bmp = NULL;

    HWND desktop = ::GetDesktopWindow();

    if (desktop)
    {
        HDC screen_dev = ::GetDC(desktop);

        if (screen_dev)
        {
            // Create a compatible DC
            HDC dst_hdc = ::CreateCompatibleDC(screen_dev);

            if (dst_hdc)
            {
            // Create a new bitmap of icon size
            bmp = ::CreateCompatibleBitmap(screen_dev,
                                           rect.right,
                                           rect.bottom);

                if (bmp)
                {
                    // Select it into the compatible DC
                    HBITMAP old_bmp = (HBITMAP)::SelectObject(dst_hdc, bmp);

                    // Fill the background of the compatible DC
                    // with the given colour
                    ::SetBkColor(dst_hdc, bkColor);

                    ::ExtTextOut(dst_hdc, 0, 0, ETO_OPAQUE, &rect,
                                 NULL, 0, NULL);

                    // Draw the icon into the compatible DC
                    ::DrawIconEx(dst_hdc, 0, 0, hIcon,
                                 rect.right, rect.bottom, 0,
                                 NULL, DI_NORMAL);

                    ::SelectObject(dst_hdc, old_bmp);
                }
                ::DeleteDC(dst_hdc);

            }
        }
        ::ReleaseDC(desktop, screen_dev);
    }

    // Restore settings
    DestroyIcon(hIcon);

    if (bmp == NULL)
        return false;

    LVBKIMAGE lv;
    lv.ulFlags = LVBKIF_TYPE_WATERMARK;

    lv.hbm = bmp;
    lv.xOffsetPercent = 100;

    lv.yOffsetPercent = 100;
    SetBkImage(&lv);

    return true;
}

这就是我使其工作的方式。但仍然存在一个问题:当项目被选中时,水印图像会被覆盖,并且第一列不是透明的,也会覆盖水印。

The watermark with the LVS_EX_FULLROWSELECT set

事实证明,这是因为我为控件设置了 LVS_EX_FULLROWSELECT 样式。删除该样式最终使水印图像的行为与资源管理器中的行为完全相同。

现在(请击鼓):添加和提交对话框的屏幕截图

The TortoiseSVN Add dialog showing its watermark
The TortoiseSVN Commit dialog showing its watermark

如果有人知道如何使用 LVBKIF_FLAG_ALPHABLEND 标志,请告诉我!