^(코딩캣)^ = @"코딩"하는 고양이;
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (9) 아이콘 [完]
입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차..
API/COM
2021. 2. 10. 12:30

Windows 쉘 익스텐션 개발 가이드 - (9) 아이콘 [完]

API/COM
2021. 2. 10. 12:30

입문자를 위한 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)

 

9 단계. 특정 형식의 파일에 대해 아이콘을 사용자화 하는 쉘 익스텐션(Shell Extension)

실습 프로젝트 다운로드

ShellExtGuide9_demo.zip
34.1 kB

 

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

9 단계까지 잘 오셨습니다! 본 단계도 독자 여러분께서 요청하셨던 사항이었고, 특정 형식의 파일(여기에서는 텍스트 파일) 각각에 대해 사용자화된 아이콘을 보여주는 방법에 대해 다루어 보겠습니다. 첨부된 예제 프로젝트는 모든 버전의 Windows에서 실행될 것입니다.

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

Windows 탐색기에서 각각의 유형의 파일들은 특정한 아이콘으로 표현됨을 모두들 아실 것입니다. 비트맵 이미지는 페인트 브러시 아이콘으로 표시되고, HTML 페이지는 인터넷 익스플로러 아이콘이 그려진 종이 모양의 아이콘으로 표시됩니다. Windows 탐색기는 파일에 대해 사용자에게 어떤 아이콘으로 보여줄 것인지 여부를 레지스트리를 찾아가며 결정합니다. 그리고 파일 유형에 따라 그러한 아이콘 정보를 가지고 있는 레지스트리 키는 HKEY_CLASSES_ROOT의 하위 키입니다. 이 방식은 특정한 형식의 모든 파일이 전부 같은 모양의 아이콘으로 보여지는 결과를 갖습니다.

그러나 아이콘을 지정하는 방식이 이것뿐만은 아닙니다. 아이콘 핸들러(icon handler)라는 쉘 익스텐션을 작성함으로써 Windows 탐색기는 우리가 각각의 파일마다 아이콘을 다르게 사용자화할 수 있게 만들어 줍니다. 사실, 파일 별로 아이콘을 다르게 나타나게 하는 예는 이미 Windows 운영체제 자체에 내장되어 있습니다. Windows 디렉토리 또는 .exe파일들이 많은 디렉토리를 Windows 탐색기로 열어 보시기 바랍니다. 여러분은 각각의 .exe 파일이 자신만의 아이콘으로 표시되고 있음을 알 수 있습니다. 단 이 경우 .exe 파일에 자체 아이콘 리소스를 가지고 있는 경우에 한하며, 그렇지 않은 .exe 파일들은 일반적은 아이콘으로 나타납니다. .ico.cur 등의 파일도 각 파일마다 서로 다르게 아이콘으로 나타납니다.

이 단계에서 다룰 예제 프로젝트는 텍스트 파일에 대해 사이즈별로 각기 다른 4개의 아이콘을 보여주는 기능을 합니다. 화면에 표시된 아이콘은 다음과 같습니다.

 

쉘 익스텐션의 실행 결과.

 

확장 인터페이스

여러분은 프로젝트를 생성하는 과정에 이미 익숙해져 있을 것입니다. 그러므로 필자는 Visual C++ 마법사 단계에 대해서는 생략하겠습니다. ATL COM 프로젝트를 생성하는 데, 그 이름은 TxtFileIcons로 정하고, C++ 구현 클래스의 이름은 CTxtIconShlExt로 정합니다.

아이콘 핸들러는 두 개의 인터페이스를 구현하고 있습니다. 하나는 IPersistFile이고 다른 하나는 IExtractIcon입니다. IShellExtInit는 한 번에 여러 파일이 선택된 상태에서 쉘 익스텐션을 초기화하는 반면, IPersistFile은 한 번에 하나의 파일에 대해서만 쉘 익스텐션을 초기화함을 기억하시기 바랍니다.

IExtractIcon은 두 개의 메소드를 가지고 있는데 둘 다 Windows 탐색기에게 특정 파일에 대해 어떤 아이콘을 사용할 것인지에 대한 내용을 전달해주는 역할을 합니다.

Windows 탐색기는 보여지는 파일들 모두에 대해 COM 객체를 생성함을 기억하시기 바랍니다. 이것은 매 파일마다 C++ 클래스의 인스턴스가 생성됨을 의미합니다. 그러므로 Windows 탐색기가 버벅거리는 것을 피하기 위하여, 이번 쉘 익스텐션에서 여러분은 시간을 소비하는 작업을 피하셔야 합니다.

 

초기화 인터페이스

COM 객체에 IPersistFile 인터페이스를 추가하기 위하여 TxtIconShlExt.h를 열고 아래 표시한 내용과 같이 수정합니다.

 

#include <comdef.h>
#include <shlobj.h>
#include <atlconv.h>
 
class CTxtIconShlExt :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CTxtIconShlExt, &CLSID_TxtIconShlExt>,
    public IPersistFile { // 새로 추가
    BEGIN_COM_MAP(CTxtIconShlExt)
        COM_INTERFACE_ENTRY(IPersistFile) // 새로 추가
    END_COM_MAP()
    
    // 이 이하는 전부 새로 추가
    
    public:
        // IPersistFile
        STDMETHOD(GetClassID)(CLSID *)       { return E_NOTIMPL; }
        STDMETHOD(IsDirty)()                  { return E_NOTIMPL; }
        STDMETHOD(Save)(LPCOLESTR, BOOL)    { return E_NOTIMPL; }
        STDMETHOD(SaveCompleted)(LPCOLESTR) { return E_NOTIMPL; }
        STDMETHOD(GetCurFile)(LPOLESTR *)    { return E_NOTIMPL; }
        STDMETHOD(Load)(LPCOLESTR wszFile, DWORD /*dwMode*/) { 
            USES_CONVERSION;
            lstrcpyn(m_szFilename, OLE2CT(wszFile), MAX_PATH);
            return S_OK;
        }
    
    protected:
        TCHAR     m_szFilename[MAX_PATH]; // 요청 받은 파일에 대한 전체 경로
        DWORDLONG m_qwFileSize; // 파일의 크기
};

 

IPersistFile을 사용하는 다른 쉘 익스텐션과 마찬가지로 구현을 필요로 하는 단 하나의 메소드는 Load 뿐입니다. 왜냐하면 이 메소드를 통해 우리가 어떤 파일에 대해 작업해야 하는 지를 Windows 탐색기가 알려줄 것이기 때문입니다. Load의 구현은 인라인(inline)으로 작성되었습니다. 나중에 사용하기 위해 현재 파일의 이름을 m_szFilename 멤버 변수로 복사만 합니다.

 

IExtractIcon 인터페이스

아이콘 핸들러는 Windows 탐색기가 파일에 대한 아이콘을 필요로 할 때 호출할 메소드가 담긴 IExtractIcon 인터페이스를 구현합니다. 우리가 만들 쉘 익스텐션은 텍스트 파일에 대한 것이기 때문에, Windows 탐색기 창 또는 시작 메뉴에서 텍스트 파일이 보여지는 매 순간마다 Windows 탐색기는 우리가 구현할 IExtractIcon을 호출할 것입니다. COM 개체에 IExtractIcon을 추가하기 위하여 TxtIconShlExt.h를 열고 다음과 같이 내용을 추가합니다.

 

class CTxtIconShlExt :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CTxtIconShlExt, &CLSID_TxtIconShlExt>,
    public IPersistFile,
    public IExtractIcon { // 새로 추가
    BEGIN_COM_MAP(CTxtIconShlExt)
        COM_INTERFACE_ENTRY(IPersistFile)
        COM_INTERFACE_ENTRY(IExtractIcon) // 새로 추가
    END_COM_MAP()
    
    public:
        // IExtractIcon
        STDMETHODIMP GetIconLocation(UINT uFlags, LPTSTR szIconFile, UINT cchMax, int * piIndex, UINT * pwFlags);
        STDMETHODIMP Extract(LPCTSTR pszFile, UINT nIconIndex, HICON * phiconLarge, HICON * phiconSmall, UINT nIconSize);
};

 

Windows 탐색기에게 아이콘을 반환하는 방법에는 두 가지가 있습니다.

먼저 GetIconLocation 메소드는 아이콘을 포함하고 있는 파일의 이름 및 그 인덱스 쌍을 반환할 수 있고 이 때 인덱스는 해당 파일 내에서 아이콘을 특정하기 위해 0부터 시작하는 정수입니다. 예를 들어 C:\Windows\system32\shell32.dll/9와 같은 문자열이 반환될 수 있는데 이는 Windows 탐색기에게 shell32.dll 파일의 9번째 아이콘(0부터 셌을 때 9번째)을 알려줍니다. 다만 이와 같은 문자열은 해당 아이콘의 리소스 아이디가 9번이라는 뜻을 의미하는 것이 아닙니다. 리소스 아이디 순서(작은 값에서 큰 값 순으로)대로 볼 때 9번째 항목이라는 뜻입니다.

Extract 메소드는 아무 작업도 하지 않지만, S_FALSE를 반환하여 Windows 탐색기가 직접 아이콘을 추출하라고 알리게 됩니다.

특별한 사항이 있다면, GetIconLocation이 값을 반환한 뒤에 Windows 탐색기는 Extract 메소드를 호출할 수도 있고 그렇지 않을 수도 있다는 것입니다. Windows 탐색기는 최근에 사용한 아이콘들을 유지하고 있는 ‘아이콘 캐시(icon cache)’를 가지고 있습니다. GetIconLocation이 최근에 사용된 파일 이름 및 인덱스 쌍을 반환하고 아이콘 캐시에 이에 해당하는 아이콘이 있다면, Windows 탐색기는 Extract를 호출하지 않고 캐시된 아이콘을 사용합니다.

이 메소드는 또한 “캐시를 찾지 말 것”을 의미하는 플래그(flag)를 반환할 수도 있습니다. 이 경우 Windows 탐색기는 항상 Extract 메소드를 호출하게 되는데, 이렇게 하면 Extract는 Windows 탐색기가 보여줄 아이콘에 대한 핸들을 반환하기 위해 직접 아이콘을 로드(load)해야 합니다.

 

첫 번째 아이콘 추출 방법

처음으로 호출되는 IExtractIcon 메소드는 GetIconLocation입니다.

이 메소드는 IPersistFile::Load에서 보관한 파일 이름을 사용하여, 파일을 직접 읽은 후 앞서 설명한 파일 이름 및 인덱스 쌍을 반환합니다. GetIconLocation의 원형은 다음과 같습니다.

 

HRESULT IExtractIcon::GetIconLocation(UINT uFlags, LPTSTR szIconFile, UINT cchMax, int * piIndex, UINT * pwFlags);

 

각 매개 변수(parameter)의 의미는 다음과 같습니다.

 

uFlags

쉘 익스텐션의 작동을 바꿀 수 있는 옵션들입니다. GIL_ASYNC는 아이콘 추출 작업이 오래 걸릴 것인지 물어보기 위해 전달되는 플래그(flag)로, 만일 오래 걸린다면 쉘 익스텐션은 Windows 탐색기에게 아이콘 추출 작업을 백 그라운드에서 수행해 줄 것을 요청할 수 있습니다. 이렇게 하면 Windows 탐색기 창 자체가 버벅거리는 일이 없습니다.

다른 플래그(flag)인 GIL_FORSHELLGIL_OPENICON은 네임스페이스 확장에서 의미 있습니다. 우리가 작업할 프로젝트에서는 실행 시간이 오래 걸리는 코드가 없으므로, 이러한 플래그(flag)들을 고려하지 않아도 됩니다.

szIconFile, cchMax

szIconFile은 쉘(shell)이 제공하는 버퍼로서 우리는 사용하고자 하는 아이콘이 포함된 파일의 이름을 이 곳에 보관하면 됩니다. cchMax는 쉘(shell)이 제공한 버퍼의 크기로서, 단위는 문자 수 입니다.

piIndex

우리가 사용하고자 하는 아이콘이 szIconFile에서 몇 번째 인덱스에 해당하는지를 지정할 정수에 대한 포인터입니다(번역자 주: 이것도 쉘(shell)이 제공하는 변수를 가리키는 유효한 포인터입니다).

pwFlags

쉘(shell)이 제공한 UINT형 변수를 가리키는 유효한 포인터로서, 반환하는 플래그(flag)에 따라 Windows 탐색기의 작동을 우리가 변경할 수 있습니다. 이 곳에 들어갈 수 있는 플래그의 종류에 대하여는 잠시 후 설명하겠습니다.

GetIconLocation 메소드는 szIconFilepiIndex 매개 변수(parameter)에 내용을 채운 다음 S_OK를 반환합니다. 우리가 커스텀 아이콘을 제공하지 않기로 결정하였다면 S_FALSE를 반환합니다. 이 때 Windows 탐색기는 일반적인 아이콘(알 수 없는 파일)으로 표시해 줄 것입니다.

pwFlags를 통해 반환할 수 있는 플래그(flag)들에는 다음과 같은 종류가 있습니다.

GIL_DONTCACHE

Windows 탐색기가 szIconFile/piIndex로 지정된 파일이 최근에 사용되었는지 여부를 확인하기 위해 캐시를 검색하는 것을 못하게 합니다. 그 결과 IExtractIcon::Extract 메소드가 항상 호출됩니다. ‘두 번째 아이콘 추출 방법’에서 이 플래그에 대해 많은 이야기를 하겠습니다.

GIL_NOTFILENAME

MSDN에 따르면 GetIconLocation이 값을 반환할 때 이 플래그가 있으면 Windows 탐색기가 szIconFile/piIndex를 무시한다고 합니다. 명백하기도 이 플래그(flag)는 쉘 익스텐션이 어떻게 Windows 탐색기로 하여금 IExtractIcon::Extract를 호출하게 만드는지를 보여주는 플래그임이 확실한데, 실제로는 GetIconLocation이 값을 반환할 때 이 플래그는 Windows 탐색기의 작동에 아무 영향을 미치지 못합니다. 이와 관련해서는 나중에 설명하겠습니다.

GIL_SIMULATEDOC

이 플래그(flag)는 Windows 탐색기가 쉘 익스텐션에서 반환된 아이콘을 가져가서, “한 쪽 귀퉁이가 접힌 종이” 모양의 아이콘과 합친 다음에, 그 합성된 아이콘을 해당 파일에 적용해서 보여주라는 의미를 갖습니다. 필자는 잠시 후 이 플래그의 결과를 보여드리겠습니다.

 

첫 번째 아이콘 추출 방법에서, 우리가 작성할 쉘 익스텐션의 GetIconLocation 함수가 텍스트 파일의 크기를 계산한 다음, 그 크기에 따라 0, 1, 2, 3의 인덱스를 반환할 것입니다. 이러한 방법은 한 가지 결함을 가지고 있습니다. 여러분은 각 아이콘들의 리소스 아이디들을 추적하고 이들 리소스가 올바른 순서로 놓여 있는지를 확인해야 합니다. 우리의 쉘 익스텐션은 4개의 아이콘만 다루기 때문에 ‘회계장부정리’와 같은 이러한 작업이 어렵지는 않습니다. 그러나 여러분이 더 많은 수의 아이콘을 다루거나, 프로젝트에서 아이콘을 추가 또는 제거한 경우라면 리소스 아이디를 주의하여 관리해야 합니다.

우리가 구현할 GetIconLocation 메소드는 다음과 같습니다. 먼저 우리는 파일을 열고 그 크기를 측정합니다. 이 과정에서 오류가 발생하면 Windows 탐색기가 기본 아이콘을 적용할 수 있도록 S_FALSE를 반환합니다.

 

STDMETHODIMP CTxtIconShlExt::GetIconLocation(UINT uFlags, LPTSTR szIconFile, UINT cchMax, int * piIndex, UINT * pwFlags) {
    DWORD     dwFileSizeLo, dwFileSizeHi;
    DWORDLONG qwSize;
    HANDLE    hFile;
    
    hFile = CreateFile(m_szFilename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    
    if (hFile == INVALID_HANDLE_VALUE)
        return S_FALSE;    // 쉘에게 기본 아이콘을 사용하도록 알림
    
    dwFileSizeLo = GetFileSize(hFile, &dwFileSizeHi);
    
    CloseHandle(hFile);
    
    if (dwFileSizeLo == (DWORD)-1 &&  GetLastError() != NO_ERROR)
        return S_FALSE;    // 쉘에게 기본 아이콘을 사용하도록 알림
    
    qwSize = (DWORDLONG(dwFileSizeHi) << 32) | dwFileSizeLo;
    
    // ...

 

다음으로 우리가 사용할 아이콘들은 DLL 파일에 포함되어 있으므로, 이 DLL 파일의 경로를 가져와야 합니다. DLL 파일의 경로는 szIconFile 버퍼에 복사됩니다.

 

    // ...
    
    TCHAR szModulePath[MAX_PATH];
    
    GetModuleFileName(_Module.GetResourceInstance(), szModulePath, MAX_PATH);
    
    lstrcpyn(szIconFile, szModulePath, cchMax);
    
    // ...

 

다음으로 우리는 파일의 크기를 확인하고 그 크기에 맞추어 piIndex가 가리키는 위치의 값을 적절한 인덱스로 설정합니다(사용되는 아이콘들에 대해서는 본 게시글의 맨 윗부분을 참고 바랍니다).

 

    // ...
    
    if (qwSize == 0)
        *piIndex = 0;
    else if (qwSize < 4096)
        *piIndex = 1;
    else if (qwSize < 8192)
        *piIndex = 2;
    else
        *piIndex = 3;
    
    // ...

 

다음으로 우리는 Windows 탐색기로부터 기본 작동을 얻기 위하여 pwFlags0으로 설정합니다. 즉 Windows 탐색기는 szIconFile/piIndex로 특정된 아이콘의 캐시 여부를 아이콘 캐시를 통해 확인하라는 의미입니다. 이와 같이 설정하면, IExtractIcon::Extract 메소드는 호출되지 않을 것입니다. 그리고 나서 우리는 S_OK를 반환하여 GetIconLocation 메소드가 성공했음을 알립니다.

 

    // ...
    
    *pwFlags = 0;
    
    return S_OK;
}

 

우리는 Windows 탐색기에게 아이콘을 어디에서 찾을 수 있는지 말했기 때문에, 우리가 직접 구현하는 Extract 메소드는 항상 S_FALSE를 반환할 것입니다. 이는 Windows 탐색기에게 아이콘을 직접 추출하라는 의미입니다. 다음 절에서 필자는 Extract의 매개 변수(parameter)에 대해 설명하겠습니다.

 

STDMETHODIMP CTxtIconShlExt::Extract(LPCTSTR pszFile, UINT nIconIndex, HICON * phiconLarge, HICON * phiconSmall, UINT nIconSize) {
    return S_FALSE;    // 쉘에게 아이콘 추출을 직접 하라고 알립니다.
}

 

본 쉘 익스텐션의 작동 결과는 다음과 같습니다. (번역자 주: 아이콘이 바뀌지 않는다면, 작업 관리자에서 explorer.exe 프로세스만 강제 종료한 뒤, 다시 실행해 봅니다.)

'아이콘' 보기(1).

 

'자세히' 보기(2).

 

'큰 아이콘' 보기(3).

 

여러분이 GetIconLocation 메소드에서 pwFlags 위치가 가리키는 값을 GIL_SIMULATEDOC으로 설정하면, 아이콘은 다음과 같이 보여질 것입니다.

'아이콘' 보기(2).

 

'자세히' 보기(2).

 

'큰 아이콘' 보기(2).

 

아이콘 보기와 큰 아이콘 보기 화면에 주목하시기 바랍니다. 우리가 제공하는 ‘작은’ 크기(16 * 16 버전)의 아이콘이 사용되었습니다. 작은 아이콘 보기에서, Windows 탐색기는 원래 작은 아이콘을 더욱 작게 축소시키고 있는데 이는 별로 예뻐 보이지 않습니다.

 

두 번째 아이콘 추출 방법

두 번째 방법은 우리의 쉘 익스텐션이 스스로 아이콘을 추출하여 Windows 탐색기의 아이콘 캐시를 우회하는 것을 포함하고 있습니다.

IExtraction::Extract 메소드는 항상 호출됩니다. 그리고 이 메소드는 아이콘들을 로드해서 큰 아이콘과 작은 아이콘이라는 두 개의 HICON을 Windows 탐색기에게 반환해야 합니다. 이 방법의 이점이 있다면 리소스 아이디를 순서에 맞추어 유지하는 것을 염려하지 않아도 된다는 것입니다. 다만 쉘 익스텐션이 Windows 탐색기의 아이콘 캐시를 우회하기 때문에 많은 수의 텍스트 파일이 들어있는 디렉토리를 탐색할 때 그 탐색 속도가 느려질 수 있습니다.

GetIconLocation은 첫 번째 방법과 유사합니다. 그러나 파일의 크기를 가져오기만 하면 되기 때문에 이 메소드에서 해야 할 일이 약간 줄어들었습니다.

 

STDMETHODIMP CTxtIconShlExt::GetIconLocation(UINT uFlags, LPTSTR szIconFile, UINT cchMax, int * piIndex, UINT * pwFlags) {
    DWORD  dwFileSizeLo, dwFileSizeHi;
    HANDLE hFile;
    
    hFile = CreateFile(m_szFilename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    
    if (hFile == INVALID_HANDLE_VALUE)
        return S_FALSE;    // 쉘에게 기본 아이콘을 사용할 것을 말합니다.
    
    dwFileSizeLo = GetFileSize(hFile, &dwFileSizeHi);
    
    CloseHandle(hFile);
    
    if (dwFileSizeLo == (DWORD)-1  &&  GetLastError() != NO_ERROR)
        return S_FALSE;    // 쉘에게 기본 아이콘을 사용할 것을 말합니다.
    
    m_qwFileSize = ((DWORDLONG)dwFileSizeHi) << 32 | dwFileSizeLo;
    // ...

 

일단 파일의 크기가 보관되었으므로 Windows 탐색기에게 아이콘 캐시를 확인하지 말 것을 알리기 위해 pwFlags가 가리키는 위치의 값을 GIL_DONTCARE로 변경합니다.

우리는 szIconFile/piIndex을 통해 아이콘을 특정하지 않을 것이므로 Windows 탐색기가 szIconFile/piIndex를 무시할 수 있도록, 이 플래그를 반드시 설정해야만 합니다.

비록 필자가 테스트하고 있는 현재 버전의 쉘(shell)에서는 별 효과가 없다고 하더라도 GIL_NOTFILENAME 플래그도 포함되어 있습니다. 이 플래그의 문서화된 목적은 Windows 탐색기에게 우리가 szIconFile/piIndex의 내용을 채우지 않을 것임을 알리는 것인데, 이 플래그를 전달하는 것 자체는 의미가 없으므로(달리 말하면 어차피 우리는 Windows 탐색기에게 추출해갈 대상을 전해주지 않으므로), Windows 탐색기는 내부적으로 이 플래그의 존재 여부를 평가조차 하지 않는 듯 합니다. 뭐 어쨌든 차기 버전의 쉘(shell)에서는 GIL_NOTFILENAME 여부를 평가할 수도 있으므로 이 플래그를 명시적으로 넣어주는 것은 좋은 생각입니다.

 

    *pwFlags = GIL_NOTFILENAME | GIL_DONTCACHE;
    return S_OK;
}

 

이제 Extract 메소드를 좀 더 깊게 들여다 볼 차례입니다. 원형은 다음과 같습니다.

 

HRESULT IExtractIcon::Extract(LPCTSTR pszFile, UINT nIconIndex, HICON * phiconLarge, HICON* phiconSmall, UINT nIconSize);

 

매개 변수(parameter)는 다음과 같습니다.

pszFile/nIconIndex

아이콘이 담긴 파일과 그 내부의 인덱스 번호입니다. 이 두 값은 GetIconLocation에서 반환된 값과 같습니다.

phiconLarge, phiconSmall

작은 아이콘과 큰 아이콘을 사용하기 위해 Extract 메소드가 설정해야 하는 각각의 HICON입니다. 이들 포인터는 NULL인 채로 전달될 수 있습니다.

nIconSize

아이콘의 예상 크기입니다. 상위 워드(HIWORD)는 작은 아이콘의 디멘션(dimension, 어차피 같을 것이므로 가로 폭과 세로 높이를 아우르는 값)이고, 하위 워드(LOWORD)는 큰 아이콘의 디멘션입니다. 일반적인 조건에서 작은 아이콘은 16이고, 큰 아이콘은 32 또는 48인데 Windows 탐색기의 보기 모드가 무엇에 선택되어 있는지에 달려 있습니다. 큰 아이콘 보기는 32이고, 타일 보기 모드에서는 48입니다.

 

우리의 쉘 익스텐션에서는 GetIconLocation 메소드에서 파일이름 및 인덱스 쌍을 채워 넣지 않았으므로 pszFilenIconIndex를 무시할 수 있습니다. 우리는 파일 크기에 따른 큰 아이콘과 작은 아이콘을 로드(load)하여 Windows 탐색기에게 반환하면 됩니다.

 

STDMETHODIMP CTxtIconShlExt::Extract(LPCTSTR pszFile, UINT nIconIndex, HICON * phiconLarge, HICON * phiconSmall, UINT nIconSize) {
    UINT uIconID;
    
    // 파일 크기에 따라 어떤 아이콘을 사용할 것인지를 결정합니다.
    if (m_qwFileSize == 0)
        uIconID = IDI_ZERO_BYTES;
    else if (m_qwFileSize < 4096)
        uIconID = IDI_UNDER_4K;
    else if (m_qwFileSize < 8192)
        uIconID = IDI_UNDER_8K;
    else 
        uIconID = IDI_OVER_8K;
    
    // 아이콘을 로드합니다!
    if (phiconLarge != NULL) {
        *phiconLarge = (HICON)LoadImage(_Module.GetResourceInstance(), MAKEINTRESOURCE(uIconID), IMAGE_ICON, wLargeIconSize, wLargeIconSize, LR_DEFAULTCOLOR);
    }
     
    if (phiconSmall != NULL) {
        *phiconSmall = (HICON)LoadImage(_Module.GetResourceInstance(), MAKEINTRESOURCE(uIconID), IMAGE_ICON,
wSmallIconSize, wSmallIconSize, LR_DEFAULTCOLOR);
    }
      
    return S_OK;
}

 

이제 됐습니다! Windows 탐색기는 우리가 반환하는 아이콘을 보여주게 될 것입니다. 한 가지 주목할 것은, 두 번째 방법을 사용하면서 GetIconLocation 메소드에 GIL_SIMULATEDOC 플래그를 사용하는 것은 아무 효과가 없다는 것입니다.

 

쉘 익스텐션을 등록하기

아이콘 핸들러는 해당 유형의 파일을 다루는 레지스트리 키의 하위키로 등록됩니다. 우리의 경우 텍스트 파일을 다루고 있으므로 HKCR\txtfile의 하위키로 등록됩니다. 다른 쉘 익스텐션에서 보았듯이 txtfile 레지스트리 키에는 ShellEx 하위키가 있습니다. 여기에 다시 IconHandler 하위키가 놓입니다. 이 레지스트리 키의 기본 값은 우리가 작성한 쉘 익스텐션의 GUID입니다. 드롭 핸들러와 마찬가지로 특정한 한 형식의 파일에는 하나의 아이콘 핸들러만이 올 수 있습니다. IconHandler의 하위 키로 놓는 대신에 우리는 또한 DefaultIcon 레지스트리 키의 기본 값을 %1로 바꾸어 우리가 작성한 아이콘 핸들러가 호출되도록 만들 수 있습니다.

우리가 작성한 쉘 익스텐션을 등록하는 RGS 스크립트는 다음과 같습니다.

 

HKCR {
     NoRemove txtfile {
          NoRemove DefaultIcon = s '%%1'
          NoRemove ShellEx {
               ForceRemove IconHandler = s '{DF4F5AE4-E795-4C12-BC26-7726C27F71AE}'
          }
     }
}

 

%1이라는 문자열을 지정하기 위해, 우리는 RGS 스크립트에서 %%1이라 적어야 함을 주목하시기 바랍니다. 왜냐하면 %는 대체 가능한 매개 변수(parameter)를 지시하는 데 사용되는 특수문자이기 때문입니다(예: %MODULE%).

DefaultIcon 값을 덮어쓰기하고 있는 사실은 중요한 문제를 야기합니다. 우리가 만들 쉘 익스텐션이 제거될 때 어떻게 하면 예전의 DefaultIcon 값을 되돌릴 수 있을까요? 답은, DllRegisterServer에서 DefaultIcon 기존의 값을 보관하고 DllUnregisterServer에서 이 값을 복원하는 것입니다.

우리는 “반드시” 이 작업을 수행하여 깨끗하게 제거되고 텍스트 파일 아이콘이 우리의 쉘 익스텐션이 설치되기 전으로 되돌려 놓아야 합니다.

등록과 등록 해제가 어떻게 이루어지는지는 첨부된 프로젝트의 소스 코드를 확인하시기 바랍니다. RGS 스크립트가 처리되기 위해 ATL이 호출되기 전에 백업이 수행되고 있음을 주목합니다. 반대로 작업을 수행(RGS 스트립트를 처리한 이후에 백업)한다면 우리가 백업할 기회를 잡기도 전에 DefaultIcon은 덮어쓰기 될 것이기 때문입니다.

 

원 저자의 저작권

(C) 2000-2006 Michael Dunn. 본 시리즈는 저작권을 갖는 저작물입니다. 필자는 이 글이 인터넷 상에서 퍼지는 것을 막을 수는 없음을 알고 있습니다. 어쨌든 필자는 말할 필요가 있습니다. 여러분이 이 글을 번역하고자 한다면, 필자에게 이메일을 보내어 알려 주시기 바랍니다. 누군가 번역하는 것을 막지는 않겠습니다만, 필자는 본 글에 링크를 게시하기 위해 번역되었음을 알고자 합니다.

본 글에 첨부된 예제 소스 코드는 공공 도메인(public domain)을 동반합니다. 필자는 이와 같이 함으로써 모두에게 소스 코드가 유용하게 쓰이길 바랍니다. (다만 본 글에 대해서는 공공 도메인을 원하지 않습니다. 왜냐하면 CodeProject에서만 열람 가능하도록 하는 것이 필자의 존재감과 CodeProject 사이트에 이익이 되기 때문입니다.) 여러분이 본 글에 첨부된 예제 프로그램을 여러분이 진행중인 프로젝트에 활용하고자 할 때 필자에게 이메일을 보내어 알려주신다면 매우 감사하겠습니다(이것은 단순히 필자의 소스 코드가 타인들에게 유용한지 그렇지 않은지를 알고 싶은 개인의 호기심 충족을 위해서입니다), 다만 이것은 필수 사항은 아닙니다. 여러분의 소스 코드에 필자에 대한 감사 표시를 해 주시면 좋습니다만, 이것도 필수 사항은 아닙니다.

 

시리즈의 끝

이전 게시글: Windows 쉘 익스텐션 개발 가이드 - (8) 자세히 모드 (2/2)

 

카테고리 “API/COM”
more...
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (8) 자세히 모드 (2/2)
입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차..
API/COM
2021. 2. 10. 12:25

Windows 쉘 익스텐션 개발 가이드 - (8) 자세히 모드 (2/2)

API/COM
2021. 2. 10. 12:25

입문자를 위한 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)

 

8 단계. Windows 탐색기에서 “자세히” 보기 모드를 선택할 때 나타나는 열 항목을 추가하는 쉘 익스텐션(Shell Extension)

이전 파트에 이어서...

 

참고사항: ID3 태그 다루기

쉘 익스텐션이 어떻게 ID3 태그 정보를 읽고 저장하는 것을 보여주기에는 지금이 적기일 것 같습니다. ID3v1 태그는 고정 길이 구조로서 MP3 파일의 끝 부분에 추가됩니다. 그리고 그 구조는 다음과 같이 생겼습니다.

 

struct CID3v1Tag {
    char szTag[3]; // 항상 'T','A','G'
    char szTitle[30];
    char szArtist[30];
    char szAlbum[30];
    char szYear[4];
    char szComment[30];
    char byGenre;
};

 

모든 필드들이 보통의 char형으로 되어 있고, 문자열은 반드시 NULL 문자로 끝나야 할 필요는 없습니다.

첫 번째 필드인 szTag는 ID3 태그임을 식별하기 위한 TAG라는 문자를 포함하고 있습니다.

byGenre는 곡의 장르를 식별하는 번호입니다. 장르별로 미리 정의된 번호에 대한 목록은 ID3.org에서 확인할 수 있습니다.

우리는 또한 ID3 태그 및 이 태그가 어느 파일에서 유래되었는지 그 파일의 이름을 포함하는 추가적인 구조체를 필요로 합니다. 이 구조체는 일종의 캐시(cache)로서 사용됩니다.

 

#include <string>
#include <list>

typedef std::basic_string<TCHAR> tstring;  // TCHAR형 문자열
 
struct CID3CacheEntry {
    tstring   sFilename;
    CID3v1Tag rTag;
};
 
typedef std::list<CID3CacheEntry> list_ID3Cache;

 

CID3CacheEntry 객체는 파일 이름과 그 파일이 가지고 있는 ID3 태그를 포함하고 있습니다. list_ID3Cache 전역 변수는 CID3CacheEntry 구조체들을 포함하는 링크드 리스트(linked list)입니다.

좋습니다. 쉘 익스텐션으로 돌아가서, 여기 GetItemData 함수의 시작 부분이 있습니다. 먼저 우리는 이 메소드가 우리가 추가한 해당 열에 의해 호출된 것인지를 확인하기 위해 SHCOLUMNID 구조체를 검사합니다.

 

#include <atlconv.h>
 
STDMETHODIMP CMP3ColExt::GetItemData(LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd, VARIANT * pvarData) {
    USES_CONVERSION;
    LPCTSTR   szFilename = OLE2CT(pscd->wszFile);
    char      szField[31];
    TCHAR     szDisplayStr[31];
    bool      bUsingBuiltinCol = false;
    CID3v1Tag rTag;
    bool      bCacheHit = false;
    
    // 포맷 아아디와 컬럼 아이디가 우리가 예상하고 있는 값인지 검사합니다.
    if (pscid->fmtid == CLSID_MP3ColExt) {
        if (pscid->pid > 2) return S_FALSE;
    }
    
    // ...

 

포맷 ID가 우리의 쉘 익스텐션이 가지고 있는 GUID일 때 프로퍼티 ID는 0, 1 또는 2여야만 합니다. 왜냐하면 이러한 ID는 GetColumnInfo에서 이미 사용했습니다. 그 외 여러 가지 이유로 프로퍼티 ID가 우리가 설정한 범위를 벗어난 채로 메소드가 호출되었다면 쉘(shell)에게 그러한 데이터가 없음을 알리기 위해 S_FALSE를 반환합니다. 그러면 해당 열은 비어있는 채로 보여질 것입니다.

다음으로 우리는 포맷 ID는 FMTID_SummaryInformation과 비교합니다. 그 다음 프로퍼티 ID를 체크하여 해당 프로퍼티 ID가 우리가 제공하고 있는 것과 같은지 확인합니다.

 

    // ...
    
    else if (pscid->fmtid == FMTID_SummaryInformation) {
        bUsingBuiltinCol = true;
        
        if (pscid->pid != 2 && pscid->pid != 4 && pscid->pid != 6)
            return S_FALSE;
    } else {
        return S_FALSE;
    }
    
    // ...

 

다음으로 우리는 파일의 특성을 확인합니다. 이 파일이 사실은 디렉토리이거나 파일의 현재 상태가 ‘오프라인(즉, 다른 저장 미디어로 옮겨진 상태)’이라면 메소드를 끝냅니다. 또한 우리는 파일의 확장명도 검사합니다. .mp3가 아니라면 메소드를 종료합니다.

 

    // ...
    
    // 파일이 아닌 디렉토리에 의해 호출되었다면, 즉시 메소드를 종료할 수 있습니다.
    // 또한 파일이 오프라인 상태일 때도 메소드르를 종료할 수 있습니다.
    if (pscd->dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_OFFLINE))
        return S_FALSE;
    
    // 파일 확장명을 검사합니다. mp3 파일이 아니라면 메소드를 종료합니다.
    if (wcsicmp(pscd->pwszExt, L".mp3"))
        return S_FALSE;
    
    // ...

 

여기까지 해서 우리는 내용을 읽어서 작업할 파일과 그렇지 않을 파일을 판별하였습니다. 앞서 선언한 ID3 태그 캐시는 이 때 사용할 것입니다. MSDN은 쉘(shell)이 파일 별로 GetItemData에 대한 호출을 그룹화한다고 하였습니다. 우리는 이 특성을 이용할 수 있고, 특정 파일에 대해 ID3 태그를 캐시(cache)할 수 있습니다. 그래서 우리는 연속된 함수 호출에 의해 각 파일들을 또 다시 읽어야 할 필요가 없습니다.

먼저 우리는 m_ID3Cache 멤버 변수로서 보관되는 캐시를 하나씩 순회하면서, 캐시된 파일 이름과 함수 호출로 전달된 파일 이름을 비교합니다. 캐시에서 해당 이름을 발견하였다면, 우리는 ID3 태그를 가져옵니다.

 

    // ...
    
    // 캐시에서 파일 이름을 찾습니다.
    list_ID3Cache::const_iterator it, itEnd;
    for (it = m_ID3Cache.begin(), itEnd = m_ID3Cache.end(); !bCacheHit && it != itEnd; it++) {
        if (lstrcmpi(szFilename, it->sFilename.c_str()) == 0) {
            CopyMemory(&rTag, &it->rTag, sizeof(CID3v1Tag));
            bCacheHit = true;
        }
    }
    
    // ...

 

루프 종료 후 bCacheHitfalse가 되면 우리는 해당 파일을 직접 읽어서 ID3 태그를 가지고 있는지 확인해야 합니다. 헬퍼(helper) 함수인 ReadTagFromFile은 파일로부터 이 128바이트를 읽기 위한 복잡한 작업들을 수행하고 성공하면 TRUE를, 그렇지 않으면 FALSE를 반환합니다.

알아둘 것은 ReadTagFromFile은 파일의 마지막 128바이트를 읽을 뿐 그것이 진짜 ID3 태그인지 여부와는 관계가 없습니다.

 

    // ...
    
    // 파일에 캐시가 없다면, 파일로부터 태그를 직접 읽습니다.
    if (!bCacheHit) {
        if (!ReadTagFromFile(szFilename, &rTag)) return S_FALSE;
        // ...

 

이제 우리는 ID3 태그를 가졌습니다. 우리는 캐시의 사이즈를 체크하고 캐시가 5개의 항목으로 꽉 찼다면, 새롭게 얻은 태그를 추가하기 위하여 가장 오래된 항목을 지웁니다. (5개라는 숫자는 필자가 임의로 설정한 최대치입니다.) 우리는 새로운 CID3CacheEntry 객체를 생성하고 링크드 리스트에 이를 추가합니다.

 

    // ...
    
    // 우리는 5개의 태그가 캐시되도록 유지할 것입니다. 
    // 현재 캐시가 4개 이상의 항목을 가지고 있다면,
    // 가장 오래된 항목 순으로 제거합니다.
    while (m_ID3Cache.size() > 4)
        m_ID3Cache.pop_back();
    
    // 새롭게 얻은 ID3 태그를 캐시에 추가합니다.
    CID3CacheEntry entry;
    
    entry.sFilename = szFilename;
    CopyMemory(&entry.rTag, &rTag, sizeof(CID3v1Tag));
    
    m_ID3Cache.push_front(entry);
}  // if(!bCacheHit)의 끝

// ...

 

다음 단계는 처음 세 바이트를 검사해서 ID3 태그의 시그니처인지 확인하여 ID3 태그가 실종하는지 검사합니다. 그렇지 않다면 우리는 즉시 메소드를 종료합니다.

 

    // ...
    
    // 시그니처를 검사하여 우리가 진짜로 ID3를 가지고 있는지 확인합니다.
    if (StrCmpNA(rTag.szTag, "TAG", 3))
        return S_FALSE;
    
    // ...

 

다음은 쉘(shell)이 요청한 프로퍼티의 종류에 따라 ID3 태그의 필드를 읽습니다. 여기에서는 프로퍼티 아이디를 검사만 할 것입니다. 여기 예제가 있습니다. 제목 필드에 대하여 이렇게 작성합니다.

 

    // ...
    
    // 문자열을 구성합니다.
    if (bUsingBuiltinCol) {
        switch (pscid->pid) {
        case 2:  // 곡의 제목
            CopyMemory(szField, rTag.szTitle, countof(rTag.szTitle));
            szField[30] = '\0';
            break;
        // ...
        }
    
    // ...

 

szField 버퍼는 최대 31 글자까지 수용 가능함을 확인하시기 바랍니다. 이것은 본래 ID3v1 태그보다 1글자 더 많은 용량이지만, 문자열을 항상 NULL 문자로 끝나도록 해야 하므로 확보된 공간입니다. bUsingBuiltinCol 옵션은 FMTID/PID 쌍을 검사할 때보다 먼저 설정되었습니다. PID 하나만으로는 열을 식별하기에 충분하지 않기 때문에 이 플래그를 사용합니다. 왜냐하면 제목과 MP3 장르 열은 둘 다 PID 2번이기 때문입니다.

이 때, szField는 ID3 태그에서 읽은 문자열을 포함하고 있습니다. WinAmp의 ID3 태그 편집기는 문자열을 NULL 문자로 채우지 않고 공백 문자로 채웁니다. 때문에 우리는 원 문자열 뒤에 붙은 불필요한 문자들을 제거해야 합니다.

 

    // ...
    
    StrTrimA(szField, " ");
    
    // ...

 

마지막으로 우리는 CComVariant 객체를 생성하고 szDisplayStr 문자열을 이 안에 보관해야 합니다. 그 다음 CComVariant::Detach를 호출하여 CComVariant 객체를 Windows 탐색기가 제공한 VARIANT로 복사해야 합니다.

 

    // ...
    
    CComVariant vData(szField);
    
    vData.Detach(pvarData);
    
    return S_OK;
}

 

이것은 어떻게 보일 것인가?

새로운 열(column)은 열 설정 다이얼로그의 목록 맨 마지막에 나타납니다.

 

쉘 익스텐션을 등록하면 ‘열 설정’ 대화상자에 항목이 표시된다.

 

우리가 추가한 열은 이렇게 생겼습니다. 이들 파일은 현재 우리가 추가한 열인 “MP3 Album” 열을 기준으로 정렬된 상태입니다.

 

쉘 익스텐션에 의해 Windows 탐색기에 열이 추가되었다.

 

쉘 익스텐션을 등록하기

컬럼 핸들러는 폴더를 확장한 것이기 때문에 HKCR\Folders 레지스트리 키에 등록됩니다. 컬럼 핸들러를 등록하는 RGS 파일의 내용은 다음과 같습니다.

 

HKCR {
    NoRemove Folder {
        NoRemove Shellex {
            NoRemove ColumnHandlers {
                ForceRemove {AC146E80-3679-4BCA-9BE4-E36512573E6C} = s 'ID3v1 viewer column ext'
            }
        }
    }
}

 

또 다른 유용한 기능 - 인포팁

컬럼 핸들러가 할 수 있는 또 다른 흥미로운 기능은 특정 파일 형식에 대해 인포팁을 수정할 수 있다는 것입니다. RGS 스크립트는 .mp3 파일에 대해 인포팁 내용을 수정할 수 있습니다. (수평 스크롤의 제한으로 인해 여러 줄에 걸쳐서 적었지만, 실제 RGS 스크립트에서는 한 줄에 적혀있습니다.)

 

HKCR {
    NoRemove .mp3 {
        val InfoTip = s 'prop:Type;Author;Title;Comment;
            {AC146E80-3679-4BCA-9BE4-E36512573E6C},0;
            {AC146E80-3679-4BCA-9BE4-E36512573E6C},1;
            {AC146E80-3679-4BCA-9BE4-E36512573E6C},2;Size'
    }
}

 

prop:으로 시작하는 문자열에 Author, Title 및 Comment 필드가 나타나고 있음을 주목하시기 바랍니다. 여러분이 .mp3 파일 위에 마우스 포인터를 올렸을 때 Windows 탐색기는 우리가 만든 쉘 익스텐션을 호출하여 해당 필드에 대한 문자열을 얻을 것입니다. 개발 문서에서는 우리가 추가한 필드는 인포팁에서도 또한 나타날 것이라고 말하고 있는데(우리가 GUID와 프로퍼티 아이디를 위와 같이 작성한 이유가 이것입니다), 하지만 필자는 Windows 2000에서는 작동되는 것을 확인할 수 없었고 운영체제에 내장된 프로퍼티만이 인포팁에 나타남을 확인할 수 있었습니다. 커스텀 인포팁은 다음과 같이 나타납니다.

 

Windows 2000에서는 개발 문서에 소개된 내용과 다르게 작동된다.

 

또한 이 쉘 익스텐션은 Windows XP에서는 작동하지 않을 수 있습니다. 왜냐하면 Windows XP는 새로운 파일 형식 레지스트리 키를 도입했기 때문입니다. 필자의 Windows XP에서 인포팁 정보는 HKCR\SystemFileAssociations\audio에 보관되어 있었습니다.

 

다음 단계에서 다룰 내용

다음 9 단계에서, 우리는 또 다른 쉘 익스텐션으로서 특정 파일 형식에 대해 아이콘을 수정할 수 있는 아이콘 핸들러에 대해 다루어 보겠습니다.

 

계속 읽기

이전 게시글: Windows 쉘 익스텐션 개발 가이드 - (8) 자세히 모드 (1/2)

다음 게시글: Windows 쉘 익스텐션 개발 가이드 - (9) 아이콘 [完]

 

카테고리 “API/COM”
more...
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (8) 자세히 모드 (1/2)
입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차..
API/COM
2021. 2. 10. 11:46

Windows 쉘 익스텐션 개발 가이드 - (8) 자세히 모드 (1/2)

API/COM
2021. 2. 10. 11:46

입문자를 위한 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)

 

