입문자를 위한 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)
8 단계. Windows 탐색기에서 “자세히” 보기 모드를 선택할 때 나타나는 열 항목을 추가하는 쉘 익스텐션(Shell Extension)
이전 파트에 이어서...
참고사항: ID3 태그 다루기
쉘 익스텐션이 어떻게 ID3 태그 정보를 읽고 저장하는 것을 보여주기에는 지금이 적기일 것 같습니다. ID3v1 태그는 고정 길이 구조로서 MP3 파일의 끝 부분에 추가됩니다. 그리고 그 구조는 다음과 같이 생겼습니다.
struct CID3v1Tag {
char szTag[3]; // 항상 'T','A','G'
char szTitle[30];
char szArtist[30];
char szAlbum[30];
char szYear[4];
char szComment[30];
char byGenre;
};
모든 필드들이 보통의 char
형으로 되어 있고, 문자열은 반드시 NULL
문자로 끝나야 할 필요는 없습니다.
첫 번째 필드인 szTag
는 ID3 태그임을 식별하기 위한 TAG
라는 문자를 포함하고 있습니다.
byGenre
는 곡의 장르를 식별하는 번호입니다. 장르별로 미리 정의된 번호에 대한 목록은 ID3.org에서 확인할 수 있습니다.
우리는 또한 ID3 태그 및 이 태그가 어느 파일에서 유래되었는지 그 파일의 이름을 포함하는 추가적인 구조체를 필요로 합니다. 이 구조체는 일종의 캐시(cache)로서 사용됩니다.
#include <string>
#include <list>
typedef std::basic_string<TCHAR> tstring; // TCHAR형 문자열
struct CID3CacheEntry {
tstring sFilename;
CID3v1Tag rTag;
};
typedef std::list<CID3CacheEntry> list_ID3Cache;
CID3CacheEntry
객체는 파일 이름과 그 파일이 가지고 있는 ID3 태그를 포함하고 있습니다. list_ID3Cache
전역 변수는 CID3CacheEntry
구조체들을 포함하는 링크드 리스트(linked list)입니다.
좋습니다. 쉘 익스텐션으로 돌아가서, 여기 GetItemData
함수의 시작 부분이 있습니다. 먼저 우리는 이 메소드가 우리가 추가한 해당 열에 의해 호출된 것인지를 확인하기 위해 SHCOLUMNID
구조체를 검사합니다.
#include <atlconv.h>
STDMETHODIMP CMP3ColExt::GetItemData(LPCSHCOLUMNID pscid, LPCSHCOLUMNDATA pscd, VARIANT * pvarData) {
USES_CONVERSION;
LPCTSTR szFilename = OLE2CT(pscd->wszFile);
char szField[31];
TCHAR szDisplayStr[31];
bool bUsingBuiltinCol = false;
CID3v1Tag rTag;
bool bCacheHit = false;
// 포맷 아아디와 컬럼 아이디가 우리가 예상하고 있는 값인지 검사합니다.
if (pscid->fmtid == CLSID_MP3ColExt) {
if (pscid->pid > 2) return S_FALSE;
}
// ...
포맷 ID가 우리의 쉘 익스텐션이 가지고 있는 GUID
일 때 프로퍼티 ID는 0
, 1
또는 2
여야만 합니다. 왜냐하면 이러한 ID는 GetColumnInfo
에서 이미 사용했습니다. 그 외 여러 가지 이유로 프로퍼티 ID가 우리가 설정한 범위를 벗어난 채로 메소드가 호출되었다면 쉘(shell)에게 그러한 데이터가 없음을 알리기 위해 S_FALSE
를 반환합니다. 그러면 해당 열은 비어있는 채로 보여질 것입니다.
다음으로 우리는 포맷 ID는 FMTID_SummaryInformation
과 비교합니다. 그 다음 프로퍼티 ID를 체크하여 해당 프로퍼티 ID가 우리가 제공하고 있는 것과 같은지 확인합니다.
// ...
else if (pscid->fmtid == FMTID_SummaryInformation) {
bUsingBuiltinCol = true;
if (pscid->pid != 2 && pscid->pid != 4 && pscid->pid != 6)
return S_FALSE;
} else {
return S_FALSE;
}
// ...
다음으로 우리는 파일의 특성을 확인합니다. 이 파일이 사실은 디렉토리이거나 파일의 현재 상태가 ‘오프라인(즉, 다른 저장 미디어로 옮겨진 상태)’이라면 메소드를 끝냅니다. 또한 우리는 파일의 확장명도 검사합니다. .mp3
가 아니라면 메소드를 종료합니다.
// ...
// 파일이 아닌 디렉토리에 의해 호출되었다면, 즉시 메소드를 종료할 수 있습니다.
// 또한 파일이 오프라인 상태일 때도 메소드르를 종료할 수 있습니다.
if (pscd->dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY|FILE_ATTRIBUTE_OFFLINE))
return S_FALSE;
// 파일 확장명을 검사합니다. mp3 파일이 아니라면 메소드를 종료합니다.
if (wcsicmp(pscd->pwszExt, L".mp3"))
return S_FALSE;
// ...
여기까지 해서 우리는 내용을 읽어서 작업할 파일과 그렇지 않을 파일을 판별하였습니다. 앞서 선언한 ID3 태그 캐시는 이 때 사용할 것입니다. MSDN은 쉘(shell)이 파일 별로 GetItemData
에 대한 호출을 그룹화한다고 하였습니다. 우리는 이 특성을 이용할 수 있고, 특정 파일에 대해 ID3 태그를 캐시(cache)할 수 있습니다. 그래서 우리는 연속된 함수 호출에 의해 각 파일들을 또 다시 읽어야 할 필요가 없습니다.
먼저 우리는 m_ID3Cache
멤버 변수로서 보관되는 캐시를 하나씩 순회하면서, 캐시된 파일 이름과 함수 호출로 전달된 파일 이름을 비교합니다. 캐시에서 해당 이름을 발견하였다면, 우리는 ID3 태그를 가져옵니다.
// ...
// 캐시에서 파일 이름을 찾습니다.
list_ID3Cache::const_iterator it, itEnd;
for (it = m_ID3Cache.begin(), itEnd = m_ID3Cache.end(); !bCacheHit && it != itEnd; it++) {
if (lstrcmpi(szFilename, it->sFilename.c_str()) == 0) {
CopyMemory(&rTag, &it->rTag, sizeof(CID3v1Tag));
bCacheHit = true;
}
}
// ...
루프 종료 후 bCacheHit
가 false
가 되면 우리는 해당 파일을 직접 읽어서 ID3 태그를 가지고 있는지 확인해야 합니다. 헬퍼(helper) 함수인 ReadTagFromFile
은 파일로부터 이 128바이트를 읽기 위한 복잡한 작업들을 수행하고 성공하면 TRUE
를, 그렇지 않으면 FALSE
를 반환합니다.
알아둘 것은 ReadTagFromFile
은 파일의 마지막 128바이트를 읽을 뿐 그것이 진짜 ID3 태그인지 여부와는 관계가 없습니다.
// ...
// 파일에 캐시가 없다면, 파일로부터 태그를 직접 읽습니다.
if (!bCacheHit) {
if (!ReadTagFromFile(szFilename, &rTag)) return S_FALSE;
// ...
이제 우리는 ID3 태그를 가졌습니다. 우리는 캐시의 사이즈를 체크하고 캐시가 5개의 항목으로 꽉 찼다면, 새롭게 얻은 태그를 추가하기 위하여 가장 오래된 항목을 지웁니다. (5개라는 숫자는 필자가 임의로 설정한 최대치입니다.) 우리는 새로운 CID3CacheEntry
객체를 생성하고 링크드 리스트에 이를 추가합니다.
// ...
// 우리는 5개의 태그가 캐시되도록 유지할 것입니다.
// 현재 캐시가 4개 이상의 항목을 가지고 있다면,
// 가장 오래된 항목 순으로 제거합니다.
while (m_ID3Cache.size() > 4)
m_ID3Cache.pop_back();
// 새롭게 얻은 ID3 태그를 캐시에 추가합니다.
CID3CacheEntry entry;
entry.sFilename = szFilename;
CopyMemory(&entry.rTag, &rTag, sizeof(CID3v1Tag));
m_ID3Cache.push_front(entry);
} // if(!bCacheHit)의 끝
// ...
다음 단계는 처음 세 바이트를 검사해서 ID3 태그의 시그니처인지 확인하여 ID3 태그가 실종하는지 검사합니다. 그렇지 않다면 우리는 즉시 메소드를 종료합니다.
// ...
// 시그니처를 검사하여 우리가 진짜로 ID3를 가지고 있는지 확인합니다.
if (StrCmpNA(rTag.szTag, "TAG", 3))
return S_FALSE;
// ...
다음은 쉘(shell)이 요청한 프로퍼티의 종류에 따라 ID3 태그의 필드를 읽습니다. 여기에서는 프로퍼티 아이디를 검사만 할 것입니다. 여기 예제가 있습니다. 제목 필드에 대하여 이렇게 작성합니다.
// ...
// 문자열을 구성합니다.
if (bUsingBuiltinCol) {
switch (pscid->pid) {
case 2: // 곡의 제목
CopyMemory(szField, rTag.szTitle, countof(rTag.szTitle));
szField[30] = '\0';
break;
// ...
}
// ...
szField
버퍼는 최대 31 글자까지 수용 가능함을 확인하시기 바랍니다. 이것은 본래 ID3v1 태그보다 1글자 더 많은 용량이지만, 문자열을 항상 NULL
문자로 끝나도록 해야 하므로 확보된 공간입니다. bUsingBuiltinCol
옵션은 FMTID
/PID
쌍을 검사할 때보다 먼저 설정되었습니다. PID
하나만으로는 열을 식별하기에 충분하지 않기 때문에 이 플래그를 사용합니다. 왜냐하면 제목과 MP3 장르 열은 둘 다 PID
2번이기 때문입니다.
이 때, szField
는 ID3 태그에서 읽은 문자열을 포함하고 있습니다. WinAmp의 ID3 태그 편집기는 문자열을 NULL 문자로 채우지 않고 공백 문자로 채웁니다. 때문에 우리는 원 문자열 뒤에 붙은 불필요한 문자들을 제거해야 합니다.
// ...
StrTrimA(szField, " ");
// ...
마지막으로 우리는 CComVariant
객체를 생성하고 szDisplayStr
문자열을 이 안에 보관해야 합니다. 그 다음 CComVariant::Detach
를 호출하여 CComVariant
객체를 Windows 탐색기가 제공한 VARIANT
로 복사해야 합니다.
// ...
CComVariant vData(szField);
vData.Detach(pvarData);
return S_OK;
}
이것은 어떻게 보일 것인가?
새로운 열(column)은 열 설정 다이얼로그의 목록 맨 마지막에 나타납니다.
우리가 추가한 열은 이렇게 생겼습니다. 이들 파일은 현재 우리가 추가한 열인 “MP3 Album” 열을 기준으로 정렬된 상태입니다.
쉘 익스텐션을 등록하기
컬럼 핸들러는 폴더를 확장한 것이기 때문에 HKCR\Folders
레지스트리 키에 등록됩니다. 컬럼 핸들러를 등록하는 RGS 파일의 내용은 다음과 같습니다.
HKCR {
NoRemove Folder {
NoRemove Shellex {
NoRemove ColumnHandlers {
ForceRemove {AC146E80-3679-4BCA-9BE4-E36512573E6C} = s 'ID3v1 viewer column ext'
}
}
}
}
또 다른 유용한 기능 - 인포팁
컬럼 핸들러가 할 수 있는 또 다른 흥미로운 기능은 특정 파일 형식에 대해 인포팁을 수정할 수 있다는 것입니다. RGS 스크립트는 .mp3
파일에 대해 인포팁 내용을 수정할 수 있습니다. (수평 스크롤의 제한으로 인해 여러 줄에 걸쳐서 적었지만, 실제 RGS 스크립트에서는 한 줄에 적혀있습니다.)
HKCR {
NoRemove .mp3 {
val InfoTip = s 'prop:Type;Author;Title;Comment;
{AC146E80-3679-4BCA-9BE4-E36512573E6C},0;
{AC146E80-3679-4BCA-9BE4-E36512573E6C},1;
{AC146E80-3679-4BCA-9BE4-E36512573E6C},2;Size'
}
}
prop:
으로 시작하는 문자열에 Author, Title 및 Comment 필드가 나타나고 있음을 주목하시기 바랍니다. 여러분이 .mp3
파일 위에 마우스 포인터를 올렸을 때 Windows 탐색기는 우리가 만든 쉘 익스텐션을 호출하여 해당 필드에 대한 문자열을 얻을 것입니다. 개발 문서에서는 우리가 추가한 필드는 인포팁에서도 또한 나타날 것이라고 말하고 있는데(우리가 GUID와 프로퍼티 아이디를 위와 같이 작성한 이유가 이것입니다), 하지만 필자는 Windows 2000에서는 작동되는 것을 확인할 수 없었고 운영체제에 내장된 프로퍼티만이 인포팁에 나타남을 확인할 수 있었습니다. 커스텀 인포팁은 다음과 같이 나타납니다.
또한 이 쉘 익스텐션은 Windows XP에서는 작동하지 않을 수 있습니다. 왜냐하면 Windows XP는 새로운 파일 형식 레지스트리 키를 도입했기 때문입니다. 필자의 Windows XP에서 인포팁 정보는 HKCR\SystemFileAssociations\audio
에 보관되어 있었습니다.
다음 단계에서 다룰 내용
다음 9 단계에서, 우리는 또 다른 쉘 익스텐션으로서 특정 파일 형식에 대해 아이콘을 수정할 수 있는 아이콘 핸들러에 대해 다루어 보겠습니다.
계속 읽기
이전 게시글: Windows 쉘 익스텐션 개발 가이드 - (8) 자세히 모드 (1/2)
다음 게시글: Windows 쉘 익스텐션 개발 가이드 - (9) 아이콘 [完]