드래그앤 드랍 이야기

@codemaru · September 05, 2007 · 10 min read

리스트나 트리의 드래그 앤 드랍을 보면 반투명 이미지가 마우스와 함께 움직이죠. 별로 대단한 구현은 없습니다. CreateDragImage로 이미지를 생성해서 이동시키면 되죠. 그런데 어제 회사에 계신 한 분이 이걸로 굉장히 애를 먹고 있는걸 목격했습니다. 두 가지 문제가 있었는데 하나는 CreateDragImage로 생성한 이미지의 글자가 이상하게 나타나는 것이었고, 다른 하나는 생성한 이미지의 아이콘 위치와 글자 위치가 맞지 않는다는 문제였습니다. 저도 옆에서 같이 봤는데 소스엔 정말 특이 사항이 없더군요.

구글 검색 결과 알아낸 문제의 원인은 정말 어처구니 없었습니다. 첫 번째 문제의 원인은 XP에 있는 Clear Type 때문이었습니다. ClearType을 설정하면 안티앨리어싱된 폰트가 사용되죠. 그래서 글자가 이상하게 그려진 것이더군요. 해당 옵션을 끄니 정상적으로 그려졌습니다. 두 번째 원인은 XP에 새로 등장한 Common Control 6.0과 관련된 문제였습니다. 매니페스트 파일을 빼고 이전 Common Control을 사용하도록 하니 문제가 없어졌습니다. 하지만 둘 다 조금 웃기죠. 드래그 앤 드랍 하나 하려고 시스템 옵션을 건드려야 한다는게. 더욱 퐝당했던 사실은 이 모든 옵션에도 불구하고 탐색기는 잘된다는 것이었습니다. 물론 다른 모든 소프트웨어의 드래그앤 드랍이 잘 됐죠.

관련 소스를 찾아보니 답은 의외로 간단한데 있었습니다. CreateDragImage를 사용하지 않고 그림을 직접 그리도록 구현했더군요. 결국 머 CreateDragImage를 새로 만들었다고 생각하면 됩니다. 아래 소느는 파일질라에서 관련 부분을 발췌한 것 입니다. 안티앨리어싱을 끈 폰트를 사용하고 직접 그리는게 핵심입니다. 좌표 계산 부분이 굉장히 복잡한 걸 볼 수 있습니다. 그들이 저거 맞추느라고 얼마나 고생했는지를 알 수 있습니다. ㅠㅠ

