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 旗標,請告訴我!