TortoiseSVN Logo

목록 컨트롤 배경 이미지

2007년 1월 21일 게시됨

TortoiseSVN의 주된 초점은 유용성에 있지만, 때로는 실제 가치를 더하지는 않지만 그저 멋있어 보이는 것을 추가하고 싶을 때가 있습니다.

지난주에 저는 바로 그것을 구현하고 싶었습니다. 멋있어 보이지만 사용자를 방해하지 않는 것 말입니다. Windows 탐색기는 현재 표시하는 폴더에 어떤 파일이 있는지에 따라 오른쪽 하단에 약간 보이는 이미지를 보여줍니다. 그 이미지는 워터마크처럼 거의 보이지 않습니다. 저는 저희 주요 대화 상자의 파일 목록에 그러한 워터마크 이미지를 표시하고 싶었습니다.

The watermark in Windows explorer

이 작업을 수행하는 가장 확실한 방법은 저희 대화 상자에서 파일 목록을 표시하는 데 사용하는 컨트롤이므로 CListCtrl 클래스의 SetBkImage 메서드를 사용하는 것이었습니다. 그래서 저는 그 메서드를 다음과 같이 호출했습니다.

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

하지만 방법이 있어야 합니다. 마이크로소프트는 탐색기에서 그렇게 하고 있으니까요. 물론 그들이 자신들만 알고 있는 문서화되지 않은 기능을 사용하지 않는다고 가정한다면 말입니다.
때로는 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);

아니요, 이것도 작동하지 않았습니다. 제가 사용한 비트맵에 실제 알파 채널이 없었던 걸까요? 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;
}

그렇게 해서 저는 이것을 작동시켰습니다. 한 가지 문제가 남아있었습니다: 항목이 선택될 때 워터마크 이미지가 덮어씌워졌고, 첫 번째 열이 투명하지 않아 워터마크 위로 그려지고 있었습니다.

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 플래그를 사용하는 방법을 아시는 분이 계시면, 알려주세요!

한국어 中文