코딩캣: 코딩하는 고양이.
Windows 쉘 익스텐션 개발 가이드 - (7) 비트맵 및 폴더 메뉴 (1/3)
API/COM
2021. 2. 9. 22:00

입문자를 위한 Windows Shell Extension 개발 가이드

본 게시물은 ‘codeproject.com’에 게시된 “The Complete Idiot's Guide to Writing Shell Extensions” 시리즈를 우리말로 번역한 것입니다.

원문의 주소는 “https://www.codeproject.com/script/Articles/MemberArticles.aspx?amid=152”입니다. 원문은 2000년에 작성되었지만 네이티브 수준에서 Windows 운영체제가 근본적으로 바뀌지 않는 이상 현재에도 여전히 유효한 내용입니다. 다만 소스코드가 Visual C++ 6.0을 기준으로 작성되었기 때문에 현재 버전의 Visual Studio에서 자동으로 생성해주는 코드의 형태와는 다소 차이가 있을 수 있음을 감안하시기 바랍니다.

또한 본 게시물은 원문을 최대한 직역하는 것을 지향하고 있으나, 우리말로 읽었을 때 보다 매끄럽게 하기 위하여 부득이 의역, 어순 조정 및 어휘 조정이 있음을 양해 바랍니다.

 

  1. 목차
  2. 쉘 익스텐션(Shell Extension)을 작성하기 위한 단계별 튜토리얼
    1. 파트 1
    2. 파트 2
  3. 여러 개의 파일에 대해 한번에 작동하는 쉘 익스텐션(Shell Extension)
    1. 파트 1
    2. 파트 2
  4. 파일에 대해 ‘팝업(Popup)’ 설명을 보여주는 쉘 익스텐션(Shell Extension)
  5. 사용자 정의 ‘드래그 앤 드롭(Drag and Drop)’ 기능을 제공하는 쉘 익스텐션(Shell Extension)
  6. 파일에 대한 ‘등록 정보’(또는 ‘속성’) 다이얼로그에 페이지를 추가하는 쉘 익스텐션(Shell Extension)
    1. 파트 1
    2. 파트 2
  7. ‘보내기(Send To)’ 메뉴에서 사용될 수 있는 쉘 익스텐션(Shell Extension)
  8. 컨텍스트 메뉴에 그림 출력하는 쉘 익스텐션(Shell Extension)
    및 디렉토리의 빈 공간에서 마우스 오른쪽 클릭에 응답하는 컨텍스트 메뉴 익스텐션(Shell Extension)
    1. 파트 1
    2. 파트 2
    3. 파트 3
  9. Windows 탐색기에서 “자세히” 보기 모드를 선택할 때 나타나는 열 항목을 추가하는 쉘 익스텐션(Shell Extension)
    1. 파트 1
    2. 파트 2
  10. 특정 형식의 파일에 대해 아이콘을 사용자화 하는 쉘 익스텐션(Shell Extension)

 

7 단계. 컨텍스트 메뉴에 그림 출력하는 쉘 익스텐션(Shell Extension)
및 디렉토리의 빈 공간에서 마우스 오른쪽 클릭에 응답하는 컨텍스트 메뉴 익스텐션(Shell Extension)

실습 프로젝트 다운로드

ShellExtGuide7_demo.zip
584.6 kB

 

들어가기에 앞서......

이번 단계에서 필자는 몇몇 독자들의 요청에 따라 두 가지 주제에 대해 다루어 보겠습니다.

하나는 컨텍스트 메뉴 익스텐션에서 메뉴 항목에 그림을 출력하는 것이고, 다른 하나는 디렉토리 윈도우의 빈 공간에서 마우스 오른쪽 클릭을 하였을 때 나타나는 컨텍스트 메뉴 항목을 확장해 보는 것입니다. 이번 단계를 읽기 전에 여러분은 1 단계2 단계를 읽고 컨텍스트 메뉴 익스텐션에 대한 배경지식을 가지고 있어야 합니다.