8 단계. Windows 탐색기에서 “자세히” 보기 모드를 선택할 때 나타나는 열 항목을 추가하는 쉘 익스텐션(Shell Extension)

실습 프로젝트 다운로드

ShellExtGuide8_demo.zip
51.2 kB

 

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

많은 독자 여러분께서 본 “입문자를 위한 가이드” 시리즈가 계속 되기를 원하십니다. 이번 단계에서 필자는 Windows Me 및 2000 이상의 운영체제에 대해 Windows 탐색기의 “자세히” 보기 화면에 표시되는 열을 추가하는 방법에 대해 다루어 보겠습니다. 이러한 유형의 쉘 익스텐션은 Windows NT 4.0, Windows 95 및 Windows 98에서는 지원되지 않습니다. 때문에 본 예제 프로젝트를 가지고 실습하고자 하실 경우 최신 버전의 운영체제를 사용해야만 합니다.

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

Windows Me와 2000는 “자세히”보기 모드에서 Windows 탐색기를 사용자화 할 수 있는 다양한 옵션들이 추가되었습니다. Windows 2000의 경우 여러분이 활성화시킬 수 있는 37가지의 새로운 열(column)들이 있습니다. 여러분은 두 가지 방법으로 이들 열(column)들을 나타나게 하고, 사라지게 할 수 있습니다. 먼저 열 헤더(column header)에 대해 마우스 오른쪽 버튼을 클릭하면 추가 및 제거할 수 있는 열의 종류가 컨텍스트 메뉴 형태로 몇 가지 나타납니다.

 

‘자세히’ 보기 모드에서 열 헤더 부분을 마우스 오른쪽 클릭해본다.

 

여러분이 [자세히...] 항목을 클릭한다면, Windows 탐색기는 사용 가능한 모든 열 항목들을 다이얼로그를 통해 보여줄 것이고, 이 중에서 선택할 수 있습니다.

 

‘열 설정’ 대화상자에서 표시할 열의 종류를 설정할 수 있다.

 

또한 Windows 탐색기는 이들 열 목록 속에 우리가 직접 정의한 것을 추가할 수 있도록 컬럼 핸들러 익스텐션(column handler extension)을 제공합니다.

이번 단계의 예제 프로젝트는 MP3 파일에 대한 컬럼 핸들러로서, .mp3 파일에 저장되는 ID3 태그(버전 1에 한함)로부터 다양한 필드를 읽어서 Windows 탐색기를 통해 사용자에게 보여줄 것입니다.

 

인터페이스 구현하기

여러분은 이미 프로젝트를 생성하는 과정에 대해 익숙해지셨을 것이므로 필자는 Visual C++ 마법사에 대한 내용은 생략하겠습니다. 새로운 ATL COM 프로젝트를 생성하고 그 이름을 MP3TagView로 정합니다. 또한 CMP3ColExt라는 이름의 C++ 구현 클래스를 생성합니다.

컬럼 핸들러는 단 하나의 인터페이스만 구현하면 됩니다. 바로 IColumnProvider입니다. 다른 쉘 익스텐션처럼 IShellExtInitIPersistFile을 통해 별도의 초기화 과정을 밟을 필요가 없습니다. 왜냐하면 컬럼 핸들러는 폴더에 대한 확장이고, 현재 선택된 파일에 대해서 어떤 작업을 할 필요가 없기 때문입니다. 물론 소정의 초기화 작업이 필요하기는 하지만, 이들은 IColumnProvider의 메소드를 통해 수행됩니다.

우리가 개발할 COM 개체에 IColumnProvider를 추가하기 위하여 MP3ColExt.h를 열고 아래 표시된 부분과 같이 내용을 추가합니다.

 

#include <comdef.h> // 새로 추가
#include <shlobj.h> // 새로 추가
#include <shlguid.h> // 새로 추가
 
/////////////////////////////////////////////////////////////////////////////
// CMP3ColExt
 
class CMP3ColExt :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CMP3ColExt, &CLSID_MP3ColExt>,
    public IColumnProvider { // 새로 추가
    BEGIN_COM_MAP(CMP3ColExt)
        // 새로 추가
        COM_INTERFACE_ENTRY_IID(IID_IColumnProvider, IColumnProvider)
    END_COM_MAP()
    
    public:
        // 이 이하부터 모두 새로 추가
        // IColumnProvider 
        STDMETHODIMP Initialize(LPCSHCOLUMNINIT psci) { return S_OK; }
        STDMETHODIMP GetColumnInfo(DWORD dwIndex, SHCOLUMNINFO * psci);
        STDMETHODIMP GetItemData(LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd, VARIANT * pvarData);
};

 

인터페이스 맵 부분에 COM_INTERFACE_ENTRY_IID라는 매크로가 적혀있는 것을 주목하시기 바랍니다. 이전까지 작성했던 쉘 익스텐션에서 COM_INTERFACE_ENTRY 매크로를 사용해 왔습니다. 이 때는 인터페이스가 __declspec(uuid) 구문으로 선언된 GUID를 가지고 있어야 합니다. 그러나 comdef.h의 경우 IColumnProvider에 대한 GUID를 선언하고 있지 않습니다. 그래서 우리는 이 인터페이스를 COM_INTERFACE_ENTRY를 써서 구현할 수 없습니다.

이 때 필요한 것이 COM_INTERFACE_ENTRY_IID입니다. 인터페이스의 이름과 이에 대한 IID를 명시적으로 지정해주기만 하면 됩니다. 이 방법 대신 클래스 선언 바로 직전에 다음과 같이 내용을 추가하면 기존처럼 COM_INTERFACE_ENTRY를 사용할 수도 있습니다.

 

struct __declspec(uuid("E8025004-1C42-11d2-BE2C-00A0C9A83DA1")) IColumnProvider;

 

우리는 또한 stdafx.h에서 몇 가지 설정을 바꾸어야 합니다. 우리는 Windows 2000의 기능들을 사용하고자 하므로 각종 선언과 원형들을 활성화시키기 위하여 다음과 같이 매크로 상수를 구성합니다.

#define WINVER       0x0500    // Windows 2000, 98 기능 사용
#define _WIN32_WINNT 0x0500    // Windows 2000 기능 사용
#define _WIN32_IE    0x0500    // Internet Explorer 5.0 이상의 기능 사용

 

위 매크로 상수들은 #include보다 먼저 등장해야 합니다.

 

초기화

IColumnProvider는 세 가지 메소드를 갖습니다. 그 중 하나는 Initialize로서 다음과 같은 원형을 갖습니다.

 

HRESULT IColumnProvider::Initialize(LPCSHCOLUMNINIT psci);

 

쉘(shell)은 우리에게 SHCOLUMNINIT 구조체를 전달해줄 것입니다. 이것은 Windows 탐색기에서 보여주고 있는 현재 폴더의 전체 경로를 포함하고 있습니다. 본 단계에서 구현할 예제에서는 필요한 정보가 아니므로 본 Initialize 메소드에서는 단순히 S_OK만 반환할 것입니다.

 

새로운 열(column)을 열거하기

우리가 만든 컬럼 핸들러가 등록되었음을 Windows 탐색기가 발견하였을 때, Windows 탐색기는 우리에게 어떤 열(column)들을 구현하고 있는지 물어보게 됩니다. 이 과정은 다음과 같은 원형을 가진 GetColumnInfo에서 처리됩니다.

 

HRESULT IColumnProvider::GetColumnInfo(DWORD dwIndex, SHCOLUMNINFO * psci);

 

dwIndex0부터 세는 카운터 값으로서, Windows 탐색기가 몇 번째 열에 대해 알고 싶어 하는지를 의미합니다.

psciSHCOLUMNINFO 형 구조체로서 열(column)에 대한 매개 변수(parameter)들로 우리가 내용을 채워야 하는 구조체입니다.

SHCOLUMNINFO의 첫 번째 멤버 변수는 또 다른 구조체인 SHCOLUMNID입니다. SHCOLUMNIDGUIDDWORD의 한 쌍으로 구성되어 있습니다. GUID의 경우 “포맷 아이디(format ID)”라고 부리고, DWORD의 경우 “프로퍼티 아이디(property ID)”라고 부릅니다. 이 한 쌍의 값은 한 시스템에서 특정 열(column)을 유일하게 식별할 수 있습니다. 예를 들어 저자(Author)와 같이 이미 존재하는 열(column)이라 해도 포맷 아이디와 프로퍼티 아이디가 미리 정의된 값으로 설정되어있다면 재사용할 수 있습니다.

쉘 익스텐션이 새로운 열(column)을 추가할 때, 유일한 것으로 보장되는 우리의 CLSID를 포맷 아이디로 지정한다면, 프로퍼티 아이디는 간단한 숫자로 지정할 수 있습니다.

본 단계에서 구현할 쉘 익스텐션은 두 가지 방법을 모두 사용해 볼 것입니다.

우리는 먼저 제작자(Author), 제목(Author) 및 주석(Comment) 열을 재사용할 것이고, MP3 앨범명(MP3 Album), MP3 연도(MP3 Year) 및 MP3 장르(MP3 Genre)라는 열을 새롭게 추가할 것입니다. 본 단계의 예제 프로젝트에서 GetColumnInfo는 다음과 같이 시작합니다.

 

STDMETHODIMP CMP3ColExt::GetColumnInfo(DWORD dwIndex, SHCOLUMNINFO * psci) {
    // 우리는 최대 6개의 열을 제공할 것이기 때문에
    // dwIndex가 6 이상이면 우리가 제공할 열을 모두 열거했음을 알리기 위해
    // S_FALSE를 반환합니다.
    if (dwIndex >= 6)
        return S_FALSE;
    
    // ...

 

dwIndex6 이상의 값이 전달되면 우리는 Windows 탐색기가 열에 대한 열거를 끝낼 수 있도록 S_FALSE를 반환합니다. 그렇지 않은 경우 우리는 SHCOLUMNINFO 구조체의 내용을 채워야 합니다. dwIndex의 값이 0, 1, 2인 경우 우리는 우리가 새롭게 추가할 열(column)에 대한 데이터를 반환할 것입니다. dwIndex의 값이 3, 4, 5인 경우 우리는 재사용하고자 하는 운영체제 내장 열(column)을 반환할 것입니다.

우리의 첫 번째 커스텀 열(custom column)인, ID3 태그에서 앨범 이름을 보여주는 열을 구성하는 예는 다음과 같습니다.

 

    // ...
    
    switch (dwIndex) {
    case 0: // MP3 Album - separate column
        psci->scid.fmtid = CLSID_MP3ColExt; // 포맷 ID로서 우리의 CLSID를 지정.
        psci->scid.pid   = 0; // 프로퍼티 ID로서 0번을 지정.
        psci->vt         = VT_LPSTR; // 이 열(column)의 데이터는 문자열임을 지정.
        psci->fmt        = LVCFMT_LEFT; // 문자열은 왼쪽 정렬로 출력되어야 함.
        psci->csFlags    = SHCOLSTATE_TYPE_STR; // 데이터는 문자열로 저장되어야 함.
        psci->cChars     = 32; // 기본 열 너비(단위: 문자)
        
        wcsncpy(psci->wszTitle, L"MP3 Album", MAX_COLUMN_NAME_LEN);
        wcsncpy(psci->wszDescription, L"Album name of an MP3", MAX_COLUMN_DESC_LEN);
        
        break;
        
        // ...

 

중요

이 글의 이전 버전에서는 csid.fmtid 멤버에 _Module.pguidVer라는 값을 저장하도록 작성했었는데 이는 근본적으로 틀린 내용이었습니다. 왜냐하면 GUID는 같은 버전의 ATL에서 빌드된 모든 바이너리에서 항상 똑같기 때문입니다. _Module.pguidVer를 사용하면서 같은 프로퍼티 아이디를 사용하는 서로 다른 두 개의 쉘 익스텐션이 있다면, 각 열 사이에 서로 엄청난 충돌이 발생할 것입니다.

 

우리는 포맷 아이디로서 본 단계에서 다루는 쉘 익스텐션의 GUID를 지정했고, 프로퍼티 아이디에는 열(column)의 번호를 지정했습니다. SHCOLUMNINIT 구조체의 vt 멤버는 우리가 Windows 탐색기에 반환할 데이터의 타입을 나타냅니다. VT_LPSTR은 C 언어 스타일의 문자열을 의미합니다.

 

fmt 멤버는 LVCFMT_로 시작하는 상수들 중 하나가 될 수 있으며 그 열(column)에서 문자열 정렬 방향을 지정합니다. 이 경우는 왼쪽 정렬로 하겠습니다.

 

csFlags 멤버는 열(column)에 대한 몇 가지 옵션입니다. 그러나 쉘(shell)은 모든 옵션들을 구현해 놓지는 않은 것으로 보입니다. 다음은 각 플래그(flag)에 대한 설명입니다.

 

SHCOLSTATE_TYPE_STR, SHCOLSTATE_TYPE_INTSHCOLSTATE_TYPE_DATE

Windows 탐색기가 열을 기준으로 정렬할 때 해당 열의 데이터를 어떻게 처리해야 하는지를 지정합니다. 문자열, 정수 및 날짜/시각 중 하나가 될 수 있습니다.

SHCOLSTATE_ONBYDEFAULT

마이크로소프트 개발 지원 관계자인 Dave Anderson에 따르면 다음과 같은 용도를 갖습니다.

이 작동은 여러분의 쉘 탐색기(shell browser)의 설정에 따라 달라집니다. 여러분이 폴더 옵션에서 “각 폴더의 보기 옵션 기억”을 지정하였다면, 특정한 몇몇 쉘(shell)에서 열(column)이 보여지는 상태는 레지스트리로부터 가져오게 될 것입니다. 이 때는 SHCOLSTATE_ONBYDEFAULT 플래그가 아무 효과가 없습니다.

모든 폴더의 보기 설정을 리셋하는 것은, 여러분의 열도 기본 상태로 돌아가게 할 수 있습니다. 여러분은 이 설정을 Windows 탐색기 또는 제어판의 폴더 옵션 대화상자에서 할 수 있습니다.

SHCOLSTATE_SLOW

개발 문서에 따르면 이 옵션을 포함하는 것은 각 열의 데이터를 읽어오는데 시간이 걸림을 뜻합니다. 이 때 Windows 탐색기는 쉘(shell) 익스텐션을 하나 이상의 백그라운드 스레드에서 호출하여, Windows 탐색기 창 그 자체는 사용자로부터 원활하게 응답할 수 있도록 조치합니다. 필자가 테스트해 본 바, 이 옵션의 존재 여부에 따른 차이를 발견하지 못했습니다.

Windows 2000에서 Windows 탐색기는 쉘 익스텐션이 제공하는 열에 대한 데이터를 불러오는 데 하나의 스레드만을 사용할 뿐이었습니다.

Windows XP에서 Windows 탐색기는 몇 개의 서로 다른 스레드를 통해 작업하기는 하였으나, SHCOLSTATE_SLOW 옵션의 존재 여부에 따라 돌아가는 스레드의 개수의 차이는 발견하지 못했습니다.

SHCOLSTATE_SECONDARYUI

개발 문서에 따르면 이 옵션을 전달하는 것은 열(column)이 헤더 컨트롤의 컨텍스트 메뉴에 나타나는 것을 방지해준다고 합니다. 여러분이 이 옵션을 추가해주지 않는다면, 각 열의 제목에 대해 마우스 오른쪽 버튼을 클릭하여 나타나는 컨텍스트 메뉴에서 여러분이 추가하고 있는 열이 나타난다는 뜻입니다. 이 옵션이 포함되면, 컨텍스트 메뉴에는 곧바로 나타나지 않고 [자세히...] 버튼을 누른 후 대화상자를 통해서만 나타납니다.

SHCOLSTATE_HIDDEN
이 옵션을 전달하면 “열 설정(Column Settings)” 대화상자에서 열(column)이 나타나지 않습니다. 숨겨진 열을 활성화하는 방법은 없기 때문에 이 것은 열(column)을 사용할 수 없게 만듭니다.

 

cChars 멤버는 열(column) 이본 너비를 문자 수 단위로 지정합니다. 이 값을 열(column) 이름 길이의 최대치 및 여러분이 이 열을 통해 나타날 것으로 예상하는 가장 긴 문자열만큼 설정하시기 바랍니다. 또한 여러분은 전체 문자열을 충분하게 보여주도록 2, 3문자의 여유를 더 추가하여야 합니다. 이 여유분을 넣지 않으면 열의 기본 너비는 충분히 넓지 않을 수 있고 일부 텍스트가 잘려서 표시될 수 있습니다.

 

마지막 두 멤버는 헤더 컨트롤(header control)에서 보여질 열의 이름을 보관하는 유니코드 문자열 및 열에 대한 설명을 보관하는 유니코드 문자열입니다. 현재는 이 열을 설명하는 문자열이 사용되지 않기 때문에 사용자는 이를 보는 일이 없습니다.

 

제1열과 제2열도 비슷합니다. 그러나 제1열은 데이터 타입과 정렬 방법에서 차이가 있습니다. 이 열은 연도를 나타내기 때문인데, 어떻게 정의되는지는 아래의 코드에서 확인 가능합니다.

 

    // ...
    
    case 1:     // MP3 year - separate column
        psci->scid.fmtid = CLSID_MP3ColExt;
        psci->scid.pid   = 1;
        psci->vt         = VT_LPSTR; // 문자열의 형태로 데이터를 반환할 것입니다.
        psci->fmt        = LVCFMT_RIGHT; // 문자열은 오른쪽 정렬로 나타날 것입니다.
        psci->csFlags    = SHCOLSTATE_TYPE_INT; // 정수 형태로 저장되어야 합니다.
        psci->cChars     = 6; // 열의 기본 폭을 지정합니다.
        
        wcsncpy(psci->wszTitle, L"MP3 Year", MAX_COLUMN_NAME_LEN);
        wcsncpy(psci->wszDescription, L"Year of an MP3", MAX_COLUMN_DESC_LEN);
        break;
    // ...

 

vt 멤버 변수가 VT_LPSTR로 되어 있음에 주목하시기 바랍니다. 이것은 우리가 Windows 탐색기에게 문자열을 전달할 것임을 의미합니다. 하지만 csFlags 멤버 변수는 SHCOLSTATE_TYPE_INT로 되어 있습니다. 이것은 Windows 탐색기가 우리가 전달한 데이터들을 정렬할 때 가나다 순이 아니라, 숫자로서 크기 비교를 하라는 뜻입니다. 물론 처음부터 우리가 Windows 탐색기에 문자열이 아닌 정수 형식으로 데이터를 전달할 수도 있습니다. 그러나 .mp3 파일의 ID3 태그가 발행 연도 항목을 정수가 아닌 문자열 형식으로 보관하고 있기 때문에, 위와 같이 열 정의를 하면 우리가 직접 문자열을 정수로 변환하지 않아도 됩니다.

 

dwIndex3, 4, 5일 때 우리는 재사용하고자 하는 운영체제 내장 열(column)을 반환합니다. 제3열은 저작자(Author) 열(column)로서 ID3의 아티스트(Artist) 정보를 보여줄 것입니다.

 

    // ...
    
    case 3: // MP3 artist - reusing the built-in Author column
        psci->scid.fmtid = FMTID_SummaryInformation; // 미리 정의된 FMTID
        psci->scid.pid   = 4; // 미리 정의된 author 열
        psci->vt         = VT_LPSTR; // 데이터를 문자열 형태로 반환할 것입니다.
        psci->fmt        = LVCFMT_LEFT; // 문자열은 왼쪽 정렬될 것입니다.
        psci->csFlags    = SHCOLSTATE_TYPE_STR; // 데이터는 문자열로서 정렬됩니다.
        psci->cChars     = 32;  // 열의 기본 너비(단위: 문자)
        break;
    // ...

 

FMTID_SummaryInformation은 미리 정의된 기호입니다. 저자(author) 필드는 MSDN 문서에 따르면 4번으로 등록되어 있습니다. 전체 목록은 “The Summary Information Property Set”을 참고하시기 바랍니다. 운영체제에서 미리 정의해 놓은 열을 재사용하고자 할 때, 우리는 열의 제목이나 설명을 반환하지 않습니다. 왜냐하면 쉘(shell)이 이미 이것을 다룰 것이기 때문입니다.

마지막으로 switch 구문의 끝나고 우리는 SHCOLUMNINFO 구조체를 모두 채웠음을 알리기 위하여 S_OK를 반환합니다.

 

열에 데이터 출력하기

IColumnProvider의 마지막 메소드는 GetItemData입니다. 이것은 파일에 대한 데이터를 얻어서 출력하기 위해 Windows 탐색기에 의해 호출되는 메소드입니다. 이 메소드의 원형은 다음과 같습니다.

 

HRESULT IColumnProvider::GetItemData(LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd, VARIANT * pvarData);

 

SHCOLUMNID 구조체는 Windows 탐색기가 어떤 열의 데이터를 필요로 하는지를 알려줍니다. 또한 이 구조체는 우리가 GetColumnInfo 메소드를 통해 Windows 탐색기에게 전달해 준 정보도 포함하고 있습니다.

SHCOLUMNDATA 구조체는 파일이나 디렉토리에 대한 자세한 정보를 경로와 함께 포함하고 있습니다. 우리는 이 정보를 해당 파일 또는 디렉토리에 대한 데이터를 제공할 것인지 여부를 결정하기 위해 사용할 수 있습니다.

pvarDataVARIANT 구조체에 대한 포인터로서, Windows 탐색기를 통해 우리가 보여주고자 하는 실질적인 데이터를 보관할 것입니다. VARIANT라는 자료형은 Visual Basic과 같은 스크립트 언어들이 갖는 loosely-typed 변수의 C 언어 버전입니다. 이 구조체는 두 가지 부분으로 구성되어 있는데, 하나는 타입이고 하나는 데이터입니다. ATL은 VARIANT를 초기화하고 값을 설정하는 것과 관련해 편리하게 다룰 수 있는 CComVariant라는 클래스를 제공하고 있습니다.

 

계속 읽기

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

다음 게시글: Windows 쉘 익스텐션 개발 가이드 - (8) 자세히 모드 (2/2)

 

카테고리 “API/COM”
more...
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (7) 비트맵 및 폴더 메뉴 (3/3)
입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차..
API/COM
2021. 2. 10. 10:06

Windows 쉘 익스텐션 개발 가이드 - (7) 비트맵 및 폴더 메뉴 (3/3)

API/COM
2021. 2. 10. 10:06

입문자를 위한 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)

 

두 번째 쉘 익스텐션: 디렉토리 창에서 마우스 오른쪽 버튼을 클릭하는 것을 처리하기

쉘 버전 4.71 및 그 이후 버전에서 여러분은 바탕화면 또는 파일 시스템의 디렉토리를 보여주는 아무 Windows 탐색기 창에서 마우스 오른쪽 클릭을 했을 때 나타나는 컨텍스트 메뉴를 수정할 수 있습니다. 이러한 쉘 익스텐션을 작성하는 방법은 여타 컨텍스트 메뉴 익스텐션을 작성하는 것과 비슷합니다. 다만 두 가지가 다릅니다.

 

- IShellExtInit::Initialize 메소드의 매개 변수(parameter)가 다르게 사용됩니다.

- 쉘 익스텐션이 다른 레지스트리 키에 등록됩니다.

 

필자는 이 글에서 본 쉘 익스텐션을 작성하는 모든 과정을 설명하지는 않겠습니다. 전체적인 과정은 첨부한 예제 프로젝트를 참고하시기 바랍니다.

 

IShellExtInit::Initialize에서 나타나는 차이

InitializepidlFolder 매개 변수(parameter)를 가지고 있는데, 지금까지 우리는 이를 무시하여 왔습니다. 왜냐하면 이것은 줄곧 NULL이었기 때문입니다. 드디어 이제부터 이 매개 변수(parameter)는 의미 있는 용도를 갖게 됩니다! 이 매개 변수(parameter)는 마우스 오른쪽 클릭이 발생한 디렉토리의 PIDL입니다. 두 번째 매개 변수(parameter)인 IDataObject 인터페이스형 포인터는 NULL이 됩니다. 왜냐하면 선택된 파일이 없기 때문입니다.

다음은 예제 프로젝트에서 Initialize 메소드의 구현을 가져온 것입니다.

 

STDMETHODIMP CBkgndCtxMenuExt::Initialize(LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDO, HKEY hkeyProgID) {
    // SHGetPathFromIDList API를 사용하여
    // PIDL로부터 일반적인 디렉토리 경로를 구합니다.
    return SHGetPathFromIDList(pidlFolder, m_szDirClickedIn) ? S_OK : E_INVALIDARG;
}

 

SHGetPathFromIDList 함수는 폴더의 PIDL을 받아서 문자열 형태의 전체 경로를 반환합니다. 이 경로는 나중에 사용하기 위해 멤버 변수에 보관해 둡니다. 함수의 성공 및 실패 여부에 따라 BOOL 값이 반환됩니다.

 

IShellExtInit::GetCommandString에서 나타나는 차이

Windows XP부터 Windows 탐색기는 GetCommandString에서 반환되는 동사(verb)의 이름을 검사합니다. 그리고 서로 일치하는 동사를 가진 메뉴 항목을 삭제합니다. 우리가 구현할 GetCommandString 메소드도 GCS_VERB를 포함하고 있는지 확인해야 합니다. 그리고 각 메뉴 항목에 따라 다른 동사(verb)를 반환해야 합니다.

 

STDMETHODIMP CBkgndCtxMenuExt::GetCommandString(UINT uCmd, UINT uFlags, UINT * puReserved, LPSTR pszName, UINT cchMax) {
    USES_CONVERSION;
    
    static LPCTSTR szItem0Verb = _T("CPBkgndExt0");
    static LPCTSTR szItem1Verb = _T("CPBkgndExt1");
    
    // 우리는 두 개의 메뉴 항목을 추가했으므로 idCmd가 0 또는 1이어야만 합니다.
    if ( uCmd > 1 )
        return E_INVALIDARG;
    
    // ‘플라이-바이(fly-by)’ 도움말 문자열 반환하는 내용은 생략합니다.
    
    // 새로 추가하고 있는 두 메뉴 항목 각각에 대한 동사(verb)를 반환합니다.
    if (uFlags == GCS_VERBA)
        lstrcpynA(pszName, T2CA((uCmd == 0) ? szItem0Verb : szItem1Verb), cchMax);
    else if (uFlags == GCS_VERBW)
    lstrcpynW((LPWSTR) pszName, T2CW((uCmd == 0) ? szItem0Verb : szItem1Verb), cchMax);
    
    return S_OK;
}

 

이러한 유형의 쉘 익스텐션은 HKCR\Directory\Background\ShellEx\ContextMenuHandlers에 등록됩니다. RGS 스크립트는 다음과 같이 구성합니다.

 

HKCR {
    NoRemove Directory {
        NoRemove Background {
            NoRemove ShellEx {
                NoRemove ContextMenuHandlers {
                    ForceRemove SimpleBkgndExtension = s '{9E5E1445-6CEA-4761-8E45-AA19F654571E}'
                }
            }
        }
    }
}

 

위와 같은 두 가지의 차이에도 불구하고, 쉘 익스텐션은 다른 컨텍스트 메뉴 익스텐션과 동일하게 작동합니다. 그렇지만 IContextMenu::QueryContextMenu에서 한 가지 알아둘 사항이 있습니다. Windows 98 및 Windows 2000에서 uIndex 매개 변수(parameter)는 항상 -1 다시 말하면 0xFFFFFFFF인 듯 합니다. InsertMenu에 인덱스 값으로 -1을 전달하는 것은 새로 추가될 메뉴 항목이 컨텍스트 메뉴의 최하단에 추가됨을 뜻합니다. 그러나 여러분이 uIndex의 값을 1 증가시켜서 영(0)으로 만든다면, InsertMenu0이 된 uIndex를 전달하게 되는데, 이는 컨텍스트 메뉴의 최상단에 새로운 메뉴 항목이 추가됨을 의미합니다. 예제 프로젝트의 QueryContextMenu를 참고하셔서 어떻게 메뉴 항목이 적절한 위치에 추가되는지를 확인하시기 바랍니다.

수정된 컨텍스트 메뉴는 다음과 같이 생겼습니다. 두 개의 메뉴 항목이 컨텍스트 메뉴 하단에 추가되었습니다. 필자의 짧은 식견으로 보았을 때, 이와 같은 방식으로 컨텍스트 메뉴의 끝 부분에 메뉴 항목들을 추가하는 것은 사용자 편의성에 중대한 문제가 있을 것입니다.

사용자가 [등록 정보] 또는 [속성] 항목을 선택하고자 할 때, 습관적으로 마우스 오른쪽 클릭을 하여 맨 마지막 메뉴 항목을 클릭합니다. 그런데 우리가 추가하고자 하는 메뉴 항목이 [등록 정보] 또는 [속성] 항목보다 더 아래에 있게 되면, 사용자들이 익숙하게 써오던 쉘 사용을 방해하는 것이 되고, 사용자들은 ‘깊은 빡침’을 느끼며 항의성 이메일을 보내올 수 있습니다. ^^;;

 

컨텍스트 메뉴의 최하단에 항목을 추가하는 행위는 사용자의 편의성을 해칠 수 있다.

 

다음 단계에서 다룰 내용

다음 8 단계에서는 Windows 탐색기에서 “자세히” 보기 모드를 선택하였을 때 나타나는 열(column)을 사용자화할 수 있는 컬럼 핸들러 익스텐션(column handler extension)에 대해 다루어 보겠습니다.

 

계속 읽기

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

다음 게시글: Windows 쉘 익스텐션 개발 가이드 - (8) 자세히 모드 (1/2)

 

카테고리 “API/COM”
more...
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (7) 비트맵 및 폴더 메뉴 (2/3)
입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차..
API/COM
2021. 2. 10. 09:48

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

API/COM
2021. 2. 10. 09:48

입문자를 위한 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)

 

