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

但一定有办法,因为微软在资源管理器中就是这样做的,当然假设他们没有使用他们喜欢保密的某些未公开的功能。
有时阅读 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标志,请告诉我!