Visual C++ 7.0 또는 8.0 사용자는 이전과 같이 1 단계의 본 시리즈에 들어가며......를 참고하여 컴파일하기 전 몇 가지 설정을 변경해야 함을 기억하시기 바랍니다.

 

첫 번째 쉘 익스텐션: 컨텍스트 메뉴 항목에 그림을 출력하기

이번 절에서 필자는 메뉴 항목에 그림을 추가하기 위해 필요한 추가적인 작업에 대해 다루어 보겠습니다.

이번 쉘 익스텐션에서는 그림을 출력할 것이므로, 생동감 있어 보일 것입니다. 필자는 컨텍스트 메뉴를 통해 비트맵 파일의 썸네일을 보여주는 기능이 있는 PicaView라는 프로그램을 똑같이 구현해 보겠습니다. (참고: PicaView는 ACD Systems에서 개발된 프로그램입니다. 현재는 단종되었습니다.) PicaView에 의해 확장된 컨텍스트 메뉴는 이렇게 생겼습니다.

 

컨텍스트 메뉴에 출력되는 그림.

 

우리가 다룰 쉘 익스텐션도 소스 코드를 최대한 쉽게 유지하면서, 이와 같이 비트맵 파일에 대한 썸네일을 컨텍스트 메뉴가 표시되도록 하겠습니다. 다만 필자는 색채를 정확하게 재현하는 부분까지는 고려하지 않겠습니다. 그러한 부분은 독자 여러분의 몫으로 남겨두겠습니다. ^^;;

 

초기화 인터페이스

여러분은 이제 이 단계에 능숙해졌을 것입니다. Visual C++ 마법사에 대한 설명은 생략하고, 곧바로 BmpViewExt라는 이름의 MFC가 지원되는 ATL COM 프로젝트를 생성하고, CBmpCtxMenuExt라는 이름의 C++ 클래스를 생성합니다.

지금까지 해 보았던 컨텍스트 메뉴 익스텐션과 마찬가지로 이번 쉘 익스텐션도 IShellExtInit 인터페이스를 구현합니다. COM 객체에 IShellExtInit를 추가하기 위하여 BmpCtxMenuExt.h를 열고 아래 표시한 부분과 같이 내용을 추가합니다. 물론 메뉴 항목을 구현할 때 사용하게 될 몇 가지 멤버 변수도 필요합니다.

 

#include <comdef.h> // 새로 추가
 
/////////////////////////////////////////////////////////////////////////////
// CBmpCtxMenuExt
 
class CBmpCtxMenuExt : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CBmpCtxMenuExt, &CLSID_BmpCtxMenuExt>,
    public IShellExtInit { // 새로 추가
    BEGIN_COM_MAP(CBmpCtxMenuExt)
        COM_INTERFACE_ENTRY(IShellExtInit)
    END_COM_MAP()
    
    // 이 이하로 전부 새로 추가
    public:
        // IShellExtInit
        STDMETHODIMP Initialize(LPCITEMIDLIST, LPDATAOBJECT, HKEY);
        
    protected:
        TCHAR   m_szFile[MAX_PATH];
        CBitmap m_bmp;
        UINT    m_uOurItemID;
        
        LONG m_lItemWidth, m_lItemHeight;
        LONG m_lBmpWidth, m_lBmpHeight;
        
        static const LONG m_lMaxThumbnailSize;
        static const LONG m_l3DBorderWidth;
        static const LONG m_lMenuItemSpacing;
        static const LONG m_lTotalBorderSpace;
        
        // 메뉴와 관련된 메시지를 처리하기 위한 헬퍼(helper) 메소드
        STDMETHODIMP MenuMessageHandler(UINT, WPARAM, LPARAM, LRESULT *);
        STDMETHODIMP OnMeasureItem(MEASUREITEMSTRUCT *, LRESULT *);
        STDMETHODIMP OnDrawItem(DRAWITEMSTRUCT *, LRESULT *);
};

 