메뉴 항목에 그림 출력하기

이제 됐습니다. 지금까지 보아왔던 소스 코드에 여러분은 지루함을 느끼셨으리라 충분히 이해합니다. 이제 진짜로 새롭고 흥미로운 작업을 하게 될 것입니다! IContextMenu2 인터페이스와 IContextMenu3 인터페이스에서 추가된 두 개의 메소드가 있습니다. 이들은 단순히 이 프로젝트 내에서 결과적으로 메시지 핸들러를 호출하게 될 헬퍼(helper) 함수를 호출하는 것에 지나지 않습니다.

이와 같이 호출에 호출을 거듭하게 코드를 작성한 것은 결국 같은 역할을 하는 메소드를 버전이 다르다고(하나는 IContextMenu2이고 다른 하나는 IContextMenu3) 두 번씩 작성하게 만드는 것을 방지할 수 있기 때문입니다. LRESULT * 매개 변수(parameter)와 관련해서 HandleMenuMsg2 메소드에는 다소 이상한 것이 있습니다. 아래 소스코드의 주석 부분에서 설명합니다.

 

STDMETHODIMP CBmpCtxMenuExt::HandleMenuMsg(UINT uMsg, WPARAM wParam, LPARAM lParam) {
    AFX_MANAGE_STATE(AfxGetStaticModuleState()); // MFC 초기화
    
    // res는 더미(dummy) LRESULT 변수입니다. 즉 실제로 사용되지는 않습니다.
    // IContextMenu2::HandleMenuMsg()는 값을 반환할 방법도 제공하지 않습니다.
    // 그럼에도 res가 필요한 것은 MenuMessageHandler가 호출될 때
    // IContextMenu2 또는 IContextMenu3 인터페이스에서 호출했는지에 관계없이
    // 소스 코드 수준에서 동일한 함수 호출 인터페이스를 유지하기 위함입니다.
    LRESULT res;
    
    return MenuMessageHandler(uMsg, wParam, lParam, &res);
}

STDMETHODIMP CBmpCtxMenuExt::HandleMenuMsg2(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT * pResult) {
    AFX_MANAGE_STATE(AfxGetStaticModuleState());  // MFC 초기화
    
    // 반환 값이 없는 메시지의 경우 pResult는 NULL입니다.
    // 이것이 NULL이 되면, 필자는 더미(dummy) LRESULT형 변수를 만들 것입니다.
    // 그러면 MenuMessageHandler의 pResult는 항상 유효한 주소만을 가리킬 것입니다.
    if (pResult == NULL) {
        LRESULT res;
        return MenuMessageHandler(uMsg, wParam, lParam, &res);
    } else {
        return MenuMessageHandler(uMsg, wParam, lParam, pResult);
    }
}

 

MenuMessageHandlerWM_MEASUREITEMWM_DRAWITEM을 각각의 메시지 핸들러에게 전달만 할 것입니다.

HRESULT CBmpCtxMenuExt::MenuMessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT * pResult) {
    switch (uMsg) {
    case WM_MEASUREITEM:
        return OnMeasureItem((MEASUREITEMSTRUCT *)lParam, pResult);
    case WM_DRAWITEM:
        return OnDrawItem((DRAWITEMSTRUCT *)lParam, pResult);
    }
    return S_OK;
}

 

이전에도 언급했듯이 개발 문서에 따르면 쉘(shell)은 쉘 익스텐션에게 WM_INITMENUPOPUPWM_MENUCHAR에 대해 처리할 수 있도록 해주어야 한다고 적혀있지만, 필자는 테스트하는 동안 그러한 메시지가 전달되는 것을 확인할 수 없었습니다.

 

WM_MEASUREITEM 메시지 처리하기

쉘(shell)은 우리가 만들고 있는 쉘 익스텐션에게 WM_MEASUREITEM 메시지를 보내서 이 메뉴 항목의 디멘션(dimension)을 요청합니다. 우리는 우리가 만든 메뉴 항목에 의해 호출되었는지를 확인하는 것부터 시작합니다. 검사가 통과하면, 우리는 비트맵 이미지의 디멘션(dimension, 번역자 주: 정사각형 개체에 대한 가로 길이와 세로 길이)을 가져옵니다. 그리고 메뉴 항목의 전체적인 크기를 계산합니다.

먼저 비트맵 이미지의 크기를 가져오는 부분입니다.

 

HRESULT CBmpCtxMenuExt::OnMeasureItem(MEASUREITEMSTRUCT * pmis, LRESULT * pResult) {
    BITMAP bm;
    LONG   lThumbWidth, lThumbHeight;
    
    // 우리가 만든 메뉴 항목 때문에 호출된 것이 아니라면 아무 작업을 하지 않습니다.
    if (m_uOurItemID != pmis->itemID)
        return S_OK;
    
    m_bmp.GetBitmap(&bm);
    m_lBmpWidth = bm.bmWidth;
    m_lBmpHeight = bm.bmHeight;
    
    // ...

 

그 다음 우리는 썸네일의 크기를 계산합니다. 그것으로부터 컨텍스트 메뉴 항목의 전체적인 크기를 결정합니다. 비트맵 이미지의 크기가 최대 썸네일 크기보다 작다면(이 예제 프로젝트에서는 64 * 64가 최대 크기입니다), 비트맵 이미지는 있는 그대로 메뉴에 그려질 것입니다. 그렇지 않으면 비트맵 이미지는 64 * 64 크기에 맞추어 그려질 것입니다. 비트맵 이미지의 크기를 조정해서 그린다는 것은 원래의 비트맵 이미지를 다소 왜곡(번역자 주: 가로와 세로 비율이 틀어져서 길쭉하거나 넓적하게 보여짐)하여 표현할 수 있겠으나, 썸네일 이미지를 보기 좋게 조정하는 것은 여러분들의 과제로 남기겠습니다.

 

    // ...
    
    // 썸네일 이미지의 크기를 계산합니다.
    lThumbWidth = (m_lBmpWidth <= m_lMaxThumbnailSize) ? m_lBmpWidth : m_lMaxThumbnailSize;
    
    lThumbHeight = (m_lBmpHeight <= m_lMaxThumbnailSize) ? m_lBmpHeight : m_lMaxThumbnailSize;
    
    // 썸네일 + 테두리의 폭 + 패딩(padding)을 고려한 메뉴 항목의 크기를 계산합니다.
    m_lItemWidth = lThumbWidth + m_lTotalBorderSpace;
    m_lItemHeight = lThumbHeight + m_lTotalBorderSpace;
    
    // ...

 

이제 우리는 메뉴 항목의 크기를 결정했으므로, 이 값을 우리가 메시지와 함께 받았던 MENUITEMSTRUCT 구조체에 보관합니다. Windows 탐색기는 우리가 추가하는 메뉴 항목을 위해 충분한 공간을 확보해줄 것입니다.

 

    // ...
    
    pmis->itemWidth = m_lItemWidth;
    pmis->itemHeight = m_lItemHeight;
    
    *pResult = TRUE;  // 이제 우리는 메시지를 처리했습니다.
    
    return S_OK;
}

 

WM_DRAWITEM 메시지 처리하기

우리가 WM_DRAWITEM 메시지를 전달받았을 때, Windows 탐색기는 우리가 실제로 메뉴 항목을 그릴 수 있도록 요청합니다. 우리는 썸네일 주변으로 3D 테두리를 그리기 위한 RECT를 계산하는 것으로 시작합니다. 이 때 RECTWM_MEASUREITEM 핸들러를 처리할 때 반환했던 크기와 반드시 같아야 할 필요는 없습니다. 왜냐하면 메뉴 항목은 컨텍스트 메뉴 내 다른 항목들이 더 넓을 경우 함께 넓어지기 때문입니다.

 

HRESULT CBmpCtxMenuExt::OnDrawItem(DRAWITEMSTRUCT * pdis, LRESULT * pResult) {
    CDC   dcBmpSrc;
    CDC*  pdcMenu = CDC::FromHandle(pdis->hDC);
    CRect rcItem(pdis->rcItem);  // 메뉴 항목에 대한 RECT
    CRect rcDraw;                  // 그리기 작업을 할 RECT
    
    // 우리가 추가한 메뉴 항목에 의해 호출되었는지를 검사합니다.
    if (m_uOurItemID != pdis->itemID)
        return S_OK;
    
    // rcDraw는 처음에는 WM_MEASUREITEM 이벤트를 처리하면서 얻게 된 크기에 따라
    // 설정될 것입니다. 그 후 소스 코드가 진행되면서 축소될 것입니다.
    rcDraw.left = rcItem.CenterPoint().x - m_lItemWidth/2;
    rcDraw.top = rcItem.CenterPoint().y - m_lItemHeight/2;
    rcDraw.right = rcDraw.left + m_lItemWidth;
    rcDraw.bottom = rcDraw.top + m_lItemHeight;
    
    // 썸네일 주변 패딩(padding) 공간에 따라 rcDraw 사각 영역을 축소시킵니다.
    rcDraw.DeflateRect(m_lMenuItemSpacing, m_lMenuItemSpacing);

 

그림을 출력하기 위한 첫 번째 단계로 메뉴 항목의 바탕에 색을 칠합니다.

DRAWITEMSTRUCT 구조체의 itemState 멤버는 우리가 추가한 메뉴 항목에 포커스가 주어졌는지, 그렇지 않은지 여부를 나타냅니다. 이에 따라 우리는 바탕이 될 색을 선택하면 됩니다.

 

    // ...
    
    // 메뉴 항목의 바탕을 특정 색으로 칠합니다.
    if (pdis->itemState & ODS_SELECTED)
        pdcMenu->FillSolidRect(rcItem, GetSysColor(COLOR_HIGHLIGHT));
    else
        pdcMenu->FillSolidRect(rcItem, GetSysColor(COLOR_MENU));
    
    // ...

 

그 다음으로 우리는 썸네일 이미지가 메뉴 속으로 움푹 들어간 것처럼 보이도록 ‘선큰(sunken)’ 테두리를 그립니다.

 

    // ...
    
    // 선큰(sunken) 3D 테두리를 그립니다.
    for (int i = 1; i <= m_l3DBorderWidth; i++) {
        pdcMenu->Draw3dRect(rcDraw, GetSysColor(COLOR_3DDKSHADOW), GetSysColor(COLOR_3DHILIGHT));
        rcDraw.DeflateRect(1, 1);
    }
    
    // ...

 

마지막으로 썸네일 이미지 그 자체를 그릴 차례입니다. 필자는 StretchBlt를 사용하여 간단하게 구현해 보았습니다. 결과는 그다지 예쁘지는 않지만, 그래도 필자의 목표인 코드를 간단하게 작성하는 것은 성공했습니다.

 

    // ...
    
    // 새로운 DC를 생성하고 여기에 원본 비트맵을 선택합니다.
    CBitmap* pOldBmp;
    
    dcBmpSrc.CreateCompatibleDC(&dc);
    pOldBmp = dcBmpSrc.SelectObject(&m_bmp);
    
    // 비트맵 이미지를 메뉴 DC에 입힙니다.
    pdcMenu->StretchBlt(rcDraw.left, rcDraw.top, rcDraw.Width(), rcDraw.Height(), &dcBmpSrc, 0, 0, m_lBmpWidth, m_lBmpHeight, SRCCOPY);
    
    dcBmpSrc.SelectObject(pOldBmp);
    
    *pResult = TRUE; // 우리는 이 메시지를 처리했습니다.
    
    return S_OK;
}

 

실제 쉘 익스텐션에서는 마우스가 지나갈 때마다 그림이 깜박거리지 않도록, 깜박거림이 없는 클래스를 사용하는 것이 좋습니다.

여기 메뉴의 작동 결과에 대한 스크린 샷이 있습니다. 그림이 그려진 메뉴가 마우스 포인터를 지나갈 때마다 다음과 같이 보여집니다.

 

Windows 2000에서 포커스(focus)되지 않은 컨텍스트 메뉴 항목.
Windows 2000에서 포커스(focus)된 컨텍스트 메뉴 항목.

 

그리고 버전 4.0의 쉘에서 실행할 때의 모습입니다. 메뉴의 선택 여부에 따라 색이 반전되는데 이는 다소 ‘후져’보입니다.

 

Windows NT 4.0에서 포커스(focus)되지 않은 컨텍스트 메뉴 항목.
Windows NT 4.0에서 포커스(focus)된 컨텍스트 메뉴 항목.

 

쉘 익스텐션을 등록하기

우리가 만든 비트맵 뷰어를 등록하는 것은 여타 컨텍스트 메뉴 익스텐션을 등록하는 것과 다르지 않습니다. 등록을 위한 RGS 스크립트는 다음과 같이 생겼습니다.

HKCR {
    NoRemove Paint.Picture {
        NoRemove ShellEx {
            NoRemove ContextMenuHandlers {
                BitmapPreview = s '{D6F469CD-3DC7-408F-BB5F-74A1CA2647C9}'
            }
        }
    }
}

 

염두에 두실 것은 Paint.Picture라는 파일 유형은 여기서 하드코드(hard-coded)되었다는 것입니다. .bmp 파일에 대해 그림판을 기본 연결 프로그램으로 지정하지 않았다면, Paint.Picture라는 문자열을 HKCR\.bmp이 가리키는 레지스트리 키의 이름으로 바꾸어야 합니다. 말할 것도 없이 좀 더 생산적인 코드에서는 DllRegisterServer에서 이 작업을 수행합니다. 여러분은 Paint.Picture라는 레지스트리 키의 이름이 현재 컴퓨터 설정에 적절한지를 검사할 수 있습니다. 이 주제에 대해서는 1 단계에서 설명했습니다.

 

계속 읽기

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

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

 

카테고리 “API/COM”
more...
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (7) 비트맵 및 폴더 메뉴 (1/3)
입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차..
API/COM
2021. 2. 9. 22:00

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”
more...
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (6) 보내기 메뉴
입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차..
API/COM
2021. 2. 9. 20:36

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

API/COM
2021. 2. 9. 20:36

입문자를 위한 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)

 

6 단계. ‘보내기(Send To)’ 메뉴에서 사용될 수 있는 쉘 익스텐션(Shell Extension)

실습 프로젝트 다운로드

ShellExtGuide6_demo.zip
99.3 kB

 

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

여러분은 이제 6 단계까지 왔습니다. 필자는 상대적으로 덜 사용되는 쉘 익스텐션인 ‘드롭 핸들러(Drop Handler)’를 여러분에게 소개해 드리고자 합니다. 이 유형의 쉘 익스텐션은 ‘드롭’의 목표가 되는 파일에 따라 앞으로 호출될 쉘 익스텐션이 결정되고 Windows 탐색기의 ‘드래그 앤 드롭’ 기능을 프로그램적으로 새롭게 확장할 수 있습니다.

이번 단계에서는 여러분이 쉘 익스텐션의 기본에 대해 알고 있고, 쉘(shell)과 상호작용하기 위해 사용되는 MFC 클래스들에 친숙하다고 가정합니다. 여러분이 MFC 클래스에 대해 다시 알아보고자 한다면, 4 단계로 되돌아가시기 바랍니다. 4 단계는 이번 단계에서 사용하는 것과 같은 기법을 사용합니다.

4 단계에서 필자는 마우스 오른쪽 버튼을 누른 상태로 드래그 앤 드롭을 할 때 호출되는 드래그 앤 드롭 핸들러에 대해 설명하였습니다. Windows 탐색기는 또한 마우스 왼쪽 버튼을 누른 상태로 특정 파일에 ‘드롭’하는 드래그 앤 드롭 동작을 하는 동안에도 쉘 익스텐션이 호출될 수 있도록 만들어졌습니다. 예를 들어 WinZip은 다른 파일들을 드래그하여 .zip 파일 아이콘에 ’드롭’ 하였을 때 .zip 파일에 해당 파일들을 추가할 수 있는 쉘 익스텐션을 제공하고 있습니다. 여러분이 .zip 파일로 다른 파일들을 드래그하여 가져오면 Windows 탐색기는 .zip 파일이 ‘드롭’ 동작의 대상이 될 수 있음을 알리기 위하여 .zip 파일의 아이콘을 강조하고 마우스 포인터도 [+] 모양의 아이콘이 포함된 모양으로 바꿉니다.

 

드래그 동작이 zip 파일 위를 지나갈 때 마우스 포인터에 [+] 표시가 추가된다.

 

만일 드래그 중인 마우스 포인터가 ‘드롭’할 대상이 아닌 파일 위를 지나갈 때는 아무것도 변하지 않습니다.

 

드래그 동작이 exe 파일 위로 지나갈 때는 마우스 포인터에 아무 변화도 없다.

 

드롭 핸들러는 WinZip의 경우와 같이 여러분만의 파일 형식을 개발할 때 매우 유용하게 쓰일 수 있습니다. 드롭 핸들러를 더 흥미롭게 다루기 위해, “보내기(Send To)” 메뉴에 항목을 추가할 수도 있습니다. “보내기(Send To)” 메뉴는 SendTo라는 폴더의 항목을 보여줍니다. Windows 9x의 경우 이 폴더는 Windows 디렉토리 안에 있고, Windows NT 기반의 운영체제의 경우 사용자 프로필 디렉토리 안에 있습니다. 이전 버전의 Windows의 경우 SendTo 디렉토리는 단순히 바로가기 파일들만을 가지고 있었지만, Shell Power Toys 같은 서드파티 앱을 포함하여 새로운 버전의 Windows의 경우 다음과 같이 바로가기가 아닌 파일들도 보여줄 수 있습니다.

 

컨텍스트 메뉴의 ‘보내기(N)’ 메뉴(1).

 

위 그림에서 숨어있는 드롭 핸들러를 발견하지 못했다면 SendTo 폴더에 들어있는 파일 목록을 보시기 바랍니다.

 

12/23/05  3:39a   129 3½ Floppy (A).lnk
12/28/05  1:42p     0 Desktop (create shortcut).DeskLink
12/28/05  1:42p     0 Mail Recipient.MAPIMail
12/23/05 12:31p     0 My Documents.mydocs
 5/25/06 11:05a 1,267 Notepad.lnk

 

.DeskLink와 같은 이상한 확장명이 있음에 주목하시기 바랍니다. 이들 0 바이트 파일은 “보내기(Send To)” 메뉴에 나타나기 위해 존재하는 파일들이고, 이들에 대한 쉘 익스텐션은 레지스트리에 등록되어 있습니다. 이들은 open이나 print와 같은 동사(verb)를 갖지 않기에 비록 일반적인 동작을 하는 것은 아니지만, 이들이 가지고 있는 것이 바로 드롭 핸들러입니다. 여러분이 이들 항목 중 하나를 “보내기(Send To)” 메뉴에서 선택하였을 때, Windows 탐색기는 해당 파일에 관련된 드롭 핸들러를 호출하게 됩니다.

‘드롭’ 동작 대상의 유형에 따라 메뉴 항목을 분류하면 다음과 같습니다.

 

컨텍스트 메뉴의 ‘보내기(N)’ 메뉴(2).

 

이번 단계의 예제 프로젝트는 예전에 사용되었던 “아무 폴더로 보내기(Send To Any Folder)”라는 도구의 클론이 되겠습니다. 이것은 선택한 파일을 여러분의 컴퓨터에서 접근 가능한 임의의 폴더로 이동 또는 복사되게 합니다.

 

초기화 인터페이스

이제 여러분은 지금까지 설명했던 프로젝트 생성 및 클래스 추가 등의 과정에 대해서는 익숙해 지셨을 것입니다. 그러므로 필자는 Visual C++ 마법사에 대한 설명은 생략하겠습니다. 새로운 ATL COM 프로젝트를 생성하고 그 이름을 SendToClone으로 지정합니다. 또한 CSendToShlExt라는 이름으로 C++ 클래스를 추가합니다.

드롭 핸들러는 드롭의 대상이 되는 하나의 파일에 대해서만 실행될 것이기 때문에, 쉘 익스텐션의 초기화는 IPersistFile 인터페이스를 통해 수행됩니다. IPersistFile 인터페이스는 한 번에 하나의 파일에 대해서만 호출되는 쉘 익스텐션이 사용함을 기억하시기 바랍니다.

IPersistFile은 여러 가지 메소드들을 가지고 있지만, 쉘 익스텐션에서는 Load 메소드만 구현하면 됩니다.

먼저 CSendToShlExt이 구현하는 인터페이스 목록에 IPersistFile을 추가해야 합니다. SendToShlExt.h를 열고 다음과 같이 추가합니다.

 

#include <comdef.h>
#include <shlobj.h>
 
