입문자를 위한 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)
메뉴 항목에 그림 출력하기
이제 됐습니다. 지금까지 보아왔던 소스 코드에 여러분은 지루함을 느끼셨으리라 충분히 이해합니다. 이제 진짜로 새롭고 흥미로운 작업을 하게 될 것입니다! IContextMenu2
인터페이스와 IContextMenu3
인터페이스에서 추가된 두 개의 메소드가 있습니다. 이들은 단순히 이 프로젝트 내에서 결과적으로 메시지 핸들러를 호출하게 될 헬퍼(helper) 함수를 호출하는 것에 지나지 않습니다.
이와 같이 호출에 호출을 거듭하게 코드를 작성한 것은 결국 같은 역할을 하는 메소드를 버전이 다르다고(하나는 IContextMenu2
이고 다른 하나는 IContextMenu3
) 두 번씩 작성하게 만드는 것을 방지할 수 있기 때문입니다. LRESULT *
매개 변수(parameter)와 관련해서 HandleMenuMsg2
메소드에는 다소 이상한 것이 있습니다. 아래 소스코드의 주석 부분에서 설명합니다.
STDMETHODIMP CBmpCtxMenuExt::HandleMenuMsg(UINT uMsg, WPARAM wParam, LPARAM lParam) {
AFX_MANAGE_STATE(AfxGetStaticModuleState()); // MFC 초기화
// res는 더미(dummy) LRESULT 변수입니다. 즉 실제로 사용되지는 않습니다.
// IContextMenu2::HandleMenuMsg()는 값을 반환할 방법도 제공하지 않습니다.
// 그럼에도 res가 필요한 것은 MenuMessageHandler가 호출될 때
// IContextMenu2 또는 IContextMenu3 인터페이스에서 호출했는지에 관계없이
// 소스 코드 수준에서 동일한 함수 호출 인터페이스를 유지하기 위함입니다.
LRESULT res;
return MenuMessageHandler(uMsg, wParam, lParam, &res);
}
STDMETHODIMP CBmpCtxMenuExt::HandleMenuMsg2(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT * pResult) {
AFX_MANAGE_STATE(AfxGetStaticModuleState()); // MFC 초기화
// 반환 값이 없는 메시지의 경우 pResult는 NULL입니다.
// 이것이 NULL이 되면, 필자는 더미(dummy) LRESULT형 변수를 만들 것입니다.
// 그러면 MenuMessageHandler의 pResult는 항상 유효한 주소만을 가리킬 것입니다.
if (pResult == NULL) {
LRESULT res;
return MenuMessageHandler(uMsg, wParam, lParam, &res);
} else {
return MenuMessageHandler(uMsg, wParam, lParam, pResult);
}
}
MenuMessageHandler
는 WM_MEASUREITEM
및 WM_DRAWITEM
을 각각의 메시지 핸들러에게 전달만 할 것입니다.
HRESULT CBmpCtxMenuExt::MenuMessageHandler(UINT uMsg, WPARAM wParam, LPARAM lParam, LRESULT * pResult) {
switch (uMsg) {
case WM_MEASUREITEM:
return OnMeasureItem((MEASUREITEMSTRUCT *)lParam, pResult);
case WM_DRAWITEM:
return OnDrawItem((DRAWITEMSTRUCT *)lParam, pResult);
}
return S_OK;
}
이전에도 언급했듯이 개발 문서에 따르면 쉘(shell)은 쉘 익스텐션에게 WM_INITMENUPOPUP
및 WM_MENUCHAR
에 대해 처리할 수 있도록 해주어야 한다고 적혀있지만, 필자는 테스트하는 동안 그러한 메시지가 전달되는 것을 확인할 수 없었습니다.
WM_MEASUREITEM 메시지 처리하기
쉘(shell)은 우리가 만들고 있는 쉘 익스텐션에게 WM_MEASUREITEM
메시지를 보내서 이 메뉴 항목의 디멘션(dimension)을 요청합니다. 우리는 우리가 만든 메뉴 항목에 의해 호출되었는지를 확인하는 것부터 시작합니다. 검사가 통과하면, 우리는 비트맵 이미지의 디멘션(dimension, 번역자 주: 정사각형 개체에 대한 가로 길이와 세로 길이)을 가져옵니다. 그리고 메뉴 항목의 전체적인 크기를 계산합니다.
먼저 비트맵 이미지의 크기를 가져오는 부분입니다.
HRESULT CBmpCtxMenuExt::OnMeasureItem(MEASUREITEMSTRUCT * pmis, LRESULT * pResult) {
BITMAP bm;
LONG lThumbWidth, lThumbHeight;
// 우리가 만든 메뉴 항목 때문에 호출된 것이 아니라면 아무 작업을 하지 않습니다.
if (m_uOurItemID != pmis->itemID)
return S_OK;
m_bmp.GetBitmap(&bm);
m_lBmpWidth = bm.bmWidth;
m_lBmpHeight = bm.bmHeight;
// ...
그 다음 우리는 썸네일의 크기를 계산합니다. 그것으로부터 컨텍스트 메뉴 항목의 전체적인 크기를 결정합니다. 비트맵 이미지의 크기가 최대 썸네일 크기보다 작다면(이 예제 프로젝트에서는 64 * 64
가 최대 크기입니다), 비트맵 이미지는 있는 그대로 메뉴에 그려질 것입니다. 그렇지 않으면 비트맵 이미지는 64 * 64
크기에 맞추어 그려질 것입니다. 비트맵 이미지의 크기를 조정해서 그린다는 것은 원래의 비트맵 이미지를 다소 왜곡(번역자 주: 가로와 세로 비율이 틀어져서 길쭉하거나 넓적하게 보여짐)하여 표현할 수 있겠으나, 썸네일 이미지를 보기 좋게 조정하는 것은 여러분들의 과제로 남기겠습니다.
// ...
// 썸네일 이미지의 크기를 계산합니다.
lThumbWidth = (m_lBmpWidth <= m_lMaxThumbnailSize) ? m_lBmpWidth : m_lMaxThumbnailSize;
lThumbHeight = (m_lBmpHeight <= m_lMaxThumbnailSize) ? m_lBmpHeight : m_lMaxThumbnailSize;
// 썸네일 + 테두리의 폭 + 패딩(padding)을 고려한 메뉴 항목의 크기를 계산합니다.
m_lItemWidth = lThumbWidth + m_lTotalBorderSpace;
m_lItemHeight = lThumbHeight + m_lTotalBorderSpace;
// ...
이제 우리는 메뉴 항목의 크기를 결정했으므로, 이 값을 우리가 메시지와 함께 받았던 MENUITEMSTRUCT
구조체에 보관합니다. Windows 탐색기는 우리가 추가하는 메뉴 항목을 위해 충분한 공간을 확보해줄 것입니다.
// ...
pmis->itemWidth = m_lItemWidth;
pmis->itemHeight = m_lItemHeight;
*pResult = TRUE; // 이제 우리는 메시지를 처리했습니다.
return S_OK;
}
WM_DRAWITEM 메시지 처리하기
우리가 WM_DRAWITEM
메시지를 전달받았을 때, Windows 탐색기는 우리가 실제로 메뉴 항목을 그릴 수 있도록 요청합니다. 우리는 썸네일 주변으로 3D 테두리를 그리기 위한 RECT
를 계산하는 것으로 시작합니다. 이 때 RECT
는 WM_MEASUREITEM
핸들러를 처리할 때 반환했던 크기와 반드시 같아야 할 필요는 없습니다. 왜냐하면 메뉴 항목은 컨텍스트 메뉴 내 다른 항목들이 더 넓을 경우 함께 넓어지기 때문입니다.
HRESULT CBmpCtxMenuExt::OnDrawItem(DRAWITEMSTRUCT * pdis, LRESULT * pResult) {
CDC dcBmpSrc;
CDC* pdcMenu = CDC::FromHandle(pdis->hDC);
CRect rcItem(pdis->rcItem); // 메뉴 항목에 대한 RECT
CRect rcDraw; // 그리기 작업을 할 RECT
// 우리가 추가한 메뉴 항목에 의해 호출되었는지를 검사합니다.
if (m_uOurItemID != pdis->itemID)
return S_OK;
// rcDraw는 처음에는 WM_MEASUREITEM 이벤트를 처리하면서 얻게 된 크기에 따라
// 설정될 것입니다. 그 후 소스 코드가 진행되면서 축소될 것입니다.
rcDraw.left = rcItem.CenterPoint().x - m_lItemWidth/2;
rcDraw.top = rcItem.CenterPoint().y - m_lItemHeight/2;
rcDraw.right = rcDraw.left + m_lItemWidth;
rcDraw.bottom = rcDraw.top + m_lItemHeight;
// 썸네일 주변 패딩(padding) 공간에 따라 rcDraw 사각 영역을 축소시킵니다.
rcDraw.DeflateRect(m_lMenuItemSpacing, m_lMenuItemSpacing);
그림을 출력하기 위한 첫 번째 단계로 메뉴 항목의 바탕에 색을 칠합니다.
DRAWITEMSTRUCT
구조체의 itemState
멤버는 우리가 추가한 메뉴 항목에 포커스가 주어졌는지, 그렇지 않은지 여부를 나타냅니다. 이에 따라 우리는 바탕이 될 색을 선택하면 됩니다.
// ...
// 메뉴 항목의 바탕을 특정 색으로 칠합니다.
if (pdis->itemState & ODS_SELECTED)
pdcMenu->FillSolidRect(rcItem, GetSysColor(COLOR_HIGHLIGHT));
else
pdcMenu->FillSolidRect(rcItem, GetSysColor(COLOR_MENU));
// ...
그 다음으로 우리는 썸네일 이미지가 메뉴 속으로 움푹 들어간 것처럼 보이도록 ‘선큰(sunken)’ 테두리를 그립니다.
// ...
// 선큰(sunken) 3D 테두리를 그립니다.
for (int i = 1; i <= m_l3DBorderWidth; i++) {
pdcMenu->Draw3dRect(rcDraw, GetSysColor(COLOR_3DDKSHADOW), GetSysColor(COLOR_3DHILIGHT));
rcDraw.DeflateRect(1, 1);
}
// ...
마지막으로 썸네일 이미지 그 자체를 그릴 차례입니다. 필자는 StretchBlt
를 사용하여 간단하게 구현해 보았습니다. 결과는 그다지 예쁘지는 않지만, 그래도 필자의 목표인 코드를 간단하게 작성하는 것은 성공했습니다.
// ...
// 새로운 DC를 생성하고 여기에 원본 비트맵을 선택합니다.
CBitmap* pOldBmp;
dcBmpSrc.CreateCompatibleDC(&dc);
pOldBmp = dcBmpSrc.SelectObject(&m_bmp);
// 비트맵 이미지를 메뉴 DC에 입힙니다.
pdcMenu->StretchBlt(rcDraw.left, rcDraw.top, rcDraw.Width(), rcDraw.Height(), &dcBmpSrc, 0, 0, m_lBmpWidth, m_lBmpHeight, SRCCOPY);
dcBmpSrc.SelectObject(pOldBmp);
*pResult = TRUE; // 우리는 이 메시지를 처리했습니다.
return S_OK;
}
실제 쉘 익스텐션에서는 마우스가 지나갈 때마다 그림이 깜박거리지 않도록, 깜박거림이 없는 클래스를 사용하는 것이 좋습니다.
여기 메뉴의 작동 결과에 대한 스크린 샷이 있습니다. 그림이 그려진 메뉴가 마우스 포인터를 지나갈 때마다 다음과 같이 보여집니다.
그리고 버전 4.0의 쉘에서 실행할 때의 모습입니다. 메뉴의 선택 여부에 따라 색이 반전되는데 이는 다소 ‘후져’보입니다.
쉘 익스텐션을 등록하기
우리가 만든 비트맵 뷰어를 등록하는 것은 여타 컨텍스트 메뉴 익스텐션을 등록하는 것과 다르지 않습니다. 등록을 위한 RGS 스크립트는 다음과 같이 생겼습니다.
HKCR {
NoRemove Paint.Picture {
NoRemove ShellEx {
NoRemove ContextMenuHandlers {
BitmapPreview = s '{D6F469CD-3DC7-408F-BB5F-74A1CA2647C9}'
}
}
}
}
염두에 두실 것은 Paint.Picture
라는 파일 유형은 여기서 하드코드(hard-coded)되었다는 것입니다. .bmp
파일에 대해 그림판을 기본 연결 프로그램으로 지정하지 않았다면, Paint.Picture
라는 문자열을 HKCR\.bmp
이 가리키는 레지스트리 키의 이름으로 바꾸어야 합니다. 말할 것도 없이 좀 더 생산적인 코드에서는 DllRegisterServer
에서 이 작업을 수행합니다. 여러분은 Paint.Picture
라는 레지스트리 키의 이름이 현재 컴퓨터 설정에 적절한지를 검사할 수 있습니다. 이 주제에 대해서는 1 단계에서 설명했습니다.
계속 읽기
이전 게시글: Windows 쉘 익스텐션 개발 가이드 - (7) 비트맵 및 폴더 메뉴 (1/3)
다음 게시글: Windows 쉘 익스텐션 개발 가이드 - (7) 비트맵 및 폴더 메뉴 (3/3)