IShellExtInit::Initialize에서 수행할 작업은, 마우스 오른쪽 버튼이 눌림 파일의 이름을 가져오는 것입니다. 그리고 그 확장명이 .bmp 이면 썸네일을 생성합니다.

BmpCtxMenuExt.cpp 파일에서 정적 변수에 대한 선언을 추가합니다. 이들은 썸네일의 비율과 그 테두리를 제어하게 될 것입니다. 이들이 이미지를 변화시키는 것에 대한 부담은 잠시 접어두고, 이들이 최종적으로 컨텍스트 메뉴에서 어떻게 나타나는지에 대해 직접 보도록 하시기 바랍니다.

 

const LONG CBmpCtxMenuExt::m_lMaxThumbnailSize = 64;
const LONG CBmpCtxMenuExt::m_l3DBorderWidth    = 2;
const LONG CBmpCtxMenuExt::m_lMenuItemSpacing  = 4;
const LONG CBmpCtxMenuExt::m_lTotalBorderSpace = 2 * (m_lMenuItemSpacing + m_l3DBorderWidth);

 

다음은 위 상수에 대한 의미입니다.

- m_lMaxThumbnailSize: 비트맵 이미지의 크기가 이 상수보다 크면, 가로 및 세로의 길이가 m_lMaxThumbnailSize 픽셀인 정사각형 영역에 맞추어 이미지는 축소시킬 것입니다. 비트맵 이미지가 이보다 작을 경우 변화를 주지 않고 그대로 보일 것입니다.

- m_l3DBorderWidth: 썸네일 주변으로 보여줄 3D 테두리의 두께입니다. 단위는 픽셀(pixel)입니다.

- m_lMenuItemSpacing: 3D 테두리 주변으로 둘 여백의 너비로서 단위는 픽셀(pixel)입니다. 이 값에 따라 썸네일과 그 주변의 메뉴 항목 사이를 띄워집니다.

 

또한 IShellExtInit::Initialize 메소드를 다음과 같이 정의합니다.

