^(코딩캣)^ = @"코딩"하는 고양이;

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...