입문자를 위한 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)
5 단계. 파일에 대한 ‘등록 정보’(또는 ‘속성’) 다이얼로그에 페이지를 추가하는 쉘 익스텐션(Shell Extension)
실습 프로젝트 다운로드
들어가기에 앞서......
프로퍼티 시트의 세계로 모험해 보겠습니다. 여러분이 파일 시스템 개체에 대해 등록정보(또는 속성) 다이얼로그를 띄웠을 때 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가 설치되어 있음으로 인해 나타나는 탭입니다.
초기화 인터페이스
이제 여러분은 지금까지 설명했던 프로젝트 생성 및 클래스 추가 등의 과정에 대해서는 익숙해 지셨을 것입니다. 그러므로 필자는 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.h
의 MAXPROPPAGES
라는 상수로 정의되어 있습니다. 각 파일들은 자신만의 페이지를 가질 수 있지만, 파일 이름들을 포함하는 문자열 리스트가 MAXPROPPAGES
이상인 경우 넘어가는 부분은 잘라내어 MAXPROPPAGES
에서 지정한 개수에 맞출 것입니다. 물론 현재는 MAXPROPPAGES
가 100
으로 되어 있지만, 프로퍼티 시트가 그렇게까지 많은 페이지를 보여주지는 않습니다. 실질적으로 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
은 페이지가 생성되거나 파괴될 때 호출되는 콜백 함수의 주소입니다. 이 함수의 역할은 잠시 후 설명하겠습니다.
pcRefParent
는 CComModule
에서 상속된 어떤 클래스의 멤버 변수에 대한 주소입니다. 사실 이 값은 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)