코딩캣: 코딩하는 고양이.
Windows 쉘 익스텐션 개발 가이드 - (5) 속성 다이얼로그 (1/2)
API/COM
2021. 2. 9. 19:18

입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차이가 있을 수 있음을 감안하시기 바랍니다.

또한 본 게시물은 원문을 최대한 직역하는 것을 지향하고 있으나, 우리말로 읽었을 때 보다 매끄럽게 하기 위하여 부득이 의역, 어순 조정 및 어휘 조정이 있음을 양해 바랍니다.

 

  1. 목차
  2. 쉘 익스텐션(Shell Extension)을 작성하기 위한 단계별 튜토리얼
    1. 파트 1
    2. 파트 2
  3. 여러 개의 파일에 대해 한번에 작동하는 쉘 익스텐션(Shell Extension)
    1. 파트 1
    2. 파트 2
  4. 파일에 대해 ‘팝업(Popup)’ 설명을 보여주는 쉘 익스텐션(Shell Extension)
  5. 사용자 정의 ‘드래그 앤 드롭(Drag and Drop)’ 기능을 제공하는 쉘 익스텐션(Shell Extension)
  6. 파일에 대한 ‘등록 정보’(또는 ‘속성’) 다이얼로그에 페이지를 추가하는 쉘 익스텐션(Shell Extension)
    1. 파트 1
    2. 파트 2
  7. ‘보내기(Send To)’ 메뉴에서 사용될 수 있는 쉘 익스텐션(Shell Extension)
  8. 컨텍스트 메뉴에 그림 출력하는 쉘 익스텐션(Shell Extension)
    및 디렉토리의 빈 공간에서 마우스 오른쪽 클릭에 응답하는 컨텍스트 메뉴 익스텐션(Shell Extension)
    1. 파트 1
    2. 파트 2
    3. 파트 3
  9. Windows 탐색기에서 “자세히” 보기 모드를 선택할 때 나타나는 열 항목을 추가하는 쉘 익스텐션(Shell Extension)
    1. 파트 1
    2. 파트 2
  10. 특정 형식의 파일에 대해 아이콘을 사용자화 하는 쉘 익스텐션(Shell Extension)

 

5 단계. 파일에 대한 ‘등록 정보’(또는 ‘속성’) 다이얼로그에 페이지를 추가하는 쉘 익스텐션(Shell Extension)

실습 프로젝트 다운로드

ShellExtGuide5_demo.zip
60.8 kB

 

들어가기에 앞서......

프로퍼티 시트의 세계로 모험해 보겠습니다. 여러분이 파일 시스템 개체에 대해 등록정보(또는 속성) 다이얼로그를 띄웠을 때 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가 설치되어 있음으로 인해 나타나는 탭입니다.

 

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.hMAXPROPPAGES라는 상수로 정의되어 있습니다. 각 파일들은 자신만의 페이지를 가질 수 있지만, 파일 이름들을 포함하는 문자열 리스트가 MAXPROPPAGES 이상인 경우 넘어가는 부분은 잘라내어 MAXPROPPAGES에서 지정한 개수에 맞출 것입니다. 물론 현재는 MAXPROPPAGES100으로 되어 있지만, 프로퍼티 시트가 그렇게까지 많은 페이지를 보여주지는 않습니다. 실질적으로 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은 페이지가 생성되거나 파괴될 때 호출되는 콜백 함수의 주소입니다. 이 함수의 역할은 잠시 후 설명하겠습니다.

pcRefParentCComModule에서 상속된 어떤 클래스의 멤버 변수에 대한 주소입니다. 사실 이 값은 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)

 

'API/COM' 카테고리의 다른 글
더 보기...
태그 : 
댓글