코딩캣: 코딩하는 고양이.
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' 카테고리의 다른 글
더 보기...
태그 : 
댓글