STDMETHODIMP CBmpCtxMenuExt::Initialize(LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDO, HKEY hkeyProgID) {
    AFX_MANAGE_STATE(AfxGetStaticModuleState());
    
    COleDataObject dataobj;
    HGLOBAL        hglobal;
    HDROP          hdrop;
    bool           bOK = false;
    
    dataobj.Attach(pDO, FALSE);
    
    // 첫 번째로 선택된 파일의 이름을 가져옵니다.
    // 필자는 파일의 확장명이 .bmp인지를 확인하고 보관할 것입니다.
    hglobal = dataobj.GetGlobalData(CF_HDROP);
    
    if (hglobal == NULL)
        return E_INVALIDARG;
    
    hdrop = (HDROP)GlobalLock(hglobal);
    
    if (hdrop == NULL)
        return E_INVALIDARG;
    
    // 선택된 파일들 중 첫 번째 파일의 이름을 가져옵니다.
    if (DragQueryFile(hdrop, 0, m_szFile, MAX_PATH)) {
        // 파일의 확장명이 .bmp인가?
        if (PathMatchSpec(m_szFile, _T("*.bmp"))) {
            // 비트맵을 로드하여 CBitmap 객체에 연결시킵니다.
            HBITMAP hbm = (HBITMAP)LoadImage(NULL, m_szFile, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
            if (hbm != NULL) {
                // 비트맵을 로드하는데 성공했으므로 CBitmap 객체에 전달합니다.
                m_bmp.Attach(hbm);
                bOK = true;
            }
        }
    }
    
    GlobalUnlock(hglobal);
    return bOK ? S_OK : E_FAIL;
}

 

비교적 단순한 작업을 마쳤습니다. 우리는 다음에 사용할 비트맵을 파일로부터 로드하였고, CBitmap 객체에 연결시켰습니다.

 

컨텍스트 메뉴와 상호작용하기

이전과 같이 우리는 세 가지 IContextMenu 메소드에서 필요한 작업을 수행합니다. 그리고 우리는 QueryContextMenu 메소드에서 컨텍스트 메뉴에 항목들을 새로 추가합니다. 먼저 우리는 쉘의 버전을 체크합니다. 버전이 4.71 이상일 경우 우리는 컨텍스트 메뉴 항목에 그림을 출력합니다. 그렇지 않다면 우리는 비트맵을 보여주는 메뉴 항목을 추가합니다. 후자의 경우 우리가 해야 할 것은 항목을 추가하는 것이 전부입니다.

먼저 쉘 버전을 체크해야 합니다. 쉘의 버전을 얻기 위하여 쉘이 내보내는 함수인 DllGetVersion을 호출합니다. 그런 이름의 함수를 쉘이 내보내고 있지 않다면, 그 쉘의 버전은 4.0입니다. 즉 쉘 버전 4.0에서는 DllGetVersion이라는 이름의 함수가 존재하지 않습니다.

여기서 bUseOwnerDraw는 그림을 출력할 수 있는 메뉴 항목을 사용할 수 있는지를 나타냅니다. 이 값이 참이 되면, 우리는 그림을 출력할 수 있는 메뉴 항목을 추가할 것이고(mii.fType을 설정하는 줄을 참고하기 바랍니다). 값이 거짓이면 비트맵 항목을 추가한 다음 컨텍스트 메뉴에게 사용자에게 보여줄 비트맵에 대한 핸들을 알려줍니다.

우리는 컨텍스트 메뉴에 새로운 항목을 추가하면서 m_uOurItemID 멤버변수에 그 ID를 추가했습니다. 이는 나중에 메시지가 전달될 때 ID를 식별하기 위하여 사용될 것입니다. 엄밀히 말하면 이 과정은 필요하지 않습니다, 왜냐하면 우리는 지금 하나의 메뉴 항목만을 만들었기 때문입니다. 그러나 실무적으로는 여러 개의 메뉴 항목을 만들 수도 있기 때문에 더 말할 것 없이 좋은 방식입니다.

 

class CBmpCtxMenuExt :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CBmpCtxMenuExt, &CLSID_BmpCtxMenuExt>,
    public IShellExtInit,
    public IContextMenu3 {
    BEGIN_COM_MAP(CSimpleShlExt)
        COM_INTERFACE_ENTRY(IShellExtInit)
        COM_INTERFACE_ENTRY(IContextMenu) // 새로 추가
        COM_INTERFACE_ENTRY(IContextMenu2) // 새로 추가
        COM_INTERFACE_ENTRY(IContextMenu3) // 새로 추가
    END_COM_MAP()
    
    // 이 이하로 전부 새로 추가
    public:
        // IContextMenu
        STDMETHODIMP QueryContextMenu(HMENU, UINT, UINT, UINT, UINT);
        STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO);
        STDMETHODIMP GetCommandString(UINT_PTR, UINT, UINT *, LPSTR, UINT);
        
        // IContextMenu2
        STDMETHODIMP HandleMenuMsg(UINT, WPARAM, LPARAM);
        
        // IContextMenu3
        STDMETHODIMP HandleMenuMsg2(UINT, WPARAM, LPARAM, LRESULT *);
        // ...

 

컨텍스트 메뉴 수정하기

이전과 같이 우리는 세 가지 IContextMenu 메소드에서 필요한 작업을 수행합니다. 그리고 우리는 QueryContextMenu 메소드에서 컨텍스트 메뉴에 항목들을 새로 추가합니다. 먼저 우리는 쉘의 버전을 체크합니다. 버전이 4.71 이상일 경우 우리는 컨텍스트 메뉴 항목에 그림을 출력합니다. 그렇지 않다면 우리는 비트맵을 보여주는 메뉴 항목을 추가합니다. 후자의 경우 우리가 해야 할 것은 항목을 추가하는 것이 전부입니다.

먼저 쉘 버전을 체크해야 합니다. 쉘의 버전을 얻기 위하여 쉘이 내보내는 함수인 DllGetVersion을 호출합니다. 그런 이름의 함수를 쉘이 내보내고 있지 않다면, 그 쉘의 버전은 4.0입니다. 즉 쉘 버전 4.0에서는 DllGetVersion이라는 이름의 함수가 존재하지 않습니다.

 

STDMETHODIMP CBmpCtxMenuExt::QueryContextMenu(HMENU hmenu, UINT uIndex, UINT uidCmdFirst, UINT uidCmdLast, UINT uFlags) {
    // uFlags가 CMF_DEFAULTONLY를 포함하고 있으면 아무것도 작업해선 안 됩니다.
    if (uFlags & CMF_DEFAULTONLY)
        return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0);
    
    bool bUseOwnerDraw = false;
    HINSTANCE hinstShell;
    
    hinstShell = GetModuleHandle(_T("shell32"));
    
    if (hinstShell != NULL) {
        DLLGETVERSIONPROC pProc;
        
        pProc = (DLLGETVERSIONPROC)GetProcAddress(hinstShell, "DllGetVersion");
        
        if (pProc != NULL) {
            DLLVERSIONINFO rInfo = { sizeof(DLLVERSIONINFO) };
            
            if (SUCCEEDED(pProc(&rInfo))) {
                if ((rInfo.dwMajorVersion > 4) || (rInfo.dwMajorVersion == 4 && rInfo.dwMinorVersion >= 71))
                    bUseOwnerDraw = true;
                }
            }
        }
        
        // ...

 

여기서 bUseOwnerDraw는 그림을 출력할 수 있는 메뉴 항목을 사용할 수 있는지를 나타냅니다. 이 값이 참이 되면, 우리는 그림을 출력할 수 있는 메뉴 항목을 추가할 것이고(mii.fType을 설정하는 줄을 참고하기 바랍니다). 값이 거짓이면 비트맵 항목을 추가한 다음 컨텍스트 메뉴에게 사용자에게 보여줄 비트맵에 대한 핸들을 알려줍니다.

 

    // ...
    MENUITEMINFO mii = {0};
    
    mii.cbSize = sizeof(MENUITEMINFO);
    mii.fMask  = MIIM_ID | MIIM_TYPE;
    mii.fType  = bUseOwnerDraw ? MFT_OWNERDRAW : MFT_BITMAP;
    mii.wID    = uidCmdFirst;
    
    if (!bUseOwnerDraw) {
        // 주의: 이 구문은 컨텍스트 메뉴에 원래 크기의 비트맵 이미지를 넣습니다.
        // 이러한 작동은 우리가 예상하던 그 작동이 아닙니다.
        // 비트맵 이미지의 크기를 알맞게 축소하는 것은 여러분의 과제로 남겨놓겠습니다.
        mii.dwTypeData = (LPTSTR)m_bmp.GetSafeHandle();
    }
    
    InsertMenuItem(hmenu, uIndex, TRUE, &mii);
    
    // WM_MEASUREITEM이나 WM_DRAWITEM 이벤트가 전달될 경우 확인할 수 있도록 메뉴 항목의 ID를 보관해 둡니다.
    m_uOurItemID = uidCmdFirst;
    
    // 최상위 단계의 메뉴 항목을 1개 추가했으므로 이를 쉘에게 알립니다.
    return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 1);
}

 