CImageList\* CMainFrame::CreateDragImageEx(CListCtrl \*pList, LPPOINT lpPoint)  
{  
    if (pList->GetSelectedCount() <= 0)  
        return NULL; // no row selected  
  
  
    DWORD dwStyle = GetWindowLong(pList->m\_hWnd, GWL\_STYLE) & LVS\_TYPEMASK;  
  
    CRect rectComplete(0, 0, 0, 0);  
  
    // Determine List Control Client width size  
    CRect rectClient;  
    pList->GetClientRect(rectClient);  
    int nWidth = rectClient.Width() + 50;  
  
    // Start and Stop index in view area  
    int nIndex = pList->GetTopIndex() - 1;  
    int nBottomIndex = pList->GetTopIndex() + pList->GetCountPerPage();  
    if (nBottomIndex > (pList->GetItemCount() - 1))  
        nBottomIndex = pList->GetItemCount() - 1;  
  
    while ((nIndex = pList->GetNextItem(nIndex, LVNI\_SELECTED)) != -1)  
    {  
        if (nIndex > nBottomIndex)  
            break;  
  
        CRect rectItem;  
        pList->GetItemRect(nIndex, rectItem, LVIR\_BOUNDS);  
  
        if (rectItem.left < 0)  
            rectItem.left = 0;  
  
        if (rectItem.right > nWidth)  
            rectItem.right = nWidth;  
  
        rectComplete.UnionRect(rectComplete, rectItem);  
    }  
  
    // Create memory device context  
    CClientDC dcClient(this);  
    CDC dcMem;  
    CBitmap Bitmap;  
  
    if (!dcMem.CreateCompatibleDC(&dcClient))  
        return NULL;  
  
    if (!Bitmap.CreateCompatibleBitmap(&dcClient  
                                        , rectComplete.Width()  
                                        , rectComplete.Height()))  
        return NULL;  
  
    CBitmap \*pOldMemDCBitmap = dcMem.SelectObject(&Bitmap);  
    // Use green as mask color  
    dcMem.FillSolidRect(0  
                        , 0  
                        , rectComplete.Width()  
                        , rectComplete.Height()  
                        , RGB(0,255,0));  
  
    // 안티알리아스 안된 폰트를 사용하는게 핵심  
    CFont \*pFont = pList->GetFont();  
    LOGFONT lf;  
    pFont->GetLogFont(&lf);  
    lf.lfQuality = NONANTIALIASED\_QUALITY;  
    CFont newFont;  
    newFont.CreateFontIndirect(&lf);  
  
    CFont \*oldFont = dcMem.SelectObject(&newFont);  
    ////////////////////////////////////////////////  
  
    // Paint each DragImage in the DC  
    nIndex = pList->GetTopIndex() - 1;  
    while ((nIndex = pList->GetNextItem(nIndex, LVNI\_SELECTED)) != -1)  
    {  
        if (nIndex > nBottomIndex)  
            break;  
  
        TCHAR buffer[1000];  
        LVITEM item = {0};  
        item.mask = LVIF\_TEXT | LVIF\_IMAGE;  
        item.iItem = nIndex;  
        item.pszText = buffer;  
        item.cchTextMax = 999;  
  
        pList->GetItem(&item);  
  
        // Draw the icon  
        CImageList\* pSingleImageList = pList->GetImageList((dwStyle == LVS\_ICON)  
                                        ? LVSIL\_NORMAL:LVSIL\_SMALL);  
        if (pSingleImageList)  
        {  
            CRect rectIcon;  
            pList->GetItemRect(nIndex, rectIcon, LVIR\_ICON);  
  
            IMAGEINFO info;  
            pSingleImageList->GetImageInfo(item.iImage, &info);  
            CPoint p((rectIcon.left - rectComplete.left   
                    + rectIcon.right - rectComplete.left) / 2   
                    - (info.rcImage.right - info.rcImage.left) / 2,  
                (rectIcon.top - rectComplete.top   
                + rectIcon.bottom - rectComplete.top) / 2   
                - (info.rcImage.bottom - info.rcImage.top) / 2   
                + ((dwStyle == LVS\_ICON) ? 2 : 0));  
  
            pSingleImageList->Draw( &dcMem, item.iImage,  
                                    p,  
                                    ILD\_TRANSPARENT);  
        }  
  
        // Draw the text  
        CString text;  
        text = item.pszText;  
        CRect textRect;  
        pList->GetItemRect( nIndex, textRect, LVIR\_LABEL );  
        textRect.top -= rectComplete.top - 2;  
        textRect.bottom -= rectComplete.top + 1;  
        textRect.left -= rectComplete.left - 2;  
        textRect.right -= rectComplete.left;  
  
        DWORD flags = DT\_END\_ELLIPSIS | DT\_MODIFYSTRING;  
        if (dwStyle == LVS\_ICON)  
            flags |= DT\_CENTER | DT\_WORDBREAK;  
        dcMem.DrawText(text, -1, textRect, flags);  
    }  
  
    dcMem.SelectObject(oldFont);  
  
    dcMem.SelectObject(pOldMemDCBitmap);  
  
    // Create drag image(list)  
    CImageList\* pCompleteImageList = new CImageList;  
    pCompleteImageList->Create(rectComplete.Width()  
                                , rectComplete.Height()  
                                , ILC\_COLOR32 | ILC\_MASK  
                                , 0  
                                , 1);  
    pCompleteImageList->Add(&Bitmap, RGB(0, 255, 0));   
    Bitmap.DeleteObject();  
  
    if (lpPoint)  
    {  
        lpPoint->x = rectComplete.left;  
        lpPoint->y = rectComplete.top;  
    }  
  
    return pCompleteImageList;  
}

정말 놀라운 사실은 검색을 해도해도 이 택도 아닌 현상에 대해서 MSDN은 한 줄도 언급을 하지 않고 있다는 사실입니다. 안티 앨리어싱이야 그렇다쳐도 Common Control 6.0의 CreateDragImage 구현은 분명 버그임에도 한마디도 안해주는건 너무하지 않나요. 아님 다 저렇게 직접 구현해서 쓰라는 건가. ㅠㅠ

예전에 이와 비슷한 현상이 하나 있었습니다. 그것도 XP부터 새로 생긴(?) 부드러운 스크롤이란 기능과 관련된 것이었죠. 그 부드러운 스크롤 기능을 켜 두면 오너 드로 리스트에서 이상한 현상이 발생하더군요. 물론 엄밀히 말하면 오너 드로는 아닙니다. OnPaint를 직접 구현한 리스트라는게 정확한 표현 이겠네요. 그 당시 저는 최적화를 위해서 화면에 보이는 부분만 그리도록 했는데 그놈이 스크롤만 하면 엉망이 되는 겁니다. 부드러운 스크롤을 끄니까 괜찮더군요. 결국 원인은 그 일부분만 그린데 있었습니다. 부드러운 스크롤이 뭔가 필요 없는 부분을 저장해서 버퍼로 쓰는것 같더군요. 한 두어개를 추가로 그려주니 문제가 없었습니다.

사실 이런 문제는 코멘트 한 두 줄이면 해결할 수 있는 것들인데, 없으면 또 정말 알아내기 힘든 것 같습니다.

@codemaru
돌아보니 좋은 날도 있었고, 나쁜 날도 있었다. 그런 나의 모든 소소한 일상과 배움을 기록한다. 여기에 기록된 모든 내용은 한 개인의 관점이고 의견이다. 내가 속한 조직과는 1도 상관이 없다.
(C) 2001 YoungJin Shin, 0일째 운영 중