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

입문자를 위한 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)

이전 파트에 이어서...

 

프로퍼티 페이지 콜백 함수

이제 프로퍼티 페이지 그 자체에 대해 살펴봅시다. 새로 추가될 페이지는 다음과 같이 생겼습니다. 이 모양을 잘 기억해 두었다가, 페이지가 어떻게 작동되는지를 설명할 때 참고하시기 바랍니다.

 

우리가 만들 쉘 익스텐션이 생성하는 탭

 

마지막으로 액세스된 시각 항목이 없을 수도 있습니다. 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 컨트롤로부터 날짜와 시각들을 읽어서 파일을 고칩니다. ReadDTPCtrlSetDTPCtrl과 반대의 작동(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)” 익스텐션 목록에 본 쉘 익스텐션을 추가합니다. 이 작업은 예제 프로젝트의 DllRegisterServerDllUnregisterServer 함수에 적혀있습니다.

 

다음 단계에서 다룰 내용

6 단계에서 우리는 또 다른 형태의 쉘 익스텐션인 드롭 핸들러에 대해 다루어 보겠습니다. 이것은 쉘 개체가 파일 속으로 ‘드롭’될 때 호출되는 쉘 익스텐션입니다.

 

계속 읽기

이전 게시글: Windows 쉘 익스텐션 개발 가이드 - (5) 속성 다이얼로그 (1/2)

다음 게시글: Windows 쉘 익스텐션 개발 가이드 - (6) 보내기 메뉴

 

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