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);

そして、それは実際にうまくいきました。しかし、まず第一に、画像はべた塗りで描画され、画像の透明なピクセルはすべて黒で描画されました。そして、コントロールの内容がスクロールされても、画像は右下に留まりませんでした。スクロールイベントハンドラーで画像の位置を設定しても、アルファチャンネルで画像を正しく描画することも、画像を右下に留めることもできませんでした。明らかに、私は間違った道を進んでいました。

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

しかし、これらの 3 つの定義のうち、ドキュメント化されているのは最初のものだけです。いや、正確にはそうではありません。MSDNLVBKIF_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);

いいえ、どちらもうまくいきませんでした。もしかしたら、私が使用したビットマップには本物のアルファチャンネルがなかったのでしょうか? 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 のグリッチなしにそこに留まりました。しかし(常に「しかし」がありますが)、画像はアルファチャンネル付きで表示されませんでした。透明であるべき場所が黒で描画されました。しかし、それはまさに 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 フラグを追加すると、ビットマップが消えてしまいました。異なるビットマップを試したり、異なる画像エディターを使用してアルファチャンネル付きのビットマップを作成したりしましたが、何も機能しませんでした。エクスプローラーがこれに使用している shell.dll からビットマップを抽出さえしました。それらでさえ機能しませんでした!

しかし、目標に非常に近づいているのに諦める?私らしくない :)
簡単な解決策は、白い背景のビットマップを単に使用することでしょう。これは、ユーザーがデフォルトのシステムカラーを変更していないほとんどのシステムでは見栄えが良いです。しかし、実際にシステムカラーを変更するユーザーもいれば、赤やその他の色付きの背景を使用するユーザーもいます。そのようなシステムでは、背景画像は非常に醜く見えるでしょう。したがって、これは真の解決策ではありません。

私が最終的に思いついたのは、ユーザーが設定したシステム背景に背景が設定された空のビットマップに、画像をアルファブレンドして描画することでした。

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;
}

そして、それが私がこれを機能させた方法です。1 つ問題が残っていました。項目が選択されるとウォーターマーク画像が上書きされ、最初の列も透明ではなく、ウォーターマークを上書きしていました。

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 フラグの使い方を知っている人がいれば、ぜひ教えてください!