입문자를 위한 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)
이전 파트에 이어서...
프로퍼티 페이지 콜백 함수
이제 프로퍼티 페이지 그 자체에 대해 살펴봅시다. 새로 추가될 페이지는 다음과 같이 생겼습니다. 이 모양을 잘 기억해 두었다가, 페이지가 어떻게 작동되는지를 설명할 때 참고하시기 바랍니다.
마지막으로 액세스된 시각 항목이 없을 수도 있습니다. FAT 파일 시스템만이 마지막으로 액세스된 날짜 항목을 기록하고 있습니다(번역자 주: FAT 파일 시스템은 시각까지는 기록하지 않는다는 의미입니다). 다른 파일 시스템은 시각까지 기록을 합니다만, 필자는 파일 시스템에 따라 이를 다르게 처리하는 기능까지는 구현하지 않겠습니다. 마지막으로 액세스된 시각까지 기록할 수 있는 파일 시스템에서 시각은 항상 사용자가 지정한 날짜의 자정으로 기록될 것입니다.
추가되는 페이지는 두 개의 콜백 함수와 두 개의 메시지 핸들러를 가지고 있습니다. 이들의 원형은 FileTimeShlExt.cpp
의 상단에 적혀있습니다.
BOOL CALLBACK PropPageDlgProc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
UINT CALLBACK PropPageCallbackProc(HWND hwnd, UINT uMsg, LPPROPSHEETPAGE ppsp);
BOOL OnInitDialog(HWND hwnd, LPARAM lParam);
BOOL OnApply(HWND hwnd, PSHNOTIFY * phdr);
이 다이얼로그 프로시저는 매우 단순해서 WM_INITDIALOG
, PSN_APPLY
, DTN_DATETIMECHANGE
라는 단 세 개의 메시지만을 처리합니다. 그 중 WM_INITDIALOG
부분은 다음과 같이 생겼습니다.
BOOL CALLBACK PropPageDlgProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
BOOL bRet = FALSE;
switch(uMsg) {
case WM_INITDIALOG:
bRet = OnInitDialog(hwnd, lParam);
break;
// ...
이어서 PSN_APPLY
에 대한 이벤트 처리입니다. 사용자가 [확인] 또는 [적용] 버튼을 누를 때 이 이벤트가 전달됩니다.
// ...
case WM_NOTIFY: {
NMHDR* phdr = (NMHDR *)lParam;
switch (phdr->code) {
case PSN_APPLY:
bRet = OnApply(hwnd, (PSHNOTIFY *)phdr);
break;
// ...
마지막으로 DTN_DATETIMECHANGE
가 있습니다. 이것은 간단합니다. 우리가 만들고 있는 페이지의 부모 윈도우인 프로퍼티 시트에 메시지를 보내서 [적용] 버튼을 활성화시키는 것입니다.
// ...
case DTN_DATETIMECHANGE:
// 사용자가 DTP 컨트롤의 값을 변경하였다면,
// [적용] 버튼을 활성화시키도록 부모 윈도우에게 메시지를 보냅니다.
SendMessage(GetParent(hwnd), PSM_CHANGED, (WPARAM)hwnd, 0);
break;
} // switch 끝
} // case WM_NOTIFY의 끝
break;
} // switch의 끝
return bRet;
}
지금까지는 순조로웠습니다. 페이지가 생성되거나 파괴될 때 또 다른 콜백 함수가 호출됩니다. AddPages
에서 생성된 문자열 복사본을 해제해야 하므로 우리는 후자에 대해 다루면 됩니다. ppsp
매개 변수(parameter)는 페이지를 생성하는 데 사용했던 PROPSHEETPAGE
구조체를 가리키고 있습니다. 그리고 lParam
멤버는 해제되어야 할 문자열 복사본을 여전히 가리키고 있습니다.
UINT CALLBACK PropPageCallbackProc(HWND hwnd, UINT uMsg, LPPROPSHEETPAGE ppsp) {
if (uMsg == PSPCB_RELEASE)
free((void *)ppsp->lParam);
return 1;
}
이 함수는 페이지가 생성될 때 호출될 때 호출되기 때문에 항상 1
을 반환합니다. 0
을 반환하면 페이지가 생성되지 않을 수 있습니다. 1
을 반환하는 것은 페이지에게 자기자신이 정상적으로 생성되고 있음을 알려줍니다. 페이지가 파괴될 때 호출될 때는 이 함수의 반환 값이 무엇이든 되었든 무시됩니다.
프로퍼티 페이지 메시지 핸들러
OnInitDialog
에서 중요한 작업들이 많이 발생했습니다. lParam
매개 변수(parameter)는 본 페이지를 생성하는데 사용된 PROPSHEETPAGE
구조체를 가리킵니다. 이것의 lParam
멤버는 앞서 말한 그 문자열 복사본을 가리킵니다. 우리는 OnApply
메서드에서 파일 이름이 필요할 것이라고 했는데, 그 파일 이름을 SetWindowLong
을 호출하여 윈도우에 저장합니다.
BOOL OnInitDialog(HWND hwnd, LPARAM lParam) {
PROPSHEETPAGE * ppsp = (PROPSHEETPAGE*) lParam;
LPCTSTR szFile = (LPCTSTR) ppsp->lParam;
HANDLE hFind;
WIN32_FIND_DATA rFind;
// 나중에 사용하기 위하여, 파일 이름에 대한 포인터를 윈도우 사용자 데이터에 보관합니다.
SetWindowLong(hwnd, GWL_USERDATA, (LONG)szFile);
// ...
다음으로 우리는 FindFirstFile
을 사용하여 특정 파일에 대한 생성된 날짜 및 시각, 수정된 날짜 및 시각 그리고 마지막으로 액세스된 날짜 및 시각을 가져옵니다. 가져오는 데 성공하였다면 DTP 컨트롤은 이들 날짜 및 시각으로 초기화될 것입니다.
// ...
hFind = FindFirstFile(szFile, &rFind);
if (hFind != INVALID_HANDLE_VALUE) {
// DTP 컨트롤들을 초기화합니다.
SetDTPCtrl(hwnd, IDC_MODIFIED_DATE, IDC_MODIFIED_TIME, &rFind.ftLastWriteTime);
SetDTPCtrl(hwnd, IDC_ACCESSED_DATE, 0, &rFind.ftLastAccessTime);
SetDTPCtrl(hwnd, IDC_CREATED_DATE, IDC_CREATED_TIME, &rFind.ftCreationTime);
FindClose(hFind);
}
// ...
SetDTPCtrl
은 DTP 컨트롤의 내용을 설정하는 유틸리티 함수입니다. 여러분은 이 함수에 대한 코드를 FileTimeShlExt.cpp
에서 확인하실 수 있습니다.
추가적으로 페이지의 상단에 있는 스태틱(static) 컨트롤을 통해 파일에 대한 전체 경로가 나타날 것입니다.
// ...
PathSetDlgItemPath(hwnd, IDC_FILENAME, szFile);
return FALSE;
}
OnApply
핸들러는 이와 반대의 작동을 합니다. 즉, DTP 컨트롤의 값을 읽고 파일에 대해 생성된 날짜 및 시각, 수정된 날짜 및 시각 그리고 마지막으로 수정된 날짜 및 시각을 고칩니다. 이에 대한 첫 번째 단계로 GetWindowLong
을 사용하여 고칠 파일에 대한 전체 경로가 담긴 문자열 포인터를 얻습니다.
BOOL OnApply(HWND hwnd, PSHNOTIFY * phdr) {
LPCTSTR szFile = (LPCTSTR)GetWindowLong(hwnd, GWL_USERDATA);
HANDLE hFile;
FILETIME ftModified, ftAccessed, ftCreated;
// 파일을 엽니다.
hFile = CreateFile(szFile, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
// ...
파일을 열 수 있다면, DTP 컨트롤로부터 날짜와 시각들을 읽어서 파일을 고칩니다. ReadDTPCtrl
은 SetDTPCtrl
과 반대의 작동(DTP 컨트롤의 현재 값으로 파일을 수정)을 합니다.
if (hFile != INVALID_HANDLE_VALUE) {
// DTP 컨트롤에서 날짜와 시각을 가져옵니다.
ReadDTPCtrl(hwnd, IDC_MODIFIED_DATE, IDC_MODIFIED_TIME, &ftModified);
ReadDTPCtrl(hwnd, IDC_ACCESSED_DATE, 0, &ftAccessed);
ReadDTPCtrl(hwnd, IDC_CREATED_DATE, IDC_CREATED_TIME, &ftCreated);
// 파일의 생성된 날짜 및 시각, 수정된 날짜 및 시각, 마지막으로 액세스된 날짜 및 시각을 고칩니다.
SetFileTime(hFile, &ftCreated, &ftAccessed, &ftModified);
CloseHandle ( hFile );
} else {
// 오류 처리 부분은 생략합니다.
}
// 사용자가 [확인] 버튼을 클릭하였을 때 창이 닫힐 수 있도록 PSNRET_ERROR를 반환합니다.
SetWindowLong(hwnd, DWL_MSGRESULT, PSNRET_NOERROR);
return TRUE;
}
쉘 익스텐션을 등록하기
프로퍼티 시트 익스텐션을 등록하는 것은 컨텍스트 메뉴 익스텐션 또는 드래그 앤 드롭 익스텐션을 등록하는 것과 비슷합니다. 앞서 실습한 쉘 익스텐션들은 텍스트 파일과 같은 특정 파일에 대해서만 호출되도록 등록할 수 있었습니다만, 이 프로퍼티 시트 익스텐션은 임의의 파일에 대해서 작동되도록 만들었기 때문에 HKEY_CLASSES_ROOT\*
키의 하위키로 등록해 보겠습니다. 본 쉘 익스텐션을 등록하기 위한 RGS 스크립트는 다음과 같이 생겼습니다.
HKCR {
NoRemove * {
NoRemove shellex {
NoRemove PropertySheetHandlers {
{3FCEF010-09A4-11D4-8D3B-D12F9D3D8B02}
}
}
}
}
여러분은 본 쉘 익스텐션의 GUID
가 문자열 형태의 기본 값이 아니라, 레지스트리 키의 이름으로서 등록되는 것을 확인하실 수 있습니다. 필자가 확인한 개발문서 및 관련 서적들에서는 다소 논쟁이 존재하기는 하지만, 필자가 테스트를 해 보았을 때 둘 다 작동을 하였습니다. 때문에 필자는 Dino Esposito님의 서적인 “Visual C++ Windows Shell Programming”에서 설명하고 있는 방식에 따르겠습니다. 레지스트리 키의 이름으로 본 쉘 익스텐션의 GUID
를 지정합니다.
Windows NT 기반의 운영체제에서는, “승인된(approved)” 익스텐션 목록에 본 쉘 익스텐션을 추가합니다. 이 작업은 예제 프로젝트의 DllRegisterServer
및 DllUnregisterServer
함수에 적혀있습니다.
다음 단계에서 다룰 내용
6 단계에서 우리는 또 다른 형태의 쉘 익스텐션인 드롭 핸들러에 대해 다루어 보겠습니다. 이것은 쉘 개체가 파일 속으로 ‘드롭’될 때 호출되는 쉘 익스텐션입니다.
계속 읽기
이전 게시글: Windows 쉘 익스텐션 개발 가이드 - (5) 속성 다이얼로그 (1/2)
다음 게시글: Windows 쉘 익스텐션 개발 가이드 - (6) 보내기 메뉴