列表控件背景图片
虽然 TortoiseSVN 的主要重点在于易用性,但有时我喜欢添加一些实际上并没有增加价值,但只是看起来不错的东西。
上周我想要实现的就是这样:一些看起来不错但不会打扰用户的东西。Windows 资源管理器会在右下角显示一个稍微可见的图像,具体取决于当前文件夹中显示的文件。该图像几乎不可见,就像水印一样。我想在我们的主对话框文件列表中显示这样的水印图像。

完成此操作的显而易见的步骤是使用 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 通道正确绘制,也无法使图像停留在右下角,即使在滚动事件处理程序中设置了图像位置也是如此。显然,我走错了路。

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

但肯定有一种方法,因为 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;
}
这就是我使其工作的方式。但仍然存在一个问题:当项目被选中时,水印图像会被覆盖,并且第一列不是透明的,也会覆盖水印。

事实证明,这是因为我为控件设置了 LVS_EX_FULLROWSELECT
样式。删除该样式最终使水印图像的行为与资源管理器中的行为完全相同。
现在(请击鼓):添加和提交对话框的屏幕截图


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