입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차이가 있을 수 있음을 감안하시기 바랍니다.
또한 본 게시물은 원문을 최대한 직역하는 것을 지향하고 있으나, 우리말로 읽었을 때 보다 매끄럽게 하기 위하여 부득이 의역, 어순 조정 및 어휘 조정이 있음을 양해 바랍니다.
- 목차
- 쉘 익스텐션(Shell Extension)을 작성하기 위한 단계별 튜토리얼
- 여러 개의 파일에 대해 한번에 작동하는 쉘 익스텐션(Shell Extension)
- 파일에 대해 ‘팝업(Popup)’ 설명을 보여주는 쉘 익스텐션(Shell Extension)
- 사용자 정의 ‘드래그 앤 드롭(Drag and Drop)’ 기능을 제공하는 쉘 익스텐션(Shell Extension)
- 파일에 대한 ‘등록 정보’(또는 ‘속성’) 다이얼로그에 페이지를 추가하는 쉘 익스텐션(Shell Extension)
- ‘보내기(Send To)’ 메뉴에서 사용될 수 있는 쉘 익스텐션(Shell Extension)
- 컨텍스트 메뉴에 그림 출력하는 쉘 익스텐션(Shell Extension)
및 디렉토리의 빈 공간에서 마우스 오른쪽 클릭에 응답하는 컨텍스트 메뉴 익스텐션(Shell Extension) - Windows 탐색기에서 “자세히” 보기 모드를 선택할 때 나타나는 열 항목을 추가하는 쉘 익스텐션(Shell Extension)
- 특정 형식의 파일에 대해 아이콘을 사용자화 하는 쉘 익스텐션(Shell Extension)
2 단계. 여러 개의 파일에 대해 한번에 작동하는 쉘 익스텐션(Shell Extension)
실습 프로젝트 다운로드:
들어가기에 앞서…
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 클래스인 string
및 list
클래스를 사용할 것입니다. 그러므로 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) 하고 있는지 테스트할 것입니다. 우리는 이를 위해 각 파일에 대해 DllRegisterServer
와 DllUnregisterServer
를 내보내기 하고 있는지 확인할 것이고, 그렇지 않은 파일들을 무시할 것입니다.
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
가 성공하면 DllRegisterServer
와 DllUnregisterServer
함수를 내보내기(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()
우리는 다음의 두 항목을 메뉴에 추가할 것입니다: 하나는 선택된 파일들을 등록하는 항목이고, 다른 하나는 등록 해제하는 항목입니다. 이들 항목은 다음과 같이 보여질 것입니다.
우리의 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)