우리는 컨텍스트 메뉴에 새로운 항목을 추가하면서 m_uOurItemID 멤버변수에 그 ID를 추가했습니다. 이는 나중에 메시지가 전달될 때 ID를 식별하기 위하여 사용될 것입니다. 엄밀히 말하면 이 과정은 필요하지 않습니다, 왜냐하면 우리는 지금 하나의 메뉴 항목만을 만들었기 때문입니다. 그러나 실무적으로는 여러 개의 메뉴 항목을 만들 수도 있기 때문에 더 말할 것 없이 좋은 방식입니다.

 

상태 표시줄에 ‘플라이-바이 도움말(fly-by help)’을 보여주기

플라이 바이 도움말을 표시하는 것은 이전 단계에서 해 보았던 것과 다르지 않습니다. Windows 탐색기는 상태 표시줄에 출력할 문자열을 얻어가기 위해 GetCommandString을 호출할 것입니다.

#include <atlconv.h>  // ATL 문자열 변환 매크로를 사용하기 위하여 필요한 헤더 파일
  
STDMETHODIMP CBmpCtxMenuExt::GetCommandString(UINT uCmd, UINT uFlags, UINT* puReserved, LPSTR pszName, UINT cchMax) {
    USES_CONVERSION;
    static LPCTSTR szHelpString = _T("Select this thumbnail to view the entire picture.");
    
    // idCmd를 검사합니다.
    // 우리는 하나의 메뉴 항목만을 추가했기 때문에 반드시 0 값만이 허용됩니다.
    if (uCmd != 0)
        return E_INVALIDARG;
    
    // Windows 탐색기가 문자열을 요구할 때,
    // 미리 준비된 문자열을 Windows 탐색기가 제공한 버퍼에 복사합니다.
    if (uFlags & GCS_HELPTEXT) {
        if (uFlags & GCS_UNICODE) {
            // pszName을 유니코드 문자열로 변환해야 할 때,
            // 문자열 복사 함수도 유니코드 버전을 씁니다.
            lstrcpynW((LPWSTR)pszName, T2CW(szHelpString), cchMax);
        } else { 
            // 유니코드를 요하지 않을 경우 ANSI 버전으로 복사합니다.
            lstrcpynA(pszName, T2CA(szHelpString), cchMax);
        }
    }
    
    return S_OK;
}

 