class CSendToShlExt : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CSendToShlExt, &CLSID_SendToShlExt>,
    public IPersistFile { // 새로 추가
    BEGIN_COM_MAP(CSendToShlExt)
        COM_INTERFACE_ENTRY(IPersistFile)
    END_COM_MAP()
    
    public:
        // IPersistFile
        STDMETHOD(GetClassID)(LPCLSID)     { return E_NOTIMPL; } // 새로 추가
        STDMETHOD(IsDirty)()                { return E_NOTIMPL; } // 새로 추가
        STDMETHOD(Load)(LPCOLESTR, DWORD)   { return S_OK;      } // 새로 추가
        STDMETHOD(Save)(LPCOLESTR, BOOL)    { return E_NOTIMPL; } // 새로 추가
        STDMETHOD(SaveCompleted)(LPCOLESTR) { return E_NOTIMPL; } // 새로 추가
        STDMETHOD(GetCurFile)(LPOLESTR*)    { return E_NOTIMPL; } // 새로 추가
        
        // ...

 

그런데 Load는 별 다른 작업 없이 그냥 S_OK를 반환하고 있습니다. Load 메소드는 ‘드롭’의 대상이 되는 파일의 전체 경로를 전달받기는 하지만, 지금 만들고 있는 쉘 익스텐션에서는 이를 사용하지 않을 것이므로 무시합니다.

 

드래그 앤 드롭 작동에 참여하기

이 작동을 구현하기 위하여 우리는 ‘드롭’이 발생하는 원천, 즉 Windows 탐색기와 통신할 필요가 있습니다. 우리의 쉘 익스텐션은 드래그되는 파일의 목록을 전달받고, Windows 탐색기에게 사용자의 ‘드롭’ 동작을 응할 것인지 여부를 말합니다. 이 통신 과정은 IDropTarget 인터페이스를 통해 수행됩니다. IDropTarget 인터페이스에는 다음과 같은 메소드가 들어 있습니다.

- DragEnter: 사용자의 드래그 동작이 특정 파일 위를 지나갈 때 호출됩니다. 이 메소드의 반환 값은 사용자가 해당 파일 위에 ‘드롭’할 때 Windows 탐색기에게 그 ‘드롭’ 동작에 응할 것인지 여부를 나타냅니다.

- DragOver: 쉘 익스텐션에서는 호출되지 않습니다.

- DragLeave: 사용자의 드래그 동작이 특정 파일 위에서 ‘드롭’하지 않고 그냥 벗어났을 때 호출됩니다.

- Drop: 사용자가 대상 파일에 ‘드롭’했을 때 호출됩니다. 쉘 익스텐션이 실질적으로 작동하는 부분입니다.

CSendToShlExt 클래스에 IDropTarget 인터페이스를 추가하기 위하여 SendToShlExt.h를 열고 다음과 같이 내용을 추가합니다.

 

class CSendToShlExt : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CSendToShlExt, &CLSID_SendToShlExt>,
    public IPersistFile,
    public IDropTarget { // 새로 추가
    BEGIN_COM_MAP(CSendToShlExt)
        COM_INTERFACE_ENTRY(IPersistFile)
        COM_INTERFACE_ENTRY(IDropTarget) // 새로 추가
    END_COM_MAP()
    
    public:
        // IDropTarget
        STDMETHODIMP DragEnter(IDataObject* pDataObj, DWORD grfKeyState, POINTL pt, DWORD* pdwEffect); // 새로 추가
        STDMETHODIMP DragOver(DWORD grfKeyState, POINTL pt, DWORD* pdwEffect) { return E_NOTIMPL; } // 새로 추가
        STDMETHODIMP DragLeave(); // 새로 추가
        STDMETHODIMP Drop(IDataObject* pDataObj, DWORD grfKeyState, POINTL pt, DWORD* pdwEffect); // 새로 추가
        
    protected:
        CStringList m_lsDroppedFiles; // 새로 추가
}

 

이전 단계에서 실습한 바와 같이, 우리는 드래그되고 있었던 파일들의 목록을 문자열 리스트로 보관하겠습니다. DragOver 메소드는 호출될 일이 없을 것이므로 특별히 구현할 것도 없습니다. 이제부터 나머지 세 가지 메소드에 대해 다루어 보겠습니다.

 

DragEnter

DragEnter의 원형은 다음과 같습니다.

HRESULT IDropTarget::DragEnter(
    IDataObject* pDataObj,
    DWORD        grfKeyState,
    POINTL       pt,
    DWORD*       pdwEffect);

 

pDataObjIDataObject 인터페이스형 포인터로서 드래그되고 있는 파일들을 하나씩 열거할 수 있습니다.

grfKeyState는 플래그(flag)들의 집합으로서 Shift 키의 눌림 여부 및 어느 마우스 버튼이 클릭되었는지를 전달합니다.

ptPOINT와 동일하게 작동하는 POINTL 구조체로서 마우스 포인터의 좌표를 나타냅니다.

pdwEffectDWORD에 대한 포인터로서, 우리가 ‘드롭’ 동작을 허용할 것인지, 허용한다면 마우스 포인터에 어떤 아이콘을 덧붙일 것인지 여부를 Windows 탐색기에게 반환할 장소입니다.

앞서 언급하였듯이, DragEnter는 일반적으로 사용자의 드래그 동작이 처음으로 대상이 될 수 있는 파일 위로 지나갈 때 호출된다고 하였습니다. 그러나 이 함수는 또한 사용자가 “보내기(Send To)” 메뉴의 항목을 클릭하였을 때도 호출됩니다. 따라서 우리는 기능적으로 드래그 앤 드롭이 발생하지 않은 때에도 DragEnter에서 작업을 수행할 수 있습니다.

DragEnter 구현은 먼저 드래그되고 있는 파일들의 목록으로 문자열 리스트를 채울 것입니다. 파일 시스템에 존재하는 어떤 개체이든 복사와 이동이 가능하기 때문에, 이 쉘 익스텐션은 모든 파일과 디렉토리를 받아들입니다. 이를 위해 DragEnter가 시작하는 내용은 여러분도 이미 친숙해지셨을 COleDataObjectIDataObject에 연결시키고 하나씩 열거하는 작업입니다.

 

STDMETHODIMP CSendToShlExt::DragEnter(IDataObject * pDataObj, DWORD grfKeyState, POINTL pt, DWORD * pdwEffect) {
    AFX_MANAGE_STATE(AfxGetStaticModuleState());  // MFC 초기화
    
    COleDataObject dataobj;
    TCHAR          szItem[MAX_PATH];
    UINT           uNumFiles;
    HGLOBAL        hglobal;
    HDROP          hdrop;
     
    dataobj.Attach(pDataObj, FALSE);
    
    // 데이터 오브젝트로부터 리스트의 항목들을 하나씩 읽어옵니다.
    // 이들 항목은 HDROP 형태로 보관되어있기 때문에 HDROP 핸들을 가져와서 드래그 앤 드롭 API를 적용합니다.
    hglobal = dataobj.GetGlobalData(CF_HDROP);
    
    if (hdrop != NULL) {
        hdrop = (HDROP)GlobalLock(hglobal);
        
        uNumFiles = DragQueryFile(hdrop, 0xFFFFFFFF, NULL, 0);
        
        for (UINT uFile = 0; uFile < uNumFiles; uFile++) {
            if (DragQueryFile(hdrop, uFile, szItem, MAX_PATH) != 0)
            m_lsDroppedFiles.AddTail(szItem);
        }
        
        GlobalUnlock(hglobal);
    }
    
    // ...

 

이제 pdwEffect를 통해 값을 반환할 차례입니다. 우리가 반환할 수 있는 효과에는 다음과 같은 것들이 있습니다.

- DROPEFFECT_COPY: Windows 탐색기에게 드래그된 파일이 본 쉘 익스텐션에 의해 복사될 것임을 알려줍니다.

- DROPEFFECT_MOVE: Windows 탐색기에게 드래그된 파일이 본 쉘 익스텐션에 의해 이동될 것임을 알려줍니다.

- DROPEFFECT_LINK: Windows 탐색기에게 드래그된 파일이 본 쉘 익스텐션에 의해 링크될 것임을 알려줍니다.

- DROPEFFECT_NONE: Windows 탐색기에게 본 쉘 익스텐션은 드래그된 파일들을 받지 않을 것임을 알려줍니다.

 

우리가 반환하게 될 단 하나의 효과는 DROPEFFECT_COPY입니다.

본 쉘 익스텐션에서는 DROPEFFECT_MOVE를 반환하지 않겠습니다. 왜냐하면 이 효과는 ‘드롭’ 동작이 발생할 때 Windows 탐색기가 원래 있던 파일들을 삭제할 수 있기 때문입니다.

또한 DROPEFFECT_LINE를 반환할 수 있습니다. 그러나 마우스 포인터 모양이 바로가기 아이콘을 만들 때처럼 작은 화살표가 덧붙어서 나타나게 됩니다. 이 경우 사용자에게 오해를 불러일으킬 소지가 있습니다.

클립보드를 읽을 수 없어서 파일 리스트가 비어있다면, Windows 탐색기에게 ‘드롭’ 작업을 받지 않음을 알려주기 위하여 DROPEFFECT_NONE을 반환합니다.

 

    if (m_lsDroppedFiles.GetCount() > 0) {
        *pdwEffect = DROPEFFECT_COPY;
        return S_OK;
    } else {
        *pdwEffect = DROPEFFECT_NONE;
        return E_INVALIDARG;
    }
}

 

DragLeave

DragLeave는 사용자의 드래그 동작이 우리가 설정할 목표 아이콘을 벗어났을 경우에 호출됩니다. 이 메소드는 “보내기(Send To)” 메뉴에서는 사용되지 않습니다만, SendTo 폴더를 열고 해당 아이콘에 직접 드래그 동작이 가해질 경우에는 호출될 수 있습니다.

우리는 여기서 리소스 정리 같은 작업을 할 일이 없으므로 S_OK만 반환해줍니다.

 

STDMETHODIMP CSendToShlExt::DragLeave() {
    return S_OK;
}

 

Drop

사용자가 “보내기(Send To)” 메뉴 항목에서 우리가 설정할 항목을 클릭했을 때, Windows 탐색기는 Drop을 호출합니다. 이 메소드의 원형은 다음과 같습니다.

 

HRESULT IDropTarget::Drop(IDataObject * pDataObj, DWORD grfKeyState, POINTL pt, DWORD * pdwEffect);

 

첫 번째 매개 변수(parameter)는 DragEnter와 동일합니다.

Drop 메소드는 pdwEffect 파라미터를 통해 최종 효과를 반환해야 합니다.

우리가 구현할 Drop 메소드는 메인 다이얼로그 창을 생성하고 파일 이름들의 목록을 여기로 전달할 것입니다. 다이얼로그 안에서 관계된 모든 작업을 수행할 것이고, DoModal 메소드가 값을 반환할 때 우리는 ‘드롭’ 작업에 대한 최종 효과를 Windows 탐색기에게 반환할 것입니다.

 

STDMETHODIMP CSendToShlExt::Drop(IDataObject * pDataObj, DWORD grfKeyState, POINTL pt, DWORD * pdwEffect) {
    AFX_MANAGE_STATE(AfxGetStaticModuleState());  // MFC 초기화
    
    CSendToCloneDlg dlg(&m_lsDroppedFiles);
    
    dlg.DoModal();
    
    *pdwEffect = DROPEFFECT_COPY;
    
    return S_OK;
}

 

다이얼로그는 다음과 같이 생겼습니다.

 

테마가 적용되지 않은 쉘 익스텐션 다이얼로그.

 

딱히 복잡하지 않은 MFC 다이얼로그입니다. 이에 대한 소스 코드는 SendToCloneDlg.cpp에서 보실 수 있습니다. 필자는 이미 “CShellFileOp - SHFileOperation의 래퍼 클래스”라는 게시글을 통해 CShellFileOp 클래스를 소개한 바 있고, 이를 사용하여 실제로 파일을 옮기거나 복사하였습니다.

2 단계에서 다루었던 쉘 익스텐션과 마찬가지로 우리는 Windows XP에서 실행되는 경우 테마 적용을 하기 위해 매니페스트(manifest)를 DLL에 리소스로서 추가할 필요가 있습니다. 그러나 이런 경우 매니페스트만을 추가하는 것만으로는 충분하지 않습니다. 왜냐하면 다이얼로그를 생성하고 관리하는 소스 코드는 MFC 내부에 있기 때문입니다.

MFC 자체가 ISOLATION_AWARE_ENABLED 심볼을 적용하여 컴파일되지 않은 관계로, 테마를 사용하기 위해 꼭 필요한 IsolationAwareXxx 래퍼를 사용할 수 없습니다.

이 상황을 잘 설명한 글로 Dave Anderson 님의 뉴스그룹 스레드가 있습니다. 이 글을 요약하자면, 우리가 다이얼로그를 보여주기 전에, Windows가 우리가 추가한 매니페스트를 사용하고 우리의 다이얼로그에 버전 6.0 공용 컨트롤을 적용할 수 있도록 우리는 activation context API를 사용해야 합니다. 이에 대한 코드는 CActCtx 클래스에 캡슐화되어 있고 Drop 메소드 안에서 사용됩니다.

 

STDMETHODIMP CSendToShlExt::Drop(...) {
    AFX_MANAGE_STATE(AfxGetStaticModuleState());  // MFC 초기화
    
    CSendToCloneDlg dlg(&m_lsDroppedFiles);
    CAxtCtx ctx;
    
    dlg.DoModal();
    *pdwEffect = DROPEFFECT_COPY;
    
    return S_OK;
}

 

이와 같이 코드를 수정하면 다이얼로그는 Windows XP 테마가 적용되어 표시될 것입니다.

 

 

잠깐! Windows 탐색기에게 우리가 만든 드롭 핸들러를 어떻게 알려줄 수 있을까요? 어떻게 하면 “보내기(Send To)” 메뉴에 새로운 항목을 추가시킬 수 있을까요? 다음 절에서 설명하겠습니다.

 

쉘 익스텐션을 등록하기

드롭 핸들러를 등록하는 것은 다른 쉘 익스텐션을 등록하는 것과는 다소 다릅니다. HKEY_CLASSES_ROOT의 하위 키로 ‘연결 프로그램’에 대한 내용을 생성해야 하기 때문입니다. AppWizard가 생성한 RGS 스크립트가 다음과 같이 있을 텐데, 표시된 부분은 여러분이 직접 추가하거나 수정할 부분을 뜻합니다.

 

HKCR {
    .SendToClone = s 'CLSID\{B7F3240E-0E29-11D4-8D3B-80CD3621FB09}' // 새로 추가
    NoRemove CLSID {
        ForceRemove {B7F3240E-0E29-11D4-8D3B-80CD3621FB09} = s 'Send To Any Folder Clone' { // 기본값 수정 가능
            InprocServer32 = s '%MODULE%' {
                val ThreadingModel = s 'Apartment'
            }
            
            val NeverShowExt = s '' // 새로 추가
            DefaultIcon = s '%MODULE%,0' // 새로 추가
            shellex { // 새로 추가
                DropHandler = s '{B7F3240E-0E29-11D4-8D3B-80CD3621FB09}' // 새로 추가
            }
        }
    }
}

 

첫 번째 줄이 ‘연결 프로그램’에 대한 것입니다.

이 줄은 새로운 확장명을 정의합니다. .SendToClone이라는 확장명은 우리가 만들 드롭 핸들러가 적용될 대상 아이콘을 만들기 위해서만 사용될 것입니다. .SendToClone 레지스트리 키의 기본 값이 CLSID\라는 접두어가 붙은 것을 주의하셔야 합니다. 이것은 Windows 탐색기에게 해당 연결 프로그램에 대한 내용이 HKCR\CLSID에 있음을 알려줍니다. 원래 이런 내용들은 보편적으로 쓰이는 확장명처럼 HKEY_CLASSES_ROOT 위치에 저장됩니다. 예를 들어 확장명 .txt에 대한 레지스트리 키는 다시 HKEY\txtfile를 가리킵니다. 그러나 모든 데이터를 한 곳에 몰아서 보관하기 위해서 드롭 핸들러에 대한 연결 프로그램 정보는 관습적으로 CLSID 레지스트리 키 위치에 보관되는 것으로 보입니다.

“Send To Any Folder Clone” 문자열은, 여러분이 SendTo 폴더를 열고 해당 확장명에 대한 0 바이트 파일을 선택했을 때, Windows 탐색기에서 파일 유형을 설명하기 위해 보여줄 문자열입니다.

NeverShowExt 값은 Windows 탐색기에게 .SendToClone의 확장명을 갖는 파일에 대해 그 확장명을 보여주어서는 안 된다고 알려주는 값입니다.

DefaultIcon 키는 .SendToClone 확장명을 갖는 파일에 대해 사용할 아이콘들의 위치를 담고 있습니다.

마지막으로 DropHandler라는 서브 키를 갖는 ShellEx 레지스트리 키가 있습니다. 해당 확장명을 갖는 파일에 대해서는 하나의 드롭 핸들러만이 존재할 것이기 때문에 DropHandler의 하위 키를 두고 그 위치에 GUID를 넣는 대신, DropHandler에 직접 GUID를 넣겠습니다.

이제 남은 것은 SendTo 폴더에 우리의 메뉴 항목이 나타나도록 파일을 생성하는 것입니다. 이 과정은 DllRegisterServer에서 수행할 수 있고, DllUnregisterServer를 통해 파일을 삭제할 수도 있습니다. 파일 생성의 예는 다음과 같습니다.

 

    // ...
    
    LPITEMIDLIST pidl;
    TCHAR        szSendtoPath[MAX_PATH];
    HANDLE       hFile;
    LPMALLOC     pMalloc;
    
    if (SUCCEEDED(SHGetSpecialFolderLocation(NULL, CSIDL_SENDTO, &pidl))) {
        if (SHGetPathFromIDList(pidl, szSendtoPath)) {
            PathAppend(szSendtoPath, _T("Some other folder.SendToClone"));
            
            hFile = CreateFile(szSendtoPath, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
            CloseHandle(hFile);
        }
        
        if (SUCCEEDED(SHGetMalloc(&pMalloc))) {
            pMalloc->Free ( pidl );
            pMalloc->Release();
        }
    }
    
    // ...

 

“보내기(Send To)” 메뉴 항목은 다음과 같이 바뀌어 있습니다.

 

[단계 6]의 쉘 익스텐션 실행 결과.

 

DllUnregisterServer는 파일을 삭제하기 위해 유사한 코드가 작성되어 있습니다. 이 코드는 모든 버전의 Windows에서 작동됩니다(물론 4.0 이상의 버전입니다).

여러분이 작성할 쉘 익스텐션이 쉘 버전 4.71 이상의 버전에서만 작동될 것으로 알고 있는 상황이라면, SHGetSprcialFolderLocation을 사용하는 대신 SHGetSpecialFolderPath 함수를 사용할 수 있습니다.

Windows NT 기반의 운영체제에서는, “승인된(approved)” 익스텐션 목록에 본 쉘 익스텐션을 추가합니다. 이 작업은 예제 프로젝트의 DllRegisterServerDllUnregisterServer 함수에 적혀있습니다.

 

다음 단계에서 다룰 내용

다음 7 단계에서, 필자는 독자들의 질문에 답하고 새로운 두 가지 유형의 컨텍스트 메뉴 익스텐션에 대해 보여줄 것입니다.

 

계속 읽기

이전 게시글: Windows 쉘 익스텐션 개발 가이드 - (5) 속성 다이얼로그 (2/2)

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

 

카테고리 “API/COM”
more...
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (5) 속성 다이얼로그 (2/2)
입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차..
API/COM
2021. 2. 9. 20:20

Windows 쉘 익스텐션 개발 가이드 - (5) 속성 다이얼로그 (2/2)

API/COM
2021. 2. 9. 20:20

입문자를 위한 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)

 

5 단계. 파일에 대한 ‘등록 정보’(또는 ‘속성’) 다이얼로그에 페이지를 추가하는 쉘 익스텐션(Shell Extension)

이전 파트에 이어서...

 

프로퍼티 페이지 콜백 함수

이제 프로퍼티 페이지 그 자체에 대해 살펴봅시다. 새로 추가될 페이지는 다음과 같이 생겼습니다. 이 모양을 잘 기억해 두었다가, 페이지가 어떻게 작동되는지를 설명할 때 참고하시기 바랍니다.

 

우리가 만들 쉘 익스텐션이 생성하는 탭

 

마지막으로 액세스된 시각 항목이 없을 수도 있습니다. FAT 파일 시스템만이 마지막으로 액세스된 날짜 항목을 기록하고 있습니다(번역자 주: FAT 파일 시스템은 시각까지는 기록하지 않는다는 의미입니다). 다른 파일 시스템은 시각까지 기록을 합니다만, 필자는 파일 시스템에 따라 이를 다르게 처리하는 기능까지는 구현하지 않겠습니다. 마지막으로 액세스된 시각까지 기록할 수 있는 파일 시스템에서 시각은 항상 사용자가 지정한 날짜의 자정으로 기록될 것입니다.

추가되는 페이지는 두 개의 콜백 함수와 두 개의 메시지 핸들러를 가지고 있습니다. 이들의 원형은 FileTimeShlExt.cpp의 상단에 적혀있습니다.

 

BOOL CALLBACK PropPageDlgProc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
UINT CALLBACK PropPageCallbackProc(HWND hwnd, UINT uMsg, LPPROPSHEETPAGE ppsp);
BOOL OnInitDialog(HWND hwnd, LPARAM lParam);
BOOL OnApply(HWND hwnd, PSHNOTIFY * phdr);

 

이 다이얼로그 프로시저는 매우 단순해서 WM_INITDIALOG, PSN_APPLY, DTN_DATETIMECHANGE라는 단 세 개의 메시지만을 처리합니다. 그 중 WM_INITDIALOG 부분은 다음과 같이 생겼습니다.

 

BOOL CALLBACK PropPageDlgProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    BOOL bRet = FALSE;
    
    switch(uMsg) {
    case WM_INITDIALOG:
        bRet = OnInitDialog(hwnd, lParam);
        break;
    
    // ...

 

이어서 PSN_APPLY에 대한 이벤트 처리입니다. 사용자가 [확인] 또는 [적용] 버튼을 누를 때 이 이벤트가 전달됩니다.

 

    // ...
    
    case WM_NOTIFY: {
        NMHDR* phdr = (NMHDR *)lParam;
        
        switch (phdr->code) {
        case PSN_APPLY:
            bRet = OnApply(hwnd, (PSHNOTIFY *)phdr);
            break;
        // ...

 

마지막으로 DTN_DATETIMECHANGE가 있습니다. 이것은 간단합니다. 우리가 만들고 있는 페이지의 부모 윈도우인 프로퍼티 시트에 메시지를 보내서 [적용] 버튼을 활성화시키는 것입니다.

 

            // ...
            
            case DTN_DATETIMECHANGE:
                // 사용자가 DTP 컨트롤의 값을 변경하였다면,
                // [적용] 버튼을 활성화시키도록 부모 윈도우에게 메시지를 보냅니다.
                SendMessage(GetParent(hwnd), PSM_CHANGED, (WPARAM)hwnd, 0);
                break;
            }  // switch 끝
        }  // case WM_NOTIFY의 끝
        break;
    }  // switch의 끝
    
    return bRet;
}

 

지금까지는 순조로웠습니다. 페이지가 생성되거나 파괴될 때 또 다른 콜백 함수가 호출됩니다. AddPages에서 생성된 문자열 복사본을 해제해야 하므로 우리는 후자에 대해 다루면 됩니다. ppsp 매개 변수(parameter)는 페이지를 생성하는 데 사용했던 PROPSHEETPAGE 구조체를 가리키고 있습니다. 그리고 lParam 멤버는 해제되어야 할 문자열 복사본을 여전히 가리키고 있습니다.

 

UINT CALLBACK PropPageCallbackProc(HWND hwnd, UINT uMsg, LPPROPSHEETPAGE ppsp) {
    if (uMsg == PSPCB_RELEASE)
        free((void *)ppsp->lParam);
    return 1;
}

 

이 함수는 페이지가 생성될 때 호출될 때 호출되기 때문에 항상 1을 반환합니다. 0을 반환하면 페이지가 생성되지 않을 수 있습니다. 1을 반환하는 것은 페이지에게 자기자신이 정상적으로 생성되고 있음을 알려줍니다. 페이지가 파괴될 때 호출될 때는 이 함수의 반환 값이 무엇이든 되었든 무시됩니다.

 

프로퍼티 페이지 메시지 핸들러

OnInitDialog에서 중요한 작업들이 많이 발생했습니다. lParam 매개 변수(parameter)는 본 페이지를 생성하는데 사용된 PROPSHEETPAGE 구조체를 가리킵니다. 이것의 lParam 멤버는 앞서 말한 그 문자열 복사본을 가리킵니다. 우리는 OnApply 메서드에서 파일 이름이 필요할 것이라고 했는데, 그 파일 이름을 SetWindowLong을 호출하여 윈도우에 저장합니다.

 

BOOL OnInitDialog(HWND hwnd, LPARAM lParam) {        
    PROPSHEETPAGE * ppsp = (PROPSHEETPAGE*) lParam;
    LPCTSTR         szFile = (LPCTSTR) ppsp->lParam;
    HANDLE          hFind;
    WIN32_FIND_DATA rFind;
    
    // 나중에 사용하기 위하여, 파일 이름에 대한 포인터를 윈도우 사용자 데이터에 보관합니다.
    SetWindowLong(hwnd, GWL_USERDATA, (LONG)szFile);
    
    // ...

 

다음으로 우리는 FindFirstFile을 사용하여 특정 파일에 대한 생성된 날짜 및 시각, 수정된 날짜 및 시각 그리고 마지막으로 액세스된 날짜 및 시각을 가져옵니다. 가져오는 데 성공하였다면 DTP 컨트롤은 이들 날짜 및 시각으로 초기화될 것입니다.

 

    // ...
    hFind = FindFirstFile(szFile, &rFind);
    
    if (hFind != INVALID_HANDLE_VALUE) {
        // DTP 컨트롤들을 초기화합니다.
        SetDTPCtrl(hwnd, IDC_MODIFIED_DATE, IDC_MODIFIED_TIME, &rFind.ftLastWriteTime);
        SetDTPCtrl(hwnd, IDC_ACCESSED_DATE, 0, &rFind.ftLastAccessTime);
        SetDTPCtrl(hwnd, IDC_CREATED_DATE, IDC_CREATED_TIME, &rFind.ftCreationTime);
        
        FindClose(hFind);
    }
    // ...

 

SetDTPCtrl은 DTP 컨트롤의 내용을 설정하는 유틸리티 함수입니다. 여러분은 이 함수에 대한 코드를 FileTimeShlExt.cpp에서 확인하실 수 있습니다.

추가적으로 페이지의 상단에 있는 스태틱(static) 컨트롤을 통해 파일에 대한 전체 경로가 나타날 것입니다.

 

    // ...
    PathSetDlgItemPath(hwnd, IDC_FILENAME, szFile);
    
    return FALSE;
}

 

OnApply 핸들러는 이와 반대의 작동을 합니다. 즉, DTP 컨트롤의 값을 읽고 파일에 대해 생성된 날짜 및 시각, 수정된 날짜 및 시각 그리고 마지막으로 수정된 날짜 및 시각을 고칩니다. 이에 대한 첫 번째 단계로 GetWindowLong을 사용하여 고칠 파일에 대한 전체 경로가 담긴 문자열 포인터를 얻습니다.

 

BOOL OnApply(HWND hwnd, PSHNOTIFY * phdr) {
    LPCTSTR  szFile = (LPCTSTR)GetWindowLong(hwnd, GWL_USERDATA);
    HANDLE   hFile;
    FILETIME ftModified, ftAccessed, ftCreated;
    
    // 파일을 엽니다.
    hFile = CreateFile(szFile, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    // ...

 

파일을 열 수 있다면, DTP 컨트롤로부터 날짜와 시각들을 읽어서 파일을 고칩니다. ReadDTPCtrlSetDTPCtrl과 반대의 작동(DTP 컨트롤의 현재 값으로 파일을 수정)을 합니다.

 

        if (hFile != INVALID_HANDLE_VALUE) {
        // DTP 컨트롤에서 날짜와 시각을 가져옵니다.
        ReadDTPCtrl(hwnd, IDC_MODIFIED_DATE, IDC_MODIFIED_TIME, &ftModified);
        ReadDTPCtrl(hwnd, IDC_ACCESSED_DATE, 0, &ftAccessed);
        ReadDTPCtrl(hwnd, IDC_CREATED_DATE, IDC_CREATED_TIME, &ftCreated);
        
        // 파일의 생성된 날짜 및 시각, 수정된 날짜 및 시각, 마지막으로 액세스된 날짜 및 시각을 고칩니다.
        SetFileTime(hFile, &ftCreated, &ftAccessed, &ftModified);
        
        CloseHandle ( hFile );
    } else {
        // 오류 처리 부분은 생략합니다.
    }
    
    // 사용자가 [확인] 버튼을 클릭하였을 때 창이 닫힐 수 있도록 PSNRET_ERROR를 반환합니다.
    SetWindowLong(hwnd, DWL_MSGRESULT, PSNRET_NOERROR);
    
    return TRUE;
}

 

쉘 익스텐션을 등록하기

프로퍼티 시트 익스텐션을 등록하는 것은 컨텍스트 메뉴 익스텐션 또는 드래그 앤 드롭 익스텐션을 등록하는 것과 비슷합니다. 앞서 실습한 쉘 익스텐션들은 텍스트 파일과 같은 특정 파일에 대해서만 호출되도록 등록할 수 있었습니다만, 이 프로퍼티 시트 익스텐션은 임의의 파일에 대해서 작동되도록 만들었기 때문에 HKEY_CLASSES_ROOT\* 키의 하위키로 등록해 보겠습니다. 본 쉘 익스텐션을 등록하기 위한 RGS 스크립트는 다음과 같이 생겼습니다.

 

HKCR {
    NoRemove * {
        NoRemove shellex {
            NoRemove PropertySheetHandlers {
                {3FCEF010-09A4-11D4-8D3B-D12F9D3D8B02}
            }
        }
    }
}

 

여러분은 본 쉘 익스텐션의 GUID가 문자열 형태의 기본 값이 아니라, 레지스트리 키의 이름으로서 등록되는 것을 확인하실 수 있습니다. 필자가 확인한 개발문서 및 관련 서적들에서는 다소 논쟁이 존재하기는 하지만, 필자가 테스트를 해 보았을 때 둘 다 작동을 하였습니다. 때문에 필자는 Dino Esposito님의 서적인 “Visual C++ Windows Shell Programming”에서 설명하고 있는 방식에 따르겠습니다. 레지스트리 키의 이름으로 본 쉘 익스텐션의 GUID를 지정합니다.

Windows NT 기반의 운영체제에서는, “승인된(approved)” 익스텐션 목록에 본 쉘 익스텐션을 추가합니다. 이 작업은 예제 프로젝트의 DllRegisterServerDllUnregisterServer 함수에 적혀있습니다.

 

다음 단계에서 다룰 내용

6 단계에서 우리는 또 다른 형태의 쉘 익스텐션인 드롭 핸들러에 대해 다루어 보겠습니다. 이것은 쉘 개체가 파일 속으로 ‘드롭’될 때 호출되는 쉘 익스텐션입니다.

 

계속 읽기

이전 게시글: Windows 쉘 익스텐션 개발 가이드 - (5) 속성 다이얼로그 (1/2)

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

 

카테고리 “API/COM”
more...
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (5) 속성 다이얼로그 (1/2)
입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차..
API/COM
2021. 2. 9. 19:18

Windows 쉘 익스텐션 개발 가이드 - (5) 속성 다이얼로그 (1/2)

API/COM
2021. 2. 9. 19:18

입문자를 위한 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)

 

5 단계. 파일에 대한 ‘등록 정보’(또는 ‘속성’) 다이얼로그에 페이지를 추가하는 쉘 익스텐션(Shell Extension)

실습 프로젝트 다운로드

ShellExtGuide5_demo.zip
60.8 kB

 

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

프로퍼티 시트의 세계로 모험해 보겠습니다. 여러분이 파일 시스템 개체에 대해 등록정보(또는 속성) 다이얼로그를 띄웠을 때 Windows 탐색기는 “일반”이라는 이름이 붙은 탭과 함께 프로퍼티 시트를 보여줄 것입니다. 이 때 프로퍼티 시트 핸들러(property sheet handler)라고 하는 쉘 익스텐션을 사용하면 프로퍼티 시트에 페이지를 추가할 수 있습니다.

이 단계에서 필자는 여러분이 쉘 익스텐션의 기본을 이해하고 있고, STL 콜렉션 클래스에 대해 익숙할 것이라 가정하겠습니다. STL에 대해 다시 익히고자 하는 분은 이 단계에서 쓰이는 것과 같은 기법이 쓰이는 3 단계을 참고하시기 바랍니다.

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

Windows 사용자라면 Windows 탐색기의 등록정보(또는 속성) 다이얼로그가 친숙하실 것입니다. 특히 프로퍼티 시트(property sheet)는 하나 이상의 페이지를 포함할 수 있습니다. 각 프로퍼티 시트는 “일반” 이라는 탭 속에 전체 경로, 수정된 날짜 기타 사항들을 나열하고 있습니다. Windows 탐색기는 또한 우리가 프로퍼티 시트 핸들러 익스텐션(property sheet handler extension)을 사용하여 페이지를 직접 추가할 수 있게 해주고 있습니다.

프로퍼티 시트 핸들러는 또한 특정 제어판 항목에서 페이지를 추가 또는 교체할 수 있는데, 이번 단계에서는 그러한 내용을 다루지 않겠습니다. 제어판 항목을 확장하는 것에 관심있는 분은 필자의 또 다른 글인 “제어판 항목에 사용자 페이지 추가하기”를 읽어보시기 바랍니다.

이번 단계에서는 특정 파일에 대한 등록정보(또는 속성) 다이얼로그에서 사용자가 직접 생성된 날짜, 수정된 날짜, 마지막으로 액세스한 날짜 등을 직접 수정할 수 있는 쉘 익스텐션을 설명합니다. 또한 필자는 이번에는 MFC나 ATL 사용 없이 SDK를 직접 호출함으로써 프로퍼티 페이지를 다루겠습니다. 필자는 쉘 익스텐션에서 MFC 또는 WTL 프로퍼티 페이지를 사용하지 않았기 때문에, 이와 같은 방식이 까다로워 보일 수 있습니다.

왜냐하면 Windows 탐색기는 프로퍼티 시트가 HPROPSHEETPAGE라는 핸들을 받을 수 있을 것으로 알고 있고, MFC는 CPropertyPage 구현에서 그러한 작업을 사용자에게 감추어왔기 때문입니다.

여러분이 인터넷 바로가기 파일인 .url 파일에 대해 등록정보(또는 속성) 다이얼로그를 띄웠을 때, 여러분은 프로퍼티 시트 핸들러가 작동함을 보실 수 있습니다. “라디오 방송국 안내”라는 이름의 탭이 이번 단계의 쉘 익스텐션으로 인해 등장했습니다. 또한 “웹 문서”라는 이름의 탭은 Internet Explorer가 설치되어 있음으로 인해 나타나는 탭입니다.

 

Internet Explorer가 생성한 ‘웹 문서’ 탭.

 

초기화 인터페이스

이제 여러분은 지금까지 설명했던 프로젝트 생성 및 클래스 추가 등의 과정에 대해서는 익숙해 지셨을 것입니다. 그러므로 필자는 Visual C++ 마법사에 대한 설명은 생략하겠습니다. 새로운 ATL COM 프로젝트를 생성하고 그 이름을 FileTime으로 지정합니다. 또한 CFileTimeShlExt라는 이름으로 C++ 클래스를 추가합니다.

프로퍼티 시트 핸들러는 선택된 다수의 파일들에 대해 한번에 작동될 수 있으므로 초기화 인터페이스로 IShellExtInit를 사용합니다. 따라서 우리는 CFileTimeShlExt 클래스가 구현하는 인터페이스 목록에 IShellExtInit를 추가합니다. 다시 반복하지만 지금부터 필자는 이러한 단계에 여러분께서 이제 익숙해지셨을 것이라 생각하여 세세한 과정을 반복 설명하지는 않겠습니다.

이 클래스는 또한 선택된 파일들의 이름을 보관할 수 있는 문자열 리스트가 필요합니다.

 

typedef list<basic_string<TCHAR>> string_list;
// ...
protected:
    string_list m_lsFiles;
// ...

 

Initialize 메소드는 2 단계에서 구현했던 내용(선택된 파일들의 이름을 하나씩 읽어서 문자열 리스트에 보관하는 과정)과 동일합니다. 이 메소드는 다음과 같이 시작합니다.

 

STDMETHODIMP CFileTimeShlExt::Initialize(LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hProgID) {
    TCHAR     szFile[MAX_PATH];
    UINT      uNumFiles;
    HDROP     hdrop;
    FORMATETC etc = {CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL};
    STGMEDIUM stg;
    INITCOMMONCONTROLSEX iccex = {sizeof(INITCOMMONCONTROLSEX), ICC_DATE_CLASSES};
    
    // 공용 컨트롤들을 초기화합니다.
    InitCommonControlsEx(&iccex);
    
    // ...

 

우리가 새로 만들 페이지에는 날짜/시간 선택자(DTP: date/time picker) 컨트롤이 포함되어 있기 때문에 공용 컨트롤을 초기화해 주었습니다. 그 다음 우리는 IDataObject 인터페이스 구현과 씨름해야 하고, 선택된 각 파일들을 하나씩 순회할 수 있는 HDROP 핸들도 얻습니다.

 

    // ...
    
    // 데이터 오브젝트로부터 목록을 읽은 후 HDROP 형식으로 보관합니다.
    // 그 다음 HDROP에 대해 드래그 앤 드롭 API를 사용할 것입니다.
    if (FAILED( pDataObj->GetData(&etc, &stg)))
        return E_INVALIDARG;
    
    // HDROP 핸들을 얻습니다.
    hdrop = (HDROP)GlobalLock(stg.hGlobal);
    
    if (hdrop == NULL) {
        ReleaseStgMedium(&stg);
        return E_INVALIDARG;
    }
    
    // 몇 개의 파일들이 선택되었는지 확인합니다.
    uNumFiles = DragQueryFile(hdrop, 0xFFFFFFFF, NULL, 0);
    
    // ...

 

다음으로 선택된 파일들을 실제로 하나씩 순회하는 루프가 등장합니다. 이번 쉘 익스텐션은 디렉토리가 아닌 파일에만 작동될 것이므로 루프 도중 만나게 되는 모든 디렉토리는 무시될 것입니다.

 

    // ...
    
    for (UINT uFile = 0; uFile < uNumFiles; uFile++) {
        // 파일 이름을 하나씩 읽습니다.
        if (DragQueryFile(hdrop, uFile, szFile, MAX_PATH))
            continue;
        
        // 디렉토리 여부를 확인하여 건너뜁니다.
        // 물론 디렉토리도 날짜와 시각을 수정할 수는 있지만,
        // 필자는 이번 예제에서 이에 대해 다루지 않겠습니다.
        if (PathIsDirectory(szFile))
            continue;
        
        // 작업을 수행할 파일 리스트에 현재의 파일 이름을 추가합니다.
        m_lsFiles.push_back ( szFile );
    }  // for 루프의 끝
    
    // 리소스를 해제합니다.
    GlobalUnlock(stg.hGlobal);
    ReleaseStgMedium(&stg);
    
    // ...

 

문자열 리스트에서 파일 이름들을 하나씩 순회하는 것은 이전 단계에서 했던 것과 동일합니다. 그러나 한 가지 새로운 것이 등장합니다. 프로퍼티 시트는 자신이 가질 수 있는 페이지의 최대 수가 제한되어 있습니다. 이는 prsht.hMAXPROPPAGES라는 상수로 정의되어 있습니다. 각 파일들은 자신만의 페이지를 가질 수 있지만, 파일 이름들을 포함하는 문자열 리스트가 MAXPROPPAGES 이상인 경우 넘어가는 부분은 잘라내어 MAXPROPPAGES에서 지정한 개수에 맞출 것입니다. 물론 현재는 MAXPROPPAGES100으로 되어 있지만, 프로퍼티 시트가 그렇게까지 많은 페이지를 보여주지는 않습니다. 실질적으로 34개 전후가 최대입니다.

 

    // ...
    
    // 몇 개의 파일들이 선택되었는지 검사합니다.
    // 프로퍼티 페이지의 최대 개수보다 더 크면 문자열 리스트를 잘라냅니다.
    if (m_lsFiles.size() > MAXPROPPAGES)
        m_lsFiles.resize(MAXPROPPAGES);
    
    // 우리가 작업할 수 있는 파일이 하나라도 있다면 S_OK를 반환합니다.
    // 그렇지 않다면 E_FAIL을 반환하여 이번 마우스 오른쪽 클릭에 대해서는,
    // 다시 호출되는 일이 없게 합니다.
    return (m_lsFiles.size() > 0) ? S_OK : E_FAIL;
}

 

프로퍼티 페이지 추가하기

Initialize 메소드가 S_OK를 반환하면 Windows 탐색기는 새롭게 등장하는 인터페이스인 IShellPropSheetExt에 대한 포인터를 요구합니다.

IShellPropSheet는 다소 단순한 편이어서 구현이 필요한 메소드는 딱 하나면 됩니다. FileTiimeShlExt.h를 열고 IShellPropSheetExt 인터페이스를 추가하기 위해 다음과 같이 내용을 추가합니다.

 

