입문자를 위한 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)
3 단계. 파일에 대해 ‘팝업(Popup)’ 설명을 보여주는 쉘 익스텐션(Shell Extension)
실습 프로젝트 다운로드:
들어가기에 앞서......
본 가이드의 1 단계과 2 단계에서 필자는 컨텍스트 메뉴 확장을 작성하는 방법에 대해 보여드렸습니다. 이번 3 단계에서는 조금 다른 유형의 쉘 익스텐션에 대해 설명하고자 합니다. 또한 쉘과 메모리를 공유하는 방법과 ATL과 함께 MFC를 병용하는 방법에 대해서 설명하겠습니다. 이번 단계에서는 여러분이 앞서 두 개의 단계를 통해 쉘 익스텐션의 기본에 대해 이해하고 있고, MFC를 알고 있다고 가정하겠습니다.
액티브 데스크톱(Active Desktop) 쉘에서는 새로운 기능을 도입하였습니다. 바로 여러분이 특정 개체에 마우스 포인터를 올려 놓았을 때 이 개체에 대한 설명을 보여주는 툴팁(tooltip)입니다. 예를 들어, “내 문서” 아이콘에 마우스 포인터를 오랫동안 올려 놓으면 다음과 같이 툴팁이 표시됩니다.
“내 컴퓨터” 또는 “제어판” 등의 다른 개체들도 비슷한 툴팁을 가지고 있습니다. 이러한 형태의 텍스트는 마우스 포인터가 올려진 파일, 폴더, 개체에 대해 정보를 제공하는 툴팁이라는 의미에서 인포팁(infotip)이라 명명합니다. 우리도 인포팁 익스텐션(infotop extension)을 사용하여 이와 같이 쉘의 다른 개체에 대해 인포팁 문자열을 제공할 수 있습니다.
여러분이 이미 보았을 수도 있겠지만 이러한 인포팁 익스텐션의 예는 WinZip에서 확인할 수 있습니다. 압축 파일에 대해 마우스 오른쪽 버튼을 올려 놓으면 WinZip은 다음과 같은 내용을 표시해 줍니다.
본 게시글의 예제 프로젝트는 파일의 크기와 함께 첫 번째 줄의 내용을 표시해주는 신속한 텍스트 파일 뷰어가 되겠습니다. 이 익스텐션은 .txt
파일에 대해 마우스 포인터를 올려 놓았을 때 인포팁의 형태로 보여집니다.
Visual C++ 7.0 또는 8.0 사용자는 이전과 같이 단계 1의 본 시리즈에 들어가며......를 참고하여 컴파일하기 전 몇 가지 설정을 변경해야 함을 기억하시기 바랍니다.
AppWizard를 사용하여 시작하기
AppWizard를 실행하고 새로운 ATL COM 프로젝트를 생성합니다. 이 프로젝트를 TxtInfo
라고 명명하겠습니다. 우리는 MFC를 사용할 것이기 때문에 “Support MFC” 체크상자에 체크를 하고 [Finish]를 클릭합니다.
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를 사용할 수 있습니다.
초기화 인터페이스
이전 게시글의 컨텍스트 메뉴 확장에서 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
는 이번 단계에서는 사용되지 않을 것입니다. ppwszTip
은 LPWSTR
형 포인터로서 우리는 우리가 직접 메모리를 할당하고 이에 대한 포인터를 전달해야 합니다.
먼저 우리는 파일을 열고 그 내용을 읽어야 합니다. 우리는 일찍이 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
에서 문자열을 읽어 툴팁으로 보여줄 것입니다.
쉘 익스텐션을 등록하기
인포팁 익스텐션은 컨텍스트 메뉴 익스텐션과 약간 다른 방식으로 등록됩니다. 이번 쉘 익스텐션도 물론 HKEY_CLASSES_ROOT
에 등록되지만, 우리가 다루고자 하는 파일 확장명에 대한 레지스트리 키에 직접 등록됩니다. 즉 이 경우에는 HKCR\.txt
에 등록된다는 것입니다.
특이한 것은 또 있습니다. 여러분은 ShellEx
의 서브 키로 추가될 레지스트리 키의 이름이 TooltipHandlers
와 같은 논리적이고 추상적인 이름이 될 것으로 생각하겠지만, 이 이름은 {00021500-0000-0000-C000-000000000046}
으로 미리 정해져 있습니다.
필자가 생각하기에 마이크로소프트는 우리가 모르는 쉘 익스텐션들을 더 가지고 있을 것 같습니다. 여러분이 레지스트리를 자세히 찾아본다면 GUID
로 된 이름을 가진 서브키들이 다른 ShellEx
에도 있음을 알 수 있습니다. 이 GUID
들은 IQueryInfo
의 GUID
로 나타납니다.
우리가 개발한 쉘 익스텐션이 .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) 익스텐션 목록에 추가해야 한다고 하였습니다. 이런 작업을 수행하는 것이 첨부된 예제 프로젝트에 포함된 DllRegisterServer
및 DllUnregisterServer
함수입니다.
다음 단계에서 다룰 내용
4 단계에서 우리는 다시 컨텍스트 메뉴 익스텐션의 세계로 돌아와서 또 다른 유형의 쉘 익스텐션인 드래그 앤 드롭 핸들러에 대해 살펴보겠습니다. 우리는 또한 MFC를 보다 많이 사용해볼 것입니다.
계속 읽기
다음 게시글: Windows 쉘 익스텐션 개발 가이드 - (2) 여러 개의 파일 (2/2)
이전 게시글: Windows 쉘 익스텐션 개발 가이드 - (4) 드래그 앤 드롭