사용자 선택에 따라 작업 수행하기

IContextMenu와 관련해서 마지막으로 구현할 메소드는 InvokeCommand입니다. 이 메소드는 우리가 새로 추가한 컨텍스트 메뉴 항목을 사용자가 클릭했을 때 호출됩니다. 여기에서는 사용자가 클릭했을 때 .bmp 파일에 연결된 프로그램을 띄워서 해당 비트맵 이미지를 열게 하기 위하여 ShellExecute를 호출할 것입니다.

 

STDMETHODIMP CBmpCtxMenuExt::InvokeCommand(LPCMINVOKECOMMANDINFO pInfo) {
    // lpVerb가 실제 존재하는 문자열을 가리키고 있을 경우
    // 이 메소드의 호출을 무시하고 끝냅니다.
    if (HIWORD(pInfo->lpVerb) != 0)
        return E_INVALIDARG;
    
    // 하나의 항목만 새로 추가했으므로 Command ID는 0밖에 없습니다.
    if (LOWORD(pInfo->lpVerb) != 0)
        return E_INVALIDARG;
    
    // 기본 연결 프로그램을 띄워서 비트맵 파일을 엽니다.
    int nRet;
    
    nRet = (int)ShellExecute(pInfo->hwnd, _T("open"), m_szFile, NULL, NULL, SW_SHOWNORMAL);
    
    return (nRet > 32) ? S_OK : E_FAIL;
}

 

계속 읽기

이전 게시글: Windows 쉘 익스텐션 개발 가이드 - (6) 보내기 메뉴

다음 게시글: Windows 쉘 익스텐션 개발 가이드 - (7) 비트맵 및 폴더 메뉴 (2/3)

 

'API/COM' 카테고리의 다른 글
더 보기...
태그 : 
댓글