class CFileTimeShlExt :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CFileTimeShlExt, &CLSID_FileTimeShlExt>,
    public IShellExtInit,
    public IShellPropSheetExt { // 새로 추가
    
    BEGIN_COM_MAP(CFileTimeShlExt)
        COM_INTERFACE_ENTRY(IShellExtInit)
        COM_INTERFACE_ENTRY(IShellPropSheetExt)
    END_COM_MAP()
    
    public:
        // IShellPropSheetExt
        STDMETHODIMP AddPages(LPFNADDPROPSHEETPAGE, LPARAM); // 새로 추가
        STDMETHODIMP ReplacePage(UINT, LPFNADDPROPSHEETPAGE, LPARAM) { return E_NOTIMPL; } // 새로 추가

 

AddPage 메소드가 우리가 구현할 메소드입니다. ReplacePage 메소드는 제어판 항목에서 페이지들을 교체하는 쉘 익스텐션에서만 사용되므로 우리는 이것을 여기서 구현할 필요는 없습니다. Windows 탐색기는 구성할 등록 정보(또는 속성) 창에 페이지를 추가할 수 있도록 AddPages 메소드를 호출할 것입니다.

AddPages가 호출될 때 전달되는 매개 변수(parameter)는 함수 포인터와 LPARAM이고 둘 다 쉘이 사용합니다. lpfnAddPageProc은 우리가 실질적으로 페이지를 추가하기 위해 호출하게 될 쉘 내부의 함수를 가리키는 포인터입니다. lParam은 쉘에게 매우 중요하지만 수수께끼 같은 값입니다. 따라서 우리는 이 값을 변경하지 않고 전달받은 그대로 lpfnAddPageProc이 가리키는 함수에 전달만 하겠습니다.

 

STDMETHODIMP CFileTimeShlExt::AddPages(LPFNADDPROPSHEETPAGE lpfnAddPageProc, LPARAM lParam) {
    PROPSHEETPAGE  psp;
    HPROPSHEETPAGE hPage;
    TCHAR          szPageTitle[MAX_PATH];
    string_list::const_iterator it, itEnd;
    
    for (it = m_lsFiles.begin(), itEnd = m_lsFiles.end(); it != itEnd; it++) {
        // 반복자 it는 리스트에서 파일 이름을 하나씩 가져옵니다.
        // 프로퍼티 페이지가 소유할 수 있도록 해당 문자열의 복사본을 새로 할당합니다.
        LPCTSTR szFile = _tcsdup(it->c_str());
        
    // ...

 

먼저 파일 이름에 대한 복사본을 생성합니다. 그 이유는 조금 있다가 설명하겠습니다.

다음으로 우리가 추가할 페이지의 탭에 대한 제목 문자열을 구성합니다. 이 문자열은 파일 이름을 그대로 가져와서 써도 됩니다. 참고로 24자가 넘으면 자동으로 문자열이 잘릴 수 있는데 이 문자수의 기준은 임의적이어서 필자가 테스트해 본 바로는 24자까지는 괜찮게 보였습니다. 그렇지만 글자가 탭의 경계를 벗어나서 보여지지 않도록 글자수를 제한할 필요는 있습니다.

 

    // ...
    
    // 파일 이름으로부터 상위 경로와 확장명을 제거합니다.
    // 이 문자열은 페이지의 제목이 될 것이고, 
    // 길이 또한 탭의 너비에 맞추어 24자로 제한합니다.
    lstrcpyn(szPageTitle, it->c_str(), MAX_PATH);
    PathStripPath(szPageTitle);
    PathRemoveExtension(szPageTitle);
    szPageTitle[24] = '\0';
    
    // ...

 

프로퍼티 페이지를 만들고 추가하기 위하여 SDK를 직접 호출할 것이기 때문에 우리는 PROPSHEETPAGE 구조체를 가지고 손에 기름때를 묻혀가며 다소 지저분한 작업을 해야 합니다. 구조체를 구성하는 예는 다음과 같습니다.

 

    // ...
    
    psp.dwSize      = sizeof(PROPSHEETPAGE);
    psp.dwFlags     = PSP_USEREFPARENT | PSP_USETITLE | PSP_USEICONID | PSP_USECALLBACK;
    psp.hInstance   = _Module.GetResourceInstance();
    psp.pszTemplate = MAKEINTRESOURCE(IDD_FILETIME_PROPPAGE);
    psp.pszIcon     = MAKEINTRESOURCE(IDI_TAB_ICON);
    psp.pszTitle    = szPageTitle;
    psp.pfnDlgProc  = PropPageDlgProc;
    psp.lParam      = (LPARAM) szFile;
    psp.pfnCallback = PropPageCallbackProc;
    psp.pcRefParent = (UINT *)&_Module.m_nLockCnt;
    
    // ...

 

정확한 작업을 위해 우리가 주의해야 하는 몇 가지 중요한 사항들이 있습니다.

1. pszIcon은 탭 제목과 함께 보여질 16 * 16 크기의 아이콘에 대한 리소스 ID입니다. 물론 아이콘을 지정하는 것은 선택적인 사항이지만, 우리가 추가할 페이지가 돋보이도록 필자는 아이콘을 추가하겠습니다.

2. pfnDlgProc은 우리가 추가할 페이지의 다이얼로그 프로시저에 대한 주소입니다.

3. lParam은 지금 추가하고 있는 페이지와 관계된 파일의 이름이자 직전에 생성했던 복사본인 szFile을 직접 지정합니다.

4. pfnCallback은 페이지가 생성되거나 파괴될 때 호출되는 콜백 함수의 주소입니다. 이 함수의 역할은 잠시 후 설명하겠습니다.

pcRefParentCComModule에서 상속된 어떤 클래스의 멤버 변수에 대한 주소입니다. 사실 이 값은 DLL이 잠긴 횟수입니다. 프로퍼티 시트가 보여질 때마다 쉘은 이 값을 하나씩 증가시킴으로써 프로퍼티 시트가 보여지는 동안 DLL이 메모리에서 해제되는 것을 방지해 줍니다. 프로퍼티 시트가 파괴되면 이 값도 하나씩 감소합니다.

 

구조체의 구성이 완료되면 우리는 프로퍼티 페이지를 생성하기 위해 API를 호출합니다.

 

    // ...
    
    hPage = CreatePropertySheetPage(&psp);
    
    // ...

 

위 함수의 호출이 성공하면 우리는 쉘에서 제공한 콜백 함수를 호출하여 프로퍼티 시트에 새로운 페이지를 추가합니다. 이 콜백 함수는 성패 여부에 따라 BOOL을 반환할 것인데, 만일 실패하면 지금까지 만들었던 프로퍼티 시트 페이지를 파괴해야 합니다.

 

    // ...
        
        if (hPage != NULL) {
            // 프로퍼티 시트에 페이지를 추가하도록, 쉘에서 제공한 콜백 함수를 호출합니다.
            if (!lpfnAddPageProc(hPage, lParam))
                DestroyPropertySheetPage(hPage);
        }
    }  // for 루프 종료
    
    return S_OK;
}

 

객체의 수명주기를 고려하여 세심하게 고려해 주어야 할 상황

이제 파일 이름을 나타내는 문자열에 대해 왜 복사본을 만들었는지 설명할 차례가 되었습니다. AddPages가 값을 반환할 때, 쉘은 IShellPropSheetExt 인터페이스에 대한 참조 횟수를 감소시킵니다. 따라서 CFileTimeShlExt 클래스형 객체가 파괴될 수 있는데 이는 프로퍼티 페이지를 포함하고 있는 다이얼로그 및 이것을 다루는 다이얼로그 프로시저가 CFileTimeShlExt의 멤버인 m_lsFiles에 접근할 수 없음을 뜻합니다.

필자의 해법은 각 파일 이름에 대한 복사본을 생성하여 페이지마다 그 포인터를 전달하는 것이었습니다. 대신 각 페이지는 자신만의 메모리를 소유하고 있기 때문에, 다이얼로그가 파괴될 때 이 복사본을 직접 할당 해제해 주어야 합니다.

선택된 파일이 하나 이상일 때, 각 페이지는 자신과 관계된 파일 이름에 대한 복사본을 얻습니다. 그리고 이 복사본은 이어서 설명할 PropPageCallbackProc 함수에서 해제됩니다.

그렇기 때문에 AddPages 메소드에서 다음과 같은 부분은 중요합니다.

 

psp.lParam = (LPARAM) szFile;

 

이 문장은 복제된 문자열에 대한 포인터를 PROPSHEETPAGE 구조체에 보관하고, 페이지에 대한 다이얼로그 프로시저가 이를 사용할 수 있게 합니다.

 

계속 읽기

이전 게시글: Windows 쉘 익스텐션 개발 가이드 - (4) 드래그 앤 드롭

다음 게시글: Windows 쉘 익스텐션 개발 가이드 - (5) 속성 다이얼로그 (2/2)

 

카테고리 “API/COM”
more...
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (4) 드래그 앤 드롭
입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차..
API/COM
2021. 1. 28. 11:58

Windows 쉘 익스텐션 개발 가이드 - (4) 드래그 앤 드롭

API/COM
2021. 1. 28. 11:58

입문자를 위한 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)

 

4 단계. 사용자 정의 ‘드래그 앤 드롭(Drag and Drop)’ 기능을 제공하는 쉘 익스텐션(Shell Extension)

실습 프로젝트 다운로드:

ShellExtGuide4_demo.zip
89.2 kB

 

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

본 가이드의 1 단계 및 2 단계에서, 필자는 특정 형식의 파일에 대해 마우스 오른쪽 버튼을 클릭하였을 때 호출될 컨텍스트 메뉴 익스텐션을 작성하는 방법에 대해 보여드렸습니다. 이번 단계에서, 필자는 약간 다른 형태의 컨텍스트 메뉴 확장인 드래그 앤 드롭 핸들러(drag and drop handler)에 대해 다루고자 합니다. 이것은 사용자가 마우스 오른쪽 버튼을 누르고 드래그 앤 드롭을 하였을 때 컨텍스트 메뉴에 나타나는 항목을 확장하는 것입니다. 또한 필자는 본 단계가 다루는 예제에서 보다 많은 MFC 사용 예를 보여드리겠습니다.

4 단계는 여러분이 쉘 익스텐션의 기본을 익혔고 MFC에 친숙한 것으로 간주합니다. 이번 단계에서 다룰 쉘 익스텐션은 Windows 2000 이후의 운영체제에서 하드 링크를 생성하는 실제 유틸리티입니다. 그러나 이전 버전의 윈도우 운영체제를 사용하고 있다 하더라도 이 단계의 내용을 따라오실 수 있습니다.

파워 유저들 및 몇몇 일반 유저들도 알겠지만, Windows 탐색기에서 마우스 오른쪽 버튼을 눌러서 드래그 앤 드롭을 할 수 있습니다. 이 때 버튼을 떼면, Windows 탐색기는 여러분이 취할 수 있는 동작의 목록을 컨텍스트 메뉴를 통해 보여줍니다. 일반적으로 이런 동작에는 이동, 복사 및 바로가기 만들기가 있습니다.

 

드래그 인 드롭 핸들러의 작동 (1)

 

Windows 탐색기는 드래그 앤 드롭 핸들러를 사용하여 우리가 컨텍스트 메뉴에 항목을 추가할 수 있도록 합니다. 이 유형의 쉘 익스텐션은 사용자가 마우스 오른쪽 버튼으로 드래그 앤 드롭했을 때 호출되고, 이 때 쉘 익스텐션은 제공할 수 있는 작업들에 대한 항목을 컨텍스트 메뉴에 추가합니다. 드래그 앤 드롭 핸들러를 제공하는 예로 WinZip이 있습니다. 압축 파일을 마우스 오른쪽 버튼으로 드래그하였을 때 WinZip이 컨텍스트 메뉴에 추가하는 항목은 다음과 같습니다.

 

드래그 인 드롭 핸들러의 작동 (2)

 

이와 같은 경우 WinZip이 제공하는 쉘 익스텐션은 마우스 오른쪽 버튼으로 드래그 앤 드롭 동작을 했을 때 나타나지만, 압축파일이 드래그되고 있을 때에 한합니다.

이번 포스트에서는 NTFS 볼륨에 있는 파일에 대해 하드링크를 생성할 목적으로, Windows 2000에서 추가된 API인 CreateHardLink를 사용해보겠습니다. 우리는 보통의 바로가기를 만드는 것과 같은 방식으로 사용자가 하드링크를 만들 수 있도록 컨텍스트 메뉴에 하드링크를 만드는 항목을 하나 추가해 보겠습니다.

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

 

AppWizard를 사용하여 시작하기

AppWizard를 실행하고 새로운 ATL COM 프로젝트를 생성합니다. 이 프로젝트를 HardLink라고 명명하겠습니다. 우리는 MFC를 사용할 것이기 때문에 이전과 마찬가지로 “Support MFC” 체크상자에 체크를 하고 [Finish]를 클릭합니다.

DLL에 COM 개체를 추가하기 위해 “클래스 뷰(Class View)” 화면으로 이동하여 “HardLink classes” 트리 항목에 대해 마우스 오른쪽 버튼을 클릭하고 [New ATL Object] 항목을 클릭합니다. Visual C++ 7.0 이상의 버전에서는 해당 트리 항목에 대해 마우스 오른쪽 버튼을 클릭하고 “추가(D)”→”클래스 추가(C)...”를 클릭합니다. 영어 버전의 Visual Studio에서는 “Add”→”Add Class” 메뉴입니다. 이전과 같이 마법사 화면에서 “Simple Object”를 선택하고, 개체의 이름을 HardLinkShlExt으로 정합니다. 이 마법사는 쉘 익스텐션을 구현하게 될 CHardLinkShlExt 이름의 C++ 클래스를 생성할 것입니다.

 

초기화 인터페이스

앞서 실습해 보았던 컨텍스트 메뉴 확장과 마찬가지로, Windows 탐색기는 IShellExtInit 인터페이스를 사용하여 우리를 호출할 것입니다. 먼저 우리는 CHardLinkShlExt가 구현하는 인터페이스에 IShellExtInit를 추가할 필요가 있습니다. HardLinkShlExt.h를 열고 다음과 같이 줄을 추가합니다.

 

#include <comdef.h>
#include <shlobj.h>
 
class CHardLinkShlExt :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CHardLinkShlExt, &CLSID_HarkLinkShlExt>,
    public IShellExtInit { // 새로 추가
    
    BEGIN_COM_MAP(CHardLinkShlExt)
        COM_INTERFACE_ENTRY(IShellExtInit) // 새로 추가
    END_COM_MAP()
    
    public:
    // IShellExtInit
        STDMETHODIMP Initialize(LPCITEMIDLIST, LPDATAOBJECT, HKEY); // 새로 추가
    
    // ...

 

 

비트맵을 보관할 변수와 마우스 오른쪽 버튼으로 드래그되고 있는 파일들의 이름을 보관할 변수도 필요합니다.

 

    // ...
    
    protected:
        Bitmap     m_bitmap;
        TCHAR       m_szFolderDroppedIn[MAX_PATH];
        CStringList m_lsDroppedFiles;
    
    // ...

 

우리는 또한 shlwapi.dll에 있는 함수 원형이 나타나게 해서 CreateHarkLink를 사용할 수 있게 해야 하므로, stdafx.h에 몇 가지 #define 구문을 추가합니다.

 

#define WINVER 0x0500
#define _WIN32_WINNT 0x0500
#define _WIN32_IE 0x0400

 

0x0500 값을 갖는 매크로 상수 WINVER를 선언하는 것은 Windows 98 및 Windows 2000에 대해 특화된 기능들을 사용 가능하게 만들어 줍니다.

0x0500 값을 갖는 매크로 상수 _WIN32_WINNT를 선언하는 것은 Windows 2000에 대해 특화된 Windows NT 기능들을 사용 가능하게 만들어 줍니다.

0x0400 값을 갖는 매크로 상수 _WIN32_IE를 선언하는 것은 Internet Explorer 4.0에서 도입된 기능들을 사용 가능하게 만들어 줍니다.

이제 Initialize 메소드를 봅시다. 이번 시간에 필자는 현재 마우스 오른쪽 버튼으로 드래그되고 있는 파일들의 목록을 MFC에서 액세스하여 사용하는 방법에 대해 보여드릴 것입니다.

MFC는 IDataObject 인터페이스의 래퍼(wrapper) 클래스인 COleDataObject라는 클래스를 가지고 있습니다. 이전까지 우리는 IDataObject의 메소드를 직접 호출해야했지만, 다행스럽게도 MFC는 이러한 작업을 보다 쉽게 지원해 줍니다. 여러분이 다루는 메모리를 리셋할 때 사용될 Initialize 메소드의 원형은 다음과 같습니다.

 

 

HRESULT IShellExtInit::Initialize(
    LPCITEMIDLIST pidlFolder,
    LPDATAOBJECT  pDataObj,
    HKEY          hProgID);

 

드래그 앤 드롭을 확장할 때, pidlFolder는 선택된 항목들이 ‘드롭’되는 폴더의 PIDL입니다. PIDL에 대해서는 나중에 상세히 설명하겠습니다.

pDataObj는 ‘드롭’되는 항목들을 모두 열거할 수 있는 IDataObject 인터페이스 형의 포인터입니다.

hProgIDHKEY_CLASSES_ROOT의 하위 키로 우리가 개발하고 있는 쉘 익스텐션을 등록시킨 바로 그 레지스트리 키에 대한 HKEY입니다.

첫 번째로 해야 할 것은 컨텍스트 메뉴의 항목에 보여 줄 비트맵을 로드(load)하는 것입니다. 그리고 나서 COleDataObject형 변수에 IDataObject 인터페이스형 포인터를 연결시키는 것입니다.

 

HRESULT CHardLinkShlExt::Initialize(
    LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hProgID) {
    
    AFX_MANAGE_STATE(AfxGetStaticModuleState());  // MFC 초기화
    
    COleDataObject dataobj;
    HGLOBAL        hglobal;
    HDROP          hdrop;
    TCHAR          szRoot[MAX_PATH];
    TCHAR          szFileSystemName[256];
    TCHAR          szFile[MAX_PATH];
    UINT           uFile, uNumFiles;
    
    m_bitmap.LoadBitmap(IDB_LINKBITMAP);
    
    dataobj.Attach(pDataObj, FALSE);
    
    // ...

 

Attach 메소드를 호출할 때 두 번째 매개 변수(parameter)에 FALSE를 전달하는 것은, dataobj 객체가 파괴될 때 IDataObject형 인터페이스 포인터를 참조 해제시키니 말라는 의미를 갖고 있습니다.

두 번째로 해야 할 것은 항목들이 ‘드롭’되는 디렉토리를 가져오는 것입니다. 우리는 이 디렉토리에 대한 PIDL을 이미 전달받았습니다. 그렇지만, 경로는 어떻게 가져올 수 있을까요?

PIDL은 pointer to an ID list의 줄임말이라고 하였습니다. PIDL은 Windows 탐색기가 나타내는 계층 구조 안에서 임의의 개체를 유일하게 식별할 수 있는 방법입니다. PIDL의 구체적인 구조는 개체에 따라 달라집니다. 그러나 여러분이 직접 네임스페이스 확장을 작성할 것이 아니라면, PIDL의 내부 구조에 대해서 걱정할 필요는 없습니다.

PIDL로부터 폴더(디렉토리)의 경로를 얻고자 할 때, SHGetPathFromIDList라는 Shell API를 사용하면 PIDL을 통상적인 파일 및 폴더 경로로 변환할 수 있습니다. 만일 드롭하는 위치가 파일 시스템에 있는 디렉토리가 아닌 경우(예: 제어판)에 SHGetPathFromIDList의 실행이 실패하고 우리는 더 이상 작업하지 않고 메소드를 끝낼 수 있습니다.

 

    // ...
    
    if (!SHGetPathFromIDList(pidlFolder, m_szFolderDroppedIn))
        return E_FAIL;
    
    // ...

 

다음으로 우리는 드롭하려는 대상 디렉토리가 NTFS 볼륨에 있는 디렉토리인지를 검사해야 합니다. 우리는 이미 확보한 디렉토리 경로로부터 루트 요소(예를 들어 E:\)들을 추출할 수 있고, 이를 통해 볼륨의 정보를 가져올 수 있습니다. 만일 파일 시스템이 NTFS가 아닌 경우, 하드링크를 만들 수 없기 때문에 값을 반환하고 메소드를 끝냅니다.

 

    // ...
    
    lstrcpy(szRoot, m_szFolderDroppedIn);
    PathStripToRoot(szRoot);
    
    if (!GetVolumeInformation(szRoot, NULL, 0, NULL, NULL, NULL, szFileSystemName, 256)) {
        // 파일 시스템을 판독할 수 없을 때...
        return E_FAIL;
    }
    
    if (0 != lstrcmpi(szFileSystemName, _T("ntfs"))) {
        // 파일 시스템이 NTFS가 아닌 경우 하드링크를 지원하지 않습니다.
    return E_FAIL;
    }
    
    // ...

 

다음으로 우리는 데이터 개체로부터 HDROP 핸들을 가져옵니다. 이것은 ‘드롭’되는 파일들을 열거하는데 쓰입니다. 이 부분은 3 단계와 비슷하지만, 이번 단계에서는 데이터를 액세스하는 데 MFC 클래스를 사용해 보겠습니다. COleDataObjectFORMATETCSTGMEDIUM 구조를 알아서 설정합니다.

 

    // ...
    
    hglobal = dataobj.GetGlobalData(CF_HDROP);
    
    if ( NULL == hglobal )
        return E_INVALIDARG;
    
    hdrop = (HDROP)GlobalLock(hglobal);
    
    if (hdrop == NULL)
        return E_INVALIDARG;
    
    // ...

 

우리는 ‘드롭’되는 파일들을 하나씩 확인하기 위해 HDROP 핸들을 사용할 것입니다. 각 파일에 대해 우리는 이것이 디렉토리인지 파일인지를 검사해야 합니다. 디렉토리는 하드링크될 수 없으므로 이 때는 값을 반환하고 메소드를 종료합니다.

 

    // ...
    
    uNumFiles = DragQueryFile(hdrop, 0xFFFFFFFF, NULL, 0);
    
    for (uFile = 0; uFile < uNumFiles; uFile++) {
        if (DragQueryFile(hdrop, uFile, szFile, MAX_PATH)) {
            if (PathIsDirectory(szFile)) {
                // 디렉토리가 발견되었으므로 메소드를 종료합니다.
                m_lsDroppedFiles.RemoveAll();
                break;
            }
        
        // ...

 

우리는 또한 ‘드롭’되려는 파일들이 ‘드롭’하려는 디렉토리와 같은 볼륨에 존재하는 것인지 확인할 필요가 있습니다. 이를 위해 필자가 처음에 시도해 본 것은 각 파일들의 경로와 ‘드롭’ 대상 디렉토리간 경로를 비교하여 그 루트 요소(드라이브 명)가 다를 경우 값을 반환하고 메소드를 종료하는 방식이었습니다. 그러나 이것은 완전한 해법이 아닙니다. 왜냐하면 NTFS 볼륨에서 여러분은 또 다른 볼륨을 경로 중간에 마운트(mount)할 수 있기 때문입니다. 예를 들어, C: 드라이브에서 또 다른 볼륨을 C:\dev라는 경로에 마운트했다면, 이 단계에서 작성하고 있는 소스 코드는 C:\dev를 일반적인 디렉토리로 간주하여 C: 드라이브의 다른 어딘가에 하드링크(hardlink)를 만들려 할 것입니다.

루트 요소를 비교하는 방법은 다음과 같습니다.

 

    // ...
    
    if (!PathIsSameRoot(szFile, m_szFolderDroppedIn)) {
        // 다른 볼륨으로부터 드롭된 파일이라면, 메소드를 종료합니다.
        m_lsDroppedFiles.RemoveAll();
        break;
    }
    
    // ...

 

파일이 지금까지 설명했던 두 가지 검사(디렉토리 여부, 볼륨 일치 여부)를 통과했다면, 우리는 이것을 CStringList(CString에 대한 링크드 리스트)형 객체인 m_lsDroppedFiles에 추가합니다.

 

    // ...
    
    // 드롭된 파일들을 우리가 선언한 리스트에 추가합니다.
    m_lsDroppedFiles.AddTail(szFile);
    }
} // for 루프 끝

 

for 루프를 끝낸 뒤에 우리는 리소스들을 해제하고 Windows 탐색기로 값을 반환시킵니다. 문자열 리스트가 파일 이름을 하나라도 가지고 있다면, 우리는 컨텍스트 메뉴를 변경하기 위해 S_OK를 반환합니다. 그렇지 않으면 이번 드래그 앤 드롭 이벤트에 대해서는 다시 호출될 일이 없도록 하기 위하여 E_FAIL을 반환합니다.

 

    // ...
    
    GlobalUnlock(hglobal);
    
    return (m_lsDroppedFiles.GetCount() > 0) ? S_OK : E_FAIL;
}

 

컨텍스트 메뉴 수정하기

이미 실습해보았던 컨텍스트 메뉴 익스텐션과 마찬가지로, 드래그 앤 드롭 핸들러도 IContextMenu 인터페이스를 구현하여 컨텍스트 메뉴와 상호작용 합니다. 우리가 만들고 있는 IContextMenu에 대해 컨텍스트 메뉴 항목을 추가하려면, HardLinkShlExt.h를 열고 다음과 같이 내용을 추가합니다.

 

class CHardLinkShlExt :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CHardLinkShlExt, &CLSID_HardLinkShlExt>,
    public IShellExtInit,
    public IContextMenu { // 새로 추가
    
    BEGIN_COM_MAP(CHardLinkShlExt)
        COM_INTERFACE_ENTRY(IShellExtInit)
        COM_INTERFACE_ENTRY(IContextMenu)
    END_COM_MAP()
    
public:
    // IContextMenu
    STDMETHODIMP GetCommandString(UINT, UINT, UINT *, LPSTR, UINT) { return E_NOTIMPL; } // 새로 추가
    STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO); // 새로 추가
    STDMETHODIMP QueryContextMenu(HMENU, UINT, UINT, UINT, UINT); // 새로 추가

 

드래그 앤 드롭 핸들러의 경우 GetCommandString이 호출될 일이 없으므로 이 메소드에 대해서 코드를 작성할 필요가 없습니다.

Windows 탐색기는 우리가 컨텍스트 메뉴 항목을 수정할 수 있도록 QueryContextMenu를 호출합니다. 여기서 여러분이 생소해하실 만한 내용은 없습니다. 우리는 단지 컨텍스트 메뉴에 항목을 하나 추가하고 비트맵을 설명하기만 하면 됩니다.

 

HRESULT CHardLinkShlExt::QueryContextMenu(HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags) {
    
    AFX_MANAGE_STATE(AfxGetStaticModuleState());  // MFC 초기화
    
    // uFlags가 CMF_DEFAULTONLY를 포함하고 있을 경우 여기서 아무것도 해선 안 됩니다.
    if (uFlags & CMF_DEFAULTONLY)
        return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0);
    
    // 하드링크 생성을 위한 메뉴 항목 추가하기.
    InsertMenu(hmenu, uMenuIndex, MF_STRING | MF_BYPOSITION, uidFirstCmd, _T("Create hard link(s) here"));
    
    if (m_bitmap.GetSafeHandle() != NULL) {
        SetMenuItemBitmaps(hmenu, uMenuIndex, MF_BYPOSITION,
        (HBITMAP)m_bitmap.GetSafeHandle(), NULL);
    }
    
    // 최상위 메뉴 항목 1개를 추가했으므로 정수 1을 반환합니다.
    return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 1);
}

 

드래그 앤 드롭 핸들러의 경우 GetCommandString이 호출될 일이 없으므로 이 메소드에 대해서 코드를 작성할 필요가 없습니다.

Windows 탐색기는 우리가 컨텍스트 메뉴 항목을 수정할 수 있도록 QueryContextMenu를 호출합니다. 여기서 여러분이 생소해하실 만한 내용은 없습니다. 우리는 단지 컨텍스트 메뉴에 항목을 하나 추가하고 비트맵을 설명하기만 하면 됩니다.

 

HRESULT CHardLinkShlExt::QueryContextMenu(HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags) {
    
    AFX_MANAGE_STATE(AfxGetStaticModuleState());  // MFC 초기화
    
    // uFlags가 CMF_DEFAULTONLY를 포함하고 있을 경우 여기서 아무것도 해선 안 됩니다.
    if (uFlags & CMF_DEFAULTONLY)
        return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0);
    
    // 하드링크 생성을 위한 메뉴 항목 추가하기.
    InsertMenu(hmenu, uMenuIndex, MF_STRING | MF_BYPOSITION,
    uidFirstCmd, _T("Create hard link(s) here") );
    
    if (m_bitmap.GetSafeHandle() != NULL) {
    SetMenuItemBitmaps(hmenu, uMenuIndex, MF_BYPOSITION,
        (HBITMAP)m_bitmap.GetSafeHandle(), NULL);
    }
    
    // 최상위 메뉴 항목 1개를 추가했으므로 정수 1을 반환합니다.
    return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 1);
}

 

이와 같이 작성한 쉘 익스텐션의 모양은 이렇습니다.

 

[단계 3]의 쉘 익스텐션 작동 예.

 

하드링크 만들기

Windows 탐색기는 사용자가 우리가 추가한 컨텍스트 메뉴 항목을 클릭했을 때 InvokeCommand 메소드를 호출합니다. 이 때 우리는 ‘드롭’된 파일들에 대해 모두 하드링크를 만들 것입니다. 하드링크의 이름은 “Hard link to <파일이름>”으로 정하고, 중복되는 이름이 있다면 “Hard link (<번호>) to <파일이름>”으로 정할 것입니다. 이 때 번호는 최대 99까지 증가할 수 있는데 이 제한은 운영체제가 하는 것이 아니고, 필자가 임의로 설정한 것입니다.

먼저 lpVerb 매개 변수(parameter)를 체크합니다. 우리는 컨텍스트 메뉴에 1개의 항목만을 추가했기 때문에 이것은 항상 0이 되어야 합니다.

 

HRESULT CHardLinkShlExt::InvokeCommand(LPCMINVOKECOMMANDINFO pInfo) {
    
    AFX_MANAGE_STATE(AfxGetStaticModuleState());  // MFC 초기화
    
    TCHAR    szNewFilename[MAX_PATH+32];
    CString  sSrcFile;
    TCHAR    szSrcFileTitle[MAX_PATH];
    CString  sMessage;
    UINT     uLinkNum;
    POSITION pos;
    
    
    // 우리가 추가한 항목을 클릭해서 호출된 것인지 재차 확인합니다.
    // lpVerb는 반드시 0이어야 합니다.
    if (pInfo->lpVerb != 0)
        return E_INVALIDARG;
    
    // ...

 

다음으로 우리는 문자열 리스트의 시작 위치를 가리키는 POSITION형 값을 가져와야 합니다. POSITION는 여러분이 직접 사용하지는 않을 내부적인 데이터 타입입니다. 대신에 여러분은 CStringList 클래스의 다른 메소드에 이를 전달은 해야 합니다. 리스트의 시작 부분에 대한 POSITION 값을 얻기 위해서 우리는 GetHeadPosition 메소드를 호출합니다.

 

// ... 

pos = m_lsDroppedFiles.GetHeadPosition();
ASSERT (pos != NULL);

// ...

 

리스트가 비어 있다면 posNULL이 됩니다. 그러나 리스트는 (앞서 설명한 바와 같이 작업이 되어 있으므로) 비어있는 일이 없어야 합니다. 그래서 필자는 그러한 경우를 체크하기 위하여 ASSERT문을 추가했습니다. 다음에 등장하는 루프 구문은 리스트에서 파일 이름들을 하나씩 순회하면서 각각에 대한 하드링크를 생성합니다.

 

    // ... 
    
    while (pos != NULL) {
        // 파일 이름을 하나씩 순회하며 가져옵니다.
        sSrcFile = m_lsDroppedFiles.GetNext(pos);
        
        // 예를 들어 C:\xyz\foo\stuff.exe라는 파일 경로에 대해 stuff.exe만을 추출합니다.
        lstrcpy(szSrcFileTitle, sSrcFile);
        PathStripPath(szSrcFileTitle);
        
        // 파일 이름에 대한 하드링크를 생성합니다. 
        // 먼저 Hard link to stuff.exe와 같은 이름으로 생성을 시도할 것입니다.
        wsprintf(szNewFilename, _T("%sHard link to %s"),
        m_szFolderDroppedIn, szSrcFileTitle);
        
        // ...

 

GetNextpos가 가리키는 위치에 있는 CString을 반환합니다. 그리고 pos가 다음 문자열을 가리키도록 위치를 옮깁니다. pos가 가장 마지막 문자열을 가리키고 있었다면, NULL이 되면서 루프가 끝납니다.

이 때 szNewFilename은 하드링크의 전체 경로를 갖게 됩니다. 우리는 해당 경로에 이미 하드링크가 있는지 여부를 검사하고, 이미 존재한다면 우리는 2부터 99까지 숫자를 붙여가며 중복되지 않는 이름을 찾습니다. 우리는 또한 하드링크의 이름(NULL 문자 포함)의 총 문자수가 MAX_PATH를 넘지 않도록 확인해야 합니다.

 


        // ... 
        
        for (uLinkNum = 2; PathFileExists(szNewFilename) && uLinkNum < 100; uLinkNum++) {
            
            // 하드링크에 부여할 다른 이름을 찾습니다.
            wsprintf(szNewFilename, _T("%sHard link (%u) to %s"), m_szFolderDroppedIn, uLinkNum, szSrcFileTitle);
            
            // 파일의 이름이 MAX_PATH보다 길어지면 오류 메시지를 표시합니다.
            if (lstrlen(szNewFilename) >= MAX_PATH) {
                sMessage.Format(_T("Failed to make a link to %s.\nDo you want to continue making links?"), (LPCTSTR)sSrcFile);
                if (MessageBox(pInfo->hwnd, sMessage, _T("Create Hard Links"), MB_ICONQUESTION | MB_YESNO) == IDNO)
                    break;
                else
                    continue;
}
}  // for 루프 종료

// ...

 

메시지 상자는 여러분이 원할 경우 전체 작업을 중단하게 할 수 있습니다. 그 다음 우리는 앞서 임의로 제한했던 최대 개수 99에 도달했는지 여부도 확인합니다. 우리는 다시 사용자가 전체의 작동을 중단하게 할 수 있습니다.

 

    // ... 
    
    if (uLinkNum == 100) {
        sMessage.Format(_T("Failed to make a link to %s.\nDo you want to continue making links?"), (LPCTSTR) sSrcFile);
        if (MessageBox(pInfo->hwnd, sMessage, _T("Create Hard Links"), MB_ICONQUESTION | MB_YESNO) == IDNO)
            break;
        else
            continue;
    }
    
    // ...

 

이제 남은 것은 진짜로 하드링크를 생성하는 것입니다. 필자는 명확성을 위해 오류 처리 코드를 생략합니다.

        // ... 
        
        CreateHardLink ( szNewFilename, sSrcFile, NULL );
    }  // while 루프 종료
    
    return S_OK;
}

 

Windows 탐색기에서 일반 파일과 하드링크의 차이는 없어 보입니다. 둘 다 평범한 파일처럼 생겼습니다. 그러나 어느 한 쪽의 내용을 수정했다면, 다른 한 쪽에서 열었을 때도 수정된 내용으로 나타날 것입니다.

 

하드링크는 겉만 다를 뿐 같은 파일이다.

 

쉘 익스텐션을 등록하기

드래그 앤 드롭 핸들러를 등록하는 것은 컨텍스트 메뉴 익스텐션을 등록하는 것보다 간단합니다. 모든 핸들러는 디렉토리에서 ‘드롭’ 행위가 발생하므로 HKCR\Directory의 하위 키로 등록됩니다. 그러나 개발 문서에 따르면 HKCR\Directory에만 등록하는 것으로는 충분하지 않다고 합니다. 여러분은 또한 바탕화면에 ‘드롭’하는 경우에 대응하기 위하여 HKCR\Folder에도 등록하고, 루트 디렉토리에 ‘드롭’하는 경우에 대응하기 위하여 HKCR\Drive에도 등록할 필요가 있습니다.

위 세 가지 경우를 고려한 RGS 스크립트는 다음과 같습니다.

 

HKCR {
    NoRemove Directory {
        NoRemove shellex {
            NoRemove DragDropHandlers {
                ForceRemove HardLinkShlExt = s '{3C06DFAE-062F-11D4-8D3B-919D46C1CE19}'
            }
        }
    }
    NoRemove Folder {
        NoRemove shellex {
            NoRemove DragDropHandlers {
                ForceRemove HardLinkShlExt = s '{3C06DFAE-062F-11D4-8D3B-919D46C1CE19}'
            }
        }
    }
    NoRemove Drive {
        NoRemove shellex {
            NoRemove DragDropHandlers {
                ForceRemove HardLinkShlExt = s '{3C06DFAE-062F-11D4-8D3B-919D46C1CE19}'
            }
        }
    }
}

 

이전에 실습해 보았던 쉘 익스텐션과 마찬가지로, Windows NT 기반의 운영체제의 경우 우리는 “승인된(approved)” 확장 목록에 우리가 만든 쉘 익스텐션을 등록해야 합니다. 이 작업은 예제 프로젝트의 DllRegisterServerDllUnregisterServer 함수가 수행합니다.

 

Windows 2000 및 NTFS 환경이 아닌 경우

이전 버전의 Windows를 사용하고 있다거나 사용 가능한 NTFS 볼륨이 없다고 하더라도, 여러분은 여전히 예제 프로젝트를 컴파일 및 실행해볼 수 있습니다. stdafx.h를 열고 다음과 같이 적혀있는 줄의 주석 기호(//)를 제거합니다.

 

// #define NOT_ON_WIN2K

 

주석 표시를 제거하면, 예제 프로젝트의 소스 코드는 파일 시스템을 검사하는 부분을 건너 뛸 것이고 NTFS가 아닌 경우에도 실행은 됩니다. 실제로 하드링크를 만드는 대신 메시지 상자를 띄울 것입니다.

 

다음 단계에서 다룰 내용

5 단계에서 여러분은 새로운 형태의 쉘 익스텐션으로 파일에 대한 등록정보(또는 속성) 다이얼로그에 페이지를 추가하는 ‘프로퍼티 시트 핸들러(property sheet handler)’에 대해 다룰 것입니다.

 

계속 읽기

이전 게시글: Windows 쉘 익스텐션 개발 가이드 - (3) 팝업/인포팁

다음 게시글: Windows 쉘 익스텐션 개발 가이드 - (5) 속성 다이얼로그 (1/2)

 

카테고리 “API/COM”
more...
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (3) 팝업/인포팁
입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차..
API/COM
2021. 1. 21. 13:31

Windows 쉘 익스텐션 개발 가이드 - (3) 팝업/인포팁

API/COM
2021. 1. 21. 13:31

입문자를 위한 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)

 

3 단계. 파일에 대해 ‘팝업(Popup)’ 설명을 보여주는 쉘 익스텐션(Shell Extension)

실습 프로젝트 다운로드:

ShellExtGuide3_demo.zip
31.6 kB

 

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

본 가이드의 1 단계2 단계에서 필자는 컨텍스트 메뉴 확장을 작성하는 방법에 대해 보여드렸습니다. 이번 3 단계에서는 조금 다른 유형의 쉘 익스텐션에 대해 설명하고자 합니다. 또한 쉘과 메모리를 공유하는 방법과 ATL과 함께 MFC를 병용하는 방법에 대해서 설명하겠습니다. 이번 단계에서는 여러분이 앞서 두 개의 단계를 통해 쉘 익스텐션의 기본에 대해 이해하고 있고, MFC를 알고 있다고 가정하겠습니다.

액티브 데스크톱(Active Desktop) 쉘에서는 새로운 기능을 도입하였습니다. 바로 여러분이 특정 개체에 마우스 포인터를 올려 놓았을 때 이 개체에 대한 설명을 보여주는 툴팁(tooltip)입니다. 예를 들어, “내 문서” 아이콘에 마우스 포인터를 오랫동안 올려 놓으면 다음과 같이 툴팁이 표시됩니다.

 

새롭게 추가된 툴팁(tooltip) 형태의 쉘 익스텐션(1).

 

“내 컴퓨터” 또는 “제어판” 등의 다른 개체들도 비슷한 툴팁을 가지고 있습니다. 이러한 형태의 텍스트는 마우스 포인터가 올려진 파일, 폴더, 개체에 대해 정보를 제공하는 툴팁이라는 의미에서 인포팁(infotip)이라 명명합니다. 우리도 인포팁 익스텐션(infotop extension)을 사용하여 이와 같이 쉘의 다른 개체에 대해 인포팁 문자열을 제공할 수 있습니다.

여러분이 이미 보았을 수도 있겠지만 이러한 인포팁 익스텐션의 예는 WinZip에서 확인할 수 있습니다. 압축 파일에 대해 마우스 오른쪽 버튼을 올려 놓으면 WinZip은 다음과 같은 내용을 표시해 줍니다.

 

WinZip의 인포팁 익스텐션.

 

본 게시글의 예제 프로젝트는 파일의 크기와 함께 첫 번째 줄의 내용을 표시해주는 신속한 텍스트 파일 뷰어가 되겠습니다. 이 익스텐션은 .txt 파일에 대해 마우스 포인터를 올려 놓았을 때 인포팁의 형태로 보여집니다.

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

 

AppWizard를 사용하여 시작하기

AppWizard를 실행하고 새로운 ATL COM 프로젝트를 생성합니다. 이 프로젝트를 TxtInfo라고 명명하겠습니다. 우리는 MFC를 사용할 것이기 때문에 “Support MFC” 체크상자에 체크를 하고 [Finish]를 클릭합니다.

 

Support MFC를 클릭하여 COM 프로젝트에 MFC를 사용 가능하게 한다.

 

DLL에 COM 개체를 추가하기 위해 “클래스 뷰(Class View)” 화면으로 이동하여 “TxtInfo classes” 트리 항목에 대해 마우스 오른쪽 버튼을 클릭하고 [New ATL Object] 항목을 클릭합니다. Visual C++ 7.0 이상의 버전에서는 해당 트리 항목에 대해 마우스 오른쪽 버튼을 클릭하고 “추가(D)”→”클래스 추가(C)...”를 클릭합니다. 영어 버전의 Visual Studio에서는 “Add”→”Add Class” 메뉴입니다. 이전과 같이 마법사 화면에서 “Simple Object”를 선택하고, 개체의 이름을 TxtInfoShlExt으로 정합니다. 이 마법사는 쉘 익스텐션을 구현하게 될 CTxtInfoShlExt 이름의 C++ 클래스를 생성할 것입니다.

또한 클래스 뷰(Class View) 트리에서 여러분은 CTxtInfoApp이라는 이름의 클래스가 보일 것입니다. 이 클래스는 CWinApp에서 파생된 클래스입니다. 이 클래스가 존재하고 theApp이라는 이름의 전역 객체(전역 변수)가 존재함으로 인해 우리는 ATL을 사용하지 않고도 보통의 MFC 기반 DLL을 만들듯이 MFC를 사용할 수 있습니다.

 

MFC 프로젝트와 같이 CWinApp의 파생 클래스가 추가된 프로젝트.

 

초기화 인터페이스

이전 게시글의 컨텍스트 메뉴 확장에서 Windows 탐색기가 우리가 개발하는 개체를 초기화하기 위한 IShellExtInit 인터페이스를 구현하였습니다. 일부 형태의 쉘 익스텐션을 초기화하는 또 다른 인터페이스가 존재합니다. 바로 IPersistFile이며 인포팁 익스텐션에서 사용하는 인터페이스가 이것입니다. 이것은 IShellExtInit와 무엇이 다를까요?

IShellExtInit::Initialize 메소드가 선택된 파일들을 모두 열거할 수 있는 IDataObject형 포인터를 전달받음을 앞서 살펴 보았습니다. 오로지 하나의 파일에만 작동되는 쉘 익스텐션의 경우 IPersistFile을 사용합니다. 마우스 포인터는 한 번에 하나 이상의 개체에 동시에 올려질 수 없듯이, 인포팁 익스텐션도 한 번에 하나의 파일에 대해서만 작동됩니다. 그래서 IPersistFile이 사용됩니다.

먼저 우리는 CTxtInfoShlExt가 구현하는 인터페이스로 IPersistFile을 추가할 필요가 있습니다. TxtInfoShlExt.h를 열고 다음과 같이 내용을 추가합니다.

 

#include <comdef.h>
#include <shlobj.h>
 
class CTxtInfoShlExt :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CTxtInfoShlExt, &CLSID_TxtInfoShlExt>,
    public IPersistFile { // 새로 추가
    
    BEGIN_COM_MAP(CTxtInfoShlExt)
        COM_INTERFACE_ENTRY(IPersistFile) // 새로 추가
    END_COM_MAP()
    
    // ...

 

또한 인포팁 익스텐션이 초기화를 진행하는 동안 Windows 탐색기가 전달해 준 파일 이름을 보관해 둘 필드(field)도 필요합니다.

 

protected:
    CString m_sFilename;

 

우리는 필요한 곳 어디에서나 MFC 객체를 사용할 수 있음을 기억하시기 바랍니다.

IPersistFile에 대한 문서를 보면, 여러분은 여러 가지 메소드들을 보게 될 것입니다. 다행스럽게도, 우리의 목적인 쉘(인포팁) 익스텐션에 대해서 우리는 단 하나의 메소드인 Load만을 구현하면 되고 나머지는 무시해도 됩니다. IPersistFile이 가지고 있는 메소드들의 원형은 다음과 같습니다.

 

class CTxtInfoShlExt : ... {
    // ...
    
    public:
        STDMETHODIMP GetClassID(LPCLSID)      { return E_NOTIMPL; }
        STDMETHODIMP IsDirty()                { return E_NOTIMPL; }
        STDMETHODIMP Load(LPCOLESTR, DWORD);
        STDMETHODIMP Save(LPCOLESTR, BOOL)    { return E_NOTIMPL; }
        STDMETHODIMP SaveCompleted(LPCOLESTR) { return E_NOTIMPL; }
        STDMETHODIMP GetCurFile(LPOLESTR*)    { return E_NOTIMPL; }
};

 

Load를 제외한 나머지 메소드들은 E_NOTIMPL을 반환합니다. 이는 우리가 해당 메소드를 구현하지 않았다는 의미입니다. 그리고 더욱 희망적인 사실은, 우리가 구현하게 될 Load 메소드는 매우 간단하다는 것입니다. 우리는 단지 Windows 탐색기가 우리에게 전달해 준 파일의 이름을 보관만 하면 된다는 것입니다. 이 파일 이름은 마우스 포인터가 올라가 있는 파일에 대한 전체 경로입니다.

 

HRESULT CTxtInfoShlExt::Load(LPCOLESTR wszFilename, DWORD dwMode) {
    
    AFX_MANAGE_STATE(AfxGetStaticModuleState());  // MFC를 쓰기 위한 내부 초기화
    
    // 파일 이름을 CString으로 변환 후 보관 
    m_sFilename = wszFilename;
    
    return S_OK;
}

 

메소드 본문에서 첫 번째 줄은 중요합니다. AFX_MANAGE_STATE 매크로는 MFC가 적절하게 작동되기 위해 필요한 사항입니다. 우리가 개발 중인 DLL은 MFC가 아닌 프로그램에서 로드할 것이므로, MFC를 사용하는 함수가 내보내기(export) 되었을 때는 수동으로 MFC를 초기화시켜 주어야 합니다. 여러분이 이 줄을 삽입하지 않는다면 대부분 리소스와 관계될 MFC 함수들이 충돌하거나 어셜션(assertion) 실패를 일으킬 것입니다.

m_sFilename에 파일의 이름이 저장되었습니다. 이는 나중에 사용될 것입니다. 지금 필자는 Windows 탐색기가 전달한 파일 이름 문자열을 CString으로 변환하는 과정에서 대입 연산자만을 사용하면 되는 이점을 활용하고 있습니다. DLL이 ANSI 컴파일로 설정되어 있다면 CString은 내부적으로 ANSI 문자열로 처리될 것입니다.

 

툴팁을 위한 텍스트 생성하기

Windows 탐색기가 우리가 개발중인 익스텐션의 Load 메소드를 호출한 후, 다른 형태의 인터페이스인 IQueryInfo형 포인터를 얻기 위하여 QueryInterface를 호출합니다. IQueryInterface는 단 두 개의 메소드를 가지고, 그나마도 실질적으로 하나의 메소드만 사용되는 간단한 인터페이스입니다. TxtInfoShlExt.h를 열고 다음과 같이 내용을 추가합니다.

 

class CTxtInfoShlExt : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CTxtInfoShlExt, &CLSID_TxtInfoShlExt>,
    public IPersistFile,
    public IQueryInfo { // 새로 추가
    
    BEGIN_COM_MAP(CTxtInfoShlExt)
        COM_INTERFACE_ENTRY(IPersistFile)
        COM_INTERFACE_ENTRY(IQueryInfo) // 새로 추가
    END_COM_MAP()
    
    // ...

 

그 다음 IQueryInfo 메소드에 대한 원형을 추가합니다.

class CTxtInfoShlExt : ... {
    
    // ...
    
    STDMETHODIMP GetInfoFlags(DWORD *) { return E_NOTIMPL; } // 새로 추가
    STDMETHODIMP GetInfoTip(DWORD, LPWSTR *); // 새로 추가
    
    // ...
};

 

GetInfoFlags 메소드는 사용되지 않으므로 우리는 단지 E_NOTIMPL을 반환해주면 됩니다.

GetInfoTip가 바로 우리가 툴팁으로 보여주기 위한 문자열을 Windows 탐색기에게 반환하는 곳입니다. 이 메소드는 초반부터 지루한 선언들로 시작합니다.

 

HRESULT CTxtInfoShlExt::GetInfoTip(DWORD dwFlags, LPWSTR * ppwszTip) {
    
    AFX_MANAGE_STATE(AfxGetStaticModuleState());  // MFC를 초기화합니다.
    
    USES_CONVERSION;
    
    LPMALLOC   pMalloc;
    CStdioFile file;
    DWORD      dwFileSize;
    CString    sFirstLine;
    BOOL       bReadLine;
    CString    sTooltip;
    
    // ...

 

이번에도 MFC를 초기화하기 위해 다른 내용보다 먼저 AFX_MANAGE_STATE 호출 문장이 등장했습니다. 이 문장은 메소드 내에서 변수 선언보다도 더 먼저 등장해야 합니다. 왜냐하면 로컬변수의 생성자 내부에도 MFC가 포함될 수 있기 때문입니다.

dwFlags는 이번 단계에서는 사용되지 않을 것입니다. ppwszTipLPWSTR형 포인터로서 우리는 우리가 직접 메모리를 할당하고 이에 대한 포인터를 전달해야 합니다.

먼저 우리는 파일을 열고 그 내용을 읽어야 합니다. 우리는 일찍이 Load 메소드를 통해 보관해둔 필드(field)를 통해 파일 이름을 알고 있습니다.

 

    // ...
    
    if (!file.Open(m_sFilename, CFile::modeRead | CFile::shareDenyWrite))
        return E_FAIL;
    
    // ...

 

이제 우리는 쉘 메모리 할당자를 통해 버퍼를 할당받아야 하므로 IMalloc 인터페이스를 필요로 합니다. 우리는 SHGetMalloc를 호출함으로써 이를 수행합니다.

 

    // ...
    
    if (FAILED(SHGetMalloc(&pMalloc)))
        return E_FAIL;
    
    // ...

 

필자는 추후에 이 IMalloc에 대해 할 이야기가 많이 있습니다. 어쨌든 다음 단계로 해당 파일의 크기를 가져오고, 첫 번째 줄을 읽습니다.

 

    // ...
    
    // 파일의 크기를 가져옵니다. 
    dwFileSize = file.GetLength();
    
    // 파일로부터 첫 번째 줄을 읽습니다. 
    bReadLine = file.ReadString(sFirstLine);
    
    // ...

 

bReadLine은 파일의 길이가 0 바이트이거나 액세스할 수 없는 경우 빼고는 대체로 TRUE가 될 것입니다. 다음에 수행할 작업으로 파일의 크기를 알려주는 툴팁의 첫 부분을 생성하는 것입니다.

 

    // ...
    
    sTooltip.Format(_T("File size: %lu"), dwFileSize);
    
    // ...

 

또한 첫 번째 줄을 읽어오는 데 성공하였다면 툴팁에 그 내용을 덧붙입니다.

 

    // ...
    
    if (bReadLine) {
        sTooltip += _T("\n");
        sTooltip += sFirstLine;
    }
    
    // ...

 

이제 툴팁으로 보여줄 문자열을 완성했으므로 우리는 버퍼를 할당받아야 합니다. 바로 지금이 IMalloc을 사용해야 할 때입니다. SHGetMalloc가 반환한 포인터는 쉘이 가지고 있는 IMalloc 인터페이스에 대한 복사본입니다. 이 인터페이스를 통해 할당받는 모든 메모리는 쉘의 프로세스 영역에 존재하게 됩니다. 그래서 쉘은 이를 사용할 수 있습니다. 더 중요한 것은, 쉘이 이를 할당 해제할 수 있기 때문에 우리가 해야 될 것은 버퍼를 할당받기만 하고 이후에는 잊어버리면 된다는 것입니다. 쉘은 할당된 메모리의 사용이 끝나면 이를 알아서 할당 해제할 것입니다.

또 한 가지 알아야 할 사항으로, 우리가 쉘에게 반환해야 할 문자열은 반드시 유니코드 문자열이어야 한다는 것입니다. Alloc을 호출 시 sizeof(wchar_t)의 배수 크기가 지정되는 이유이기도 합니다. 단순히 lstrlen(sToolTip)을 기준으로 메모리를 할당받을 경우 실제 필요한 메모리에 비해 절반밖에 할당이 되지 않을 수 있습니다.

 

    // ...
    
    *ppwszTip = (LPWSTR)pMalloc->Alloc((1 + lstrlen(sTooltip)) * sizeof(wchar_t));
    
    if (*ppwszTip == NULL) {
        pMalloc->Release();
        return E_OUTOFMEMORY;
    }
    
    // 버퍼에 툴팁 문자열을 복사할 때 유니코드형 문자열 함수를 사용하시기 바랍니다.
    wcscpy(*ppwszTip, T2COLE(LPCTSTR(sTooltip)));
    
    // ...

 

마지막으로 앞서 받았던 IMalloc 인터페이스형 포인터를 참조 해제합니다.

 

    // ...
    
    pMalloc->Release();
    
    return S_OK;
}

 

소스 코드는 거의 다 작성했습니다. Windows 탐색기는 *ppwszTip에서 문자열을 읽어 툴팁으로 보여줄 것입니다.

 

[단계 3]의 쉘 익스텐션 작동 예.

 

쉘 익스텐션을 등록하기

인포팁 익스텐션은 컨텍스트 메뉴 익스텐션과 약간 다른 방식으로 등록됩니다. 이번 쉘 익스텐션도 물론 HKEY_CLASSES_ROOT에 등록되지만, 우리가 다루고자 하는 파일 확장명에 대한 레지스트리 키에 직접 등록됩니다. 즉 이 경우에는 HKCR\.txt에 등록된다는 것입니다.

특이한 것은 또 있습니다. 여러분은 ShellEx의 서브 키로 추가될 레지스트리 키의 이름이 TooltipHandlers와 같은 논리적이고 추상적인 이름이 될 것으로 생각하겠지만, 이 이름은 {00021500-0000-0000-C000-000000000046}으로 미리 정해져 있습니다.

필자가 생각하기에 마이크로소프트는 우리가 모르는 쉘 익스텐션들을 더 가지고 있을 것 같습니다. 여러분이 레지스트리를 자세히 찾아본다면 GUID로 된 이름을 가진 서브키들이 다른 ShellEx에도 있음을 알 수 있습니다. 이 GUID들은 IQueryInfoGUID로 나타납니다.

우리가 개발한 쉘 익스텐션이 .txt 파일에 대해 Windows 탐색기가 호출할 수 있도록 등록하는 RGS 스크립트는 다음과 같습니다.

 

HKCR {
    NoRemove .txt {
        NoRemove ShellEx {
            {00021500-0000-0000-C000-000000000046} = s '{F4D78AE1-05AB-11D4-8D3B-444553540000}'
        }
    }
}

위 코드를 ‘복붙’하면 여러분은 쉽게 다른 쉘 익스텐션도 호출되게 할 수 있습니다. 또한 “.txt”라는 부분을 여러분이 원하는 다른 확장명으로 변경할 수도 있습니다.

다만 유감스럽게도, 여러분은 이 인포팁 익스텐션을 모든 형식의 파일에 대해 호출되게 할 목적으로 HKCR\* 또는 HKCR\AllFileSystemObject에 등록할 수는 없습니다.

앞서 실습했던 쉘 익스텐션에 대해 NT 기반의 운영체제에서 작동되게 하려면, 쉘 익스텐션을 승인된(approved) 익스텐션 목록에 추가해야 한다고 하였습니다. 이런 작업을 수행하는 것이 첨부된 예제 프로젝트에 포함된 DllRegisterServerDllUnregisterServer 함수입니다.

 

다음 단계에서 다룰 내용

4 단계에서 우리는 다시 컨텍스트 메뉴 익스텐션의 세계로 돌아와서 또 다른 유형의 쉘 익스텐션인 드래그 앤 드롭 핸들러에 대해 살펴보겠습니다. 우리는 또한 MFC를 보다 많이 사용해볼 것입니다.

 

계속 읽기

다음 게시글: Windows 쉘 익스텐션 개발 가이드 - (2) 여러 개의 파일 (2/2)

이전 게시글: Windows 쉘 익스텐션 개발 가이드 - (4) 드래그 앤 드롭

 

카테고리 “API/COM”
more...
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (2) 여러 개의 파일 (2/2)
입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차..
API/COM
2021. 1. 20. 18:10

Windows 쉘 익스텐션 개발 가이드 - (2) 여러 개의 파일 (2/2)

API/COM
2021. 1. 20. 18:10

입문자를 위한 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)

 

2 단계. 여러 개의 파일에 대해 한번에 작동하는 쉘 익스텐션(Shell Extension)

이전 파트에 이어서...

 

플라이-바이 도움말(fly-by help)과 동사(verb) 전달하기

전에 언급한대로, GetCommandString 메소드는 Windows 탐색기가 ‘플라이-바이 도움말(fly-by help)’나 우리가 추가하고 있는 메뉴 항목(command) 중 하나에 대한 동사(verb)를 얻을 때에 호출되는 메소드입니다.

이번에는 2개의 메뉴 항목을 추가했기 때문에, Windows 탐색기가 어떤 메뉴 항목으로 우리를 호출하였는지 판별하기 위해 uCmdID 매개 변수(parameter)를 확인해야 합니다.

 

#include <atlconv.h>

HRESULT CDLLRegShlExt::GetCommandString(
    UINT uCmdID, UINT uFlags, UINT * puReserved, LPSTR szName, UINT cchMax) {
    
    USES_CONVERSION;
    
    LPCTSTR szPrompt;
    
    if (uFlags & GCS_HELPTEXT) {
        switch (uCmdID) {
        case 0:
            szPrompt = _T("Register all selected COM servers");
            break;
        case 1:
            szPrompt = _T("Unregister all selected COM servers");
            break;
        default:
            return E_INVALIDARG;
        }
        
        // ...

 

uCmdID의 값이 0이면, 우리는 컨텍스트 메뉴에 추가된 첫 번째 항목(등록)이 클릭되어 호출된 것입니다. 이 값이 1이면 우리는 컨텍스트 메뉴에 추가된 두 번째 항목(등록 해제)이 클릭되어 호출된 것입니다. 이에 따라 우리가 준비한 문자열을 선택한 다음 Windows가 제공한 버퍼에 이를 복사합니다. 필요 시 유니코드로 변환을 먼저 합니다.

 

이 단계에서 실습하고 있는 쉘 익스텐션에서 필자는 동사(verb)를 제공하는 소스 코드 또한 작성하였습니다. 그러나 필자가 테스트하는 동안, Windows 탐색기는 동사(verb)를 요구할 목적으로 GetCommandString을 호출하지는 않았기 때문에 동사(verb)가 작동될 기회가 없었습니다.

필자는 이 글에서 동사(verb)와 관련된 설명은 생략하겠으나, 여러분이 관심이 있다면 예제 프로젝트에서 참고해보실 수 있습니다.

 

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

우리가 추가한 컨텍스트 메뉴 항목을 사용자가 클릭할 때, Windows 탐색기는 쉘 익스텐션 내부의 InvokeCommand 메소드를 호출합니다. InvokeCommandlpVerb의 상위 워드(HIWORD)를 먼저 체크합니다. 이 값이 0이 아닌 값이라면, 사용자가 호출하려는 동사(verb)의 이름을 뜻합니다. 이 프로젝트에서 동사(verb)가 제대로 작동하는지 여부를 알지 못하기 때문에(적어도 Windows 98에서는 그렇다는 뜻입니다), 우리는 이 부분을 무시할 것입니다.

동사(verb)에 대한 것이 아니라면, lpVerb의 하위 워드(LOWORD)는 0 또는 1일 것이므로, 이를 통해서 사용자가 첫 번째로 추가한 메뉴 항목을 클릭한 것인지, 두 번째로 추가한 메뉴 항목을 클릭한 것인지를 알 수 있습니다.

 

HRESULT CDLLRegShlExt::InvokeCommand(LPCMINVOKECOMMANDINFO pCmdInfo) {
    // lpVerb가 특정 문자열에 대한 포인터일 경우, 
    // 이번 호출을 무시하고 메소드를 끝냅니다.
    if (HIWORD(pInfo->lpVerb) != 0)
        return E_INVALIDARG;
    
    // lpVerb가 우리가 추가한 메뉴 항목에 대한 Command ID인 0 또는 1인지 확인합니다.
    switch (LOWORD(pInfo->lpVerb)) {
    case 0:
    case 1: {
        CProgressDlg dlg(&m_lsFiles, pInfo);
        
        dlg.DoModal();
        return S_OK;
    }
    default:
        return E_INVALIDARG;
    }
}

 

lpVerb0 또는 1일 때, 우리는 (ATL 클래스인 CDialogImpl에서 파생된) 프로세스 다이얼로그를 띄우고 파일 이름들의 목록을 전달할 것입니다.

실제 작업들은 모두 CProcessDlg 클래스에서 이루어집니다. OnInitDialog 메소드는 리스트 컨트롤(list control)을 초기화하고, 그 다음 CProgrssDlg::DoWork 메소드를 호출합니다. DoWork 메소드는 CDLLRegShlExt::Initialize 메소드에서 생성된 문자열 리스트를 순회(iterate)하고 각 파일에 대해 적절한 함수를 호출합니다.

기본 소스 코드는 다음과 같습니다만 명확하게 보여드리기 위하여 오류 체크 부분과 리스트 컨트롤에 항목을 추가하는 부분은 삭제하였습니다. 그러나 리스트를 순회하고 각 파일 이름에 대해 작업하는 과정을 보이기에는 충분합니다.

 

void CProgressDlg::DoWork() {
    
    HRESULT (STDAPICALLTYPE * pfn)();
    string_list::const_iterator it;
    HINSTANCE hinst;
    LPCSTR    pszFnName;
    HRESULT   hr;
    WORD      wCmd;
    
    wCmd = LOWORD(m_pCmdInfo->lpVerb);
    
    // 2 가지의 항목을 컨텍스트 메뉴에 추가했기 때문에,
    // 0과 1을 벗어나는 lpVerb에 대해서는 호출을 무시합니다.
    if (wCmd > 1)
        return;
    
    // 앞으로 어떤 함수를 호출할 것인지를 결정합니다.
    // GetProcAddress는 함수 이름을 받을 때 ANSI 문자열만을 받기 때문에
    // _T 매크로를 사용하지 않음을 확인하시기 바랍니다.
    // 번역자 주: wCmd = 1이면 등록 해제, wCmd = 0이면 등록
    pszFnName = wCmd ? "DllUnregisterServer" : "DllRegisterServer";
    
    for (it = m_pFileList->begin(); it != m_pFileList->end(); it++) {
        // 리스트에 담긴 각 파일들을 로드
        hinst = LoadLibrary(it->c_str());
        
        if (hinst == NULL)
            continue;
        
        // 로드된 모듈에 대해 등록 또는 등록 해제 함수에 대한 주소 얻기
        (FARPROC &)pfn = GetProcAddress(hinst, pszFnName);
        
        // 현재 모듈에서 해당 함수 및 그 주소를 찾지 못했다면 리스트의 다음 파일 처리
        if (pfn == NULL)
            continue;
        
        // 함수를 호출
        hr = pfn();
        
        // 오류 처리 및 여기서 호출된 함수들의 반환 값에 따른 처리는 생략합니다.
    }  // for 루프의 끝
    
    // ...

 

DoWork의 나머지 부분은 오류 처리와 리소스 정리에 대한 것입니다. 전체 소스 코드는 첨부된 예제 프로젝트의 ProgrssDlg.cpp에서 확인하실 수 있습니다.

 

쉘 익스텐션을 등록하기

DllReg 쉘 익스텐션은 ‘인 프로시저(in-proc)’ COM 서버로서 작동하기 때문에 DLL과 OCX 파일 을 선택한 경우에 대해 호출되어야 합니다. 1 단계에서와 같이 이러한 작업들은 RGS 스크립트인 DllRegShlExt.rgs를 통해 지정될 수 있습니다. 각각의 확장 파일에 대해 컨텍스트 메뉴 핸들러로서 우리가 개발한 DLL을 등록하기 위해 필요한 스크립트는 다음과 같습니다.

 

HKCR {
    NoRemove dllfile {
        NoRemove shellex {
            NoRemove ContextMenuHandlers {
                ForceRemove DLLRegSvr = s '{8AB81E72-CB2F-11D3-8D3B-AC2F34F1FA3C}'
            }
        }
    }
    NoRemove ocxfile {
        NoRemove shellex {
            NoRemove ContextMenuHandlers {
                ForceRemove DLLRegSvr = s '{8AB81E72-CB2F-11D3-8D3B-AC2F34F1FA3C}'
            }
        }
    }
}

 

RGS 파일의 문법과 NoRemoveForceRemove 키워드는 1 단계에서 설명 되었습니다.

이전에 실습했던 쉘 익스텐션과 마찬가지로 NT 기반의 운영체제에서는 우리가 개발한 쉘 익스텐션을 “승인된(approved)” 익스텐션 목록에 추가해야 합니다. 이를 수행하는 소스 코드는 DllRegisterServerDllUnregisterServer 함수에 적혀 있습니다. 매우 간단한 레지스트리 접근이기 때문에 필자는 이 글에서 이를 언급하지 않겠습니다. 첨부된 예제 프로젝트에서 직접 확인하시기 바랍니다.

 

쉘 익스텐션의 실제 작동 확인하기

우리가 추가한 두 메뉴 항목 중 하나를 클릭하였을 때, 대화상자가 뜨면서 작업 결과를 보여주게 됩니다.

 

[단계 2]의 예제 프로젝트 실행 결과(1).
[단계 2]의 예제 프로젝트 실행 결과(2).

 

리스트 컨트롤은 선택된 각 파일의 목록 및 등록 또는 동록 해제의 성공 여부를 보여줍니다. 여러분이 파일을 선택할 때 리스트 하단에 각 파일에 대해 좀 더 상세한 내용이 보여집니다. 이 때 등록 또는 등록 해제가 실패하였다면 오류 메시지에 대한 설명이 함께 나타날 것입니다.

 

Windows XP에서 테마 적용 없이 [단계 2]의 예제 프로젝트를 실행한 결과.

위 스크린샷을 주목하시기 바랍니다. Windows XP에서 실행할 경우 다이얼로그에 Windows XP가 적용되지 않았습니다. “Windows XP 비주얼 스타일을 사용하기(Using Windows XP Visual Styles)”라는 제목의 MSDN 게시글에 따르면, 우리가 만든 사용자 인터페이스(UI)에 Windows XP 테마를 적용하려면 두 가지 작업이 필요합니다.

첫 번째로 우리가 만든 DLL이 공용 컨트롤 버전 6.0을 사용할 것임을 운영체제에게 알리는 것입니다. 이는 리소스에 매니페스트(manifest)를 추가함으로써 가능합니다. 여러분은 위 MSDN 게시글의 메니페스트 XML 내용에서 필요한 부분을 복사하여 프로젝트의 res 서브 디렉토리에 dllreg.manifest라는 이름의 파일로 저장합니다. 그리고 다음의 내용을 리소스 파일에 추가합니다.

(번역자 주: 원문에 제시된 링크는 현재 접속할 수 없습니다. 대신 첨부된 pdf 파일을 참고하시기 바랍니다.)

 

ISOLATIONAWARE_MANIFEST_RESOURCE_ID RT_MANIFEST "res\\dllreg.manifest"

 

두 번째로 stdafx.h#include 구문보다 우선하여 다음의 내용을 추가합니다.

 

#define ISOLATION_AWARE_ENABLED 1

 

2006년 5월 기준으로 MSDN 게시글에서는 SIDEBYSIDE_COMMONCONTROLS라는 이름의 심볼을 이야기하고 있습니다만, 필자가 SDK를 살펴본 바로는 ISOLATION_AWARE_ENABLED만이 사용되고 있습니다. 여러분이 더 최신 버전의 SDK를 설치하였고 ISOLATION_AWARE_ENABLED 심볼을 사용하였을 때 효과가 없다면, SIDEBYSIDE_COMMONCONTROLS를 사용해보시기 바랍니다.

이와 같이 수정 후, 다시 빌드하면 다이얼로그는 다음과 같이 Windows XP의 새로운 테마가 적용되어 표시될 것입니다.

 

Windows XP에서 테마가 적용되어 [단계 2]의 예제 프로젝트를 실행한 결과.

 

쉘 익스텐션을 등록하는 다른 방법들

지금까지 우리가 만든 쉘 익스텐션은 특정한 몇몇 형식의 파일에 대해서만 호출되었습니다. 임의의 파일에 대해 쉘 익스텐션이 호출되도록 하려면 HKCR\* 경로의 키에 컨텍스트 메뉴 핸들러로서 DLL을 등록하면 됩니다.

HKCR\* 키는 모든 종류의 파일에 대해 호출될 쉘 익스텐션 목록을 가지고 있습니다. 개발 문서에 따르면 이 때의 쉘 익스텐션은 모든 쉘 오브젝트(Shell Object: 파일, 디렉토리, 가상 폴더, 제어판 항목 등)에 대해 호출된다고 적혀있지만, 필자가 테스트해 본 바로는 그렇지 않았습니다. 우리가 만드는 쉘 익스텐션은 파일 시스템에 존재하는 파일에 대해서만 호출되었습니다.

쉘 버전 4.71 이상에서는, HKCR\AllFileSystemObjects이라는 레지스트리 키가 존재합니다. 이 키에 등록하면, 우리가 만든 쉘 익스텐션은 파일 시스템에 있는 모든 파일 및 디렉토리에 대해 호출되고 드라이브와 같은 루트 폴더에 대해서는 호출되지 않습니다(드라이브 등의 루트 경로에 대해 쉘 익스텐션이 호출되게 하려면 HKCR\Drive 레지스트리 키에 등록하면 됩니다).

그러나 일부 버전의 Windows의 경우, 이 키에 DLL을 등록할 경우 이상하게 작동될 수 있습니다. 예를 들어, Windows 98에서 우리가 만든 쉘 익스텐션의 항목들끼리 연속적으로 나타나지 않고 다른 항목과 섞여서 나타날 수 있습니다. 이는 Windows XP에서는 발생하지 않는 문제입니다.

여러분은 쉘 익스텐션이 디렉토리에서만 호출되도록 작성할 수도 있습니다. 그러한 쉘 익스텐션의 예를 찾는 분은, 필자의 또 다른 게시글인 “컴파일러가 만드는 임시 파일 제거 유틸리티(A Utility to Clean Up Compiler Temp Files)”을 참고하시기 바랍니다.

마지막으로 4.71 이상 버전의 쉘에서 여러분은 사용자가 폴더의 빈 공간(바탕화면의 빈 영역 포함)에 대해 마우스 오른쪽 클릭을 하여 나타나는 컨텍스트 메뉴에 대해서도 항목을 추가할 수 있습니다. 여러분이 개발하고자 하는 쉘 익스텐션이 이와 같이 작동되게 하려면 HKCR\Directory\Background\shellex\ContextMenuHandlers에 DLL을 등록하면 됩니다. 이런 경우 IShellExtInit::Initialize에 전달되는 매개 변수(parameter)가 약간 달라집니다. 필자는 이 주제에 대해 이후의 단계에서 다루겠습니다.

 

다음 단계에서 다룰 내용

3 단계에서, 우리는 새로운 유형의 쉘 익스텐션으로서 쉘 오브젝트(shell object)에 대한 팝업 설명을 보여주는 QueryInfo 핸들러에 대해 다루어 보겠습니다. 필자는 또한 쉘 익스텐션에서 MFC를 사용하는 방법에 대해서도 보여드릴 것입니다.

계속 읽기

이전 게시글: Windows 쉘 익스텐션 개발 가이드 - (2) 여러 개의 파일 (1/2)

다음 게시글: Windows 쉘 익스텐션 개발 가이드 - (3) 팝업/인포팁

 

카테고리 “API/COM”
more...
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (2) 여러 개의 파일 (1/2)
입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차..
API/COM
2021. 1. 19. 23:13

Windows 쉘 익스텐션 개발 가이드 - (2) 여러 개의 파일 (1/2)

API/COM
2021. 1. 19. 23:13

입문자를 위한 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)

 

2 단계. 여러 개의 파일에 대해 한번에 작동하는 쉘 익스텐션(Shell Extension)

실습 프로젝트 다운로드:

ShellExtGuide2_demo.zip
66.5 kB

 

들어가기에 앞서…

1 단계에서 필자는 쉘 익스텐션을 작성하는 방법을 설명하고 또한 한 번에 하나의 파일에 대해서만 작동되는 간단한 컨텍스트 메뉴 확장에 대해 보여드린 바 있습니다.

이번 단계에서 필자는 여러 개 선택된 파일에 대해 마우스 오른쪽 버튼 클릭을 하였을 때 한 번에 다루는 방법에 대해 보여드리고자 합니다. 이 글에서 예제로 제공되는 쉘 익스텐션은 COM 서버를 등록하거나 등록해제 하는 유틸리티를 구현하고 있습니다. 또한 ATL 다이얼로그 클래스인 CDialogImpl을 사용하는 방법에 대해서 시연하고 있습니다.

또한 필자는 이번 단계를 통해 미리 지정된 특정 형식의 파일이 아닌, 임의의 파일에 대해서 여러분이 만드는 쉘 익스텐션이 호출될 수 있도록 만드는 특별한 레지스트리 키에 대해서도 설명하겠습니다.

이번 단계는 여러분이 이미 1 단계를 읽고 컨텍스트 메뉴 확장에 대한 기본 지식을 알고 있을 것이라는 가정으로 작성되었습니다. 추가적으로 여러분은 이 글을 읽기 전에 COM, ATL 및 STL에서 제공하는 컬렉션 클래스의 기본을 이해하고 있어야 합니다.

Visual C++ 7.0 및 8.0 사용자는 컴파일을 하기 전에 몇 가지 설정을 변경할 필요가 있습니다. 1 단계의 ‘본 시리즈에 들어가며......’ 섹션을 참조하시기 바랍니다.

 

AppWizard를 사용하여 시작하기

AppWizard를 실행하고 새로운 ATL COM 프로젝트를 생성합니다. 이 프로젝트를 DllReg라고 명명하겠습니다. AppWizard의 모든 설정들은 기본 상태로 두고, [Finish] 버튼을 클릭합니다.

Visual C++ 7.0 이상의 버전에서는 “Attributed” 체크 박스의 체크를 해제합니다. 왜냐하면 우리는 이 예제 프로그램에서 Attributed ATL을 사용하지 않을 것이기 때문입니다.

DLL에 COM 개체를 추가하기 위해 “클래스 뷰(Class View)” 화면으로 이동하여 “DllReg classes” 트리 항목에 대해 마우스 오른쪽 버튼을 클릭하고 [New ATL Object] 항목을 클릭합니다. Visual C++ 7.0 이상의 버전에서는 해당 트리 항목에 대해 마우스 오른쪽 버튼을 클릭하고 “추가(D)”→”클래스 추가(C)...”를 클릭합니다. 영어 버전의 Visual Studio에서는 “Add”→”Add Class” 메뉴입니다.

ATL Object Wizard 화면에서, 첫 번째 단계를 보면 이미 Simple Object 항목이 선택되어 있을 것입니다. 가볍게 [Next] 버튼을 클릭합니다. 두 번째 단계에서 “Short Name” 에디트 상자에 DllRegShlExt를 입력합니다. 나머지 에디트 상자들은 자동으로 채워질 것입니다.

마법사는 OLE 오토메이션(Automation)을 통해 C 또는 스크립트 언어에서 사용 가능한 COM 객체를 자동으로 생성합니다. 우리가 만드는 쉘 익스텐션은 Windows 탐색기에서만 사용될 것이기 때문에, 우리는 몇 가지 설정을 조정함으로써 오토메이션 기능을 제거할 수 있습니다. ‘Attributes’ 페이지로 이동하여 인터페이스 타입을 ‘Custom’으로 변경합니다. 또한 ‘Aggregation’ 속성을 ‘No’로 변경합니다.

[OK] 버튼을 누르면 마법사는 COM 객체를 구현하기 위한 기본 코드들이 포함된 CDLLRegShlExt라는 이름의 클래스를 생성합니다. 우리는 이 클래스에 코드를 추가할 것입니다.

우리는 리스트 뷰(ListView) 컨트롤과 STL 클래스인 stringlist 클래스를 사용할 것입니다. 그러므로 stdafx.h에서 ATL 헤더들을 포함하는 지시문 뒤에 다음과 같이 내용을 추가합니다.

 

#include <atlwin.h> // 없으면 추가
#include <commctrl.h> // 없으면 추가
#include <string> // 새로 추가
#include <list> // 새로 추가

typedef std::list<std::basic_string<TCHAR>> string_list; // 새로 추가

 

초기화 인터페이스

우리의 IShellExtInit::Initialize 구현 내용은 1 단계에서 다루었던 쉘 익스텐션과 다소 다를 것입니다. 여기에는 두 가지 이유가 있습니다.

첫째로, 우리는 선택된 여러 개의 파일을 열거(enumerate)할 것입니다. 둘째로, 우리는 선택될 파일 각각에 대해서 그들이 등록 및 등록 해제 함수들을 내보내기(export) 하고 있는지 테스트할 것입니다. 우리는 이를 위해 각 파일에 대해 DllRegisterServerDllUnregisterServer를 내보내기 하고 있는지 확인할 것이고, 그렇지 않은 파일들을 무시할 것입니다.

1 단계에서 이미 해 보았듯이, C++ 클래스에서 마법사가 자동으로 생성할 일부 코드들을 삭제하고 IShellExtInit 인터페이스를 추가합니다.

 

#include <shlobj.h> // 새로 추가
#include <comdef.h> // 새로 추가
 
class ATL_NO_VTABLE CDLLRegShlExt :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CDLLRegShlExt, &CLSID_DllRegShlExt>,
    public IDllRegShlExt, // 이 줄은 삭제해도 됨
    public IShellExtInit { // 새로 추가 
    
    BEGIN_COM_MAP(CDLLRegShlExt)
        COM_INTERFACE_ENTRY(IDllRegShlExt)
        COM_INTERFACE_ENTRY(IShellExtInit)
    END_COM_MAP()
    
    // ...

 

우리의 CDLLRegShlExt 클래스는 또한 다음과 같은 몇 가지 멤버 변수들을 필요로 합니다.

 

    // ...
    
    protected:
    HBITMAP     m_hRegBmp;
    HBITMAP     m_hUnregBmp;
    string_list m_lsFiles;
    
    // ...

 

CDLLRegShlExt 생성자는 컨텍스트 메뉴에 사용할 두 가지 비트맵을 로드(load)합니다.

 

CDLLRegShlExt::CDLLRegShlExt() {
    m_hRegBmp = LoadBitmap(_Module.GetModuleInstance(), MAKEINTRESOURCE(IDB_REGISTERBMP));
    m_hUnregBmp = LoadBitmap(_Module.GetModuleInstance(), MAKEINTRESOURCE(IDB_UNREGISTERBMP));
}

 

이제, 우리는 Initialize 함수를 작성할 준비가 되었습니다. Initialize 함수는 다음과 같은 단계를 수행합니다.

1. 현재 디렉토리(current directory)는 Windows 탐색기가 열고 있는 디렉토리 경로로 변경하기.

2. 선택된 파일을 모두 열거하기.

3. DLL 파일과 OCX 파일에 대해 LoadLibrary를 사용하여 로드를 시도해 보기.

4. LoadLibrary가 성공하면 DllRegisterServerDllUnregisterServer 함수를 내보내기(export) 하고 있는지 확인하기.

5. 두 함수가 내보내기(export)되었음을 확인하면 파일 리스트로 쓰일 m_lsFiles에 파일 이름 추가하기.

 

HRESULT CDLLRegShlExt::Initialize(
    LPCITEMIDLIST pidlFolder,
    LPDATAOBJECT pDataObj,
    HKEY hProgID) {
    
    UINT      uNumFiles;
    HDROP     hdrop;
    FORMATETC etc = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
    STGMEDIUM stg = { TYMED_HGLOBAL };
    HINSTANCE hinst;
    HRESULT (STDAPICALLTYPE * pfn)();
    
    // ...

 

선언된 지역변수가 매우 많습니다! 먼저 수행할 단계는 전달된 pDataObj에서 HDROP을 얻는 것입니다. 이것은 지난 1 단계에서 해 보았던 것과 비슷합니다.

 

    // ...
    
    // 데이터 객체로부터 폴더 목록을 읽습니다.
    // 이것은 HDROP 포맷으로 저장되어 있기 때문에 HDROP 핸들을 가져와서
    // 드래그 앤 드롭 API와 함께 사용합니다.
    if (FAILED(pDO->GetData(&etc, &stg)))
        return E_INVALIDARG;
    
    // HDROP 핸들을 얻습니다.
    hdrop = (HDROP)GlobalLock(stg.hGlobal);
    
    if (hdrop == NULL) {
        ReleaseStgMedium(&stg);
        return E_INVALIDARG;
    }
    
    // 몇 개의 파일들이 선택되었는지를 확인합니다. 
    uNumFiles = DragQueryFile(hdrop, 0xFFFFFFFF, NULL, 0);
    
    // ...

 

곧 이어서 for 루프가 나옵니다. 이 루프에서는 DragQueryFile 함수를 사용하여 파일 이름을 하나씩 가져오고, LoadLibrary를 통해 로드를 시도합니다. 예제 프로젝트는 이 작업을 하기 전 현재 디렉토리를 수정하고 있으나 이 글에서는 너무 길어지므로 이를 생략하고 설명합니다.

 

    // ...
    
    for (UINT uFile = 0; uFile < uNumFiles; uFile++) {
        // 루프에 따라 파일 이름을 하나씩 가져옵니다.
        if (DragQueryFile(hdrop, uFile, szFile, MAX_PATH) == 0)
            continue;
        
        // DLL 파일로서 로드를 시도합니다.
        hinst = LoadLibrary(szFile);
        
        if (hinst == NULL)
            continue;
        
        // ...

 

이제 우리는 해당 모듈이 두 개의 필수 함수를 내보내기(export)하고 있는지 확인합니다.

    // ...
    
    // DllRegisterServer의 주소를 가져옵니다.
    (FARPROC &)pfn = GetProcAddress(hinst, "DllRegisterServer");
    
    // 찾을 수 없는 경우 이 파일은 건너뜁니다.
    if (pfn == NULL) {
        FreeLibrary(hinst);
        continue;
    }
    
    // DllUnregisterServer의 주소를 가져옵니다.
    (FARPROC &)pfn = GetProcAddress(hinst, "DllUnregisterServer");
    
    // 두 함수가 모두 내보내기 되었음이 확인되면,
    // 우리는 이 파일에 대해 작업을 수행할 수 있기 때문에,
    // 파일 리스트로 쓰일 m_lsFiles에 추가합니다.
    if (pfn != NULL)
        m_lsFiles.push_back(szFile);
    
    FreeLibrary ( hinst );
}  // for 루프의 끝

 

모듈에서 두 함수가 내보내기(export)된 상태로 존재한다면, 그 파일 이름을 m_lsFiles에 추가합니다. m_lsFiles는 문자열을 보관할 list 형의 STL 콜렉션 객체입니다. 이 리스트 객체는 나중에 각 파일을 등록 또는 등록 해제하기 위하여 반복(iterate)할 때 사용될 것입니다.

마지막으로 할 것은 Initialize를 호출하여 로드된 리소스들을 해제하고, Windows 탐색기에게 값을 반환하는 것입니다.

 

    // 리소스 반환
    GlobalUnlock(stg.hGlobal);
    ReleaseStgMedium(&stg);
    
    // 선택한 파일들 중에서 우리가 작업할 파일이 하나라도 있다면 S_OK를 반환하고,
    // 그렇지 않다면 이번 마우스 오른쪽 버튼 클릭에서는 우리가 할 것이 없으므로,
    // E_INVALIDARG를 반환하여 이번 건과 관련해서 호출되지 않도록 한다.
    return (m_lsFiles.size() > 0) ? S_OK : E_INVALIDARG;
}

 

여러분이 예제 프로젝트의 소스 코드를 유심히 본다면, 여러분은 필자가 파일 이름들을 보고 있는 디렉토리 경로를 확인해야 함을 알게 될 것입니다.

어쩌면 여러분은 왜 필자가 pidlFolder 매개 변수(parameter)를 사용하지 않는 것인지 궁금해 하실 수도 있습니다. 물론 이 매개 변수(parameter)에 대해 “컨텍스트 메뉴가 표시되고 있는 항목을 포함하고 있는 폴더에 대한 항목 식별자 리스트”라고 문서화 되어있기는 하지만 글쎄요…… 필자가 테스트 해 보았더니 이 매개 변수(parameter)는 항상 NULL이었습니다. 그래서 이건 유용하지가 않습니다.

 

컨텍스트 메뉴에 항목들을 추가하기

다음으로 해야 할 것은 IContextMenu 메소드들입니다. 전에 이미 해 보았듯이 우리는 다음과 같은 내용을 소스 코드에 추가함으로써 CDllRegShlExt 클래스가 구현하는 인터페이스 목록에 IContextMenu를 추가합니다.

 

class ATL_NO_VTABLE CDLLRegShlExt :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CDLLRegShlExt, &CLSID_DllRegShlExt>,
    public IShellExtInit,
    public IContextMenu { // 새로 추가
    
    BEGIN_COM_MAP(CDLLRegShlExt)
        COM_INTERFACE_ENTRY(IShellExtInit)
        COM_INTERFACE_ENTRY(IContextMenu)
    END_COM_MAP()

 

우리는 다음의 두 항목을 메뉴에 추가할 것입니다: 하나는 선택된 파일들을 등록하는 항목이고, 다른 하나는 등록 해제하는 항목입니다. 이들 항목은 다음과 같이 보여질 것입니다.

 

DLL의 등록 또는 등록 해제하는 쉘 익스텐션.

 

우리의 QueryContextMenu 구현은 앞서 설명한 1 단계와 비슷하게 시작합니다. 먼저 우리는 uFlags를 확인하고 CMF_DEFAULTONLY 옵션이 존재할 경우 즉시 값을 반환하고 종료합니다.

 

HRESULT CDLLRegShlExt::QueryContextMenu(
    HMENU hmenu, UINT uMenuIndex, 
    UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags) {
    
    UINT uCmdID = uidFirstCmd;
    
    // CMF_DEFAULTONLY 옵션이 포함되어 있을 경우 아무것도 하지 않아야 합니다.
    if (uFlags & CMF_DEFAULTONLY)
        return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0);
    
    // ...

 

다음으로 우리는 컨텍스트 메뉴에 “Register servers” 항목을 추가합니다. 새롭게 추가된 것이 있다면 우리는 각 항목에 비트맵을 설정한다는 것입니다. 이것은 WinZip이 메뉴 항목에 바이스(vice)에 끼워진 폴더 아이콘을 작게 표시해주는 것과 같은 것입니다.

 

    // ...
    
    // 등록 항목과 등록 해제 항목을 컨텍스트 메뉴에 추가한다.
    InsertMenu(hmenu, uMenuIndex, MF_STRING | MF_BYPOSITION, uCmdID++, _T("Register server(s)"));
    
    // 등록 항목에 비트맵 설정하기
    if (m_hRegBmp != NULL)
        SetMenuItemBitmaps(hmenu, uMenuIndex, MF_BYPOSITION, m_hRegBmp, NULL);
    
    uMenuIndex++;
    
    // ...

 

SetMenuItemBitmaps API는 컨텍스트 메뉴의 “Register server(s)” 항목에 우리가 추가할 작은 기어 모양의 아이콘을 보여주려는 방법입니다. uCmdID가 증가되었음을 주목하시기 바랍니다. 그 다음에 우리는 InsertMenu를 한번 더 호출할 때 Command ID는 이전에 지정한 값보다 최소한 1 이상은 더 큰 값이어야 합니다. 이 단계의 마지막에서 uMenuIndex는 증가되어 다음에 한번 더 추가할 메뉴 항목이 이번에 추가한 메뉴 항목 다음에 나타나도록 합니다.

두 번째 메뉴 항목에 대해 말하자면, 지금까지 설명했던 첫 번째 메뉴 항목을 추가하는 것과 똑같이 소스 코드를 작성하면 됩니다.

 

    // ...
    
    InsertMenu(hmenu, uMenuIndex, MF_STRING | MF_BYPOSITION, uCmdID++, _T("Unregister server(s)"));
    
    // 등록 해제 항목에 대해 비트맵을 설정합니다.
    if (m_hUnregBmp != NULL)
        SetMenuItemBitmaps(hmenu, uMenuIndex, MF_BYPOSITION, m_hUnregBmp, NULL);
    
    // ...

 

끝으로 우리는 Windows 탐색기에게 컨텍스트 메뉴에 몇 개의 항목을 추가했는지를 알려줍니다.

 

    // ...
    
    return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 2);
}

계속 읽기

이전 게시글: Windows 쉘 익스텐션 개발 가이드 - (1) 튜토리얼 (2/2)

다음 게시글: Windows 쉘 익스텐션 개발 가이드 - (2) 여러 개의 파일 (2/2)

 

카테고리 “API/COM”
more...
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (1) 튜토리얼 (2/2)
입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차..
API/COM
2021. 1. 19. 16:22

Windows 쉘 익스텐션 개발 가이드 - (1) 튜토리얼 (2/2)

API/COM
2021. 1. 19. 16:22

입문자를 위한 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)

 

1 단계. 쉘 익스텐션(Shell Extension)을 작성하기 위한 단계별 튜토리얼

이전 파트에 이어서...

 

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

그 다음 호출될 수 있는 IContextMenu 메소드에는 GetCommandString이 있습니다.

사용자가 Windows 탐색기 창에서 텍스트 파일에 대해 마우스 오른쪽 클릭을 하였을 때, 또는 텍스트 파일을 하나 선택하고 [파일(F)] 메뉴를 클릭하였을 때, 상태 표시줄은 우리가 추가한 컨텍스트 메뉴 항목에 마우스 포인터가 올라가면 ‘플라이-바이(fly-by) 도움말’을 보여주게 됩니다.

이 때 우리가 작성하는 GetCommandString 메소드는 Windows 탐색기를 통해 보여줄 문자열을 반환하게 될 것입니다.

GetCommandString의 원형은 다음과 같습니다.

 

HRESULT IContextMenu::GetCommandString(
    UINT idCmd, UINT uFlags, UINT * pwReserved, LPSTR pszName, UINT cchMax);

 

idCmd는 어떤 메뉴에 마우스 포인터가 올라가 있는지를 나타내는 0부터 시작하는 카운트입니다. 우리는 단 하나의 항목만을 추가했기 때문에 idCmd 값은 항상 0이 될 것입니다. 그러나 우리가 3개의 항목을 추가했다고 가정했을 때, idCmd0, 1, 2 중 하나가 될 것입니다.

uFlags는 또 다른 설정들의 집합으로서, 필자는 이에 대해 나중에 설명하겠습니다.

pwReserved에 대해서는 무시할 수 있습니다.

pszName은 쉘이 가지고 있는 버퍼에 대한 포인터로서, 우리는 상태 표시줄에 보여줄 문자열들을 이 곳에 저장하게 됩니다.

cchMax는 이 버퍼의 크기입니다.

반환하는 값은 S_OK 또는 E_FAIL 같은 평범한 HRESULT 상수입니다.

 

GetCommandString은 또한 메뉴 항목에 대한 “동사(verb)”를 가져오기 위해 호출될 수 있습니다. “동사(verb)”란 파일에 대해 가해질 수 있는 어떤 행위를 나타내는 언어 중립적인 문자열입니다. 이에 대해 ShellExecute에 관련된 문서들에서는 많은 것들을 이야기 하고 있습니다.

 

“동사(verb)”에 대한 이야기는 별도의 글에서 설명하는 것이 나을 것 같습니다. 그러나 간략하게 말하면, “동사(verb)”는 ‘열기(open)’, ‘인쇄(print)’처럼 레지스트리에 기재될 수도 있고, 컨텍스트 메뉴 확장에 의해 동적으로 생성될 수도 있습니다. 이것은 쉘 익스텐션에서 구현된 파일에 대한 행위가 ShellExecute이 호출될 때 실행될 수 있도록 해 줍니다.

어쨌든 필자가 이 모든 것을 언급한 이유는 GetCommandString이 왜 호출되는지를 여러분에게 설명하기 위함이었습니다. Windows 탐색기가 ‘플라이-바이(fly-by) 도움말’ 문자열을 원한다면 우리는 이를 제공해주면 됩니다. 또 Windows 탐색기가 “동사(verb)”를 요구한다면, 우리는 이 요청을 무시하면 됩니다. 이것이 uFlags 매개 변수(parameter)를 다루는 방법입니다.

uFlagsGCS_HELPTEXT 설정을 포함하고 있다면, Windows 탐색기는 ‘플라이-바이(fly-by) 도움말’을 요청하고 있는 것입니다. 추가적으로 GCS_UNICODE 설정을 포함하고 있다면, 우리는 반드시 유니코드 문자열로 반환해야 합니다.

GetCommandString은 다음과 같이 작성할 수 있습니다.

 

#include <atlconv.h>  // ATL 문자열 변환 매크로를 위해 include합니다.
HRESULT CSimpleShlExt::GetCommandString(
    UINT idCmd, UINT uFlags, UINT * pwReserved, LPSTR pszName, UINT cchMax) {
    
    USES_CONVERSION;
    
    // idCmd를 검사합니다.
    // 우리는 메뉴에 하나의 항목만을 추가할 것이므로 이 값은 항상 0이어야 합니다.
    if (idCmd != 0)
        return E_INVALIDARG;
    
    // Windows 탐색기가 도움말 문자열을 요청할 경우,
    // 제공된 버퍼에 우리가 가진 문자열을 복사하면 됩니다.
    if (uFlags & GCS_HELPTEXT) {
        LPCTSTR szText = _T("This is the simple shell extension's help");
        
        // Windows가 유니코드 문자열을 요청하는지 여부를 확인하여 유니코드인 경우...
        if (uFlags & GCS_UNICODE) {
            // 먼저 pszName을 유니코드 문자열로 캐스팅해야 합니다.
            // 이를 위해 유니코드 문자열 복사 API를 사용합니다.
            lstrcpynW((LPWSTR)pszName, T2CW(szText), cchMax);
        } else {
            // 도움말 문자열을 반환하기 위해 ANSI 문자열 복사 API를 사용합니다.
            lstrcpynA(pszName, T2CA(szText), cchMax);
        }
        
        return S_OK;
    }
    
    return E_INVALIDARG;
}

 

필자는 문자열을 하드코드(hardcoded)하여 적절한 문자열 집합으로 변환하도록 작성하였습니다. 여러분이 ATL 변환 매크로를 사용해 본 적이 없다면, 문자열 래퍼(wrapper) 클래스에 대한 필자의 글을 참고하시기 바랍니다. 그러면 유니코드 문자열을 COM 메소드나 OLE 함수에 전달하는 것이 한결 쉬워질 것입니다.

참고할만한 중요한 것은 lstrcpyn API는 목적지 문자열이 NULL 문자로 끝남을 보증한다는 것입니다. 이것이 CRT 함수인 strncpy와의 다른 점으로서, strncpy는 원본 문자열의 길이가 cchMax보다 크거나 같을 때 목적지 문자열에 NULL 문자를 붙이지 않습니다.

그러므로 필자는 항상 lstrcpyn를 사용할 것을 강력히 권장합니다. 그러면 여러분은 strncpy를 사용할 때와는 달리 결과 문자열이 NULL 문자로 끝났는지 여부를 확인할 필요가 없어집니다.

 

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

IContextMenu에 대해 마지막으로 살펴볼 메소드는 InvokeCommand입니다. 이 메소드는 우리가 추가한 메뉴 항목을 사용자가 클릭했을 때 호출됩니다. InvokeCommand의 원형은 다음과 같습니다.

 

HRESULT IContextMenu::InvokeCommand(
    LPCMINVOKECOMMANDINFO pCmdInfo);

 

CMINVOKECOMMANDINFO 구조체는 무수히 많은 정보들을 가지고 있습니다, 그러나 우리는 lpVerbhwnd에 대해서만 다루어 보겠습니다.

lpVerb는 두 가지 역할을 맡고 있습니다. 하나는 실행되려는 동사의 이름이고, 다른 하나는 클릭된 메뉴 항목의 인덱스입니다. hwnd는 사용자가 우리가 만든 쉘 익스텐션을 실행하는 Windows 탐색기 창의 핸들입니다. 이 핸들은 우리가 보여주고자 하는 사용자 인터페이스의 부모 윈도우를 지정할 때 사용할 수 있습니다.

우리가 하나의 메뉴 항목을 만들었기 때문에, 우리는 lpVerb를 확인할 것입니다. 이 값이 영(0)일 경우 우리는 우리가 만든 메뉴 항목이 클릭되었음을 알 수 있습니다. 이 때 해야 할 작업으로 필자가 생각할 수 있었던 가장 간단한 작업은 메시지 상자를 띄우는 것입니다. 그리고 이것이 우리가 작성하는 코드가 하는 일의 전부입니다. 메시지 상자는 선택된 파일 이름을 보여줌으로써 제대로 작동됨을 확인시켜 줍니다.

 

HRESULT CSimpleShlExt::InvokeCommand(LPCMINVOKECOMMANDINFO pCmdInfo) {
    // lpVerb가 실제로 존재하는 문자열을 가리기고 있다면,
    // 이 함수 호출을 무시하고 종료합니다.
    if (HIWORD(pCmdInfo->lpVerb) != 0)
        return E_INVALIDARG;
    
    // 각 메뉴 항목에 해당하는 명령 인덱스를 얻습니다.
    // 여기서는 하나의 메뉴 항목만을 가지고 있으므로 영(0) 만이 유효합니다.
    switch (LOWORD(pCmdInfo->lpVerb)) {
    case 0: {
        TCHAR szMsg[MAX_PATH + 32];
        
        wsprintf(szMsg, _T("The selected file was:\n\n%s"), m_szFile);
        
        MessageBox(pCmdInfo->hwnd, szMsg, _T("SimpleShlExt"), MB_ICONINFORMATION);
        
        return S_OK;
    }
    default: {
        return E_INVALIDARG;
    }
    }
}

 

기타 세부적인 사항

마법사가 자동으로 생성한 코드 중 우리가 필요로 하지 않는 OLE Automation 기능을 제거할 수 있습니다. 첫 번째로 우리는 SimpleShlExt.rgs 파일(이 파일의 목적은 다음 절에서 설명하겠습니다)에서 몇 가지 레지스트리 키를 제거할 수 있습니다.

 

HKCR {
    SimpleExt.SimpleShlExt.1 = s 'SimpleShlExt Class' {
        CLSID = s '{5E2121EE-0300-11D4-8D3B-444553540000}'
    }
    SimpleExt.SimpleShlExt = s 'SimpleShlExt Class' {
        CLSID = s '{5E2121EE-0300-11D4-8D3B-444553540000}'
        CurVer = s 'SimpleExt.SimpleShlExt.1'
    }
    NoRemove CLSID {
        ForceRemove {5E2121EE-0300-11D4-8D3B-444553540000} = s 'SimpleShlExt Class' {
            ProgID = s 'SimpleExt.SimpleShlExt.1'
            VersionIndependentProgID = s 'SimpleExt.SimpleShlExt'
            InprocServer32 = s '%MODULE%' {
                val ThreadingModel = s 'Apartment'
            }
        'TypeLib' = s '{73738B1C-A43E-47F9-98F0-A07032F2C558}'
        }
    }
}

 

우리는 또한 DLL 리소스에서 타입 라이브러리를 제거할 수 있습니다.

“View” 메뉴에서 “Resource Includes...”를 클릭합니다. “Compile-time directives” 상자에서 여러분은 다음과 같이 타입 라이브러리가 include된 것을 보실 수 있습니다.

 

[View] 메뉴에서 [Resource Includes...] 항목을 클릭한다.
“Compile-time directives”에 자동으로 추가되어있는 내용.

 

이 줄(1 TYPELIB "SimpleExt.tlb")을 삭제합니다. Visual C++가 include를 수정한다고 경고하면 [OK]를 누릅니다.

Visual C++ 7.0 이상에서는 이 과정이 다른 위치에 있습니다. “리소스 뷰(Resource View)” 탭에서 SimpleExt.rc를 마우스 오른쪽 버튼으로 클릭하고 컨텍스트 메뉴에서 “Resource Includes”를 클릭합니다.

이제 우리는 타입 라이브러리를 제거했습니다. 우리는 두 개의 줄을 수정하여 ATL에게 타입 라이브러리와 관련된 어떤 작업도 하지 않도록 알려주어야 합니다.

SimpleExt.cpp를 열고, DllRegisterServer 함수로 이동합니다. 그리고 RegisterServer를 호출할 때 지정되는 매개 변수(parameter)를 FALSE로 수정합니다.

 

STDAPI DllRegisterServer() {
    // ...
    return _Module.RegisterServer(FALSE); // TRUE로 되어있는 것을 FALSE로 변경
}

 

DllUnregisterServer도 같은 수정을 합니다.

 

STDAPI DllUnregisterServer() {
    // ...
    return _Module.UnregisterServer(FALSE); // TRUE로 되어있는 것을 FALSE로 변경
}

 

쉘 익스텐션을 등록하기

이제 우리는 COM 인터페이스를 모두 구현하였습니다. 그런데…… 어떻게 하면 Windows 탐색기가 우리가 만든 쉘 익스텐션을 사용하게 만들 수 있을까요? ATL은 우리가 만든 DLL을 COM 서버로 등록하는 소스 코드를 자동으로 생성합니다. 그러나 이것은 우리가 만든 DLL을 다른 어플리케이션이 사용할 수 있게 등록하는 과정일 뿐입니다. Windows 탐색기에게 우리가 만든 쉘 익스텐션을 사용하게 하려면, 우리는 다음과 같이 텍스트 파일에 대한 정보를 가지고 있는 레지스트리 키(key) 아래에 DLL을 등록할 필요가 있습니다.

 

HKEY_CLASSES_ROOT\txtfile

 

이 키의 하위에 ShellEx라는 이름을 가진 키를 둡니다. 이는 텍스트 파일에 대해 실행될 수 있는 쉘 익스텐션들의 목록을 보관하고 있습니다.

특히 ShellEx 키 하위에 놓일 ContextMenuHandler이라는 이름의 키는 컨텍스트 메뉴 확장에 대한 목록을 보관합니다. 각 쉘 익스텐션은 ContextMenuHandlers의 하위 키를 생성하고 그 하위 키의 기본 값으로 자신의 GUID를 설정합니다. 우리가 만든 예제 프로그램에서 우리는 다음과 같은 경로의 하위 키를 생성할 것입니다.

 

HKEY_CLASSES_ROOT\txtfile\ShellEx\ContextMenuHandlers\SimpleShlExt

 

그리고 이 키의 기본값으로 우리의 GUID{5E2121EE-0300-11D4-8D3B-444553540000}를 설정합니다.

이 작업을 위해 여러분이 어떠한 코드를 작성할 필요는 없습니다. “File View” 탭에 나타나는 파일 목록들을 보다 보면, 여러분은 SimpleShlExt.rgs라는 이름의 파일을 볼 것입니다. 이것은 ATL이 파싱하는 텍스트 파일로서, COM 서버가 등록될 때 레지스트리에 추가할 항목들이 무엇인지, 그리고 COM 서버가 등록 해제될 때 레지스트리에서 삭제될 항목들이 무엇인지를 ATL에게 알려줍니다.

다음은 Windows 탐색기가 우리가 만든 쉘 익스텐션의 존재를 알 수 있도록, 우리가 조정해야 할 레지스트리 항목의 예를 적은 것입니다.

 

HKCR {
    NoRemove txtfile {
        NoRemove ShellEx {
            NoRemove ContextMenuHandlers {
                ForceRemove SimpleShlExt = s '{5E2121EE-0300-11D4-8D3B-444553540000}'
            }
        }
    }
}

 

각 줄은 레지스트리 키 이름이고 KHCRHKEY_CLASSES_ROOT의 줄임말입니다.

NoRemove 키워드는 COM 서버가 등록 해제되어도 삭제되어서는 안 되는 레지스트리 키를 의미합니다. 특정 줄에서 발견되는 또 다른 키워드 ForceRemove는 새롭게 키가 작성될 때 기존에 레지스트리에 존재하고 있던 키는 삭제하라는 뜻입니다.

해당 줄의 나머지 부분은 문자열(앞에 붙은 글자 s의 의미이기도 합니다)로서, SimpleShlExt 키의 기본값으로 보관될 것입니다.

 

여기서 필자는 내용을 덧붙이고자 합니다.

우리가 만든 쉘 익스텐션을 등록할 때 키가 생성되는 위치는 HKEY_CLASSES_ROOT\txtfile입니다. 그러나 txtfile은 영구적인 이름도 아니고 미리 예약된 이름도 아닙니다. 여러분이 HKEY_CLASSES_ROOT\.txt 경로의 키를 열면 그 확장명에 대한 이름이 키의 기본값으로 저장되어 있음을 보게 될 것입니다. 이러한 구조는 두 가지 부작용이 있습니다.

 

–“txtfile”이 올바른 키의 이름이 아니기 때문에 RGS 스크립트를 신뢰성 있게 사용할 수 없습니다.

– 몇몇 텍스트 편집기들은 .txt 파일에 자기 자신을 연결 프로그램으로 지정하며 설치될 수 있습니다. 이는 HKEY_CLASSES_ROOT\.txt 키의 기본값을 변경하기 때문에, 쉘 익스텐션이 작동하지 않게 될 수 있습니다.

 

이것은 확실히 필자에게 결함처럼 보입니다. 마이크로소프트도 이를 인지하고 있을 것입니다. 왜냐하면 최근에 만들어지고 있는 쉘 익스텐션(예를 들어 QueryInfo 확장)은 파일 확장명으로 이름 붙인 키에 직접 등록하기 때문입니다.

필자의 사견은 여기까지입니다. 마지막으로 하나의 사항이 남아 있습니다. Windows NT 계열에서, 우리가 만든 쉘 익스텐션을 “승인된(approved)” 확장 목록에 추가할 것이 권장됩니다. 이것은 시스템 정책적으로 승인 목록에 없는 확장 프로그램이 로드(load)되는 것을 방지하기 위함입니다. 이 목록은 다음과 같은 경로에 보관되어 있습니다.

 

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved

 

이 키에서 우리는 이름이 우리가 만든 쉘 익스텐션의 GUID인 문자열을 생성합니다. 문자열의 내용은 아무거나 될 수 있습니다. 이러한 작업은 예제 프로그램 소스 코드의 DllRegisterServerDllUnregisterServer 함수에 있지만, 이 곳에서 보이지는 않겠습니다. 이 내용은 매우 간단한 레지스트리 액세스에 대한 것이므로, 여러분은 쉽게 찾으실 수 있습니다.

 

쉘 익스텐션을 디버그하기

나중에 가면 여러분은 그리 간단하지 않은 쉘 익스텐션을 작성하게 될 것이고, 이를 디버그하게 될 상황도 생깁니다. 프로젝트 설정을 열고 디버그 탭에 있는 “Executable for debug session” 에디트 상자에 Windows 탐색기의 전체 경로를 입력합니다. 예를 들여 C:\Windows\Explorer.exe처럼 입력하면 됩니다.

여러분이 Windows NT 기반을 사용하고 있고 이전에 DesktopProcess 레지스트리 키를 설정한 적이 있다면, 디버그를 위해 [F5] 버튼을 눌렀을 때 새로운 탐색기 창이 나타나게 될 것입니다. 그 창에서 여러분이 작업하는 한, 여러분은 DLL을 다시 빌드(build)하여도 문제가 없습니다. 왜냐하면 그 탐색기 창을 닫아버리면 여러분이 만들고 있는 쉘 익스텐션은 알아서 언로드(unload)될 것이기 때문입니다.

Windows 9x 운영체제를 사용하고 있다면, 여러분은 디버거를 실행하기 전 쉘을 종료해야 할 것입니다. [시작] 버튼을 누르고 “시스템 종료”를 클릭합니다. [Ctrl]+[Alt]+[Shift]를 누른 상태에서 [취소] 버튼을 클릭하면 Windows 탐색기가 종료되면서 작업표시줄이 사라지는 것을 보게 되실 것입니다. 그 다음 Microsoft Visual C++로 돌아가서 F5를 누르고 디버그를 시작합니다. 디버그를 중단하고자 할 때 [Shift]+[F5]를 눌러 Windows 탐색기를 종료합니다. 디버그를 완전히 끝내고자 할 때 명령 프롬프트에서 explorer를 실행 후 평소처럼 쉘(shell)을 재시작하면 됩니다.

 

결과물은 어떻게 생겼는가?

다음과 같이 컨텍스트 메뉴에 우리가 추가한 메뉴 항목이 나타납니다. 또한 우리가 추가한 메뉴 항목에 대해 ‘플라이-바이(fly-by) 도움말’이 나타날 것입니다.

 

텍스트 파일에 대해 마우스 오른쪽 버튼 클릭을 하면 쉘 익스텐션이 나타난다.

 

그리고 이를 클릭하였을 때 다음과 같이 파일 이름을 보여주는 메시지 상자가 뜰 것입니다.

 

쉘 익스텐션이 정상적으로 작동한다.

 

계속 읽기

이전 게시글: Windows 쉘 익스텐션 개발 가이드 - (1) 튜토리얼 (1/2)

다음 게시글: Windows 쉘 익스텐션 개발 가이드 - (2) 여러 개의 파일 (1/2)

 

카테고리 “API/COM”
more...
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (1) 튜토리얼 (1/2)
입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차..
API/COM
2021. 1. 16. 23:07

Windows 쉘 익스텐션 개발 가이드 - (1) 튜토리얼 (1/2)

API/COM
2021. 1. 16. 23:07

입문자를 위한 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)

 

1 단계. 쉘 익스텐션(Shell Extension)을 작성하기 위한 단계별 튜토리얼

실습 프로젝트 다운로드

ShellExtGuide1_demo.zip
38.4 kB

여러분이 이 게시물의 토론 게시판에 질문하기 전 꼭 읽어주시기 바랍니다.

본래 이 시리즈는 Visual C++ 6.0 사용자를 기준으로 작성되었습니다. Visual C++ 8.0 (Visual Studio 2005)이 출시된 만큼, 필자는 본 글을 Visual C++ 7.1(Visual Studio 2003)까지 다루도록 업데이트해야 할 필요가 있음을 느꼈습니다(Visual C++ 7.1이 제공하는 자동 업데이트가 있기는 하지만 그다지 매끄럽게 작동되는 것이 아니어서 독자 여러분이 첨부된 예제 프로그램을 실습하고자 할 때 좌절하실 수 있습니다). 필자가 글의 내용을 검토하고 본 시리즈를 업데이트 하면서, Visual C++ 7.1의 새로운 기능을 반영하고자 업데이트 할 것이고, 소스 코드의 다운로드 코너에서 Visual C++ 7.1 형식의 프로젝트를 포함시킬 것입니다.

 

Visual Studio 2005 사용자를 위한 주의사항

Visual C++ 2005 Express Edition은 ATL과 MFC를 포함하고 있지 않습니다. 그러나 본 시리즈는 ATL을 사용하고 경우에 따라서 MFC도 사용할 것이기 때문에, 본 글에 첨부된 예제 프로그램을 실습하고자 하는 분은 Express Edition을 사용할 수 없습니다.

여러분이 Visual C++ 6.0을 사용하고 있다면, 여러분은 Platform SDK를 업데이트할 필요가 있습니다. 웹 설치 버전을 사용할 수도 있고, 오프라인 상태에서 설치하는 경우를 위하여 CAB 파일 버전ISO 파일 버전도 마련되어 있습니다. 여러분은 Visual C++ 탐색 경로에 SDK include와 라이브러리 경로가 추가되었는지 확인해야 합니다. 이러한 경로는 “Platform SDK” 프로그램 그룹의 “Visual Studio Registration” 폴더에서 찾을 수 있습니다. 여러분이 Visual C++ 7.0 또는 8.0을 사용하고 있다 하더라도 Platform SDK를 업데이트하여 최신의 헤더 파일과 라이브러리를 유지하는 것이 좋습니다.

 

Windows® Server 2003 SP1 Platform SDK Web Install
PSDK-amd64.exe
1.29MB
PSDK-ia64.exe
1.38MB
PSDK-x86.exe
1.26MB

 

Windows® Server 2003 SP1 Platform SDK Full Download
PSDK-FULL.part01.rar
9.80MB
PSDK-FULL.part02.rar
9.80MB
PSDK-FULL.part03.rar
9.80MB
PSDK-FULL.part04.rar
9.80MB
PSDK-FULL.part05.rar
9.80MB
PSDK-FULL.part06.rar
9.80MB
PSDK-FULL.part07.rar
9.80MB
PSDK-FULL.part08.rar
9.80MB
PSDK-FULL.part09.rar
9.80MB
PSDK-FULL.part10.rar
9.80MB
PSDK-FULL.part11.rar
9.80MB
PSDK-FULL.part12.rar
9.80MB
PSDK-FULL.part13.rar
9.80MB
PSDK-FULL.part14.rar
9.80MB
PSDK-FULL.part15.rar
9.80MB
PSDK-FULL.part16.rar
9.80MB
PSDK-FULL.part17.rar
9.80MB
PSDK-FULL.part18.rar
9.80MB
PSDK-FULL.part19.rar
9.80MB
PSDK-FULL.part20.rar
9.80MB
PSDK-FULL.part21.rar
9.80MB
PSDK-FULL.part22.rar
9.80MB
PSDK-FULL.part23.rar
9.80MB
PSDK-FULL.part24.rar
9.80MB
PSDK-FULL.part25.rar
9.80MB
PSDK-FULL.part26.rar
9.80MB
PSDK-FULL.part27.rar
9.80MB
PSDK-FULL.part28.rar
9.80MB
PSDK-FULL.part29.rar
9.80MB
PSDK-FULL.part30.rar
9.80MB
PSDK-FULL.part31.rar
9.80MB
PSDK-FULL.part32.rar
9.80MB
PSDK-FULL.part33.rar
9.80MB
PSDK-FULL.part34.rar
9.80MB
PSDK-FULL.part35.rar
9.80MB
PSDK-FULL.part36.rar
9.80MB
PSDK-FULL.part37.rar
9.80MB
PSDK-FULL.part38.rar
9.80MB
PSDK-FULL.part39.rar
9.80MB
PSDK-FULL.part40.rar
9.80MB
PSDK-FULL.part41.rar
9.80MB
PSDK-FULL.part42.rar
9.80MB
PSDK-FULL.part43.rar
9.80MB
PSDK-FULL.part44.rar
2.27MB

 

Visual C++ 7.0 사용자를 위한 주의사항

여러분이 Platform DSK를 업데이트 하지 않았다면 include 경로의 기본값을 반드시 변경해야 합니다. 다음과 같이 ($VCInstallDir)include 보다 위쪽, 다시 말하면 목록의 첫 번째 줄이 $(VCInstallDir)PlatformSDK\include로 되어있는지 확인합니다.

 

[도구(T)] 메뉴의 [옵션(O)] 항목을 클릭한다.
“VC++ 디렉터리”의 “포함 파일”에서 Platform SDK의 포함 파일 경로를 맨 위로 한다.

 

필자는 아직 Visual C++ 8.0을 사용해본 적이 없기 때문에 Visual C++ 8.0(Visual Studio 2008)에서도 예제 프로그램이 컴파일되는지는 모르겠습니다. 다행히도 Visual C++ 6.0에서 7.0으로 변환하는 작업보다는 Visual C++ 7.0에서 8.0으로 변환하는 작업이 훨씬 더 수월합니다. Visual C++ 8.0에서 어려움을 겪으셨다면 이 글에 대한 포럼에 질문해주시기 바랍니다.

 

본 시리즈에 들어가며......

쉘 익스텐션(Shell Extension)은 몇 가지 기능들을 윈도우 쉘(Shell), Windows 탐색기에 추가하는 COM 객체입니다. 많은 종류의 쉘 익스텐션들이 존재하지만 이들에 대해 이해하기 쉽게 작성된 문서는 많지 않습니다. 물론 필자가 처음 글을 쓰기 시작한 이래 최근 6년 동안 상황이 나아졌다고 자부할 수 있습니다. 쉘에 대해 더 깊이 있게 알고자 하는 분들이 있다면, 필자는 Dino Esposito님이 작성하신 훌륭한 서적인 “Visual C++ Windows Shell Programming(ISBN 1861001843)”을 읽어볼 것을 강력히 권해 드립니다. 그러나 이 책을 갖고 있지 않은 분들 또는 쉘 익스텐션에 대해서만 알고 싶으신 분들을 위해 필자는 이 글을 작성합니다.

이 글을 통해 여러분은 경악할 수도 있고, 또는 쉘 익스텐션을 어떻게 작성하는지에 대해 이해할 수도 있습니다. 본 가이드는 여러분이 COM과 ATL의 기본을 익혔다고 가정할 것입니다. 독자 여러분이 COM 기본지식을 모르고 있다면, 필자의 COM 소개 글을 먼저 읽어보시길 바랍니다.

1 단계는 쉘 익스텐션의 일반적인 소개를 포함하고 있습니다. 그리고 이후 등장하는 단계들에 대한 맛보기로서 간단한 컨텍스트 메뉴 확장을 포함하고 있습니다.

“쉘 익스텐션(Shell Extension)”이라는 용어는 두 단어로 구성되어 있습니다.

쉘(shell)은 Windows 탐색기를 의미하고 익스텐션(extension)은 여러분이 작성하고, 미리 약속된 이벤트(예: .doc 파일을 선택하고 마우스 오른쪽 버튼을 클릭하는 경우)가 발생할 때 Windows 탐색기가 실행하는 프로그램 코드를 의미합니다. 그러므로 쉘 익스텐션은 Windows 탐색기에 기능을 추가하는 COM 객체의 일종입니다.

쉘 익스텐션은 ‘인 프로세스(in-process) COM 서버’로서 Windows 탐색기와의 통신을 다루는 몇 가지 인터페이스를 구현하고 있습니다. ATL은 쉘 익스텐션을 빠르게 불러와서 실행하도록 만드는 가장 쉬운 방법입니다, 왜냐하면 여러분이 QueryInterfaceAddRef를 거듭하여 작성하지 않아도 되기 때문입니다. 또한 ATL로 쉘 익스텐션을 작성하는 것은 Windows NT 기반의 운영체제에서 디버그를 할 때 좀 더 편하게 만들어 줍니다. 이에 대해서는 나중에 설명하도록 하겠습니다.

쉘 익스텐션에는 여러 종류가 있습니다. 그리고 각 유형들은 각기 다른 이벤트가 발생할 때 실행됩니다. 여기 주로 쓰이는 몇 가지 유형과 그들이 실행되는 상황에 대해 정리되어 있습니다.

 

유형 언제 실행되는가? 무엇을 하는가?
컨텍스트 메뉴 핸들러
(Context Menu Handler)
파일 또는 폴더에 대해 사용자가 마우스 오른쪽 클릭을 했을 경우에 실행됩니다. 특히 4.71 이상 버전의 쉘에서는 디렉토리 창의 여백을 마우스 오른쪽 클릭했을 때도 실행됩니다. 컨텍스트 메뉴에 항목을 추가합니다.
프로퍼티 시트 핸들러
(Property Sheet Handler)
파일에 대한 등록 정보(혹은 속성) 대화상자를 열 때 실행됩니다. 프로퍼티 시트에 페이지를 추가합니다.
드래그 앤 드롭 핸들러
(Drag and Drop Handler)
디렉토리 창 또는 바탕 화면에서 사용자가 항목들을 마우스 오른쪽 버튼을 눌러 ‘드래그 앤 드롭’ 할 때 실행됩니다. 컨텍스트 메뉴에 항목을 추가합니다.
드롭 핸들러
(Drop Handler)
사용자가 파일 위에다 항목들을 ‘드롭’ 할 때 실행됩니다. 미리 설계된 작업들을 수행합니다.
QueryInfo 핸들러
(QueryInfo Handler)
파일 또는 ‘내 컴퓨터(내 PC)’와 같은 쉘 객체에 마우스 포인터를 올려놓고 있을 때 실행됩니다. Windows 탐색기가 툴팁(Tooltip)을 통해 출력할 문자열을 반환합니다.

 

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

지금쯤 여러분은 Windows 탐색기에서 쉘 익스텐션이 어떻게 생긴 것인지 궁금해 하실 것입니다. 예를 들어 WinZip은 여러 가지 유형의 쉘 익스텐션을 포함하고 있고, 그 중 하나가 컨텍스트 메뉴 핸들러입니다. 다음은 압축 파일을 선택하고 마우스 오른쪽 클릭을 하였을 때 WinZip이 어떻게 쉘(shell)을 확장하는지를 보여주고 있습니다.

 

WinZip에 의해 확장된 컨텍스트 메뉴.

 

WinZip은 메뉴 항목들을 추가하는 코드와 ‘플라이-바이’ 도움말(fly-by help: Windows 탐색기의 상태 표시줄에 나타나는 텍스트)를 포함하고 있고 사용자가 이 명령 중 하나를 선택하였을 때 적절한 작업을 수행합니다.

 

WinZip에 의해 확장된 컨텍스트 메뉴 항목을 선택하면 나타나는 플라이-바이 도움말.

 

WinZip은 또한 드래그 앤 드롭 핸들러도 포함하고 있습니다. 이 유형은 컨텍스트 메뉴 핸들러와 매우 유사하지만 사용자가 마우스 오른쪽 버튼을 누른 상태로 파일을 드래그했을 때 실행됩니다. 다음은 WinZip의 드래그 앤 드롭 핸들러가 컨텍스트 메뉴에 무엇을 추가하고 있는지를 보여주고 있습니다.

 

zip 파일을 하나 선택하고 마우스 오른쪽 버튼을 눌러 드래그하면 나타나는 메뉴.

 

이외에 더 많은 유형들이 존재하고 마이크로소프트는 각 Windows 버전마다 더 많은 것들을 추가하고 있습니다. 지금부터 우리는 작성하기에 가장 간단하고, 실행 결과를 확인하기도 쉬운 컨텍스트 메뉴 확장에 대해 살펴보겠습니다.

코딩하기에 앞서, 작업을 더 쉽게 만들어 줄 수 있는 몇 가지 팁을 알려드리겠습니다. 여러분이 쉘 익스텐션을 호출하여(번역자 주: 마우스 오른쪽 클릭을 하여) Windows 탐색기가 이를 로드(load)할 때, 쉘 익스텐션은 메모리에서 잠시 머물러 있게 됩니다. 그리고 이것은 DLL 파일을 다시 빌드(build)하는 것을 불가능하게 만듭니다. Windows 탐색기가 쉘 익스텐션을 자주 언로드(unload)하도록 하기 위하여 다음과 같은 경로의 레지스트리 키를 생성합니다.

 

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Explorer\AlwaysUnloadDLL

 

그리고 이 키의 기본값을 1로 설정합니다. Windows 9x의 경우 이것이 여러분이 할 수 있는 최선입니다.

HKLM\Software\Microsoft\Windows\CurrentVersion\Explorer\AlwaysUnloadDLL

 

Windows NT의 경우 다음 경로의 레지스트리 키로 이동합니다.

 

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer

그리고 이 키에 이름이 DesktopProcessDWORD를 생성하고 그 값을 1로 설정합니다. 이것은 바탕화면과 작업표시줄이 하나의 프로세스에서 작동되도록 만들고, Windows 탐색기 창이 각각 자신의 프로세스에서 실행되도록 만듭니다.

또한 이것은 여러분이 하나의 Windows 탐색기 창을 가지고 디버그하고 그 창을 닫을 때 여러분이 만들고 있는 DLL 파일도 자동으로 언로드(unload)하도록 만들어 줌으로써 “파일이 사용 중입니다.”와 같은 문제를 방지해 줍니다. 레지스트리의 변경 사항은 여러분이 로그오프(logoff)하고 다시 로그온(logon)할 때 적용됩니다.

 

HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer에 추가된 DWORD 값.

 

Windows 9x에서 쉘 익스텐션을 디버그(debug)하는 것에 대하여는 나중에 설명하겠습니다.

 

AppWizard를 사용하여 시작하기

먼저 우리는 메시지 상자를 띄움으로써 이것이 작동 중임을 확인할 수 있는 쉘 익스텐션을 만들어 보겠습니다. 우리는 이 쉘 익스텐션을 .txt 파일에 걸어 놓아서 사용자가 텍스트 파일에 대해 마우스 오른쪽 클릭을 하였을 때 실행되게 만들 것입니다.

이제 시작해 볼 시간입니다. 필자는 아직 이 쉘 익스텐션을 어떻게 사용하는지에 대해 설명도 하지 않았습니다. 하지만 걱정하지 않아도 됩니다. 필자는 과정을 따라 설명하게 될 것입니다. 필자는 개념들이 설명되었다면, 예제 프로그램이 작성되는 과정을 따라 설명하는 것이 더 낫다고 생각하였습니다. 물론 필자는 모든 것을 먼저 설명한 후, 나중에 코드를 작성하게 해 드릴 수도 있습니다. 그러나 이것은 여러분이 흡수하기 어렵습니다. 어쨌든 Visual C++을 실행하고 바로 시작해 보겠습니다.

AppWizard를 실행하고 새 ATL COM 프로그램을 생성합니다. 우리는 이것을 SimpleExt라고 부르겠습니다. AppWizard의 기본 설정을 그대로 두고 [Finish]를 클릭합니다. 이제 우리는 당장이라도 DLL로 빌드될 수 있는 빈 ATL 프로젝트가 생겼지만, 쉘 익스텐션을 할 수 있는 COM 객체를 만들어야 합니다.

ATL COM AppWizard를 선택하여 새 프로젝트를 생성한다.
모든 설정을 그대로 두고 [Finish]를 클릭한다.

DLL에 COM 개체를 추가하기 위해 “클래스 뷰(Class View)” 화면으로 이동하여 “HardLink classes” 트리 항목에 대해 마우스 오른쪽 버튼을 클릭하고 [New ATL Object] 항목을 클릭합니다. Visual C++ 7.0 이상의 버전에서는 해당 트리 항목에 대해 마우스 오른쪽 버튼을 클릭하고 “추가(D)”→”클래스 추가(C)...”를 클릭합니다. 영어 버전의 Visual Studio에서는 “Add”→”Add Class” 메뉴입니다.

 

클래스 뷰 화면에서 [New ATL Object…]를 선택한다.
Visual C++ 7.0 이상의 버전에서 COM 개체를 추가할 수 있는 메뉴.

ATL Object Wizard 화면에서 첫 번째 단계는 이미 “Simple Object”라는 것을 선택하고 있습니다. 그냥 [Next >] 버튼을 클릭합니다.

[Objects] 범주에서 [Simple Object]를 선택하고 [Next >]를 누른다.

두 번째 단계에서 “Short Name”이라 적힌 에디트 상자에 SimpleShlExt라고 적습니다. 그러면 나머지 에디트 상자들은 자동으로 내용이 채워질 것입니다.

기본적으로 이 마법사는 C에서도 사용 가능하고, OLE Automation을 통해 스크립트 기반 언어에서도 사용할 수 있는 COM 객체를 생성하게 됩니다. 우리의 쉘 익스텐션은 Windows 탐색기에서만 사용될 것이기 때문에 우리는 몇 가지 설정들에 대해 Automation 관련 기능을 제거하도록 수정할 수 있습니다. 우선 ‘Attribute’ 페이지로 이동하여 ‘Interface Type’을 ‘Custom’으로 변경 후 ‘Aggregation’에 대해 ‘No’를 선택합니다.

 

우선 [Names] 탭의 [Short Name] 항목에 “SimpleShlExt”라고 적는다.
그 다음 [Attributes] 탭에서 ‘Apartment’, ‘Custom’, ‘No’ 순으로 클릭한다.

[확인] 버튼을 누르면 마법사는 COM 객체를 구현하기 위한 기본 코드가 포함된 CSimpleShlExt 클래스를 생성하고 이를 현재의 프로젝트에 추가할 것입니다. 이 클래스에 우리는 추가적으로 코딩을 하게 될 것입니다.

 

초기화 인터페이스

우리가 만들고 있는 쉘 익스텐션이 로드(load)되면, Windows 탐색기는 QueryInterface 함수를 호출하여 IShellExtInit 인터페이스에 대한 포인터를 얻어갈 것입니다. 이 인터페이스에는 단 하나의 메소드만 들어있습니다. Initialize로서 그 원형은 다음과 같습니다.

 

HRESULT IShellExtInit::Initialize(
    LPCITEMIDLIST pidlFolder,
    LPDATAOBJECT pDataObj,
    HKEY hProgID);

 

Windows 탐색기는 위 메소드를 호출함으로써 우리에게 다양한 정보를 전달합니다.

pidlFolder는 현재 선택된 파일들이 포함된 폴더의 PIDL입니다. PIDL은 ‘pointer to an ID list’의 약어로서 쉘에 있는 어느 객체이든, 그것이 파일시스템 객체이든 아니든 관계없이 유일하게 식별하도록 해 주는 데이터 구조체입니다.

pDataObj는 현재 선택된 파일들의 이름을 얻을 수 있는 IDataObject 인터페이스형 포인터입니다.

hProgID는 우리의 등록 정보가 포함된 레지스트리 키에 접근할 수 있는 HKEY입니다.

이 예제는 간단하기 때문에 우리는 단지 pDataObj 매개 변수(parameter)만을 사용하게 될 것입니다.

우리가 만들고 있는 COM 객체에 IShellExtInit를 추가하기 위해서, SimpleShlExt.h 파일을 열고 다음과 같이 굵게 표시된 줄을 추가하시기 바랍니다. 우리는 프로젝트가 생성되는 과정에서 자동으로 생성된 인터페이스를 사용하지 않을 것이므로, 마법사가 생성한 일부 COM 관련 코드들은 필요하지 않을 것입니다. 그래서 필자는 삭제될 수 있는 코드를 취소선으로 표시하였습니다.

 

#include <shlobj.h> // 새로 추가
#include <comdef.h> // 새로 추가
 
class ATL_NO_VTABLE CSimpleShlExt :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CSimpleShlExt, &CLSID_SimpleShlExt>,
    public ISimpleShlExt, // 이 줄은 삭제해도 됨
    public IShellExtInit {
    
    BEGIN_COM_MAP(CSimpleShlExt)
        COM_INTERFACE_ENTRY(ISimpleShlExt)
        COM_INTERFACE_ENTRY(IShellExtInit)
    END_COM_MAP()

 

COM_MAP은 ATL이 QueryInterface를 어떻게 구현할 것인지에 대한 것입니다. 이것은 우리가 만든 COM 객체를 다른 프로그램이 어떤 형식의 인터페이스로서 캐스팅하여 가져갈 수 있는지를 ATL에게 알려줍니다.

클래스 선언 내부에 Initialize에 대한 원형을 추가합니다. 우리는 또한 파일 이름을 보관할 버퍼가 필요합니다. 이를 선언합니다.

 

    // ...
    
    protected:
    TCHAR m_szFile[MAX_PATH];
    
    public:
    // IShellExtInit
    STDMETHODIMP Initialize(LPCITEMIDLIST, LPDATAOBJECT, HKEY);
    
    // ...

 

그 다음 SimpleShlExt.cpp 파일에서 이 함수의 정의를 추가합니다.

 

STDMETHODIMP CSimpleShlExt::Initialize(
    LPCITEMIDLIST pidlFolder,
    LPDATAOBJECT pDataObj,
    HKEY hProgID) {
    
    // ...
    
    }

 

우리가 해야 할 것은 마우스 오른쪽 클릭된 파일의 이름을 가져오고 그 이름을 메시지 상자로 보여주는 것입니다. 선택된 파일이 하나 이상이면, 여러분은 선택된 파일 모두를 pDataObj 인터페이스 포인터를 통해 접근할 수 있습니다, 그러나 우리는 이 단계에서 예제 프로그램을 단순하게 유지하기 위하여, 첫 번째 파일 이름만을 보도록 하겠습니다.

파일 이름들은 WS_EX_ACCEPTFILES 스타일이 적용된 윈도우에 파일을 드래그 앤 드롭 할 때와 동일한 형식으로 보관됩니다. 이것은 우리가 파일 이름을 얻고자 할 때 DragQueryFile과 똑같은 API를 사용한다는 뜻입니다. 우리는 IDataObject에 포함된 데이터에 대한 핸들을 얻는 것으로 시작하겠습니다.

HRESULT CSimpleShlExt::Initialize(...) {
    FORMATETC fmt = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
    STGMEDIUM stg = { TYMED_HGLOBAL };
    HDROP     hDrop;
    
    // 데이터 객체에서 CF_HDROP 데이터를 찾습니다. 
    // 그러한 데이터가 없다면 Windows 탐색기로 오류 코드를 반환합니다.
    if (FAILED(pDataObj->GetData(&fmt, &stg)))
        return E_INVALIDARG;
    
    // 실제 데이터에 대한 핸들을 얻습니다.
    hDrop = (HDROP)GlobalLock(stg.hGlobal);
    
    // 이것이 정상 작동하는지 확인합니다.
    if (hDrop == NULL)
        return E_INVALIDARG;
    
    // ...
    
}

 

모든 것을 오류 체크하고, 특히 포인터를 체크하는 것은 필수적으로 중요합니다. 우리의 쉘 익스텐션은 Windows 탐색기의 처리 공간에서 작동되기 때문에, 우리가 만들고 있는 앱이 충돌하면 Windows 탐색기도 함께 강제 종료하게 됩니다. Windows 9x에서는 그러한 충돌 때문에 컴퓨터를 재부팅해야 할 수도 있습니다.

HDROP 핸들을 만들었기 때문에 지금부터 우리는 우리가 원하는 파일의 이름을 가져올 수 있습니다.

 

HRESULT CSimpleShlExt::Initialize(...) {
    FORMATETC fmt = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
    STGMEDIUM stg = { TYMED_HGLOBAL };
    HDROP     hDrop;
    
    // ...
    
    // 상태 확인 - 적어도 하나 이상의 파일 이름을 가지고 있는지 확인한다.
    UINT uNumFiles = DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0);
    HRESULT hr = S_OK;
    
    if (uNumFiles == 0) {
        GlobalUnlock(stg.hGlobal);
        ReleaseStgMedium(&stg);
        return E_INVALIDARG;
    }
    
    // 첫 번째 파일의 이름을 얻어와서 멤버변수 m_szFile에 보관합니다.
    if (DragQueryFile(hDrop, 0, m_szFile, MAX_PATH) == 0)
        hr = E_INVALIDARG;
    
    GlobalUnlock(stg.hGlobal);
    ReleaseStgMedium(&stg);
    
    return hr;
}

 

이 메소드가 E_INVALIDARG를 반환한다면 Windows 탐색기는 마우스 오른쪽 버튼 클릭을 재차 하더라도 우리가 만드는 쉘 익스텐션을 실행하지 않을 것입니다.

S_OK를 반환할 때 Windows 탐색기는 QueryInterface 메소드를 다시 호출하여, IContextMenu라는 또 다른 인터페이스 포인터를 받아갈 것입니다.

 

컨텍스트 메뉴와 상호작용하기 위한 인터페이스

Windows 탐색기는 우리가 만든 쉘 익스텐션이 일단 초기화 되었다면, 우리가 컨텍스트 메뉴에 항목을 추가하고 플라이-바이 도움말을 제공하고, 사용자의 선택에 따른 작업을 수행할 수 있도록 IContextMenu 인터페이스의 메소드들을 호출할 것입니다.

우리가 만들고 있는 쉘 익스텐션에 IContextMenu 인터페이스를 추가하는 것은 IShellExtInit를 추가했던 것과 비슷합니다. SimpleShlExt.h를 열고 아래와 같이 줄을 추가합니다. 그리고 IContextMenu 메소드의 원형들을 추가합니다.

 

class ATL_NO_VTABLE CSimpleShlExt : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CSimpleShlExt, &CLSID_SimpleShlExt>,
    public IShellExtInit,
    public IContextMenu { // 새로 추가
    
    BEGIN_COM_MAP(CSimpleShlExt)
        COM_INTERFACE_ENTRY(IShellExtInit)
        COM_INTERFACE_ENTRY(IContextMenu)
    END_COM_MAP()
    
    // ...
    
    // IContextMenu에 있는 메소드 원형 추가
    public:
    STDMETHODIMP GetCommandString(UINT, UINT, UINT*, LPSTR, UINT);
    STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO);
    STDMETHODIMP QueryContextMenu(HMENU, UINT, UINT, UINT, UINT);
}

 

컨텍스트 메뉴 수정하기

IContextMenu는 세 가지 메소드를 가지고 있습니다.

그 중 첫 번째 메소드인 QueryContextMenu는 우리에게 컨텍스트 메뉴를 수정할 수 있게 해 줍니다. QueryContextMenu의 원형은 다음과 같습니다.

 

HRESULT IContextMenu::QueryContextMenu(
    HMENU hmenu,
    UINT uMenuIndex,
    UINT uidFirstCmd,
    UINT uidLastCmd,
    UINT uFlags);

 

hmenu는 컨텍스트 메뉴에 대한 핸들입니다.

uMenuIndex는 우리가 메뉴 항목을 추가하게 되는 컨텍스트 메뉴 위치(순서)입니다.

uidFirstCmduidLastCmd는 우리가 컨텍스트 메뉴 각 항목에 부여할 수 있는 Command ID의 범위입니다.

uFlags는 Windows 탐색기가 왜 QueryContextMenu를 호출하였는지 이유를 나타내고 있습니다. 이것에 대해서는 나중에 설명하겠습니다.

반환 값은 여러분이 누구에게 물어보는지에 따라 다르게 문서화되어 있습니다. Dino Esposito의 책에 따르면 이 반환 값은 QueryContextMenu에 따라 추가된 메뉴 항목의 개수입니다. Visual C++ 6.0 및 그 이후 버전에 부속되는 MSDN에 따르면, 우리가 가장 마지막으로 추가한 메뉴 항목의 Command ID입니다.

온라인 버전의 MSDN에서는 이렇게 설명하고 있습니다:

성공하면 심각도는 SEVERITY_SUCCESS로 설정되고 코드 값은 (여러분이) 사용한 Command ID 값 중 (uidFirstCmd로부터) 가장 큰 값에 대한 오프셋(offset)으로 설정된 HRESULT입니다.

예를 들어 idCmdFirst5이고, 여러분이 컨텍스트 메뉴에 5, 7, 8의 Command ID를 갖는 항목을 추가했다면, 이 반환 값은 MAKE_HRESULT(SEVERITY_SUCCESS, 0, 8 - 5 + 1)이어야 합니다. 그 외의 경우 OLE 오류 값을 반환합니다.

필자는 지금까지 Dino의 설명을 따라 소스 코드를 작성해왔고, 문제 없이 작동되었습니다. 사실 이 저자가 반환 값을 구성하는 방법은 uidFirstCmd부터 1씩 증가해가며 Command ID를 사용하는 한 온라인 MSDN과 동일합니다.

우리가 만들고 있는 예제는 단 하나의 항목만을 컨텍스트 메뉴에 추가할 것이므로, QueryContextMenu 함수도 다음과 같이 간단하게 작성될 수 있습니다.

 

HRESULT CSimpleShlExt::QueryContextMenu(
    HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags) {
    // uFlags가 CMF_DEFAULTONLY를 포함하고 있다면 우리는 특별히 할 것이 없습니다.
    if (uFlags & CMF_DEFAULTONLY)
        return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0);

    InsertMenu(hmenu, uMenuIndex, MF_BYPOSITION, 
    uidFirstCmd, _T("SimpleShlExt Test Item"));

    return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 1);
}

 

먼저 우리가 확인해야 할 것은 uFlags입니다. 여러분은 MSDN을 통해 이 값이 될 수 있는 전체 목록을 확인할 수 있습니다만, 컨텍스트 메뉴 확장에 한한다면 CMF_DEFAULTONLY 하나만이 중요합니다. 이 설정은 네임스페이스 확장(namespace extension)에게 기본 메뉴 항목만을 추가할 것을 알려줍니다. 이 설정이 되어 있다면 쉘 익스텐션은 컨텍스트 메뉴에 어떤 항목도 새로 추가해서는 안 됩니다. 이것이 uFlagsCMF_DEFAULTONLY를 포함하고 있을 때 즉시 0을 반환하는 이유입니다.

이 설정이 없을 경우 우리는 hmenu로 전달된 컨텍스트 메뉴에 항목을 추가하고, 우리가 1개의 항목을 새로 추가하였음을 쉘(shell)에게 알리기 위해 1을 반환합니다.

 

계속 읽기

다음 게시글: Windows 쉘 익스텐션 개발 가이드 - (1) 튜토리얼 (2/2)

 

카테고리 “API/COM”
more...

“API/COM” (33건)