^(코딩캣)^ = @"코딩"하는 고양이;
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (4) 드래그 앤 드롭
입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차..
API/COM
2021. 1. 28. 11:58

Windows 쉘 익스텐션 개발 가이드 - (4) 드래그 앤 드롭

API/COM
2021. 1. 28. 11:58

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

 

4 단계. 사용자 정의 ‘드래그 앤 드롭(Drag and Drop)’ 기능을 제공하는 쉘 익스텐션(Shell Extension)

실습 프로젝트 다운로드:

ShellExtGuide4_demo.zip
89.2 kB

 

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

본 가이드의 1 단계 및 2 단계에서, 필자는 특정 형식의 파일에 대해 마우스 오른쪽 버튼을 클릭하였을 때 호출될 컨텍스트 메뉴 익스텐션을 작성하는 방법에 대해 보여드렸습니다. 이번 단계에서, 필자는 약간 다른 형태의 컨텍스트 메뉴 확장인 드래그 앤 드롭 핸들러(drag and drop handler)에 대해 다루고자 합니다. 이것은 사용자가 마우스 오른쪽 버튼을 누르고 드래그 앤 드롭을 하였을 때 컨텍스트 메뉴에 나타나는 항목을 확장하는 것입니다. 또한 필자는 본 단계가 다루는 예제에서 보다 많은 MFC 사용 예를 보여드리겠습니다.

4 단계는 여러분이 쉘 익스텐션의 기본을 익혔고 MFC에 친숙한 것으로 간주합니다. 이번 단계에서 다룰 쉘 익스텐션은 Windows 2000 이후의 운영체제에서 하드 링크를 생성하는 실제 유틸리티입니다. 그러나 이전 버전의 윈도우 운영체제를 사용하고 있다 하더라도 이 단계의 내용을 따라오실 수 있습니다.

파워 유저들 및 몇몇 일반 유저들도 알겠지만, Windows 탐색기에서 마우스 오른쪽 버튼을 눌러서 드래그 앤 드롭을 할 수 있습니다. 이 때 버튼을 떼면, Windows 탐색기는 여러분이 취할 수 있는 동작의 목록을 컨텍스트 메뉴를 통해 보여줍니다. 일반적으로 이런 동작에는 이동, 복사 및 바로가기 만들기가 있습니다.

 

드래그 인 드롭 핸들러의 작동 (1)

 

Windows 탐색기는 드래그 앤 드롭 핸들러를 사용하여 우리가 컨텍스트 메뉴에 항목을 추가할 수 있도록 합니다. 이 유형의 쉘 익스텐션은 사용자가 마우스 오른쪽 버튼으로 드래그 앤 드롭했을 때 호출되고, 이 때 쉘 익스텐션은 제공할 수 있는 작업들에 대한 항목을 컨텍스트 메뉴에 추가합니다. 드래그 앤 드롭 핸들러를 제공하는 예로 WinZip이 있습니다. 압축 파일을 마우스 오른쪽 버튼으로 드래그하였을 때 WinZip이 컨텍스트 메뉴에 추가하는 항목은 다음과 같습니다.

 

드래그 인 드롭 핸들러의 작동 (2)

 

이와 같은 경우 WinZip이 제공하는 쉘 익스텐션은 마우스 오른쪽 버튼으로 드래그 앤 드롭 동작을 했을 때 나타나지만, 압축파일이 드래그되고 있을 때에 한합니다.

이번 포스트에서는 NTFS 볼륨에 있는 파일에 대해 하드링크를 생성할 목적으로, Windows 2000에서 추가된 API인 CreateHardLink를 사용해보겠습니다. 우리는 보통의 바로가기를 만드는 것과 같은 방식으로 사용자가 하드링크를 만들 수 있도록 컨텍스트 메뉴에 하드링크를 만드는 항목을 하나 추가해 보겠습니다.

Visual C++ 7.0 또는 8.0 사용자는 이전과 같이 1 단계의 본 시리즈에 들어가며......를 참고하여 컴파일하기 전 몇 가지 설정을 변경해야 함을 기억하시기 바랍니다.

 

AppWizard를 사용하여 시작하기

AppWizard를 실행하고 새로운 ATL COM 프로젝트를 생성합니다. 이 프로젝트를 HardLink라고 명명하겠습니다. 우리는 MFC를 사용할 것이기 때문에 이전과 마찬가지로 “Support MFC” 체크상자에 체크를 하고 [Finish]를 클릭합니다.

DLL에 COM 개체를 추가하기 위해 “클래스 뷰(Class View)” 화면으로 이동하여 “HardLink classes” 트리 항목에 대해 마우스 오른쪽 버튼을 클릭하고 [New ATL Object] 항목을 클릭합니다. Visual C++ 7.0 이상의 버전에서는 해당 트리 항목에 대해 마우스 오른쪽 버튼을 클릭하고 “추가(D)”→”클래스 추가(C)...”를 클릭합니다. 영어 버전의 Visual Studio에서는 “Add”→”Add Class” 메뉴입니다. 이전과 같이 마법사 화면에서 “Simple Object”를 선택하고, 개체의 이름을 HardLinkShlExt으로 정합니다. 이 마법사는 쉘 익스텐션을 구현하게 될 CHardLinkShlExt 이름의 C++ 클래스를 생성할 것입니다.

 

초기화 인터페이스

앞서 실습해 보았던 컨텍스트 메뉴 확장과 마찬가지로, Windows 탐색기는 IShellExtInit 인터페이스를 사용하여 우리를 호출할 것입니다. 먼저 우리는 CHardLinkShlExt가 구현하는 인터페이스에 IShellExtInit를 추가할 필요가 있습니다. HardLinkShlExt.h를 열고 다음과 같이 줄을 추가합니다.

 

#include <comdef.h>
#include <shlobj.h>
 
class CHardLinkShlExt :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CHardLinkShlExt, &CLSID_HarkLinkShlExt>,
    public IShellExtInit { // 새로 추가
    
    BEGIN_COM_MAP(CHardLinkShlExt)
        COM_INTERFACE_ENTRY(IShellExtInit) // 새로 추가
    END_COM_MAP()
    
    public:
    // IShellExtInit
        STDMETHODIMP Initialize(LPCITEMIDLIST, LPDATAOBJECT, HKEY); // 새로 추가
    
    // ...

 

 

비트맵을 보관할 변수와 마우스 오른쪽 버튼으로 드래그되고 있는 파일들의 이름을 보관할 변수도 필요합니다.

 

    // ...
    
    protected:
        Bitmap     m_bitmap;
        TCHAR       m_szFolderDroppedIn[MAX_PATH];
        CStringList m_lsDroppedFiles;
    
    // ...

 

우리는 또한 shlwapi.dll에 있는 함수 원형이 나타나게 해서 CreateHarkLink를 사용할 수 있게 해야 하므로, stdafx.h에 몇 가지 #define 구문을 추가합니다.

 

#define WINVER 0x0500
#define _WIN32_WINNT 0x0500
#define _WIN32_IE 0x0400

 

0x0500 값을 갖는 매크로 상수 WINVER를 선언하는 것은 Windows 98 및 Windows 2000에 대해 특화된 기능들을 사용 가능하게 만들어 줍니다.

0x0500 값을 갖는 매크로 상수 _WIN32_WINNT를 선언하는 것은 Windows 2000에 대해 특화된 Windows NT 기능들을 사용 가능하게 만들어 줍니다.

0x0400 값을 갖는 매크로 상수 _WIN32_IE를 선언하는 것은 Internet Explorer 4.0에서 도입된 기능들을 사용 가능하게 만들어 줍니다.

이제 Initialize 메소드를 봅시다. 이번 시간에 필자는 현재 마우스 오른쪽 버튼으로 드래그되고 있는 파일들의 목록을 MFC에서 액세스하여 사용하는 방법에 대해 보여드릴 것입니다.

MFC는 IDataObject 인터페이스의 래퍼(wrapper) 클래스인 COleDataObject라는 클래스를 가지고 있습니다. 이전까지 우리는 IDataObject의 메소드를 직접 호출해야했지만, 다행스럽게도 MFC는 이러한 작업을 보다 쉽게 지원해 줍니다. 여러분이 다루는 메모리를 리셋할 때 사용될 Initialize 메소드의 원형은 다음과 같습니다.

 

 

HRESULT IShellExtInit::Initialize(
    LPCITEMIDLIST pidlFolder,
    LPDATAOBJECT  pDataObj,
    HKEY          hProgID);

 

드래그 앤 드롭을 확장할 때, pidlFolder는 선택된 항목들이 ‘드롭’되는 폴더의 PIDL입니다. PIDL에 대해서는 나중에 상세히 설명하겠습니다.

pDataObj는 ‘드롭’되는 항목들을 모두 열거할 수 있는 IDataObject 인터페이스 형의 포인터입니다.

hProgIDHKEY_CLASSES_ROOT의 하위 키로 우리가 개발하고 있는 쉘 익스텐션을 등록시킨 바로 그 레지스트리 키에 대한 HKEY입니다.

첫 번째로 해야 할 것은 컨텍스트 메뉴의 항목에 보여 줄 비트맵을 로드(load)하는 것입니다. 그리고 나서 COleDataObject형 변수에 IDataObject 인터페이스형 포인터를 연결시키는 것입니다.

 

HRESULT CHardLinkShlExt::Initialize(
    LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hProgID) {
    
    AFX_MANAGE_STATE(AfxGetStaticModuleState());  // MFC 초기화
    
    COleDataObject dataobj;
    HGLOBAL        hglobal;
    HDROP          hdrop;
    TCHAR          szRoot[MAX_PATH];
    TCHAR          szFileSystemName[256];
    TCHAR          szFile[MAX_PATH];
    UINT           uFile, uNumFiles;
    
    m_bitmap.LoadBitmap(IDB_LINKBITMAP);
    
    dataobj.Attach(pDataObj, FALSE);
    
    // ...

 

Attach 메소드를 호출할 때 두 번째 매개 변수(parameter)에 FALSE를 전달하는 것은, dataobj 객체가 파괴될 때 IDataObject형 인터페이스 포인터를 참조 해제시키니 말라는 의미를 갖고 있습니다.

두 번째로 해야 할 것은 항목들이 ‘드롭’되는 디렉토리를 가져오는 것입니다. 우리는 이 디렉토리에 대한 PIDL을 이미 전달받았습니다. 그렇지만, 경로는 어떻게 가져올 수 있을까요?

PIDL은 pointer to an ID list의 줄임말이라고 하였습니다. PIDL은 Windows 탐색기가 나타내는 계층 구조 안에서 임의의 개체를 유일하게 식별할 수 있는 방법입니다. PIDL의 구체적인 구조는 개체에 따라 달라집니다. 그러나 여러분이 직접 네임스페이스 확장을 작성할 것이 아니라면, PIDL의 내부 구조에 대해서 걱정할 필요는 없습니다.

PIDL로부터 폴더(디렉토리)의 경로를 얻고자 할 때, SHGetPathFromIDList라는 Shell API를 사용하면 PIDL을 통상적인 파일 및 폴더 경로로 변환할 수 있습니다. 만일 드롭하는 위치가 파일 시스템에 있는 디렉토리가 아닌 경우(예: 제어판)에 SHGetPathFromIDList의 실행이 실패하고 우리는 더 이상 작업하지 않고 메소드를 끝낼 수 있습니다.

 

    // ...
    
    if (!SHGetPathFromIDList(pidlFolder, m_szFolderDroppedIn))
        return E_FAIL;
    
    // ...

 

다음으로 우리는 드롭하려는 대상 디렉토리가 NTFS 볼륨에 있는 디렉토리인지를 검사해야 합니다. 우리는 이미 확보한 디렉토리 경로로부터 루트 요소(예를 들어 E:\)들을 추출할 수 있고, 이를 통해 볼륨의 정보를 가져올 수 있습니다. 만일 파일 시스템이 NTFS가 아닌 경우, 하드링크를 만들 수 없기 때문에 값을 반환하고 메소드를 끝냅니다.

 

    // ...
    
    lstrcpy(szRoot, m_szFolderDroppedIn);
    PathStripToRoot(szRoot);
    
    if (!GetVolumeInformation(szRoot, NULL, 0, NULL, NULL, NULL, szFileSystemName, 256)) {
        // 파일 시스템을 판독할 수 없을 때...
        return E_FAIL;
    }
    
    if (0 != lstrcmpi(szFileSystemName, _T("ntfs"))) {
        // 파일 시스템이 NTFS가 아닌 경우 하드링크를 지원하지 않습니다.
    return E_FAIL;
    }
    
    // ...

 

다음으로 우리는 데이터 개체로부터 HDROP 핸들을 가져옵니다. 이것은 ‘드롭’되는 파일들을 열거하는데 쓰입니다. 이 부분은 3 단계와 비슷하지만, 이번 단계에서는 데이터를 액세스하는 데 MFC 클래스를 사용해 보겠습니다. COleDataObjectFORMATETCSTGMEDIUM 구조를 알아서 설정합니다.

 

    // ...
    
    hglobal = dataobj.GetGlobalData(CF_HDROP);
    
    if ( NULL == hglobal )
        return E_INVALIDARG;
    
    hdrop = (HDROP)GlobalLock(hglobal);
    
    if (hdrop == NULL)
        return E_INVALIDARG;
    
    // ...

 

우리는 ‘드롭’되는 파일들을 하나씩 확인하기 위해 HDROP 핸들을 사용할 것입니다. 각 파일에 대해 우리는 이것이 디렉토리인지 파일인지를 검사해야 합니다. 디렉토리는 하드링크될 수 없으므로 이 때는 값을 반환하고 메소드를 종료합니다.

 

    // ...
    
    uNumFiles = DragQueryFile(hdrop, 0xFFFFFFFF, NULL, 0);
    
    for (uFile = 0; uFile < uNumFiles; uFile++) {
        if (DragQueryFile(hdrop, uFile, szFile, MAX_PATH)) {
            if (PathIsDirectory(szFile)) {
                // 디렉토리가 발견되었으므로 메소드를 종료합니다.
                m_lsDroppedFiles.RemoveAll();
                break;
            }
        
        // ...

 

우리는 또한 ‘드롭’되려는 파일들이 ‘드롭’하려는 디렉토리와 같은 볼륨에 존재하는 것인지 확인할 필요가 있습니다. 이를 위해 필자가 처음에 시도해 본 것은 각 파일들의 경로와 ‘드롭’ 대상 디렉토리간 경로를 비교하여 그 루트 요소(드라이브 명)가 다를 경우 값을 반환하고 메소드를 종료하는 방식이었습니다. 그러나 이것은 완전한 해법이 아닙니다. 왜냐하면 NTFS 볼륨에서 여러분은 또 다른 볼륨을 경로 중간에 마운트(mount)할 수 있기 때문입니다. 예를 들어, C: 드라이브에서 또 다른 볼륨을 C:\dev라는 경로에 마운트했다면, 이 단계에서 작성하고 있는 소스 코드는 C:\dev를 일반적인 디렉토리로 간주하여 C: 드라이브의 다른 어딘가에 하드링크(hardlink)를 만들려 할 것입니다.

루트 요소를 비교하는 방법은 다음과 같습니다.

 

    // ...
    
    if (!PathIsSameRoot(szFile, m_szFolderDroppedIn)) {
        // 다른 볼륨으로부터 드롭된 파일이라면, 메소드를 종료합니다.
        m_lsDroppedFiles.RemoveAll();
        break;
    }
    
    // ...

 

파일이 지금까지 설명했던 두 가지 검사(디렉토리 여부, 볼륨 일치 여부)를 통과했다면, 우리는 이것을 CStringList(CString에 대한 링크드 리스트)형 객체인 m_lsDroppedFiles에 추가합니다.

 

    // ...
    
    // 드롭된 파일들을 우리가 선언한 리스트에 추가합니다.
    m_lsDroppedFiles.AddTail(szFile);
    }
} // for 루프 끝

 

for 루프를 끝낸 뒤에 우리는 리소스들을 해제하고 Windows 탐색기로 값을 반환시킵니다. 문자열 리스트가 파일 이름을 하나라도 가지고 있다면, 우리는 컨텍스트 메뉴를 변경하기 위해 S_OK를 반환합니다. 그렇지 않으면 이번 드래그 앤 드롭 이벤트에 대해서는 다시 호출될 일이 없도록 하기 위하여 E_FAIL을 반환합니다.

 

    // ...
    
    GlobalUnlock(hglobal);
    
    return (m_lsDroppedFiles.GetCount() > 0) ? S_OK : E_FAIL;
}

 

컨텍스트 메뉴 수정하기

이미 실습해보았던 컨텍스트 메뉴 익스텐션과 마찬가지로, 드래그 앤 드롭 핸들러도 IContextMenu 인터페이스를 구현하여 컨텍스트 메뉴와 상호작용 합니다. 우리가 만들고 있는 IContextMenu에 대해 컨텍스트 메뉴 항목을 추가하려면, HardLinkShlExt.h를 열고 다음과 같이 내용을 추가합니다.

 

class CHardLinkShlExt :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CHardLinkShlExt, &CLSID_HardLinkShlExt>,
    public IShellExtInit,
    public IContextMenu { // 새로 추가
    
    BEGIN_COM_MAP(CHardLinkShlExt)
        COM_INTERFACE_ENTRY(IShellExtInit)
        COM_INTERFACE_ENTRY(IContextMenu)
    END_COM_MAP()
    
public:
    // IContextMenu
    STDMETHODIMP GetCommandString(UINT, UINT, UINT *, LPSTR, UINT) { return E_NOTIMPL; } // 새로 추가
    STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO); // 새로 추가
    STDMETHODIMP QueryContextMenu(HMENU, UINT, UINT, UINT, UINT); // 새로 추가

 

드래그 앤 드롭 핸들러의 경우 GetCommandString이 호출될 일이 없으므로 이 메소드에 대해서 코드를 작성할 필요가 없습니다.

Windows 탐색기는 우리가 컨텍스트 메뉴 항목을 수정할 수 있도록 QueryContextMenu를 호출합니다. 여기서 여러분이 생소해하실 만한 내용은 없습니다. 우리는 단지 컨텍스트 메뉴에 항목을 하나 추가하고 비트맵을 설명하기만 하면 됩니다.

 

HRESULT CHardLinkShlExt::QueryContextMenu(HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags) {
    
    AFX_MANAGE_STATE(AfxGetStaticModuleState());  // MFC 초기화
    
    // uFlags가 CMF_DEFAULTONLY를 포함하고 있을 경우 여기서 아무것도 해선 안 됩니다.
    if (uFlags & CMF_DEFAULTONLY)
        return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0);
    
    // 하드링크 생성을 위한 메뉴 항목 추가하기.
    InsertMenu(hmenu, uMenuIndex, MF_STRING | MF_BYPOSITION, uidFirstCmd, _T("Create hard link(s) here"));
    
    if (m_bitmap.GetSafeHandle() != NULL) {
        SetMenuItemBitmaps(hmenu, uMenuIndex, MF_BYPOSITION,
        (HBITMAP)m_bitmap.GetSafeHandle(), NULL);
    }
    
    // 최상위 메뉴 항목 1개를 추가했으므로 정수 1을 반환합니다.
    return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 1);
}

 

드래그 앤 드롭 핸들러의 경우 GetCommandString이 호출될 일이 없으므로 이 메소드에 대해서 코드를 작성할 필요가 없습니다.

Windows 탐색기는 우리가 컨텍스트 메뉴 항목을 수정할 수 있도록 QueryContextMenu를 호출합니다. 여기서 여러분이 생소해하실 만한 내용은 없습니다. 우리는 단지 컨텍스트 메뉴에 항목을 하나 추가하고 비트맵을 설명하기만 하면 됩니다.

 

HRESULT CHardLinkShlExt::QueryContextMenu(HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags) {
    
    AFX_MANAGE_STATE(AfxGetStaticModuleState());  // MFC 초기화
    
    // uFlags가 CMF_DEFAULTONLY를 포함하고 있을 경우 여기서 아무것도 해선 안 됩니다.
    if (uFlags & CMF_DEFAULTONLY)
        return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0);
    
    // 하드링크 생성을 위한 메뉴 항목 추가하기.
    InsertMenu(hmenu, uMenuIndex, MF_STRING | MF_BYPOSITION,
    uidFirstCmd, _T("Create hard link(s) here") );
    
    if (m_bitmap.GetSafeHandle() != NULL) {
    SetMenuItemBitmaps(hmenu, uMenuIndex, MF_BYPOSITION,
        (HBITMAP)m_bitmap.GetSafeHandle(), NULL);
    }
    
    // 최상위 메뉴 항목 1개를 추가했으므로 정수 1을 반환합니다.
    return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 1);
}

 

이와 같이 작성한 쉘 익스텐션의 모양은 이렇습니다.

 

[단계 3]의 쉘 익스텐션 작동 예.

 

하드링크 만들기

Windows 탐색기는 사용자가 우리가 추가한 컨텍스트 메뉴 항목을 클릭했을 때 InvokeCommand 메소드를 호출합니다. 이 때 우리는 ‘드롭’된 파일들에 대해 모두 하드링크를 만들 것입니다. 하드링크의 이름은 “Hard link to <파일이름>”으로 정하고, 중복되는 이름이 있다면 “Hard link (<번호>) to <파일이름>”으로 정할 것입니다. 이 때 번호는 최대 99까지 증가할 수 있는데 이 제한은 운영체제가 하는 것이 아니고, 필자가 임의로 설정한 것입니다.

먼저 lpVerb 매개 변수(parameter)를 체크합니다. 우리는 컨텍스트 메뉴에 1개의 항목만을 추가했기 때문에 이것은 항상 0이 되어야 합니다.

 

HRESULT CHardLinkShlExt::InvokeCommand(LPCMINVOKECOMMANDINFO pInfo) {
    
    AFX_MANAGE_STATE(AfxGetStaticModuleState());  // MFC 초기화
    
    TCHAR    szNewFilename[MAX_PATH+32];
    CString  sSrcFile;
    TCHAR    szSrcFileTitle[MAX_PATH];
    CString  sMessage;
    UINT     uLinkNum;
    POSITION pos;
    
    
    // 우리가 추가한 항목을 클릭해서 호출된 것인지 재차 확인합니다.
    // lpVerb는 반드시 0이어야 합니다.
    if (pInfo->lpVerb != 0)
        return E_INVALIDARG;
    
    // ...

 

다음으로 우리는 문자열 리스트의 시작 위치를 가리키는 POSITION형 값을 가져와야 합니다. POSITION는 여러분이 직접 사용하지는 않을 내부적인 데이터 타입입니다. 대신에 여러분은 CStringList 클래스의 다른 메소드에 이를 전달은 해야 합니다. 리스트의 시작 부분에 대한 POSITION 값을 얻기 위해서 우리는 GetHeadPosition 메소드를 호출합니다.

 

// ... 

pos = m_lsDroppedFiles.GetHeadPosition();
ASSERT (pos != NULL);

// ...

 

리스트가 비어 있다면 posNULL이 됩니다. 그러나 리스트는 (앞서 설명한 바와 같이 작업이 되어 있으므로) 비어있는 일이 없어야 합니다. 그래서 필자는 그러한 경우를 체크하기 위하여 ASSERT문을 추가했습니다. 다음에 등장하는 루프 구문은 리스트에서 파일 이름들을 하나씩 순회하면서 각각에 대한 하드링크를 생성합니다.

 

    // ... 
    
    while (pos != NULL) {
        // 파일 이름을 하나씩 순회하며 가져옵니다.
        sSrcFile = m_lsDroppedFiles.GetNext(pos);
        
        // 예를 들어 C:\xyz\foo\stuff.exe라는 파일 경로에 대해 stuff.exe만을 추출합니다.
        lstrcpy(szSrcFileTitle, sSrcFile);
        PathStripPath(szSrcFileTitle);
        
        // 파일 이름에 대한 하드링크를 생성합니다. 
        // 먼저 Hard link to stuff.exe와 같은 이름으로 생성을 시도할 것입니다.
        wsprintf(szNewFilename, _T("%sHard link to %s"),
        m_szFolderDroppedIn, szSrcFileTitle);
        
        // ...

 

GetNextpos가 가리키는 위치에 있는 CString을 반환합니다. 그리고 pos가 다음 문자열을 가리키도록 위치를 옮깁니다. pos가 가장 마지막 문자열을 가리키고 있었다면, NULL이 되면서 루프가 끝납니다.

이 때 szNewFilename은 하드링크의 전체 경로를 갖게 됩니다. 우리는 해당 경로에 이미 하드링크가 있는지 여부를 검사하고, 이미 존재한다면 우리는 2부터 99까지 숫자를 붙여가며 중복되지 않는 이름을 찾습니다. 우리는 또한 하드링크의 이름(NULL 문자 포함)의 총 문자수가 MAX_PATH를 넘지 않도록 확인해야 합니다.

 


        // ... 
        
        for (uLinkNum = 2; PathFileExists(szNewFilename) && uLinkNum < 100; uLinkNum++) {
            
            // 하드링크에 부여할 다른 이름을 찾습니다.
            wsprintf(szNewFilename, _T("%sHard link (%u) to %s"), m_szFolderDroppedIn, uLinkNum, szSrcFileTitle);
            
            // 파일의 이름이 MAX_PATH보다 길어지면 오류 메시지를 표시합니다.
            if (lstrlen(szNewFilename) >= MAX_PATH) {
                sMessage.Format(_T("Failed to make a link to %s.\nDo you want to continue making links?"), (LPCTSTR)sSrcFile);
                if (MessageBox(pInfo->hwnd, sMessage, _T("Create Hard Links"), MB_ICONQUESTION | MB_YESNO) == IDNO)
                    break;
                else
                    continue;
}
}  // for 루프 종료

// ...

 

메시지 상자는 여러분이 원할 경우 전체 작업을 중단하게 할 수 있습니다. 그 다음 우리는 앞서 임의로 제한했던 최대 개수 99에 도달했는지 여부도 확인합니다. 우리는 다시 사용자가 전체의 작동을 중단하게 할 수 있습니다.

 

    // ... 
    
    if (uLinkNum == 100) {
        sMessage.Format(_T("Failed to make a link to %s.\nDo you want to continue making links?"), (LPCTSTR) sSrcFile);
        if (MessageBox(pInfo->hwnd, sMessage, _T("Create Hard Links"), MB_ICONQUESTION | MB_YESNO) == IDNO)
            break;
        else
            continue;
    }
    
    // ...

 

이제 남은 것은 진짜로 하드링크를 생성하는 것입니다. 필자는 명확성을 위해 오류 처리 코드를 생략합니다.

        // ... 
        
        CreateHardLink ( szNewFilename, sSrcFile, NULL );
    }  // while 루프 종료
    
    return S_OK;
}

 

Windows 탐색기에서 일반 파일과 하드링크의 차이는 없어 보입니다. 둘 다 평범한 파일처럼 생겼습니다. 그러나 어느 한 쪽의 내용을 수정했다면, 다른 한 쪽에서 열었을 때도 수정된 내용으로 나타날 것입니다.

 

하드링크는 겉만 다를 뿐 같은 파일이다.

 

쉘 익스텐션을 등록하기

드래그 앤 드롭 핸들러를 등록하는 것은 컨텍스트 메뉴 익스텐션을 등록하는 것보다 간단합니다. 모든 핸들러는 디렉토리에서 ‘드롭’ 행위가 발생하므로 HKCR\Directory의 하위 키로 등록됩니다. 그러나 개발 문서에 따르면 HKCR\Directory에만 등록하는 것으로는 충분하지 않다고 합니다. 여러분은 또한 바탕화면에 ‘드롭’하는 경우에 대응하기 위하여 HKCR\Folder에도 등록하고, 루트 디렉토리에 ‘드롭’하는 경우에 대응하기 위하여 HKCR\Drive에도 등록할 필요가 있습니다.

위 세 가지 경우를 고려한 RGS 스크립트는 다음과 같습니다.

 

HKCR {
    NoRemove Directory {
        NoRemove shellex {
            NoRemove DragDropHandlers {
                ForceRemove HardLinkShlExt = s '{3C06DFAE-062F-11D4-8D3B-919D46C1CE19}'
            }
        }
    }
    NoRemove Folder {
        NoRemove shellex {
            NoRemove DragDropHandlers {
                ForceRemove HardLinkShlExt = s '{3C06DFAE-062F-11D4-8D3B-919D46C1CE19}'
            }
        }
    }
    NoRemove Drive {
        NoRemove shellex {
            NoRemove DragDropHandlers {
                ForceRemove HardLinkShlExt = s '{3C06DFAE-062F-11D4-8D3B-919D46C1CE19}'
            }
        }
    }
}

 

이전에 실습해 보았던 쉘 익스텐션과 마찬가지로, Windows NT 기반의 운영체제의 경우 우리는 “승인된(approved)” 확장 목록에 우리가 만든 쉘 익스텐션을 등록해야 합니다. 이 작업은 예제 프로젝트의 DllRegisterServerDllUnregisterServer 함수가 수행합니다.

 

Windows 2000 및 NTFS 환경이 아닌 경우

이전 버전의 Windows를 사용하고 있다거나 사용 가능한 NTFS 볼륨이 없다고 하더라도, 여러분은 여전히 예제 프로젝트를 컴파일 및 실행해볼 수 있습니다. stdafx.h를 열고 다음과 같이 적혀있는 줄의 주석 기호(//)를 제거합니다.

 

// #define NOT_ON_WIN2K

 

주석 표시를 제거하면, 예제 프로젝트의 소스 코드는 파일 시스템을 검사하는 부분을 건너 뛸 것이고 NTFS가 아닌 경우에도 실행은 됩니다. 실제로 하드링크를 만드는 대신 메시지 상자를 띄울 것입니다.

 

다음 단계에서 다룰 내용

5 단계에서 여러분은 새로운 형태의 쉘 익스텐션으로 파일에 대한 등록정보(또는 속성) 다이얼로그에 페이지를 추가하는 ‘프로퍼티 시트 핸들러(property sheet handler)’에 대해 다룰 것입니다.

 

계속 읽기

이전 게시글: Windows 쉘 익스텐션 개발 가이드 - (3) 팝업/인포팁

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

 

카테고리 “API/COM”
more...
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (3) 팝업/인포팁
입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차..
API/COM
2021. 1. 21. 13:31

Windows 쉘 익스텐션 개발 가이드 - (3) 팝업/인포팁

API/COM
2021. 1. 21. 13:31

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

 

3 단계. 파일에 대해 ‘팝업(Popup)’ 설명을 보여주는 쉘 익스텐션(Shell Extension)

실습 프로젝트 다운로드:

ShellExtGuide3_demo.zip
31.6 kB

 

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

본 가이드의 1 단계2 단계에서 필자는 컨텍스트 메뉴 확장을 작성하는 방법에 대해 보여드렸습니다. 이번 3 단계에서는 조금 다른 유형의 쉘 익스텐션에 대해 설명하고자 합니다. 또한 쉘과 메모리를 공유하는 방법과 ATL과 함께 MFC를 병용하는 방법에 대해서 설명하겠습니다. 이번 단계에서는 여러분이 앞서 두 개의 단계를 통해 쉘 익스텐션의 기본에 대해 이해하고 있고, MFC를 알고 있다고 가정하겠습니다.

액티브 데스크톱(Active Desktop) 쉘에서는 새로운 기능을 도입하였습니다. 바로 여러분이 특정 개체에 마우스 포인터를 올려 놓았을 때 이 개체에 대한 설명을 보여주는 툴팁(tooltip)입니다. 예를 들어, “내 문서” 아이콘에 마우스 포인터를 오랫동안 올려 놓으면 다음과 같이 툴팁이 표시됩니다.

 

새롭게 추가된 툴팁(tooltip) 형태의 쉘 익스텐션(1).

 

“내 컴퓨터” 또는 “제어판” 등의 다른 개체들도 비슷한 툴팁을 가지고 있습니다. 이러한 형태의 텍스트는 마우스 포인터가 올려진 파일, 폴더, 개체에 대해 정보를 제공하는 툴팁이라는 의미에서 인포팁(infotip)이라 명명합니다. 우리도 인포팁 익스텐션(infotop extension)을 사용하여 이와 같이 쉘의 다른 개체에 대해 인포팁 문자열을 제공할 수 있습니다.

여러분이 이미 보았을 수도 있겠지만 이러한 인포팁 익스텐션의 예는 WinZip에서 확인할 수 있습니다. 압축 파일에 대해 마우스 오른쪽 버튼을 올려 놓으면 WinZip은 다음과 같은 내용을 표시해 줍니다.

 

WinZip의 인포팁 익스텐션.

 

본 게시글의 예제 프로젝트는 파일의 크기와 함께 첫 번째 줄의 내용을 표시해주는 신속한 텍스트 파일 뷰어가 되겠습니다. 이 익스텐션은 .txt 파일에 대해 마우스 포인터를 올려 놓았을 때 인포팁의 형태로 보여집니다.

Visual C++ 7.0 또는 8.0 사용자는 이전과 같이 단계 1의 본 시리즈에 들어가며......를 참고하여 컴파일하기 전 몇 가지 설정을 변경해야 함을 기억하시기 바랍니다.

 

AppWizard를 사용하여 시작하기

AppWizard를 실행하고 새로운 ATL COM 프로젝트를 생성합니다. 이 프로젝트를 TxtInfo라고 명명하겠습니다. 우리는 MFC를 사용할 것이기 때문에 “Support MFC” 체크상자에 체크를 하고 [Finish]를 클릭합니다.

 

Support MFC를 클릭하여 COM 프로젝트에 MFC를 사용 가능하게 한다.

 

DLL에 COM 개체를 추가하기 위해 “클래스 뷰(Class View)” 화면으로 이동하여 “TxtInfo classes” 트리 항목에 대해 마우스 오른쪽 버튼을 클릭하고 [New ATL Object] 항목을 클릭합니다. Visual C++ 7.0 이상의 버전에서는 해당 트리 항목에 대해 마우스 오른쪽 버튼을 클릭하고 “추가(D)”→”클래스 추가(C)...”를 클릭합니다. 영어 버전의 Visual Studio에서는 “Add”→”Add Class” 메뉴입니다. 이전과 같이 마법사 화면에서 “Simple Object”를 선택하고, 개체의 이름을 TxtInfoShlExt으로 정합니다. 이 마법사는 쉘 익스텐션을 구현하게 될 CTxtInfoShlExt 이름의 C++ 클래스를 생성할 것입니다.

또한 클래스 뷰(Class View) 트리에서 여러분은 CTxtInfoApp이라는 이름의 클래스가 보일 것입니다. 이 클래스는 CWinApp에서 파생된 클래스입니다. 이 클래스가 존재하고 theApp이라는 이름의 전역 객체(전역 변수)가 존재함으로 인해 우리는 ATL을 사용하지 않고도 보통의 MFC 기반 DLL을 만들듯이 MFC를 사용할 수 있습니다.

 

MFC 프로젝트와 같이 CWinApp의 파생 클래스가 추가된 프로젝트.

 

초기화 인터페이스

이전 게시글의 컨텍스트 메뉴 확장에서 Windows 탐색기가 우리가 개발하는 개체를 초기화하기 위한 IShellExtInit 인터페이스를 구현하였습니다. 일부 형태의 쉘 익스텐션을 초기화하는 또 다른 인터페이스가 존재합니다. 바로 IPersistFile이며 인포팁 익스텐션에서 사용하는 인터페이스가 이것입니다. 이것은 IShellExtInit와 무엇이 다를까요?

IShellExtInit::Initialize 메소드가 선택된 파일들을 모두 열거할 수 있는 IDataObject형 포인터를 전달받음을 앞서 살펴 보았습니다. 오로지 하나의 파일에만 작동되는 쉘 익스텐션의 경우 IPersistFile을 사용합니다. 마우스 포인터는 한 번에 하나 이상의 개체에 동시에 올려질 수 없듯이, 인포팁 익스텐션도 한 번에 하나의 파일에 대해서만 작동됩니다. 그래서 IPersistFile이 사용됩니다.

먼저 우리는 CTxtInfoShlExt가 구현하는 인터페이스로 IPersistFile을 추가할 필요가 있습니다. TxtInfoShlExt.h를 열고 다음과 같이 내용을 추가합니다.

 

#include <comdef.h>
#include <shlobj.h>
 
class CTxtInfoShlExt :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CTxtInfoShlExt, &CLSID_TxtInfoShlExt>,
    public IPersistFile { // 새로 추가
    
    BEGIN_COM_MAP(CTxtInfoShlExt)
        COM_INTERFACE_ENTRY(IPersistFile) // 새로 추가
    END_COM_MAP()
    
    // ...

 

또한 인포팁 익스텐션이 초기화를 진행하는 동안 Windows 탐색기가 전달해 준 파일 이름을 보관해 둘 필드(field)도 필요합니다.

 

protected:
    CString m_sFilename;

 

우리는 필요한 곳 어디에서나 MFC 객체를 사용할 수 있음을 기억하시기 바랍니다.

IPersistFile에 대한 문서를 보면, 여러분은 여러 가지 메소드들을 보게 될 것입니다. 다행스럽게도, 우리의 목적인 쉘(인포팁) 익스텐션에 대해서 우리는 단 하나의 메소드인 Load만을 구현하면 되고 나머지는 무시해도 됩니다. IPersistFile이 가지고 있는 메소드들의 원형은 다음과 같습니다.

 

class CTxtInfoShlExt : ... {
    // ...
    
    public:
        STDMETHODIMP GetClassID(LPCLSID)      { return E_NOTIMPL; }
        STDMETHODIMP IsDirty()                { return E_NOTIMPL; }
        STDMETHODIMP Load(LPCOLESTR, DWORD);
        STDMETHODIMP Save(LPCOLESTR, BOOL)    { return E_NOTIMPL; }
        STDMETHODIMP SaveCompleted(LPCOLESTR) { return E_NOTIMPL; }
        STDMETHODIMP GetCurFile(LPOLESTR*)    { return E_NOTIMPL; }
};

 

Load를 제외한 나머지 메소드들은 E_NOTIMPL을 반환합니다. 이는 우리가 해당 메소드를 구현하지 않았다는 의미입니다. 그리고 더욱 희망적인 사실은, 우리가 구현하게 될 Load 메소드는 매우 간단하다는 것입니다. 우리는 단지 Windows 탐색기가 우리에게 전달해 준 파일의 이름을 보관만 하면 된다는 것입니다. 이 파일 이름은 마우스 포인터가 올라가 있는 파일에 대한 전체 경로입니다.

 

HRESULT CTxtInfoShlExt::Load(LPCOLESTR wszFilename, DWORD dwMode) {
    
    AFX_MANAGE_STATE(AfxGetStaticModuleState());  // MFC를 쓰기 위한 내부 초기화
    
    // 파일 이름을 CString으로 변환 후 보관 
    m_sFilename = wszFilename;
    
    return S_OK;
}

 

메소드 본문에서 첫 번째 줄은 중요합니다. AFX_MANAGE_STATE 매크로는 MFC가 적절하게 작동되기 위해 필요한 사항입니다. 우리가 개발 중인 DLL은 MFC가 아닌 프로그램에서 로드할 것이므로, MFC를 사용하는 함수가 내보내기(export) 되었을 때는 수동으로 MFC를 초기화시켜 주어야 합니다. 여러분이 이 줄을 삽입하지 않는다면 대부분 리소스와 관계될 MFC 함수들이 충돌하거나 어셜션(assertion) 실패를 일으킬 것입니다.

m_sFilename에 파일의 이름이 저장되었습니다. 이는 나중에 사용될 것입니다. 지금 필자는 Windows 탐색기가 전달한 파일 이름 문자열을 CString으로 변환하는 과정에서 대입 연산자만을 사용하면 되는 이점을 활용하고 있습니다. DLL이 ANSI 컴파일로 설정되어 있다면 CString은 내부적으로 ANSI 문자열로 처리될 것입니다.

 

툴팁을 위한 텍스트 생성하기

Windows 탐색기가 우리가 개발중인 익스텐션의 Load 메소드를 호출한 후, 다른 형태의 인터페이스인 IQueryInfo형 포인터를 얻기 위하여 QueryInterface를 호출합니다. IQueryInterface는 단 두 개의 메소드를 가지고, 그나마도 실질적으로 하나의 메소드만 사용되는 간단한 인터페이스입니다. TxtInfoShlExt.h를 열고 다음과 같이 내용을 추가합니다.

 

class CTxtInfoShlExt : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CTxtInfoShlExt, &CLSID_TxtInfoShlExt>,
    public IPersistFile,
    public IQueryInfo { // 새로 추가
    
    BEGIN_COM_MAP(CTxtInfoShlExt)
        COM_INTERFACE_ENTRY(IPersistFile)
        COM_INTERFACE_ENTRY(IQueryInfo) // 새로 추가
    END_COM_MAP()
    
    // ...

 

그 다음 IQueryInfo 메소드에 대한 원형을 추가합니다.

class CTxtInfoShlExt : ... {
    
    // ...
    
    STDMETHODIMP GetInfoFlags(DWORD *) { return E_NOTIMPL; } // 새로 추가
    STDMETHODIMP GetInfoTip(DWORD, LPWSTR *); // 새로 추가
    
    // ...
};

 

GetInfoFlags 메소드는 사용되지 않으므로 우리는 단지 E_NOTIMPL을 반환해주면 됩니다.

GetInfoTip가 바로 우리가 툴팁으로 보여주기 위한 문자열을 Windows 탐색기에게 반환하는 곳입니다. 이 메소드는 초반부터 지루한 선언들로 시작합니다.

 

HRESULT CTxtInfoShlExt::GetInfoTip(DWORD dwFlags, LPWSTR * ppwszTip) {
    
    AFX_MANAGE_STATE(AfxGetStaticModuleState());  // MFC를 초기화합니다.
    
    USES_CONVERSION;
    
    LPMALLOC   pMalloc;
    CStdioFile file;
    DWORD      dwFileSize;
    CString    sFirstLine;
    BOOL       bReadLine;
    CString    sTooltip;
    
    // ...

 

이번에도 MFC를 초기화하기 위해 다른 내용보다 먼저 AFX_MANAGE_STATE 호출 문장이 등장했습니다. 이 문장은 메소드 내에서 변수 선언보다도 더 먼저 등장해야 합니다. 왜냐하면 로컬변수의 생성자 내부에도 MFC가 포함될 수 있기 때문입니다.

dwFlags는 이번 단계에서는 사용되지 않을 것입니다. ppwszTipLPWSTR형 포인터로서 우리는 우리가 직접 메모리를 할당하고 이에 대한 포인터를 전달해야 합니다.

먼저 우리는 파일을 열고 그 내용을 읽어야 합니다. 우리는 일찍이 Load 메소드를 통해 보관해둔 필드(field)를 통해 파일 이름을 알고 있습니다.

 

    // ...
    
    if (!file.Open(m_sFilename, CFile::modeRead | CFile::shareDenyWrite))
        return E_FAIL;
    
    // ...

 

이제 우리는 쉘 메모리 할당자를 통해 버퍼를 할당받아야 하므로 IMalloc 인터페이스를 필요로 합니다. 우리는 SHGetMalloc를 호출함으로써 이를 수행합니다.

 

    // ...
    
    if (FAILED(SHGetMalloc(&pMalloc)))
        return E_FAIL;
    
    // ...

 

필자는 추후에 이 IMalloc에 대해 할 이야기가 많이 있습니다. 어쨌든 다음 단계로 해당 파일의 크기를 가져오고, 첫 번째 줄을 읽습니다.

 

    // ...
    
    // 파일의 크기를 가져옵니다. 
    dwFileSize = file.GetLength();
    
    // 파일로부터 첫 번째 줄을 읽습니다. 
    bReadLine = file.ReadString(sFirstLine);
    
    // ...

 

bReadLine은 파일의 길이가 0 바이트이거나 액세스할 수 없는 경우 빼고는 대체로 TRUE가 될 것입니다. 다음에 수행할 작업으로 파일의 크기를 알려주는 툴팁의 첫 부분을 생성하는 것입니다.

 

    // ...
    
    sTooltip.Format(_T("File size: %lu"), dwFileSize);
    
    // ...

 

또한 첫 번째 줄을 읽어오는 데 성공하였다면 툴팁에 그 내용을 덧붙입니다.

 

    // ...
    
    if (bReadLine) {
        sTooltip += _T("\n");
        sTooltip += sFirstLine;
    }
    
    // ...

 

이제 툴팁으로 보여줄 문자열을 완성했으므로 우리는 버퍼를 할당받아야 합니다. 바로 지금이 IMalloc을 사용해야 할 때입니다. SHGetMalloc가 반환한 포인터는 쉘이 가지고 있는 IMalloc 인터페이스에 대한 복사본입니다. 이 인터페이스를 통해 할당받는 모든 메모리는 쉘의 프로세스 영역에 존재하게 됩니다. 그래서 쉘은 이를 사용할 수 있습니다. 더 중요한 것은, 쉘이 이를 할당 해제할 수 있기 때문에 우리가 해야 될 것은 버퍼를 할당받기만 하고 이후에는 잊어버리면 된다는 것입니다. 쉘은 할당된 메모리의 사용이 끝나면 이를 알아서 할당 해제할 것입니다.

또 한 가지 알아야 할 사항으로, 우리가 쉘에게 반환해야 할 문자열은 반드시 유니코드 문자열이어야 한다는 것입니다. Alloc을 호출 시 sizeof(wchar_t)의 배수 크기가 지정되는 이유이기도 합니다. 단순히 lstrlen(sToolTip)을 기준으로 메모리를 할당받을 경우 실제 필요한 메모리에 비해 절반밖에 할당이 되지 않을 수 있습니다.

 

    // ...
    
    *ppwszTip = (LPWSTR)pMalloc->Alloc((1 + lstrlen(sTooltip)) * sizeof(wchar_t));
    
    if (*ppwszTip == NULL) {
        pMalloc->Release();
        return E_OUTOFMEMORY;
    }
    
    // 버퍼에 툴팁 문자열을 복사할 때 유니코드형 문자열 함수를 사용하시기 바랍니다.
    wcscpy(*ppwszTip, T2COLE(LPCTSTR(sTooltip)));
    
    // ...

 

마지막으로 앞서 받았던 IMalloc 인터페이스형 포인터를 참조 해제합니다.

 

    // ...
    
    pMalloc->Release();
    
    return S_OK;
}

 

소스 코드는 거의 다 작성했습니다. Windows 탐색기는 *ppwszTip에서 문자열을 읽어 툴팁으로 보여줄 것입니다.

 

[단계 3]의 쉘 익스텐션 작동 예.

 

쉘 익스텐션을 등록하기

인포팁 익스텐션은 컨텍스트 메뉴 익스텐션과 약간 다른 방식으로 등록됩니다. 이번 쉘 익스텐션도 물론 HKEY_CLASSES_ROOT에 등록되지만, 우리가 다루고자 하는 파일 확장명에 대한 레지스트리 키에 직접 등록됩니다. 즉 이 경우에는 HKCR\.txt에 등록된다는 것입니다.

특이한 것은 또 있습니다. 여러분은 ShellEx의 서브 키로 추가될 레지스트리 키의 이름이 TooltipHandlers와 같은 논리적이고 추상적인 이름이 될 것으로 생각하겠지만, 이 이름은 {00021500-0000-0000-C000-000000000046}으로 미리 정해져 있습니다.

필자가 생각하기에 마이크로소프트는 우리가 모르는 쉘 익스텐션들을 더 가지고 있을 것 같습니다. 여러분이 레지스트리를 자세히 찾아본다면 GUID로 된 이름을 가진 서브키들이 다른 ShellEx에도 있음을 알 수 있습니다. 이 GUID들은 IQueryInfoGUID로 나타납니다.

우리가 개발한 쉘 익스텐션이 .txt 파일에 대해 Windows 탐색기가 호출할 수 있도록 등록하는 RGS 스크립트는 다음과 같습니다.

 

HKCR {
    NoRemove .txt {
        NoRemove ShellEx {
            {00021500-0000-0000-C000-000000000046} = s '{F4D78AE1-05AB-11D4-8D3B-444553540000}'
        }
    }
}

위 코드를 ‘복붙’하면 여러분은 쉽게 다른 쉘 익스텐션도 호출되게 할 수 있습니다. 또한 “.txt”라는 부분을 여러분이 원하는 다른 확장명으로 변경할 수도 있습니다.

다만 유감스럽게도, 여러분은 이 인포팁 익스텐션을 모든 형식의 파일에 대해 호출되게 할 목적으로 HKCR\* 또는 HKCR\AllFileSystemObject에 등록할 수는 없습니다.

앞서 실습했던 쉘 익스텐션에 대해 NT 기반의 운영체제에서 작동되게 하려면, 쉘 익스텐션을 승인된(approved) 익스텐션 목록에 추가해야 한다고 하였습니다. 이런 작업을 수행하는 것이 첨부된 예제 프로젝트에 포함된 DllRegisterServerDllUnregisterServer 함수입니다.

 

다음 단계에서 다룰 내용

4 단계에서 우리는 다시 컨텍스트 메뉴 익스텐션의 세계로 돌아와서 또 다른 유형의 쉘 익스텐션인 드래그 앤 드롭 핸들러에 대해 살펴보겠습니다. 우리는 또한 MFC를 보다 많이 사용해볼 것입니다.

 

계속 읽기

다음 게시글: Windows 쉘 익스텐션 개발 가이드 - (2) 여러 개의 파일 (2/2)

이전 게시글: Windows 쉘 익스텐션 개발 가이드 - (4) 드래그 앤 드롭

 

카테고리 “API/COM”
more...
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (2) 여러 개의 파일 (2/2)
입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차..
API/COM
2021. 1. 20. 18:10

Windows 쉘 익스텐션 개발 가이드 - (2) 여러 개의 파일 (2/2)

API/COM
2021. 1. 20. 18:10

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

 

2 단계. 여러 개의 파일에 대해 한번에 작동하는 쉘 익스텐션(Shell Extension)

이전 파트에 이어서...

 

플라이-바이 도움말(fly-by help)과 동사(verb) 전달하기

전에 언급한대로, GetCommandString 메소드는 Windows 탐색기가 ‘플라이-바이 도움말(fly-by help)’나 우리가 추가하고 있는 메뉴 항목(command) 중 하나에 대한 동사(verb)를 얻을 때에 호출되는 메소드입니다.

이번에는 2개의 메뉴 항목을 추가했기 때문에, Windows 탐색기가 어떤 메뉴 항목으로 우리를 호출하였는지 판별하기 위해 uCmdID 매개 변수(parameter)를 확인해야 합니다.

 

#include <atlconv.h>

HRESULT CDLLRegShlExt::GetCommandString(
    UINT uCmdID, UINT uFlags, UINT * puReserved, LPSTR szName, UINT cchMax) {
    
    USES_CONVERSION;
    
    LPCTSTR szPrompt;
    
    if (uFlags & GCS_HELPTEXT) {
        switch (uCmdID) {
        case 0:
            szPrompt = _T("Register all selected COM servers");
            break;
        case 1:
            szPrompt = _T("Unregister all selected COM servers");
            break;
        default:
            return E_INVALIDARG;
        }
        
        // ...

 

uCmdID의 값이 0이면, 우리는 컨텍스트 메뉴에 추가된 첫 번째 항목(등록)이 클릭되어 호출된 것입니다. 이 값이 1이면 우리는 컨텍스트 메뉴에 추가된 두 번째 항목(등록 해제)이 클릭되어 호출된 것입니다. 이에 따라 우리가 준비한 문자열을 선택한 다음 Windows가 제공한 버퍼에 이를 복사합니다. 필요 시 유니코드로 변환을 먼저 합니다.

 

이 단계에서 실습하고 있는 쉘 익스텐션에서 필자는 동사(verb)를 제공하는 소스 코드 또한 작성하였습니다. 그러나 필자가 테스트하는 동안, Windows 탐색기는 동사(verb)를 요구할 목적으로 GetCommandString을 호출하지는 않았기 때문에 동사(verb)가 작동될 기회가 없었습니다.

필자는 이 글에서 동사(verb)와 관련된 설명은 생략하겠으나, 여러분이 관심이 있다면 예제 프로젝트에서 참고해보실 수 있습니다.

 

사용자의 선택에 따라 수행하기

우리가 추가한 컨텍스트 메뉴 항목을 사용자가 클릭할 때, Windows 탐색기는 쉘 익스텐션 내부의 InvokeCommand 메소드를 호출합니다. InvokeCommandlpVerb의 상위 워드(HIWORD)를 먼저 체크합니다. 이 값이 0이 아닌 값이라면, 사용자가 호출하려는 동사(verb)의 이름을 뜻합니다. 이 프로젝트에서 동사(verb)가 제대로 작동하는지 여부를 알지 못하기 때문에(적어도 Windows 98에서는 그렇다는 뜻입니다), 우리는 이 부분을 무시할 것입니다.

동사(verb)에 대한 것이 아니라면, lpVerb의 하위 워드(LOWORD)는 0 또는 1일 것이므로, 이를 통해서 사용자가 첫 번째로 추가한 메뉴 항목을 클릭한 것인지, 두 번째로 추가한 메뉴 항목을 클릭한 것인지를 알 수 있습니다.

 

HRESULT CDLLRegShlExt::InvokeCommand(LPCMINVOKECOMMANDINFO pCmdInfo) {
    // lpVerb가 특정 문자열에 대한 포인터일 경우, 
    // 이번 호출을 무시하고 메소드를 끝냅니다.
    if (HIWORD(pInfo->lpVerb) != 0)
        return E_INVALIDARG;
    
    // lpVerb가 우리가 추가한 메뉴 항목에 대한 Command ID인 0 또는 1인지 확인합니다.
    switch (LOWORD(pInfo->lpVerb)) {
    case 0:
    case 1: {
        CProgressDlg dlg(&m_lsFiles, pInfo);
        
        dlg.DoModal();
        return S_OK;
    }
    default:
        return E_INVALIDARG;
    }
}

 

lpVerb0 또는 1일 때, 우리는 (ATL 클래스인 CDialogImpl에서 파생된) 프로세스 다이얼로그를 띄우고 파일 이름들의 목록을 전달할 것입니다.

실제 작업들은 모두 CProcessDlg 클래스에서 이루어집니다. OnInitDialog 메소드는 리스트 컨트롤(list control)을 초기화하고, 그 다음 CProgrssDlg::DoWork 메소드를 호출합니다. DoWork 메소드는 CDLLRegShlExt::Initialize 메소드에서 생성된 문자열 리스트를 순회(iterate)하고 각 파일에 대해 적절한 함수를 호출합니다.

기본 소스 코드는 다음과 같습니다만 명확하게 보여드리기 위하여 오류 체크 부분과 리스트 컨트롤에 항목을 추가하는 부분은 삭제하였습니다. 그러나 리스트를 순회하고 각 파일 이름에 대해 작업하는 과정을 보이기에는 충분합니다.

 

void CProgressDlg::DoWork() {
    
    HRESULT (STDAPICALLTYPE * pfn)();
    string_list::const_iterator it;
    HINSTANCE hinst;
    LPCSTR    pszFnName;
    HRESULT   hr;
    WORD      wCmd;
    
    wCmd = LOWORD(m_pCmdInfo->lpVerb);
    
    // 2 가지의 항목을 컨텍스트 메뉴에 추가했기 때문에,
    // 0과 1을 벗어나는 lpVerb에 대해서는 호출을 무시합니다.
    if (wCmd > 1)
        return;
    
    // 앞으로 어떤 함수를 호출할 것인지를 결정합니다.
    // GetProcAddress는 함수 이름을 받을 때 ANSI 문자열만을 받기 때문에
    // _T 매크로를 사용하지 않음을 확인하시기 바랍니다.
    // 번역자 주: wCmd = 1이면 등록 해제, wCmd = 0이면 등록
    pszFnName = wCmd ? "DllUnregisterServer" : "DllRegisterServer";
    
    for (it = m_pFileList->begin(); it != m_pFileList->end(); it++) {
        // 리스트에 담긴 각 파일들을 로드
        hinst = LoadLibrary(it->c_str());
        
        if (hinst == NULL)
            continue;
        
        // 로드된 모듈에 대해 등록 또는 등록 해제 함수에 대한 주소 얻기
        (FARPROC &)pfn = GetProcAddress(hinst, pszFnName);
        
        // 현재 모듈에서 해당 함수 및 그 주소를 찾지 못했다면 리스트의 다음 파일 처리
        if (pfn == NULL)
            continue;
        
        // 함수를 호출
        hr = pfn();
        
        // 오류 처리 및 여기서 호출된 함수들의 반환 값에 따른 처리는 생략합니다.
    }  // for 루프의 끝
    
    // ...

 

DoWork의 나머지 부분은 오류 처리와 리소스 정리에 대한 것입니다. 전체 소스 코드는 첨부된 예제 프로젝트의 ProgrssDlg.cpp에서 확인하실 수 있습니다.

 

쉘 익스텐션을 등록하기

DllReg 쉘 익스텐션은 ‘인 프로시저(in-proc)’ COM 서버로서 작동하기 때문에 DLL과 OCX 파일 을 선택한 경우에 대해 호출되어야 합니다. 1 단계에서와 같이 이러한 작업들은 RGS 스크립트인 DllRegShlExt.rgs를 통해 지정될 수 있습니다. 각각의 확장 파일에 대해 컨텍스트 메뉴 핸들러로서 우리가 개발한 DLL을 등록하기 위해 필요한 스크립트는 다음과 같습니다.

 

HKCR {
    NoRemove dllfile {
        NoRemove shellex {
            NoRemove ContextMenuHandlers {
                ForceRemove DLLRegSvr = s '{8AB81E72-CB2F-11D3-8D3B-AC2F34F1FA3C}'
            }
        }
    }
    NoRemove ocxfile {
        NoRemove shellex {
            NoRemove ContextMenuHandlers {
                ForceRemove DLLRegSvr = s '{8AB81E72-CB2F-11D3-8D3B-AC2F34F1FA3C}'
            }
        }
    }
}

 

RGS 파일의 문법과 NoRemoveForceRemove 키워드는 1 단계에서 설명 되었습니다.

이전에 실습했던 쉘 익스텐션과 마찬가지로 NT 기반의 운영체제에서는 우리가 개발한 쉘 익스텐션을 “승인된(approved)” 익스텐션 목록에 추가해야 합니다. 이를 수행하는 소스 코드는 DllRegisterServerDllUnregisterServer 함수에 적혀 있습니다. 매우 간단한 레지스트리 접근이기 때문에 필자는 이 글에서 이를 언급하지 않겠습니다. 첨부된 예제 프로젝트에서 직접 확인하시기 바랍니다.

 

쉘 익스텐션의 실제 작동 확인하기

우리가 추가한 두 메뉴 항목 중 하나를 클릭하였을 때, 대화상자가 뜨면서 작업 결과를 보여주게 됩니다.

 

[단계 2]의 예제 프로젝트 실행 결과(1).
[단계 2]의 예제 프로젝트 실행 결과(2).

 

리스트 컨트롤은 선택된 각 파일의 목록 및 등록 또는 동록 해제의 성공 여부를 보여줍니다. 여러분이 파일을 선택할 때 리스트 하단에 각 파일에 대해 좀 더 상세한 내용이 보여집니다. 이 때 등록 또는 등록 해제가 실패하였다면 오류 메시지에 대한 설명이 함께 나타날 것입니다.

 

Windows XP에서 테마 적용 없이 [단계 2]의 예제 프로젝트를 실행한 결과.

위 스크린샷을 주목하시기 바랍니다. Windows XP에서 실행할 경우 다이얼로그에 Windows XP가 적용되지 않았습니다. “Windows XP 비주얼 스타일을 사용하기(Using Windows XP Visual Styles)”라는 제목의 MSDN 게시글에 따르면, 우리가 만든 사용자 인터페이스(UI)에 Windows XP 테마를 적용하려면 두 가지 작업이 필요합니다.

첫 번째로 우리가 만든 DLL이 공용 컨트롤 버전 6.0을 사용할 것임을 운영체제에게 알리는 것입니다. 이는 리소스에 매니페스트(manifest)를 추가함으로써 가능합니다. 여러분은 위 MSDN 게시글의 메니페스트 XML 내용에서 필요한 부분을 복사하여 프로젝트의 res 서브 디렉토리에 dllreg.manifest라는 이름의 파일로 저장합니다. 그리고 다음의 내용을 리소스 파일에 추가합니다.

(번역자 주: 원문에 제시된 링크는 현재 접속할 수 없습니다. 대신 첨부된 pdf 파일을 참고하시기 바랍니다.)

 

ISOLATIONAWARE_MANIFEST_RESOURCE_ID RT_MANIFEST "res\\dllreg.manifest"

 

두 번째로 stdafx.h#include 구문보다 우선하여 다음의 내용을 추가합니다.

 

#define ISOLATION_AWARE_ENABLED 1

 

2006년 5월 기준으로 MSDN 게시글에서는 SIDEBYSIDE_COMMONCONTROLS라는 이름의 심볼을 이야기하고 있습니다만, 필자가 SDK를 살펴본 바로는 ISOLATION_AWARE_ENABLED만이 사용되고 있습니다. 여러분이 더 최신 버전의 SDK를 설치하였고 ISOLATION_AWARE_ENABLED 심볼을 사용하였을 때 효과가 없다면, SIDEBYSIDE_COMMONCONTROLS를 사용해보시기 바랍니다.

이와 같이 수정 후, 다시 빌드하면 다이얼로그는 다음과 같이 Windows XP의 새로운 테마가 적용되어 표시될 것입니다.

 

Windows XP에서 테마가 적용되어 [단계 2]의 예제 프로젝트를 실행한 결과.

 

쉘 익스텐션을 등록하는 다른 방법들

지금까지 우리가 만든 쉘 익스텐션은 특정한 몇몇 형식의 파일에 대해서만 호출되었습니다. 임의의 파일에 대해 쉘 익스텐션이 호출되도록 하려면 HKCR\* 경로의 키에 컨텍스트 메뉴 핸들러로서 DLL을 등록하면 됩니다.

HKCR\* 키는 모든 종류의 파일에 대해 호출될 쉘 익스텐션 목록을 가지고 있습니다. 개발 문서에 따르면 이 때의 쉘 익스텐션은 모든 쉘 오브젝트(Shell Object: 파일, 디렉토리, 가상 폴더, 제어판 항목 등)에 대해 호출된다고 적혀있지만, 필자가 테스트해 본 바로는 그렇지 않았습니다. 우리가 만드는 쉘 익스텐션은 파일 시스템에 존재하는 파일에 대해서만 호출되었습니다.

쉘 버전 4.71 이상에서는, HKCR\AllFileSystemObjects이라는 레지스트리 키가 존재합니다. 이 키에 등록하면, 우리가 만든 쉘 익스텐션은 파일 시스템에 있는 모든 파일 및 디렉토리에 대해 호출되고 드라이브와 같은 루트 폴더에 대해서는 호출되지 않습니다(드라이브 등의 루트 경로에 대해 쉘 익스텐션이 호출되게 하려면 HKCR\Drive 레지스트리 키에 등록하면 됩니다).

그러나 일부 버전의 Windows의 경우, 이 키에 DLL을 등록할 경우 이상하게 작동될 수 있습니다. 예를 들어, Windows 98에서 우리가 만든 쉘 익스텐션의 항목들끼리 연속적으로 나타나지 않고 다른 항목과 섞여서 나타날 수 있습니다. 이는 Windows XP에서는 발생하지 않는 문제입니다.

여러분은 쉘 익스텐션이 디렉토리에서만 호출되도록 작성할 수도 있습니다. 그러한 쉘 익스텐션의 예를 찾는 분은, 필자의 또 다른 게시글인 “컴파일러가 만드는 임시 파일 제거 유틸리티(A Utility to Clean Up Compiler Temp Files)”을 참고하시기 바랍니다.

마지막으로 4.71 이상 버전의 쉘에서 여러분은 사용자가 폴더의 빈 공간(바탕화면의 빈 영역 포함)에 대해 마우스 오른쪽 클릭을 하여 나타나는 컨텍스트 메뉴에 대해서도 항목을 추가할 수 있습니다. 여러분이 개발하고자 하는 쉘 익스텐션이 이와 같이 작동되게 하려면 HKCR\Directory\Background\shellex\ContextMenuHandlers에 DLL을 등록하면 됩니다. 이런 경우 IShellExtInit::Initialize에 전달되는 매개 변수(parameter)가 약간 달라집니다. 필자는 이 주제에 대해 이후의 단계에서 다루겠습니다.

 

다음 단계에서 다룰 내용

3 단계에서, 우리는 새로운 유형의 쉘 익스텐션으로서 쉘 오브젝트(shell object)에 대한 팝업 설명을 보여주는 QueryInfo 핸들러에 대해 다루어 보겠습니다. 필자는 또한 쉘 익스텐션에서 MFC를 사용하는 방법에 대해서도 보여드릴 것입니다.

계속 읽기

이전 게시글: Windows 쉘 익스텐션 개발 가이드 - (2) 여러 개의 파일 (1/2)

다음 게시글: Windows 쉘 익스텐션 개발 가이드 - (3) 팝업/인포팁

 

카테고리 “API/COM”
more...
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (2) 여러 개의 파일 (1/2)
입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차..
API/COM
2021. 1. 19. 23:13

Windows 쉘 익스텐션 개발 가이드 - (2) 여러 개의 파일 (1/2)

API/COM
2021. 1. 19. 23:13

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

 

2 단계. 여러 개의 파일에 대해 한번에 작동하는 쉘 익스텐션(Shell Extension)

실습 프로젝트 다운로드:

ShellExtGuide2_demo.zip
66.5 kB

 

들어가기에 앞서…

1 단계에서 필자는 쉘 익스텐션을 작성하는 방법을 설명하고 또한 한 번에 하나의 파일에 대해서만 작동되는 간단한 컨텍스트 메뉴 확장에 대해 보여드린 바 있습니다.

이번 단계에서 필자는 여러 개 선택된 파일에 대해 마우스 오른쪽 버튼 클릭을 하였을 때 한 번에 다루는 방법에 대해 보여드리고자 합니다. 이 글에서 예제로 제공되는 쉘 익스텐션은 COM 서버를 등록하거나 등록해제 하는 유틸리티를 구현하고 있습니다. 또한 ATL 다이얼로그 클래스인 CDialogImpl을 사용하는 방법에 대해서 시연하고 있습니다.

또한 필자는 이번 단계를 통해 미리 지정된 특정 형식의 파일이 아닌, 임의의 파일에 대해서 여러분이 만드는 쉘 익스텐션이 호출될 수 있도록 만드는 특별한 레지스트리 키에 대해서도 설명하겠습니다.

이번 단계는 여러분이 이미 1 단계를 읽고 컨텍스트 메뉴 확장에 대한 기본 지식을 알고 있을 것이라는 가정으로 작성되었습니다. 추가적으로 여러분은 이 글을 읽기 전에 COM, ATL 및 STL에서 제공하는 컬렉션 클래스의 기본을 이해하고 있어야 합니다.

Visual C++ 7.0 및 8.0 사용자는 컴파일을 하기 전에 몇 가지 설정을 변경할 필요가 있습니다. 1 단계의 ‘본 시리즈에 들어가며......’ 섹션을 참조하시기 바랍니다.

 

AppWizard를 사용하여 시작하기

AppWizard를 실행하고 새로운 ATL COM 프로젝트를 생성합니다. 이 프로젝트를 DllReg라고 명명하겠습니다. AppWizard의 모든 설정들은 기본 상태로 두고, [Finish] 버튼을 클릭합니다.

Visual C++ 7.0 이상의 버전에서는 “Attributed” 체크 박스의 체크를 해제합니다. 왜냐하면 우리는 이 예제 프로그램에서 Attributed ATL을 사용하지 않을 것이기 때문입니다.

DLL에 COM 개체를 추가하기 위해 “클래스 뷰(Class View)” 화면으로 이동하여 “DllReg classes” 트리 항목에 대해 마우스 오른쪽 버튼을 클릭하고 [New ATL Object] 항목을 클릭합니다. Visual C++ 7.0 이상의 버전에서는 해당 트리 항목에 대해 마우스 오른쪽 버튼을 클릭하고 “추가(D)”→”클래스 추가(C)...”를 클릭합니다. 영어 버전의 Visual Studio에서는 “Add”→”Add Class” 메뉴입니다.

ATL Object Wizard 화면에서, 첫 번째 단계를 보면 이미 Simple Object 항목이 선택되어 있을 것입니다. 가볍게 [Next] 버튼을 클릭합니다. 두 번째 단계에서 “Short Name” 에디트 상자에 DllRegShlExt를 입력합니다. 나머지 에디트 상자들은 자동으로 채워질 것입니다.

마법사는 OLE 오토메이션(Automation)을 통해 C 또는 스크립트 언어에서 사용 가능한 COM 객체를 자동으로 생성합니다. 우리가 만드는 쉘 익스텐션은 Windows 탐색기에서만 사용될 것이기 때문에, 우리는 몇 가지 설정을 조정함으로써 오토메이션 기능을 제거할 수 있습니다. ‘Attributes’ 페이지로 이동하여 인터페이스 타입을 ‘Custom’으로 변경합니다. 또한 ‘Aggregation’ 속성을 ‘No’로 변경합니다.

[OK] 버튼을 누르면 마법사는 COM 객체를 구현하기 위한 기본 코드들이 포함된 CDLLRegShlExt라는 이름의 클래스를 생성합니다. 우리는 이 클래스에 코드를 추가할 것입니다.

우리는 리스트 뷰(ListView) 컨트롤과 STL 클래스인 stringlist 클래스를 사용할 것입니다. 그러므로 stdafx.h에서 ATL 헤더들을 포함하는 지시문 뒤에 다음과 같이 내용을 추가합니다.

 

#include <atlwin.h> // 없으면 추가
#include <commctrl.h> // 없으면 추가
#include <string> // 새로 추가
#include <list> // 새로 추가

typedef std::list<std::basic_string<TCHAR>> string_list; // 새로 추가

 

초기화 인터페이스

우리의 IShellExtInit::Initialize 구현 내용은 1 단계에서 다루었던 쉘 익스텐션과 다소 다를 것입니다. 여기에는 두 가지 이유가 있습니다.

첫째로, 우리는 선택된 여러 개의 파일을 열거(enumerate)할 것입니다. 둘째로, 우리는 선택될 파일 각각에 대해서 그들이 등록 및 등록 해제 함수들을 내보내기(export) 하고 있는지 테스트할 것입니다. 우리는 이를 위해 각 파일에 대해 DllRegisterServerDllUnregisterServer를 내보내기 하고 있는지 확인할 것이고, 그렇지 않은 파일들을 무시할 것입니다.

1 단계에서 이미 해 보았듯이, C++ 클래스에서 마법사가 자동으로 생성할 일부 코드들을 삭제하고 IShellExtInit 인터페이스를 추가합니다.

 

#include <shlobj.h> // 새로 추가
#include <comdef.h> // 새로 추가
 
class ATL_NO_VTABLE CDLLRegShlExt :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CDLLRegShlExt, &CLSID_DllRegShlExt>,
    public IDllRegShlExt, // 이 줄은 삭제해도 됨
    public IShellExtInit { // 새로 추가 
    
    BEGIN_COM_MAP(CDLLRegShlExt)
        COM_INTERFACE_ENTRY(IDllRegShlExt)
        COM_INTERFACE_ENTRY(IShellExtInit)
    END_COM_MAP()
    
    // ...

 

우리의 CDLLRegShlExt 클래스는 또한 다음과 같은 몇 가지 멤버 변수들을 필요로 합니다.

 

    // ...
    
    protected:
    HBITMAP     m_hRegBmp;
    HBITMAP     m_hUnregBmp;
    string_list m_lsFiles;
    
    // ...

 

CDLLRegShlExt 생성자는 컨텍스트 메뉴에 사용할 두 가지 비트맵을 로드(load)합니다.

 

CDLLRegShlExt::CDLLRegShlExt() {
    m_hRegBmp = LoadBitmap(_Module.GetModuleInstance(), MAKEINTRESOURCE(IDB_REGISTERBMP));
    m_hUnregBmp = LoadBitmap(_Module.GetModuleInstance(), MAKEINTRESOURCE(IDB_UNREGISTERBMP));
}

 

이제, 우리는 Initialize 함수를 작성할 준비가 되었습니다. Initialize 함수는 다음과 같은 단계를 수행합니다.

1. 현재 디렉토리(current directory)는 Windows 탐색기가 열고 있는 디렉토리 경로로 변경하기.

2. 선택된 파일을 모두 열거하기.

3. DLL 파일과 OCX 파일에 대해 LoadLibrary를 사용하여 로드를 시도해 보기.

4. LoadLibrary가 성공하면 DllRegisterServerDllUnregisterServer 함수를 내보내기(export) 하고 있는지 확인하기.

5. 두 함수가 내보내기(export)되었음을 확인하면 파일 리스트로 쓰일 m_lsFiles에 파일 이름 추가하기.

 

HRESULT CDLLRegShlExt::Initialize(
    LPCITEMIDLIST pidlFolder,
    LPDATAOBJECT pDataObj,
    HKEY hProgID) {
    
    UINT      uNumFiles;
    HDROP     hdrop;
    FORMATETC etc = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
    STGMEDIUM stg = { TYMED_HGLOBAL };
    HINSTANCE hinst;
    HRESULT (STDAPICALLTYPE * pfn)();
    
    // ...

 

선언된 지역변수가 매우 많습니다! 먼저 수행할 단계는 전달된 pDataObj에서 HDROP을 얻는 것입니다. 이것은 지난 1 단계에서 해 보았던 것과 비슷합니다.

 

    // ...
    
    // 데이터 객체로부터 폴더 목록을 읽습니다.
    // 이것은 HDROP 포맷으로 저장되어 있기 때문에 HDROP 핸들을 가져와서
    // 드래그 앤 드롭 API와 함께 사용합니다.
    if (FAILED(pDO->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 루프가 나옵니다. 이 루프에서는 DragQueryFile 함수를 사용하여 파일 이름을 하나씩 가져오고, LoadLibrary를 통해 로드를 시도합니다. 예제 프로젝트는 이 작업을 하기 전 현재 디렉토리를 수정하고 있으나 이 글에서는 너무 길어지므로 이를 생략하고 설명합니다.

 

    // ...
    
    for (UINT uFile = 0; uFile < uNumFiles; uFile++) {
        // 루프에 따라 파일 이름을 하나씩 가져옵니다.
        if (DragQueryFile(hdrop, uFile, szFile, MAX_PATH) == 0)
            continue;
        
        // DLL 파일로서 로드를 시도합니다.
        hinst = LoadLibrary(szFile);
        
        if (hinst == NULL)
            continue;
        
        // ...

 

이제 우리는 해당 모듈이 두 개의 필수 함수를 내보내기(export)하고 있는지 확인합니다.

    // ...
    
    // DllRegisterServer의 주소를 가져옵니다.
    (FARPROC &)pfn = GetProcAddress(hinst, "DllRegisterServer");
    
    // 찾을 수 없는 경우 이 파일은 건너뜁니다.
    if (pfn == NULL) {
        FreeLibrary(hinst);
        continue;
    }
    
    // DllUnregisterServer의 주소를 가져옵니다.
    (FARPROC &)pfn = GetProcAddress(hinst, "DllUnregisterServer");
    
    // 두 함수가 모두 내보내기 되었음이 확인되면,
    // 우리는 이 파일에 대해 작업을 수행할 수 있기 때문에,
    // 파일 리스트로 쓰일 m_lsFiles에 추가합니다.
    if (pfn != NULL)
        m_lsFiles.push_back(szFile);
    
    FreeLibrary ( hinst );
}  // for 루프의 끝

 

모듈에서 두 함수가 내보내기(export)된 상태로 존재한다면, 그 파일 이름을 m_lsFiles에 추가합니다. m_lsFiles는 문자열을 보관할 list 형의 STL 콜렉션 객체입니다. 이 리스트 객체는 나중에 각 파일을 등록 또는 등록 해제하기 위하여 반복(iterate)할 때 사용될 것입니다.

마지막으로 할 것은 Initialize를 호출하여 로드된 리소스들을 해제하고, Windows 탐색기에게 값을 반환하는 것입니다.

 

    // 리소스 반환
    GlobalUnlock(stg.hGlobal);
    ReleaseStgMedium(&stg);
    
    // 선택한 파일들 중에서 우리가 작업할 파일이 하나라도 있다면 S_OK를 반환하고,
    // 그렇지 않다면 이번 마우스 오른쪽 버튼 클릭에서는 우리가 할 것이 없으므로,
    // E_INVALIDARG를 반환하여 이번 건과 관련해서 호출되지 않도록 한다.
    return (m_lsFiles.size() > 0) ? S_OK : E_INVALIDARG;
}

 

여러분이 예제 프로젝트의 소스 코드를 유심히 본다면, 여러분은 필자가 파일 이름들을 보고 있는 디렉토리 경로를 확인해야 함을 알게 될 것입니다.

어쩌면 여러분은 왜 필자가 pidlFolder 매개 변수(parameter)를 사용하지 않는 것인지 궁금해 하실 수도 있습니다. 물론 이 매개 변수(parameter)에 대해 “컨텍스트 메뉴가 표시되고 있는 항목을 포함하고 있는 폴더에 대한 항목 식별자 리스트”라고 문서화 되어있기는 하지만 글쎄요…… 필자가 테스트 해 보았더니 이 매개 변수(parameter)는 항상 NULL이었습니다. 그래서 이건 유용하지가 않습니다.

 

컨텍스트 메뉴에 항목들을 추가하기

다음으로 해야 할 것은 IContextMenu 메소드들입니다. 전에 이미 해 보았듯이 우리는 다음과 같은 내용을 소스 코드에 추가함으로써 CDllRegShlExt 클래스가 구현하는 인터페이스 목록에 IContextMenu를 추가합니다.

 

class ATL_NO_VTABLE CDLLRegShlExt :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CDLLRegShlExt, &CLSID_DllRegShlExt>,
    public IShellExtInit,
    public IContextMenu { // 새로 추가
    
    BEGIN_COM_MAP(CDLLRegShlExt)
        COM_INTERFACE_ENTRY(IShellExtInit)
        COM_INTERFACE_ENTRY(IContextMenu)
    END_COM_MAP()

 

우리는 다음의 두 항목을 메뉴에 추가할 것입니다: 하나는 선택된 파일들을 등록하는 항목이고, 다른 하나는 등록 해제하는 항목입니다. 이들 항목은 다음과 같이 보여질 것입니다.

 

DLL의 등록 또는 등록 해제하는 쉘 익스텐션.

 

우리의 QueryContextMenu 구현은 앞서 설명한 1 단계와 비슷하게 시작합니다. 먼저 우리는 uFlags를 확인하고 CMF_DEFAULTONLY 옵션이 존재할 경우 즉시 값을 반환하고 종료합니다.

 

HRESULT CDLLRegShlExt::QueryContextMenu(
    HMENU hmenu, UINT uMenuIndex, 
    UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags) {
    
    UINT uCmdID = uidFirstCmd;
    
    // CMF_DEFAULTONLY 옵션이 포함되어 있을 경우 아무것도 하지 않아야 합니다.
    if (uFlags & CMF_DEFAULTONLY)
        return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0);
    
    // ...

 

다음으로 우리는 컨텍스트 메뉴에 “Register servers” 항목을 추가합니다. 새롭게 추가된 것이 있다면 우리는 각 항목에 비트맵을 설정한다는 것입니다. 이것은 WinZip이 메뉴 항목에 바이스(vice)에 끼워진 폴더 아이콘을 작게 표시해주는 것과 같은 것입니다.

 

    // ...
    
    // 등록 항목과 등록 해제 항목을 컨텍스트 메뉴에 추가한다.
    InsertMenu(hmenu, uMenuIndex, MF_STRING | MF_BYPOSITION, uCmdID++, _T("Register server(s)"));
    
    // 등록 항목에 비트맵 설정하기
    if (m_hRegBmp != NULL)
        SetMenuItemBitmaps(hmenu, uMenuIndex, MF_BYPOSITION, m_hRegBmp, NULL);
    
    uMenuIndex++;
    
    // ...

 

SetMenuItemBitmaps API는 컨텍스트 메뉴의 “Register server(s)” 항목에 우리가 추가할 작은 기어 모양의 아이콘을 보여주려는 방법입니다. uCmdID가 증가되었음을 주목하시기 바랍니다. 그 다음에 우리는 InsertMenu를 한번 더 호출할 때 Command ID는 이전에 지정한 값보다 최소한 1 이상은 더 큰 값이어야 합니다. 이 단계의 마지막에서 uMenuIndex는 증가되어 다음에 한번 더 추가할 메뉴 항목이 이번에 추가한 메뉴 항목 다음에 나타나도록 합니다.

두 번째 메뉴 항목에 대해 말하자면, 지금까지 설명했던 첫 번째 메뉴 항목을 추가하는 것과 똑같이 소스 코드를 작성하면 됩니다.

 

    // ...
    
    InsertMenu(hmenu, uMenuIndex, MF_STRING | MF_BYPOSITION, uCmdID++, _T("Unregister server(s)"));
    
    // 등록 해제 항목에 대해 비트맵을 설정합니다.
    if (m_hUnregBmp != NULL)
        SetMenuItemBitmaps(hmenu, uMenuIndex, MF_BYPOSITION, m_hUnregBmp, NULL);
    
    // ...

 

끝으로 우리는 Windows 탐색기에게 컨텍스트 메뉴에 몇 개의 항목을 추가했는지를 알려줍니다.

 

    // ...
    
    return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 2);
}

계속 읽기

이전 게시글: Windows 쉘 익스텐션 개발 가이드 - (1) 튜토리얼 (2/2)

다음 게시글: Windows 쉘 익스텐션 개발 가이드 - (2) 여러 개의 파일 (2/2)

 

카테고리 “API/COM”
more...
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (1) 튜토리얼 (2/2)
입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차..
API/COM
2021. 1. 19. 16:22

Windows 쉘 익스텐션 개발 가이드 - (1) 튜토리얼 (2/2)

API/COM
2021. 1. 19. 16:22

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

 

1 단계. 쉘 익스텐션(Shell Extension)을 작성하기 위한 단계별 튜토리얼

이전 파트에 이어서...

 

상태 표시줄에서 플라이-바이 도움말(fly-by help)을 보여주기

그 다음 호출될 수 있는 IContextMenu 메소드에는 GetCommandString이 있습니다.

사용자가 Windows 탐색기 창에서 텍스트 파일에 대해 마우스 오른쪽 클릭을 하였을 때, 또는 텍스트 파일을 하나 선택하고 [파일(F)] 메뉴를 클릭하였을 때, 상태 표시줄은 우리가 추가한 컨텍스트 메뉴 항목에 마우스 포인터가 올라가면 ‘플라이-바이(fly-by) 도움말’을 보여주게 됩니다.

이 때 우리가 작성하는 GetCommandString 메소드는 Windows 탐색기를 통해 보여줄 문자열을 반환하게 될 것입니다.

GetCommandString의 원형은 다음과 같습니다.

 

HRESULT IContextMenu::GetCommandString(
    UINT idCmd, UINT uFlags, UINT * pwReserved, LPSTR pszName, UINT cchMax);

 

idCmd는 어떤 메뉴에 마우스 포인터가 올라가 있는지를 나타내는 0부터 시작하는 카운트입니다. 우리는 단 하나의 항목만을 추가했기 때문에 idCmd 값은 항상 0이 될 것입니다. 그러나 우리가 3개의 항목을 추가했다고 가정했을 때, idCmd0, 1, 2 중 하나가 될 것입니다.

uFlags는 또 다른 설정들의 집합으로서, 필자는 이에 대해 나중에 설명하겠습니다.

pwReserved에 대해서는 무시할 수 있습니다.

pszName은 쉘이 가지고 있는 버퍼에 대한 포인터로서, 우리는 상태 표시줄에 보여줄 문자열들을 이 곳에 저장하게 됩니다.

cchMax는 이 버퍼의 크기입니다.

반환하는 값은 S_OK 또는 E_FAIL 같은 평범한 HRESULT 상수입니다.

 

GetCommandString은 또한 메뉴 항목에 대한 “동사(verb)”를 가져오기 위해 호출될 수 있습니다. “동사(verb)”란 파일에 대해 가해질 수 있는 어떤 행위를 나타내는 언어 중립적인 문자열입니다. 이에 대해 ShellExecute에 관련된 문서들에서는 많은 것들을 이야기 하고 있습니다.

 

“동사(verb)”에 대한 이야기는 별도의 글에서 설명하는 것이 나을 것 같습니다. 그러나 간략하게 말하면, “동사(verb)”는 ‘열기(open)’, ‘인쇄(print)’처럼 레지스트리에 기재될 수도 있고, 컨텍스트 메뉴 확장에 의해 동적으로 생성될 수도 있습니다. 이것은 쉘 익스텐션에서 구현된 파일에 대한 행위가 ShellExecute이 호출될 때 실행될 수 있도록 해 줍니다.

어쨌든 필자가 이 모든 것을 언급한 이유는 GetCommandString이 왜 호출되는지를 여러분에게 설명하기 위함이었습니다. Windows 탐색기가 ‘플라이-바이(fly-by) 도움말’ 문자열을 원한다면 우리는 이를 제공해주면 됩니다. 또 Windows 탐색기가 “동사(verb)”를 요구한다면, 우리는 이 요청을 무시하면 됩니다. 이것이 uFlags 매개 변수(parameter)를 다루는 방법입니다.

uFlagsGCS_HELPTEXT 설정을 포함하고 있다면, Windows 탐색기는 ‘플라이-바이(fly-by) 도움말’을 요청하고 있는 것입니다. 추가적으로 GCS_UNICODE 설정을 포함하고 있다면, 우리는 반드시 유니코드 문자열로 반환해야 합니다.

GetCommandString은 다음과 같이 작성할 수 있습니다.

 

#include <atlconv.h>  // ATL 문자열 변환 매크로를 위해 include합니다.
HRESULT CSimpleShlExt::GetCommandString(
    UINT idCmd, UINT uFlags, UINT * pwReserved, LPSTR pszName, UINT cchMax) {
    
    USES_CONVERSION;
    
    // idCmd를 검사합니다.
    // 우리는 메뉴에 하나의 항목만을 추가할 것이므로 이 값은 항상 0이어야 합니다.
    if (idCmd != 0)
        return E_INVALIDARG;
    
    // Windows 탐색기가 도움말 문자열을 요청할 경우,
    // 제공된 버퍼에 우리가 가진 문자열을 복사하면 됩니다.
    if (uFlags & GCS_HELPTEXT) {
        LPCTSTR szText = _T("This is the simple shell extension's help");
        
        // Windows가 유니코드 문자열을 요청하는지 여부를 확인하여 유니코드인 경우...
        if (uFlags & GCS_UNICODE) {
            // 먼저 pszName을 유니코드 문자열로 캐스팅해야 합니다.
            // 이를 위해 유니코드 문자열 복사 API를 사용합니다.
            lstrcpynW((LPWSTR)pszName, T2CW(szText), cchMax);
        } else {
            // 도움말 문자열을 반환하기 위해 ANSI 문자열 복사 API를 사용합니다.
            lstrcpynA(pszName, T2CA(szText), cchMax);
        }
        
        return S_OK;
    }
    
    return E_INVALIDARG;
}

 

필자는 문자열을 하드코드(hardcoded)하여 적절한 문자열 집합으로 변환하도록 작성하였습니다. 여러분이 ATL 변환 매크로를 사용해 본 적이 없다면, 문자열 래퍼(wrapper) 클래스에 대한 필자의 글을 참고하시기 바랍니다. 그러면 유니코드 문자열을 COM 메소드나 OLE 함수에 전달하는 것이 한결 쉬워질 것입니다.

참고할만한 중요한 것은 lstrcpyn API는 목적지 문자열이 NULL 문자로 끝남을 보증한다는 것입니다. 이것이 CRT 함수인 strncpy와의 다른 점으로서, strncpy는 원본 문자열의 길이가 cchMax보다 크거나 같을 때 목적지 문자열에 NULL 문자를 붙이지 않습니다.

그러므로 필자는 항상 lstrcpyn를 사용할 것을 강력히 권장합니다. 그러면 여러분은 strncpy를 사용할 때와는 달리 결과 문자열이 NULL 문자로 끝났는지 여부를 확인할 필요가 없어집니다.

 

사용자의 선택에 따라 수행하기

IContextMenu에 대해 마지막으로 살펴볼 메소드는 InvokeCommand입니다. 이 메소드는 우리가 추가한 메뉴 항목을 사용자가 클릭했을 때 호출됩니다. InvokeCommand의 원형은 다음과 같습니다.

 

HRESULT IContextMenu::InvokeCommand(
    LPCMINVOKECOMMANDINFO pCmdInfo);

 

CMINVOKECOMMANDINFO 구조체는 무수히 많은 정보들을 가지고 있습니다, 그러나 우리는 lpVerbhwnd에 대해서만 다루어 보겠습니다.

lpVerb는 두 가지 역할을 맡고 있습니다. 하나는 실행되려는 동사의 이름이고, 다른 하나는 클릭된 메뉴 항목의 인덱스입니다. hwnd는 사용자가 우리가 만든 쉘 익스텐션을 실행하는 Windows 탐색기 창의 핸들입니다. 이 핸들은 우리가 보여주고자 하는 사용자 인터페이스의 부모 윈도우를 지정할 때 사용할 수 있습니다.

우리가 하나의 메뉴 항목을 만들었기 때문에, 우리는 lpVerb를 확인할 것입니다. 이 값이 영(0)일 경우 우리는 우리가 만든 메뉴 항목이 클릭되었음을 알 수 있습니다. 이 때 해야 할 작업으로 필자가 생각할 수 있었던 가장 간단한 작업은 메시지 상자를 띄우는 것입니다. 그리고 이것이 우리가 작성하는 코드가 하는 일의 전부입니다. 메시지 상자는 선택된 파일 이름을 보여줌으로써 제대로 작동됨을 확인시켜 줍니다.

 

HRESULT CSimpleShlExt::InvokeCommand(LPCMINVOKECOMMANDINFO pCmdInfo) {
    // lpVerb가 실제로 존재하는 문자열을 가리기고 있다면,
    // 이 함수 호출을 무시하고 종료합니다.
    if (HIWORD(pCmdInfo->lpVerb) != 0)
        return E_INVALIDARG;
    
    // 각 메뉴 항목에 해당하는 명령 인덱스를 얻습니다.
    // 여기서는 하나의 메뉴 항목만을 가지고 있으므로 영(0) 만이 유효합니다.
    switch (LOWORD(pCmdInfo->lpVerb)) {
    case 0: {
        TCHAR szMsg[MAX_PATH + 32];
        
        wsprintf(szMsg, _T("The selected file was:\n\n%s"), m_szFile);
        
        MessageBox(pCmdInfo->hwnd, szMsg, _T("SimpleShlExt"), MB_ICONINFORMATION);
        
        return S_OK;
    }
    default: {
        return E_INVALIDARG;
    }
    }
}

 

기타 세부적인 사항

마법사가 자동으로 생성한 코드 중 우리가 필요로 하지 않는 OLE Automation 기능을 제거할 수 있습니다. 첫 번째로 우리는 SimpleShlExt.rgs 파일(이 파일의 목적은 다음 절에서 설명하겠습니다)에서 몇 가지 레지스트리 키를 제거할 수 있습니다.

 

HKCR {
    SimpleExt.SimpleShlExt.1 = s 'SimpleShlExt Class' {
        CLSID = s '{5E2121EE-0300-11D4-8D3B-444553540000}'
    }
    SimpleExt.SimpleShlExt = s 'SimpleShlExt Class' {
        CLSID = s '{5E2121EE-0300-11D4-8D3B-444553540000}'
        CurVer = s 'SimpleExt.SimpleShlExt.1'
    }
    NoRemove CLSID {
        ForceRemove {5E2121EE-0300-11D4-8D3B-444553540000} = s 'SimpleShlExt Class' {
            ProgID = s 'SimpleExt.SimpleShlExt.1'
            VersionIndependentProgID = s 'SimpleExt.SimpleShlExt'
            InprocServer32 = s '%MODULE%' {
                val ThreadingModel = s 'Apartment'
            }
        'TypeLib' = s '{73738B1C-A43E-47F9-98F0-A07032F2C558}'
        }
    }
}

 

우리는 또한 DLL 리소스에서 타입 라이브러리를 제거할 수 있습니다.

“View” 메뉴에서 “Resource Includes...”를 클릭합니다. “Compile-time directives” 상자에서 여러분은 다음과 같이 타입 라이브러리가 include된 것을 보실 수 있습니다.

 

[View] 메뉴에서 [Resource Includes...] 항목을 클릭한다.
“Compile-time directives”에 자동으로 추가되어있는 내용.

 

이 줄(1 TYPELIB "SimpleExt.tlb")을 삭제합니다. Visual C++가 include를 수정한다고 경고하면 [OK]를 누릅니다.

Visual C++ 7.0 이상에서는 이 과정이 다른 위치에 있습니다. “리소스 뷰(Resource View)” 탭에서 SimpleExt.rc를 마우스 오른쪽 버튼으로 클릭하고 컨텍스트 메뉴에서 “Resource Includes”를 클릭합니다.

이제 우리는 타입 라이브러리를 제거했습니다. 우리는 두 개의 줄을 수정하여 ATL에게 타입 라이브러리와 관련된 어떤 작업도 하지 않도록 알려주어야 합니다.

SimpleExt.cpp를 열고, DllRegisterServer 함수로 이동합니다. 그리고 RegisterServer를 호출할 때 지정되는 매개 변수(parameter)를 FALSE로 수정합니다.

 

STDAPI DllRegisterServer() {
    // ...
    return _Module.RegisterServer(FALSE); // TRUE로 되어있는 것을 FALSE로 변경
}

 

DllUnregisterServer도 같은 수정을 합니다.

 

STDAPI DllUnregisterServer() {
    // ...
    return _Module.UnregisterServer(FALSE); // TRUE로 되어있는 것을 FALSE로 변경
}

 

쉘 익스텐션을 등록하기

이제 우리는 COM 인터페이스를 모두 구현하였습니다. 그런데…… 어떻게 하면 Windows 탐색기가 우리가 만든 쉘 익스텐션을 사용하게 만들 수 있을까요? ATL은 우리가 만든 DLL을 COM 서버로 등록하는 소스 코드를 자동으로 생성합니다. 그러나 이것은 우리가 만든 DLL을 다른 어플리케이션이 사용할 수 있게 등록하는 과정일 뿐입니다. Windows 탐색기에게 우리가 만든 쉘 익스텐션을 사용하게 하려면, 우리는 다음과 같이 텍스트 파일에 대한 정보를 가지고 있는 레지스트리 키(key) 아래에 DLL을 등록할 필요가 있습니다.

 

HKEY_CLASSES_ROOT\txtfile

 

이 키의 하위에 ShellEx라는 이름을 가진 키를 둡니다. 이는 텍스트 파일에 대해 실행될 수 있는 쉘 익스텐션들의 목록을 보관하고 있습니다.

특히 ShellEx 키 하위에 놓일 ContextMenuHandler이라는 이름의 키는 컨텍스트 메뉴 확장에 대한 목록을 보관합니다. 각 쉘 익스텐션은 ContextMenuHandlers의 하위 키를 생성하고 그 하위 키의 기본 값으로 자신의 GUID를 설정합니다. 우리가 만든 예제 프로그램에서 우리는 다음과 같은 경로의 하위 키를 생성할 것입니다.

 

HKEY_CLASSES_ROOT\txtfile\ShellEx\ContextMenuHandlers\SimpleShlExt

 

그리고 이 키의 기본값으로 우리의 GUID{5E2121EE-0300-11D4-8D3B-444553540000}를 설정합니다.

이 작업을 위해 여러분이 어떠한 코드를 작성할 필요는 없습니다. “File View” 탭에 나타나는 파일 목록들을 보다 보면, 여러분은 SimpleShlExt.rgs라는 이름의 파일을 볼 것입니다. 이것은 ATL이 파싱하는 텍스트 파일로서, COM 서버가 등록될 때 레지스트리에 추가할 항목들이 무엇인지, 그리고 COM 서버가 등록 해제될 때 레지스트리에서 삭제될 항목들이 무엇인지를 ATL에게 알려줍니다.

다음은 Windows 탐색기가 우리가 만든 쉘 익스텐션의 존재를 알 수 있도록, 우리가 조정해야 할 레지스트리 항목의 예를 적은 것입니다.

 

HKCR {
    NoRemove txtfile {
        NoRemove ShellEx {
            NoRemove ContextMenuHandlers {
                ForceRemove SimpleShlExt = s '{5E2121EE-0300-11D4-8D3B-444553540000}'
            }
        }
    }
}

 

각 줄은 레지스트리 키 이름이고 KHCRHKEY_CLASSES_ROOT의 줄임말입니다.

NoRemove 키워드는 COM 서버가 등록 해제되어도 삭제되어서는 안 되는 레지스트리 키를 의미합니다. 특정 줄에서 발견되는 또 다른 키워드 ForceRemove는 새롭게 키가 작성될 때 기존에 레지스트리에 존재하고 있던 키는 삭제하라는 뜻입니다.

해당 줄의 나머지 부분은 문자열(앞에 붙은 글자 s의 의미이기도 합니다)로서, SimpleShlExt 키의 기본값으로 보관될 것입니다.

 

여기서 필자는 내용을 덧붙이고자 합니다.

우리가 만든 쉘 익스텐션을 등록할 때 키가 생성되는 위치는 HKEY_CLASSES_ROOT\txtfile입니다. 그러나 txtfile은 영구적인 이름도 아니고 미리 예약된 이름도 아닙니다. 여러분이 HKEY_CLASSES_ROOT\.txt 경로의 키를 열면 그 확장명에 대한 이름이 키의 기본값으로 저장되어 있음을 보게 될 것입니다. 이러한 구조는 두 가지 부작용이 있습니다.

 

–“txtfile”이 올바른 키의 이름이 아니기 때문에 RGS 스크립트를 신뢰성 있게 사용할 수 없습니다.

– 몇몇 텍스트 편집기들은 .txt 파일에 자기 자신을 연결 프로그램으로 지정하며 설치될 수 있습니다. 이는 HKEY_CLASSES_ROOT\.txt 키의 기본값을 변경하기 때문에, 쉘 익스텐션이 작동하지 않게 될 수 있습니다.

 

이것은 확실히 필자에게 결함처럼 보입니다. 마이크로소프트도 이를 인지하고 있을 것입니다. 왜냐하면 최근에 만들어지고 있는 쉘 익스텐션(예를 들어 QueryInfo 확장)은 파일 확장명으로 이름 붙인 키에 직접 등록하기 때문입니다.

필자의 사견은 여기까지입니다. 마지막으로 하나의 사항이 남아 있습니다. Windows NT 계열에서, 우리가 만든 쉘 익스텐션을 “승인된(approved)” 확장 목록에 추가할 것이 권장됩니다. 이것은 시스템 정책적으로 승인 목록에 없는 확장 프로그램이 로드(load)되는 것을 방지하기 위함입니다. 이 목록은 다음과 같은 경로에 보관되어 있습니다.

 

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved

 

이 키에서 우리는 이름이 우리가 만든 쉘 익스텐션의 GUID인 문자열을 생성합니다. 문자열의 내용은 아무거나 될 수 있습니다. 이러한 작업은 예제 프로그램 소스 코드의 DllRegisterServerDllUnregisterServer 함수에 있지만, 이 곳에서 보이지는 않겠습니다. 이 내용은 매우 간단한 레지스트리 액세스에 대한 것이므로, 여러분은 쉽게 찾으실 수 있습니다.

 

쉘 익스텐션을 디버그하기

나중에 가면 여러분은 그리 간단하지 않은 쉘 익스텐션을 작성하게 될 것이고, 이를 디버그하게 될 상황도 생깁니다. 프로젝트 설정을 열고 디버그 탭에 있는 “Executable for debug session” 에디트 상자에 Windows 탐색기의 전체 경로를 입력합니다. 예를 들여 C:\Windows\Explorer.exe처럼 입력하면 됩니다.

여러분이 Windows NT 기반을 사용하고 있고 이전에 DesktopProcess 레지스트리 키를 설정한 적이 있다면, 디버그를 위해 [F5] 버튼을 눌렀을 때 새로운 탐색기 창이 나타나게 될 것입니다. 그 창에서 여러분이 작업하는 한, 여러분은 DLL을 다시 빌드(build)하여도 문제가 없습니다. 왜냐하면 그 탐색기 창을 닫아버리면 여러분이 만들고 있는 쉘 익스텐션은 알아서 언로드(unload)될 것이기 때문입니다.

Windows 9x 운영체제를 사용하고 있다면, 여러분은 디버거를 실행하기 전 쉘을 종료해야 할 것입니다. [시작] 버튼을 누르고 “시스템 종료”를 클릭합니다. [Ctrl]+[Alt]+[Shift]를 누른 상태에서 [취소] 버튼을 클릭하면 Windows 탐색기가 종료되면서 작업표시줄이 사라지는 것을 보게 되실 것입니다. 그 다음 Microsoft Visual C++로 돌아가서 F5를 누르고 디버그를 시작합니다. 디버그를 중단하고자 할 때 [Shift]+[F5]를 눌러 Windows 탐색기를 종료합니다. 디버그를 완전히 끝내고자 할 때 명령 프롬프트에서 explorer를 실행 후 평소처럼 쉘(shell)을 재시작하면 됩니다.

 

결과물은 어떻게 생겼는가?

다음과 같이 컨텍스트 메뉴에 우리가 추가한 메뉴 항목이 나타납니다. 또한 우리가 추가한 메뉴 항목에 대해 ‘플라이-바이(fly-by) 도움말’이 나타날 것입니다.

 

텍스트 파일에 대해 마우스 오른쪽 버튼 클릭을 하면 쉘 익스텐션이 나타난다.

 

그리고 이를 클릭하였을 때 다음과 같이 파일 이름을 보여주는 메시지 상자가 뜰 것입니다.

 

쉘 익스텐션이 정상적으로 작동한다.

 

계속 읽기

이전 게시글: Windows 쉘 익스텐션 개발 가이드 - (1) 튜토리얼 (1/2)

다음 게시글: Windows 쉘 익스텐션 개발 가이드 - (2) 여러 개의 파일 (1/2)

 

카테고리 “API/COM”
more...
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (1) 튜토리얼 (1/2)
입문자를 위한 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에서 자동으로 생성해주는 코드의 형태와는 다소 차..
API/COM
2021. 1. 16. 23:07

Windows 쉘 익스텐션 개발 가이드 - (1) 튜토리얼 (1/2)

API/COM
2021. 1. 16. 23:07

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

 

1 단계. 쉘 익스텐션(Shell Extension)을 작성하기 위한 단계별 튜토리얼

실습 프로젝트 다운로드

ShellExtGuide1_demo.zip
38.4 kB

여러분이 이 게시물의 토론 게시판에 질문하기 전 꼭 읽어주시기 바랍니다.

본래 이 시리즈는 Visual C++ 6.0 사용자를 기준으로 작성되었습니다. Visual C++ 8.0 (Visual Studio 2005)이 출시된 만큼, 필자는 본 글을 Visual C++ 7.1(Visual Studio 2003)까지 다루도록 업데이트해야 할 필요가 있음을 느꼈습니다(Visual C++ 7.1이 제공하는 자동 업데이트가 있기는 하지만 그다지 매끄럽게 작동되는 것이 아니어서 독자 여러분이 첨부된 예제 프로그램을 실습하고자 할 때 좌절하실 수 있습니다). 필자가 글의 내용을 검토하고 본 시리즈를 업데이트 하면서, Visual C++ 7.1의 새로운 기능을 반영하고자 업데이트 할 것이고, 소스 코드의 다운로드 코너에서 Visual C++ 7.1 형식의 프로젝트를 포함시킬 것입니다.

 

Visual Studio 2005 사용자를 위한 주의사항

Visual C++ 2005 Express Edition은 ATL과 MFC를 포함하고 있지 않습니다. 그러나 본 시리즈는 ATL을 사용하고 경우에 따라서 MFC도 사용할 것이기 때문에, 본 글에 첨부된 예제 프로그램을 실습하고자 하는 분은 Express Edition을 사용할 수 없습니다.

여러분이 Visual C++ 6.0을 사용하고 있다면, 여러분은 Platform SDK를 업데이트할 필요가 있습니다. 웹 설치 버전을 사용할 수도 있고, 오프라인 상태에서 설치하는 경우를 위하여 CAB 파일 버전ISO 파일 버전도 마련되어 있습니다. 여러분은 Visual C++ 탐색 경로에 SDK include와 라이브러리 경로가 추가되었는지 확인해야 합니다. 이러한 경로는 “Platform SDK” 프로그램 그룹의 “Visual Studio Registration” 폴더에서 찾을 수 있습니다. 여러분이 Visual C++ 7.0 또는 8.0을 사용하고 있다 하더라도 Platform SDK를 업데이트하여 최신의 헤더 파일과 라이브러리를 유지하는 것이 좋습니다.

 

Windows® Server 2003 SP1 Platform SDK Web Install
PSDK-amd64.exe
1.29MB
PSDK-ia64.exe
1.38MB
PSDK-x86.exe
1.26MB

 

Windows® Server 2003 SP1 Platform SDK Full Download
PSDK-FULL.part01.rar
9.80MB
PSDK-FULL.part02.rar
9.80MB
PSDK-FULL.part03.rar
9.80MB
PSDK-FULL.part04.rar
9.80MB
PSDK-FULL.part05.rar
9.80MB
PSDK-FULL.part06.rar
9.80MB
PSDK-FULL.part07.rar
9.80MB
PSDK-FULL.part08.rar
9.80MB
PSDK-FULL.part09.rar
9.80MB
PSDK-FULL.part10.rar
9.80MB
PSDK-FULL.part11.rar
9.80MB
PSDK-FULL.part12.rar
9.80MB
PSDK-FULL.part13.rar
9.80MB
PSDK-FULL.part14.rar
9.80MB
PSDK-FULL.part15.rar
9.80MB
PSDK-FULL.part16.rar
9.80MB
PSDK-FULL.part17.rar
9.80MB
PSDK-FULL.part18.rar
9.80MB
PSDK-FULL.part19.rar
9.80MB
PSDK-FULL.part20.rar
9.80MB
PSDK-FULL.part21.rar
9.80MB
PSDK-FULL.part22.rar
9.80MB
PSDK-FULL.part23.rar
9.80MB
PSDK-FULL.part24.rar
9.80MB
PSDK-FULL.part25.rar
9.80MB
PSDK-FULL.part26.rar
9.80MB
PSDK-FULL.part27.rar
9.80MB
PSDK-FULL.part28.rar
9.80MB
PSDK-FULL.part29.rar
9.80MB
PSDK-FULL.part30.rar
9.80MB
PSDK-FULL.part31.rar
9.80MB
PSDK-FULL.part32.rar
9.80MB
PSDK-FULL.part33.rar
9.80MB
PSDK-FULL.part34.rar
9.80MB
PSDK-FULL.part35.rar
9.80MB
PSDK-FULL.part36.rar
9.80MB
PSDK-FULL.part37.rar
9.80MB
PSDK-FULL.part38.rar
9.80MB
PSDK-FULL.part39.rar
9.80MB
PSDK-FULL.part40.rar
9.80MB
PSDK-FULL.part41.rar
9.80MB
PSDK-FULL.part42.rar
9.80MB
PSDK-FULL.part43.rar
9.80MB
PSDK-FULL.part44.rar
2.27MB

 

Visual C++ 7.0 사용자를 위한 주의사항

여러분이 Platform DSK를 업데이트 하지 않았다면 include 경로의 기본값을 반드시 변경해야 합니다. 다음과 같이 ($VCInstallDir)include 보다 위쪽, 다시 말하면 목록의 첫 번째 줄이 $(VCInstallDir)PlatformSDK\include로 되어있는지 확인합니다.

 

[도구(T)] 메뉴의 [옵션(O)] 항목을 클릭한다.
“VC++ 디렉터리”의 “포함 파일”에서 Platform SDK의 포함 파일 경로를 맨 위로 한다.

 

필자는 아직 Visual C++ 8.0을 사용해본 적이 없기 때문에 Visual C++ 8.0(Visual Studio 2008)에서도 예제 프로그램이 컴파일되는지는 모르겠습니다. 다행히도 Visual C++ 6.0에서 7.0으로 변환하는 작업보다는 Visual C++ 7.0에서 8.0으로 변환하는 작업이 훨씬 더 수월합니다. Visual C++ 8.0에서 어려움을 겪으셨다면 이 글에 대한 포럼에 질문해주시기 바랍니다.

 

본 시리즈에 들어가며......

쉘 익스텐션(Shell Extension)은 몇 가지 기능들을 윈도우 쉘(Shell), Windows 탐색기에 추가하는 COM 객체입니다. 많은 종류의 쉘 익스텐션들이 존재하지만 이들에 대해 이해하기 쉽게 작성된 문서는 많지 않습니다. 물론 필자가 처음 글을 쓰기 시작한 이래 최근 6년 동안 상황이 나아졌다고 자부할 수 있습니다. 쉘에 대해 더 깊이 있게 알고자 하는 분들이 있다면, 필자는 Dino Esposito님이 작성하신 훌륭한 서적인 “Visual C++ Windows Shell Programming(ISBN 1861001843)”을 읽어볼 것을 강력히 권해 드립니다. 그러나 이 책을 갖고 있지 않은 분들 또는 쉘 익스텐션에 대해서만 알고 싶으신 분들을 위해 필자는 이 글을 작성합니다.

이 글을 통해 여러분은 경악할 수도 있고, 또는 쉘 익스텐션을 어떻게 작성하는지에 대해 이해할 수도 있습니다. 본 가이드는 여러분이 COM과 ATL의 기본을 익혔다고 가정할 것입니다. 독자 여러분이 COM 기본지식을 모르고 있다면, 필자의 COM 소개 글을 먼저 읽어보시길 바랍니다.

1 단계는 쉘 익스텐션의 일반적인 소개를 포함하고 있습니다. 그리고 이후 등장하는 단계들에 대한 맛보기로서 간단한 컨텍스트 메뉴 확장을 포함하고 있습니다.

“쉘 익스텐션(Shell Extension)”이라는 용어는 두 단어로 구성되어 있습니다.

쉘(shell)은 Windows 탐색기를 의미하고 익스텐션(extension)은 여러분이 작성하고, 미리 약속된 이벤트(예: .doc 파일을 선택하고 마우스 오른쪽 버튼을 클릭하는 경우)가 발생할 때 Windows 탐색기가 실행하는 프로그램 코드를 의미합니다. 그러므로 쉘 익스텐션은 Windows 탐색기에 기능을 추가하는 COM 객체의 일종입니다.

쉘 익스텐션은 ‘인 프로세스(in-process) COM 서버’로서 Windows 탐색기와의 통신을 다루는 몇 가지 인터페이스를 구현하고 있습니다. ATL은 쉘 익스텐션을 빠르게 불러와서 실행하도록 만드는 가장 쉬운 방법입니다, 왜냐하면 여러분이 QueryInterfaceAddRef를 거듭하여 작성하지 않아도 되기 때문입니다. 또한 ATL로 쉘 익스텐션을 작성하는 것은 Windows NT 기반의 운영체제에서 디버그를 할 때 좀 더 편하게 만들어 줍니다. 이에 대해서는 나중에 설명하도록 하겠습니다.

쉘 익스텐션에는 여러 종류가 있습니다. 그리고 각 유형들은 각기 다른 이벤트가 발생할 때 실행됩니다. 여기 주로 쓰이는 몇 가지 유형과 그들이 실행되는 상황에 대해 정리되어 있습니다.

 

유형 언제 실행되는가? 무엇을 하는가?
컨텍스트 메뉴 핸들러
(Context Menu Handler)
파일 또는 폴더에 대해 사용자가 마우스 오른쪽 클릭을 했을 경우에 실행됩니다. 특히 4.71 이상 버전의 쉘에서는 디렉토리 창의 여백을 마우스 오른쪽 클릭했을 때도 실행됩니다. 컨텍스트 메뉴에 항목을 추가합니다.
프로퍼티 시트 핸들러
(Property Sheet Handler)
파일에 대한 등록 정보(혹은 속성) 대화상자를 열 때 실행됩니다. 프로퍼티 시트에 페이지를 추가합니다.
드래그 앤 드롭 핸들러
(Drag and Drop Handler)
디렉토리 창 또는 바탕 화면에서 사용자가 항목들을 마우스 오른쪽 버튼을 눌러 ‘드래그 앤 드롭’ 할 때 실행됩니다. 컨텍스트 메뉴에 항목을 추가합니다.
드롭 핸들러
(Drop Handler)
사용자가 파일 위에다 항목들을 ‘드롭’ 할 때 실행됩니다. 미리 설계된 작업들을 수행합니다.
QueryInfo 핸들러
(QueryInfo Handler)
파일 또는 ‘내 컴퓨터(내 PC)’와 같은 쉘 객체에 마우스 포인터를 올려놓고 있을 때 실행됩니다. Windows 탐색기가 툴팁(Tooltip)을 통해 출력할 문자열을 반환합니다.

 

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

지금쯤 여러분은 Windows 탐색기에서 쉘 익스텐션이 어떻게 생긴 것인지 궁금해 하실 것입니다. 예를 들어 WinZip은 여러 가지 유형의 쉘 익스텐션을 포함하고 있고, 그 중 하나가 컨텍스트 메뉴 핸들러입니다. 다음은 압축 파일을 선택하고 마우스 오른쪽 클릭을 하였을 때 WinZip이 어떻게 쉘(shell)을 확장하는지를 보여주고 있습니다.

 

WinZip에 의해 확장된 컨텍스트 메뉴.

 

WinZip은 메뉴 항목들을 추가하는 코드와 ‘플라이-바이’ 도움말(fly-by help: Windows 탐색기의 상태 표시줄에 나타나는 텍스트)를 포함하고 있고 사용자가 이 명령 중 하나를 선택하였을 때 적절한 작업을 수행합니다.

 

WinZip에 의해 확장된 컨텍스트 메뉴 항목을 선택하면 나타나는 플라이-바이 도움말.

 

WinZip은 또한 드래그 앤 드롭 핸들러도 포함하고 있습니다. 이 유형은 컨텍스트 메뉴 핸들러와 매우 유사하지만 사용자가 마우스 오른쪽 버튼을 누른 상태로 파일을 드래그했을 때 실행됩니다. 다음은 WinZip의 드래그 앤 드롭 핸들러가 컨텍스트 메뉴에 무엇을 추가하고 있는지를 보여주고 있습니다.

 

zip 파일을 하나 선택하고 마우스 오른쪽 버튼을 눌러 드래그하면 나타나는 메뉴.

 

이외에 더 많은 유형들이 존재하고 마이크로소프트는 각 Windows 버전마다 더 많은 것들을 추가하고 있습니다. 지금부터 우리는 작성하기에 가장 간단하고, 실행 결과를 확인하기도 쉬운 컨텍스트 메뉴 확장에 대해 살펴보겠습니다.

코딩하기에 앞서, 작업을 더 쉽게 만들어 줄 수 있는 몇 가지 팁을 알려드리겠습니다. 여러분이 쉘 익스텐션을 호출하여(번역자 주: 마우스 오른쪽 클릭을 하여) Windows 탐색기가 이를 로드(load)할 때, 쉘 익스텐션은 메모리에서 잠시 머물러 있게 됩니다. 그리고 이것은 DLL 파일을 다시 빌드(build)하는 것을 불가능하게 만듭니다. Windows 탐색기가 쉘 익스텐션을 자주 언로드(unload)하도록 하기 위하여 다음과 같은 경로의 레지스트리 키를 생성합니다.

 

HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Explorer\AlwaysUnloadDLL

 

그리고 이 키의 기본값을 1로 설정합니다. Windows 9x의 경우 이것이 여러분이 할 수 있는 최선입니다.

HKLM\Software\Microsoft\Windows\CurrentVersion\Explorer\AlwaysUnloadDLL

 

Windows NT의 경우 다음 경로의 레지스트리 키로 이동합니다.

 

HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer

그리고 이 키에 이름이 DesktopProcessDWORD를 생성하고 그 값을 1로 설정합니다. 이것은 바탕화면과 작업표시줄이 하나의 프로세스에서 작동되도록 만들고, Windows 탐색기 창이 각각 자신의 프로세스에서 실행되도록 만듭니다.

또한 이것은 여러분이 하나의 Windows 탐색기 창을 가지고 디버그하고 그 창을 닫을 때 여러분이 만들고 있는 DLL 파일도 자동으로 언로드(unload)하도록 만들어 줌으로써 “파일이 사용 중입니다.”와 같은 문제를 방지해 줍니다. 레지스트리의 변경 사항은 여러분이 로그오프(logoff)하고 다시 로그온(logon)할 때 적용됩니다.

 

HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer에 추가된 DWORD 값.

 

Windows 9x에서 쉘 익스텐션을 디버그(debug)하는 것에 대하여는 나중에 설명하겠습니다.

 

AppWizard를 사용하여 시작하기

먼저 우리는 메시지 상자를 띄움으로써 이것이 작동 중임을 확인할 수 있는 쉘 익스텐션을 만들어 보겠습니다. 우리는 이 쉘 익스텐션을 .txt 파일에 걸어 놓아서 사용자가 텍스트 파일에 대해 마우스 오른쪽 클릭을 하였을 때 실행되게 만들 것입니다.

이제 시작해 볼 시간입니다. 필자는 아직 이 쉘 익스텐션을 어떻게 사용하는지에 대해 설명도 하지 않았습니다. 하지만 걱정하지 않아도 됩니다. 필자는 과정을 따라 설명하게 될 것입니다. 필자는 개념들이 설명되었다면, 예제 프로그램이 작성되는 과정을 따라 설명하는 것이 더 낫다고 생각하였습니다. 물론 필자는 모든 것을 먼저 설명한 후, 나중에 코드를 작성하게 해 드릴 수도 있습니다. 그러나 이것은 여러분이 흡수하기 어렵습니다. 어쨌든 Visual C++을 실행하고 바로 시작해 보겠습니다.

AppWizard를 실행하고 새 ATL COM 프로그램을 생성합니다. 우리는 이것을 SimpleExt라고 부르겠습니다. AppWizard의 기본 설정을 그대로 두고 [Finish]를 클릭합니다. 이제 우리는 당장이라도 DLL로 빌드될 수 있는 빈 ATL 프로젝트가 생겼지만, 쉘 익스텐션을 할 수 있는 COM 객체를 만들어야 합니다.

ATL COM AppWizard를 선택하여 새 프로젝트를 생성한다.
모든 설정을 그대로 두고 [Finish]를 클릭한다.

DLL에 COM 개체를 추가하기 위해 “클래스 뷰(Class View)” 화면으로 이동하여 “HardLink classes” 트리 항목에 대해 마우스 오른쪽 버튼을 클릭하고 [New ATL Object] 항목을 클릭합니다. Visual C++ 7.0 이상의 버전에서는 해당 트리 항목에 대해 마우스 오른쪽 버튼을 클릭하고 “추가(D)”→”클래스 추가(C)...”를 클릭합니다. 영어 버전의 Visual Studio에서는 “Add”→”Add Class” 메뉴입니다.

 

클래스 뷰 화면에서 [New ATL Object…]를 선택한다.
Visual C++ 7.0 이상의 버전에서 COM 개체를 추가할 수 있는 메뉴.

ATL Object Wizard 화면에서 첫 번째 단계는 이미 “Simple Object”라는 것을 선택하고 있습니다. 그냥 [Next >] 버튼을 클릭합니다.

[Objects] 범주에서 [Simple Object]를 선택하고 [Next >]를 누른다.

두 번째 단계에서 “Short Name”이라 적힌 에디트 상자에 SimpleShlExt라고 적습니다. 그러면 나머지 에디트 상자들은 자동으로 내용이 채워질 것입니다.

기본적으로 이 마법사는 C에서도 사용 가능하고, OLE Automation을 통해 스크립트 기반 언어에서도 사용할 수 있는 COM 객체를 생성하게 됩니다. 우리의 쉘 익스텐션은 Windows 탐색기에서만 사용될 것이기 때문에 우리는 몇 가지 설정들에 대해 Automation 관련 기능을 제거하도록 수정할 수 있습니다. 우선 ‘Attribute’ 페이지로 이동하여 ‘Interface Type’을 ‘Custom’으로 변경 후 ‘Aggregation’에 대해 ‘No’를 선택합니다.

 

우선 [Names] 탭의 [Short Name] 항목에 “SimpleShlExt”라고 적는다.
그 다음 [Attributes] 탭에서 ‘Apartment’, ‘Custom’, ‘No’ 순으로 클릭한다.

[확인] 버튼을 누르면 마법사는 COM 객체를 구현하기 위한 기본 코드가 포함된 CSimpleShlExt 클래스를 생성하고 이를 현재의 프로젝트에 추가할 것입니다. 이 클래스에 우리는 추가적으로 코딩을 하게 될 것입니다.

 

초기화 인터페이스

우리가 만들고 있는 쉘 익스텐션이 로드(load)되면, Windows 탐색기는 QueryInterface 함수를 호출하여 IShellExtInit 인터페이스에 대한 포인터를 얻어갈 것입니다. 이 인터페이스에는 단 하나의 메소드만 들어있습니다. Initialize로서 그 원형은 다음과 같습니다.

 

HRESULT IShellExtInit::Initialize(
    LPCITEMIDLIST pidlFolder,
    LPDATAOBJECT pDataObj,
    HKEY hProgID);

 

Windows 탐색기는 위 메소드를 호출함으로써 우리에게 다양한 정보를 전달합니다.

pidlFolder는 현재 선택된 파일들이 포함된 폴더의 PIDL입니다. PIDL은 ‘pointer to an ID list’의 약어로서 쉘에 있는 어느 객체이든, 그것이 파일시스템 객체이든 아니든 관계없이 유일하게 식별하도록 해 주는 데이터 구조체입니다.

pDataObj는 현재 선택된 파일들의 이름을 얻을 수 있는 IDataObject 인터페이스형 포인터입니다.

hProgID는 우리의 등록 정보가 포함된 레지스트리 키에 접근할 수 있는 HKEY입니다.

이 예제는 간단하기 때문에 우리는 단지 pDataObj 매개 변수(parameter)만을 사용하게 될 것입니다.

우리가 만들고 있는 COM 객체에 IShellExtInit를 추가하기 위해서, SimpleShlExt.h 파일을 열고 다음과 같이 굵게 표시된 줄을 추가하시기 바랍니다. 우리는 프로젝트가 생성되는 과정에서 자동으로 생성된 인터페이스를 사용하지 않을 것이므로, 마법사가 생성한 일부 COM 관련 코드들은 필요하지 않을 것입니다. 그래서 필자는 삭제될 수 있는 코드를 취소선으로 표시하였습니다.

 

#include <shlobj.h> // 새로 추가
#include <comdef.h> // 새로 추가
 
class ATL_NO_VTABLE CSimpleShlExt :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CSimpleShlExt, &CLSID_SimpleShlExt>,
    public ISimpleShlExt, // 이 줄은 삭제해도 됨
    public IShellExtInit {
    
    BEGIN_COM_MAP(CSimpleShlExt)
        COM_INTERFACE_ENTRY(ISimpleShlExt)
        COM_INTERFACE_ENTRY(IShellExtInit)
    END_COM_MAP()

 

COM_MAP은 ATL이 QueryInterface를 어떻게 구현할 것인지에 대한 것입니다. 이것은 우리가 만든 COM 객체를 다른 프로그램이 어떤 형식의 인터페이스로서 캐스팅하여 가져갈 수 있는지를 ATL에게 알려줍니다.

클래스 선언 내부에 Initialize에 대한 원형을 추가합니다. 우리는 또한 파일 이름을 보관할 버퍼가 필요합니다. 이를 선언합니다.

 

    // ...
    
    protected:
    TCHAR m_szFile[MAX_PATH];
    
    public:
    // IShellExtInit
    STDMETHODIMP Initialize(LPCITEMIDLIST, LPDATAOBJECT, HKEY);
    
    // ...

 

그 다음 SimpleShlExt.cpp 파일에서 이 함수의 정의를 추가합니다.

 

STDMETHODIMP CSimpleShlExt::Initialize(
    LPCITEMIDLIST pidlFolder,
    LPDATAOBJECT pDataObj,
    HKEY hProgID) {
    
    // ...
    
    }

 

우리가 해야 할 것은 마우스 오른쪽 클릭된 파일의 이름을 가져오고 그 이름을 메시지 상자로 보여주는 것입니다. 선택된 파일이 하나 이상이면, 여러분은 선택된 파일 모두를 pDataObj 인터페이스 포인터를 통해 접근할 수 있습니다, 그러나 우리는 이 단계에서 예제 프로그램을 단순하게 유지하기 위하여, 첫 번째 파일 이름만을 보도록 하겠습니다.

파일 이름들은 WS_EX_ACCEPTFILES 스타일이 적용된 윈도우에 파일을 드래그 앤 드롭 할 때와 동일한 형식으로 보관됩니다. 이것은 우리가 파일 이름을 얻고자 할 때 DragQueryFile과 똑같은 API를 사용한다는 뜻입니다. 우리는 IDataObject에 포함된 데이터에 대한 핸들을 얻는 것으로 시작하겠습니다.

HRESULT CSimpleShlExt::Initialize(...) {
    FORMATETC fmt = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
    STGMEDIUM stg = { TYMED_HGLOBAL };
    HDROP     hDrop;
    
    // 데이터 객체에서 CF_HDROP 데이터를 찾습니다. 
    // 그러한 데이터가 없다면 Windows 탐색기로 오류 코드를 반환합니다.
    if (FAILED(pDataObj->GetData(&fmt, &stg)))
        return E_INVALIDARG;
    
    // 실제 데이터에 대한 핸들을 얻습니다.
    hDrop = (HDROP)GlobalLock(stg.hGlobal);
    
    // 이것이 정상 작동하는지 확인합니다.
    if (hDrop == NULL)
        return E_INVALIDARG;
    
    // ...
    
}

 

모든 것을 오류 체크하고, 특히 포인터를 체크하는 것은 필수적으로 중요합니다. 우리의 쉘 익스텐션은 Windows 탐색기의 처리 공간에서 작동되기 때문에, 우리가 만들고 있는 앱이 충돌하면 Windows 탐색기도 함께 강제 종료하게 됩니다. Windows 9x에서는 그러한 충돌 때문에 컴퓨터를 재부팅해야 할 수도 있습니다.

HDROP 핸들을 만들었기 때문에 지금부터 우리는 우리가 원하는 파일의 이름을 가져올 수 있습니다.

 

HRESULT CSimpleShlExt::Initialize(...) {
    FORMATETC fmt = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
    STGMEDIUM stg = { TYMED_HGLOBAL };
    HDROP     hDrop;
    
    // ...
    
    // 상태 확인 - 적어도 하나 이상의 파일 이름을 가지고 있는지 확인한다.
    UINT uNumFiles = DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0);
    HRESULT hr = S_OK;
    
    if (uNumFiles == 0) {
        GlobalUnlock(stg.hGlobal);
        ReleaseStgMedium(&stg);
        return E_INVALIDARG;
    }
    
    // 첫 번째 파일의 이름을 얻어와서 멤버변수 m_szFile에 보관합니다.
    if (DragQueryFile(hDrop, 0, m_szFile, MAX_PATH) == 0)
        hr = E_INVALIDARG;
    
    GlobalUnlock(stg.hGlobal);
    ReleaseStgMedium(&stg);
    
    return hr;
}

 

이 메소드가 E_INVALIDARG를 반환한다면 Windows 탐색기는 마우스 오른쪽 버튼 클릭을 재차 하더라도 우리가 만드는 쉘 익스텐션을 실행하지 않을 것입니다.

S_OK를 반환할 때 Windows 탐색기는 QueryInterface 메소드를 다시 호출하여, IContextMenu라는 또 다른 인터페이스 포인터를 받아갈 것입니다.

 

컨텍스트 메뉴와 상호작용하기 위한 인터페이스

Windows 탐색기는 우리가 만든 쉘 익스텐션이 일단 초기화 되었다면, 우리가 컨텍스트 메뉴에 항목을 추가하고 플라이-바이 도움말을 제공하고, 사용자의 선택에 따른 작업을 수행할 수 있도록 IContextMenu 인터페이스의 메소드들을 호출할 것입니다.

우리가 만들고 있는 쉘 익스텐션에 IContextMenu 인터페이스를 추가하는 것은 IShellExtInit를 추가했던 것과 비슷합니다. SimpleShlExt.h를 열고 아래와 같이 줄을 추가합니다. 그리고 IContextMenu 메소드의 원형들을 추가합니다.

 

class ATL_NO_VTABLE CSimpleShlExt : 
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CSimpleShlExt, &CLSID_SimpleShlExt>,
    public IShellExtInit,
    public IContextMenu { // 새로 추가
    
    BEGIN_COM_MAP(CSimpleShlExt)
        COM_INTERFACE_ENTRY(IShellExtInit)
        COM_INTERFACE_ENTRY(IContextMenu)
    END_COM_MAP()
    
    // ...
    
    // IContextMenu에 있는 메소드 원형 추가
    public:
    STDMETHODIMP GetCommandString(UINT, UINT, UINT*, LPSTR, UINT);
    STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO);
    STDMETHODIMP QueryContextMenu(HMENU, UINT, UINT, UINT, UINT);
}

 

컨텍스트 메뉴 수정하기

IContextMenu는 세 가지 메소드를 가지고 있습니다.

그 중 첫 번째 메소드인 QueryContextMenu는 우리에게 컨텍스트 메뉴를 수정할 수 있게 해 줍니다. QueryContextMenu의 원형은 다음과 같습니다.

 

HRESULT IContextMenu::QueryContextMenu(
    HMENU hmenu,
    UINT uMenuIndex,
    UINT uidFirstCmd,
    UINT uidLastCmd,
    UINT uFlags);

 

hmenu는 컨텍스트 메뉴에 대한 핸들입니다.

uMenuIndex는 우리가 메뉴 항목을 추가하게 되는 컨텍스트 메뉴 위치(순서)입니다.

uidFirstCmduidLastCmd는 우리가 컨텍스트 메뉴 각 항목에 부여할 수 있는 Command ID의 범위입니다.

uFlags는 Windows 탐색기가 왜 QueryContextMenu를 호출하였는지 이유를 나타내고 있습니다. 이것에 대해서는 나중에 설명하겠습니다.

반환 값은 여러분이 누구에게 물어보는지에 따라 다르게 문서화되어 있습니다. Dino Esposito의 책에 따르면 이 반환 값은 QueryContextMenu에 따라 추가된 메뉴 항목의 개수입니다. Visual C++ 6.0 및 그 이후 버전에 부속되는 MSDN에 따르면, 우리가 가장 마지막으로 추가한 메뉴 항목의 Command ID입니다.

온라인 버전의 MSDN에서는 이렇게 설명하고 있습니다:

성공하면 심각도는 SEVERITY_SUCCESS로 설정되고 코드 값은 (여러분이) 사용한 Command ID 값 중 (uidFirstCmd로부터) 가장 큰 값에 대한 오프셋(offset)으로 설정된 HRESULT입니다.

예를 들어 idCmdFirst5이고, 여러분이 컨텍스트 메뉴에 5, 7, 8의 Command ID를 갖는 항목을 추가했다면, 이 반환 값은 MAKE_HRESULT(SEVERITY_SUCCESS, 0, 8 - 5 + 1)이어야 합니다. 그 외의 경우 OLE 오류 값을 반환합니다.

필자는 지금까지 Dino의 설명을 따라 소스 코드를 작성해왔고, 문제 없이 작동되었습니다. 사실 이 저자가 반환 값을 구성하는 방법은 uidFirstCmd부터 1씩 증가해가며 Command ID를 사용하는 한 온라인 MSDN과 동일합니다.

우리가 만들고 있는 예제는 단 하나의 항목만을 컨텍스트 메뉴에 추가할 것이므로, QueryContextMenu 함수도 다음과 같이 간단하게 작성될 수 있습니다.

 

HRESULT CSimpleShlExt::QueryContextMenu(
    HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd, UINT uidLastCmd, UINT uFlags) {
    // uFlags가 CMF_DEFAULTONLY를 포함하고 있다면 우리는 특별히 할 것이 없습니다.
    if (uFlags & CMF_DEFAULTONLY)
        return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0);

    InsertMenu(hmenu, uMenuIndex, MF_BYPOSITION, 
    uidFirstCmd, _T("SimpleShlExt Test Item"));

    return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 1);
}

 

먼저 우리가 확인해야 할 것은 uFlags입니다. 여러분은 MSDN을 통해 이 값이 될 수 있는 전체 목록을 확인할 수 있습니다만, 컨텍스트 메뉴 확장에 한한다면 CMF_DEFAULTONLY 하나만이 중요합니다. 이 설정은 네임스페이스 확장(namespace extension)에게 기본 메뉴 항목만을 추가할 것을 알려줍니다. 이 설정이 되어 있다면 쉘 익스텐션은 컨텍스트 메뉴에 어떤 항목도 새로 추가해서는 안 됩니다. 이것이 uFlagsCMF_DEFAULTONLY를 포함하고 있을 때 즉시 0을 반환하는 이유입니다.

이 설정이 없을 경우 우리는 hmenu로 전달된 컨텍스트 메뉴에 항목을 추가하고, 우리가 1개의 항목을 새로 추가하였음을 쉘(shell)에게 알리기 위해 1을 반환합니다.

 

계속 읽기

다음 게시글: Windows 쉘 익스텐션 개발 가이드 - (1) 튜토리얼 (2/2)

 

카테고리 “API/COM”
more...
썸네일 이미지
Windows 쉘 익스텐션 개발 가이드 - (0) 목차
입문자를 위한 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에서 자동으로 생성해주는 코드의 형..
API/COM
2020. 12. 25. 09:00

Windows 쉘 익스텐션 개발 가이드 - (0) 목차

API/COM
2020. 12. 25. 09:00

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

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

 

Part 0. 목차(Index)

입문자 가이드 시리즈의 분량이 다소 많아진 관계로, 각 글에 대한 목차와 빠른 참조를 제공할 수 있는 인덱스 글을 별도로 게시하게 되었습니다. 헌신적인 분들이 필자의 글을 다음과 같이 번역 및 게재하여 주셨습니다.

 

파트 1. 쉘 익스텐션(Shell Extension)을 작성하기 위한 단계별 튜토리얼

[그림 1] - [파트 1]에서 다룰 쉘 익스텐션의 실행 결과.

파트 1은 쉘 익스텐션의 개요를 보고 이를 어떻게 디버그할 것인지에 대해 설명하고 있습니다. 예제 프로그램에는 컨텍스트 메뉴에 텍스트 파일에 대한 확장을 추가해보는 기능이 포함되어 있습니다.

파트 1로 바로가기

 

파트 2. 여러 파일 선택 시 작동되는 쉘 익스텐션(Shell Extension)

[그림 2] - [파트 2]에서 다룰 쉘 익스텐션의 실행 결과.

파트 2는 한 번에 여러 개의 파일을 선택하고 컨텍스트 메뉴를 열었을 때 작동되는 쉘 익스텐션을 작성하는 방법에 대해 설명하고 있습니다. 예제 프로그램에는 DLL 파일에 대해 레지스트리에 ‘등록(register)’ 및 ‘등록 해제(unregister)’하는 명령을 컨텍스트 메뉴에 추가하는 기능이 포함되어 있습니다.

파트 2로 바로가기

 

파트 3. 파일에 대한 ‘팝업’ 정보를 보여주는 쉘 익스텐션(Shell Extension)

[그림 3] - [파트 3]에서 다룰 쉘 익스텐션의 실행 결과.

파트 3은 텍스트 파일에 대한 ‘인포팁(infotip)’을 사용자화 하는 QueryInfo 확장에 대해 설명하고 있습니다. 또한 쉘 익스텐션에서 MFC를 사용하는 방법에 대해 설명하고 있습니다.

파트 3으로 바로가기

 

파트 4. 사용자 정의 드래그/드롭을 제공하는 쉘 익스텐션(Shell Extension)

[그림 4] - [파트 4]에서 다룰 쉘 익스텐션의 실행 결과.

파트 4는 Windows 탐색기에서 마우스 오른쪽 버튼을 누른 상태로 사용자가 드래그 앤 드롭을 하였을 때 컨텍스트 메뉴에 나타나는 항목을 추가하는 방법에 대해 설명하고 있습니다. 예제 프로그램은 해당 파일에 대한 하드 링크를 만드는 유틸리티입니다. (참고: 이 확장은 Windows 2000 이상에서만 작동됩니다. 하지만 컴파일과 실행하는 것까지는 이전 버전의 Windows에서도 가능합니다. 그 방법은 본문에 포함되어 있습니다.)

파트 4로 바로가기

 

파트 5. 파일에 대한 등록 정보(속성) 대화상자에 페이지를 추가하는 쉘 익스텐션(Shell Extension)

[그림 5] - [파트 5]에서 다룰 쉘 익스텐션의 실행 결과.

파트 5는 Windows 탐색기의 등록 정보(또는 속성) 대화상자에 새로운 페이지를 추가하는 방법에 대해 설명하고 있습니다. 예제 프로그램에는 독자 여러분이 직접 파일의 생성된 날짜, 수정된 날짜 및 마지막으로 액세스된 날짜를 수정할 수 있는 페이지를 추가시키는 기능이 포함되어 있습니다.

파트 5로 바로가기

 

파트 6. ‘보내기’ 메뉴에서 사용될 수 있는 쉘 익스텐션(Shell Extension)

[그림 6] - [파트 6]에서 다룰 쉘 익스텐션의 실행 결과.

파트 6은 ‘보내기’ 메뉴에 추가될 수 있는 드롭 핸들러 확장에 대해 설명하고 있습니다. 예제 프로그램은 “Send To Any Folder” 도구의 클론입니다.

파트 6으로 바로가기

 

파트 7. 컨텍스트 메뉴에 그림 출력 및 디렉토리 여백에 마우스 우클릭 시 메뉴 확장

[그림 7] - [파트 7]에서 다룰 쉘 익스텐션의 실행 결과(1).
[그림 8] - [파트 7]에서 다룰 쉘 익스텐션의 실행 결과(2).

파트 7은 두 가지 주제에 대해 다루어 볼 것입니다. 하나는 컨텍스트 메뉴에 그림을 출력하는 것이고, 다른 하나는 Windows 탐색기에서 파일을 선택하지 않고 그 여백에 대해 마우스 오른쪽을 클릭하였을 때 나타나는 컨텍스트 메뉴를 확장하는 방법입니다. 예제 프로그램도 두 가지 기능을 포함하고 있습니다. 하나는 .bmp 파일을 선택 후 마우스 오른쪽 클릭을 하였을 때 컨텍스트 메뉴에 이미지 썸네일(thumbnail)을 출력하는 비트맵 뷰어이고, 다른 하나는 파일 또는 폴더를 선택하지 않고 탐색기의 여백을 마우스 오른쪽으로 클릭했을 때 나타나는 컨텍스트 메뉴에 간단한 항목을 하나 추가하는 쉘 익스텐션입니다.

파트 7로 바로가기

 

파트 8. 컨텍스트 메뉴에 그림 출력 및 디렉토리 여백에 마우스 우클릭 시 메뉴 확장

[그림 9] - [파트 8]에서 다룰 쉘 익스텐션의 실행 결과.

파트 8은 Windows 2000 이상의 운영체제에서 Windows 탐색기의 ‘자세히’ 보기 모드일 때 나타나는 열을 추가하는 방법에 대해 설명하고 있습니다. 예제 프로그램은 .mp3 파일에 대해 ID3v1 태그 데이터를 보여주는 열을 추가하는 기능을 포함하고 있습니다. (이 쉘 익스텐션은 Windows 2000 이상에서 작동합니다.)

파트 8로 바로가기

 

파트 9. 특정 파일에 대해 아이콘 표시를 사용자화 하기

[그림 10] - [파트 9]에서 다룰 쉘 익스텐션의 실행 결과.

파트 9는 파일에 따라 아이콘 표시를 사용자화하는 방법에 대해 설명하고 있습니다. 예제 프로그램은 텍스트 파일에 대해 그 용량에 따라 4가지 다른 아이콘으로 보여주는 기능을 포함하고 있습니다.

파트 9로 바로가기

 

카테고리 “API/COM”
more...
썸네일 이미지
COM의 소개(파트 2) - COM 서버의 이면 (9) [完]
COM의 소개(파트 2) – COM 서버의 이면 본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM Part II - Behind the Scenes of a COM Server’을 번역한 것입니다. 원 게시물은 https://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다. 또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전..
API/COM
2020. 10. 8. 20:36

COM의 소개(파트 2) - COM 서버의 이면 (9) [完]

API/COM
2020. 10. 8. 20:36

COM의 소개(파트 2) – COM 서버의 이면

본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM Part II - Behind the Scenes of a COM Server’을 번역한 것입니다.

원 게시물은 https://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다.

또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.

  1. COM의 소개(파트 2) – COM 서버의 이면 (1)
  2. COM의 소개(파트 2) – COM 서버의 이면 (2)
  3. COM의 소개(파트 2) – COM 서버의 이면 (3)
  4. COM의 소개(파트 2) – COM 서버의 이면 (4)
  5. COM의 소개(파트 2) – COM 서버의 이면 (5)
  6. COM의 소개(파트 2) – COM 서버의 이면 (6)
  7. COM의 소개(파트 2) – COM 서버의 이면 (7)
  8. COM의 소개(파트 2) – COM 서버의 이면 (8)
  9. COM의 소개(파트 2) – COM 서버의 이면 (9) [完]

 

우리의 COM 서버를 사용하는 COM 클라이언트

이제 우리는 COM 서버를 모두 마쳤습니다. 이것을 어떻게 사용할까요? 우리의 인터페이스는 사용자 인터페이스로서, C 및 C++ 클라이언트에서만 사용할 수 있습니다. 물론 우리가 만든 coclass는 IDispatch를 구현하여 Visual Basic, Windows Scripting hsost, 웹 페이지, Perl 등 다른 어느 언어에서도 사실상 사용이 가능합니다. 그러나 이와 관련해서는 다른 글에서 설명하는 것으로 미뤄두겠습니다. 필자는 ISimpleMsgBox를 사용하는 간단한 어플리케이션을 제공하였습니다.

.....

[Test MsgBox COM Server] 메뉴는 CSimpleMsgBoxImpl 객체를 생성하고 DoSimpleMsgBox를 호출합니다. 이것은 간단한 메소드이기 때문에, 소스 코드는 그다지 길지 않습니다. 우리는 먼저 CoCreateInstance를 사용하여 COM 객체를 생성합니다.

void DoMsgBoxTest(HWND hMainWnd) {
ISimpleMsgBox* pIMsgBox;
HRESULT hr;

hr = CoCreateInstance(__uuidof(CSimpleMsgBoxImpl), // coclass의 CLSID
                        NULL, // 객체 결합은 하지 않음
                        CLSCTX_INPROC_SERVER, // 인 프로세스 서버만 사용
                        __uuidof(ISimpleMsgBox), // 우리가 원하는 형태의 인터페이스
                        (void **)&pIMsgBox); // 인터페이스 포인터를 보관할 버퍼

    if (FAILED(hr)) return;

 

그 다음 우리는 DoSimpleMsgBox를 호출하고 인터페이스 포인터를 참조 해제합니다.

    pIMsgBox->DoSimpleMsgBox(hMainWnd, _bstr_t(TEXT("Hello COM!")));
    pIMsgBox->Release();
}

 

이것이 전부입니다. 소스 코드를 보면 많은 TRACE 구문이 있어서 디버거를 통해 어플리케이션을 실행할 때 서버의 각 메소드 중 어느 것이 실행되고 있는지를 확인할 수 있습니다.

[File] 메뉴의 또 다른 항목에서는 CoFreeUnusedLibraries API를 호출하고 있어서 여러분이 서버의 DllCanUnloadNow가 호출되는 것을 직접 보실 수 있습니다.

 

기타 사항

 

COM 매크로

COM 소스 코드에서는 자세한 구현을 숨기고 있는 몇 가지 매크로들이 존재하고 있고 이는 C와 C++ 클라이언트들이 동일한 선언을 할 수 있도록 해 줍니다. 본 글에서 필자는 그러한 매크로들을 사용한 적은 없습니다, 그러나 예제 소스 코드에서는 이를 사용하고 있습니다. 그래서 여러분은 이것들이 무슨 의미인지를 알아둘 필요가 있습니다. 여기 ISimpleMsgBox의 구현이 있습니다.

struct ISimpleMsgBox : public IUnknown {
    // IUnknown 메소드
    STDMETHOD_(ULONG, AddRef)() PURE;
    STDMETHOD_(ULONG, Release)() PURE;
    STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE;

    // ISimpleMsgBox 메소드
    STDMETHOD(DoSimpleMsgBox)(HWND hwndParent, BSTR bsMessageText) PURE;
};

 

STDMETHOD 매크로는 virtual 키워드를 포함하고 있고, 반환 값이 HRESULT이며 __stdcall 호출 규약임을 자동으로 작성해줍니다. STDMETHOD_도 같습니다 그러나 여러분이 반환형을 직접 지정해야 합니다. PURE는 C++에서 완전 추상 함수를 의미하는 = 0 구문을 대신합니다.

STDMETHODSTDMETHOD_ 매크로로 선언된 함수는 각각 STDMETHODIMPSTDMETHODIMP_ 매크로를 써서 구현합니다. 예를 들어, STDMETHOD 매크로로 선언한 DoSimpleMsgBox은 다음과 같이 구현합니다.

STDMETHODIMP CSimpleMsgBoxImpl::DoSimpleMsgBox(HWND hwndParent, BSTR bsMessageText) {
    // ...
}

 

마지막으로 표준 내보내기 함수(standard exported function)는 다음과 같이 STDAPI 매크로로 선언합니다.

STDAPI DllRegisterServer()

 

STDAPI는 반환형과 호출 규약을 포함하고 있습니다.

STDAPI를 사용하면서 한 가지 번거롭게 된 것은 STDAPI의 치환에 의해 코드가 확장되는 구조상 여러분이 이 함수에 __declspec(dllexport)를 넣을 수 없다는 것입니다. 여러분은 내보내기 할 함수들에 대해 .def 파일을 사용해야 합니다.

 

COM 서버 등록과 등록 해제

필자가 앞서 언급하였듯이 COM 서버는 DllRegisterServerDllUnregisterServer 함수를 구현합니다. 그들의 역할은 우리가 만든 서버를 COM에게 알려주는 레지스트리 키들을 생성하거나 삭제하는 것입니다. 레지스트리에 대해 설명하기에는 지루하므로, 필자는 이를 반복하지는 않을 것입니다. 그러나 DllRegisterServer가 생성한 레지스트리 키에 대한 목록은 다음과 같습니다.

 

키 이름 키의 값
HKEY_CLASSES_ROOT  
    CLSID  
        {7D51904E-1645-4a8c-BDE0-0F4A44FC38C4} (기본값)="SimpleMsgBox class"
            InProcServer32 (기본값)="DLL 경로";ThreadingModel="Apartment"

 

예제 코드에 대한 참고 사항

동봉된 예제 코드는 COM 서버와 이를 시험하는 클라이언트 어플리케이션으로 구성되어 있습니다. SimpleComSvr.dsw는 워크스페이스 파일로서, COM 서버와 COM 클라이언트를 동시에 열어볼 수 있습니다. 워크스페이스와 같은 경로에 있는 두 개의 헤더 파일은 두 프로젝트에서 공동으로 사용되는 파일입니다. 두 프로젝트는 각각의 하위 디렉토리에 있습니다.

공동으로 쓰이는 헤더 파일은 다음과 같습니다.

1. ISimpleMsgBox.h: ISimpleMsgBox 인터페이스를 선언합니다.

2. SimpleMsgBoxComDef.h: __declspec(uuid()) 선언을 포함하고 있습니다. 별도의 파일에 보관한 이유는, COM 클라이언트는 CSimpleMsgBoxImplGUID만을 필요로 하고 그 클래스의 선언은 필요하지 않기 때문입니다. GUID 선언을 별도의 파일에 놓는 것은 CSimpleMsgBoxImpl의 내부 구조에 의존하지 않고 COM 클라이언트가 GUID에만 접근하도록 하기 위합니다. COM 클라이언트에게 중요한 것은 인터페이스인 ISimpleMsgBox입니다. (역자: 그것을 구체적으로 구현한 CSimpleMsgBoxImpl에 접근할 필요가 없습니다. 클라이언트 입장에서는 그저 CSimpleMsgBoxImpl에 대한 GUID만 있으면 됩니다.)

앞서 언급했듯이 여러분은 서버에서 함수를 내보내기 위하여 .def 파일이 필요합니다. 예제 소스 코드에 첨부된 .def 파일은 다음과 같은 형태로 되어 있습니다.

EXPORTS
    DllRegisterServer   PRIVATE
    DllUnregisterServer PRIVATE
    DllGetClassObject   PRIVATE
    DllCanUnloadNow     PRIVATE

 

각 줄은 함수의 이름과 PRIVATE 키워드를 갖습니다. 이 키워드의 의미는, 함수는 내보내기되지만 ‘import lib’에는 포함되지 않는다는 의미입니다. 다시 설명하면, COM 클라이언트는 ‘import lib’를 링크시킨다고 하더라도 코드를 작성할 때 이러한 함수를 직접 호출할 수 없음을 의미합니다. 이는 필수적인 사항이고, PRIVATE 키워드를 뺀다면 링커는 오류를 발생시킬 수 있습니다.

 

서버에 중단점 설정하기

여러분이 COM 서버 코드에 중단점을 넣고자 하는 경우, 여러분은 두 가지 방법을 사용할 수 있습니다. 첫 번째 방법은 COM 서버의 프로젝트를 활성 프로젝트로 설정하고 디버깅을 시작합니다. Microsoft Visual C++은 디버깅 세션을 시작하기 위한 실행 파일을 요청할 것입니다. COM 클라이언트를 필히 ‘빌드(build)’한 후에 만들어진 실행 파일의 전체 경로를 입력하면 됩니다.

다른 방법으로는 ‘TestClient(COM 클라이언트 프로젝트)’를 활성 프로젝트로 설정하고 COM 서버 프로젝트가 COM 클라이언트에 의존성을 갖도록 프로젝트 의존성을 설정합니다. 즉, 여러분이 COM 서버 측의 코드를 수정한다면, 여러분이 클라이언트 프로젝트를 빌드할 때 이 서버 프로젝트도 자동으로 빌드됩니다. 또한 여러분이 COM 클라이언트를 디버그할 때 Microsoft Visual C++이 COM 서버의 심볼을 로드하게 됩니다.

프로젝트 의존성 다이얼로그는 다음과 같이 생겼습니다.

[Project] 메뉴의 [Dependencies...]를 클릭한다.

 

프로젝트 의존성(Project Dependencies) 다이얼로그.

 

COM 서버 심볼을 로드하기 위하여, ‘TestClient’ 프로젝트 설정을 열고 ‘Debug’ 탭을 연 다음, ‘Category’ 콤보 상자에서 ‘Additional DLLs’를 선택합니다. 새로운 진입점을 추가하기 위하여 리스트 상자의 항목을 클릭하고 COM 서버의 전체 경로를 입력합니다. 다음의 예시를 참조하시기 바랍니다.

.

[Project] 메뉴의 [Settings...]를 클릭한다.

..

DLL 의존성을 설정하기 위해 DLL의 경로를 입력한다(1).
DLL 의존성을 설정하기 위해 DLL의 경로를 입력한다(2).

DLL의 경로는 여러분이 소스 코드를 어디에 압축 해제하는가에 의존하여 달라질 것입니다.

 

마무리

이전 게시글: COM의 소개(파트 2) – COM 서버의 이면 (8)

이것으로 Component Object Model(COM) 기반 클래스를 정의하고 사용하기 위한 기본적인 사항에 대해 살펴보았습니다.

다음 시리즈로 Windows 네이티브 개발을 위한 입문자를 위한 Windows 쉘 익스텐션(Shell Extension) 개발 가이드가 준비되어 있습니다.

 

카테고리 “API/COM”
more...
COM의 소개(파트 2) - COM 서버의 이면 (8)
COM의 소개(파트 2) – COM 서버의 이면 본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM Part II - Behind the Scenes of a COM Server’을 번역한 것입니다. 원 게시물은 https://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다. 또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전..
API/COM
2020. 10. 8. 19:45

COM의 소개(파트 2) - COM 서버의 이면 (8)

API/COM
2020. 10. 8. 19:45

COM의 소개(파트 2) – COM 서버의 이면

본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM Part II - Behind the Scenes of a COM Server’을 번역한 것입니다.

원 게시물은 https://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다.

또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.

  1. COM의 소개(파트 2) – COM 서버의 이면 (1)
  2. COM의 소개(파트 2) – COM 서버의 이면 (2)
  3. COM의 소개(파트 2) – COM 서버의 이면 (3)
  4. COM의 소개(파트 2) – COM 서버의 이면 (4)
  5. COM의 소개(파트 2) – COM 서버의 이면 (5)
  6. COM의 소개(파트 2) – COM 서버의 이면 (6)
  7. COM의 소개(파트 2) – COM 서버의 이면 (7)
  8. COM의 소개(파트 2) – COM 서버의 이면 (8)
  9. COM의 소개(파트 2) – COM 서버의 이면 (9) [完]

 

간단한 사용자 인터페이스

실제로 클래스팩토리의 예제를 확인하기 위하여, 본 글에 수록된 샘플 프로젝트를 살펴보는 것부터 시작하겠습니다. 이 프로젝트는 CSimpleMsgBoxImpl이라는 이름의 coclass에서 ISimpleMsgBox 인터페이스를 구현하고 있는 DLL 서버입니다.

 

인터페이스 정의

우리가 만든 새로운 인터페이스의 이름을 ISimpleMsgBox로 정하겠습니다. 모든 인터페이스들과 마찬가지로 이 인터페이스도 IUnknown에서 파생되어야 합니다. 이 인터페이스에는 단 하나의 메소드인 DoSimpleMsgBox가 있습니다. 이 메소드는 표준 반환형인 HRESULT를 반환함을 숙지하시기 바랍니다. 여러분이 작성하는 모든 메소드들은 HRESULT를 반환형으로 삼아야 합니다. 그 외 다른 형식으로 호출자에게 값을 반환하고자 할 때는 포인터 파라미터를 사용해야 합니다.

struct ISimpleMsgBox : public IUnknown {
    /* IUnknown에서 정의된 메소드들 */
    ULONG AddRef();
    ULONG Release();
    HRESULT QueryInterface(REFIID riid, void ** ppv);

    /* ISimpleMsgBox 메소드 */
    HRESULT DoSimpleMsgBox(HWND hWndParent, BSTR bsMessageText);
};

struct declspec(uuid(“{7D51904D-1645-4a8c-BDE0-0F4A44FC38C4}”)) ISimpleMsgBox;

 

__declspec가 적혀있는 줄은 ISimpleMsgBox 심볼(symbol)에 GUID를 부여하고 있습니다. 이렇게 하면 GUID는 나중에 __uuidof 연산자를 통해 얻을 수 있습니다. __declspec__uuidof는 Microsoft C++ 확장입니다.

DoSimpleMsgBox의 두 번째 파라미터는 BSTR 형입니다. BSTR은 “바이너리 문자열(binary string)”을 의미하며, 고정 크기 바이트들의 시퀸스를 나타냅니다(역자: 즉 한 글자당 2바이트짜리가 계속 이어져 있다는 뜻입니다). BSTR은 주로 Visual Basic이나 Windows Scripting Host와 같은 스크립팅 클라이언트에서 사용됩니다.

이 인터페이스는 CSimpleMsgBoxImpl이라는 이름의 C++ 클래스로 구현됩니다. 클래스의 선언은 다음과 같습니다.

class CSimpleMsgBoxImpl : public ISimpleMsgBox {
public:
CSimpleMsgBoxImpl();
virtual ~CSimpleMsgBoxImpl();

    // IUnknown 메소드
    ULONG AddRef();
    ULONG Release();
    HRESULT QueryInterface( REFIID riid, void** ppv );

    // ISimpleMsgBox methods
    HRESULT DoSimpleMsgBox( HWND hwndParent, BSTR bsMessageText );

protected:
    ULONG m_uRefCount;
};

class __declspec(uuid("{7D51904E-1645-4a8c-BDE0-0F4A44FC38C4}")) CSimpleMsgBoxImpl;

 

COM 클라이언트가 SimpleMsgBox 형식의 COM 객체를 생성하고자 할 때 코드는 다음과 같이 작성합니다.

ISimpleMsgBox* pIMsgBox;
HRESULT hr;

hr = CoCreateInstance(__uuidof(CSimpleMsgBoxImpl), // coclass의 CLSID
    NULL, // 객체 통합하지 않음
    CLSCTX_INPROC_SERVER, // 인 프로세스 DLL 서버
    __uuidof(ISimpleMsgBox), // 우리가 원하는 인터페이스에 대한 IID
    (void**) &pIMsgBox); // 인터페이스 포인터의 주소

 

클래스팩토리

 

클래스팩토리 구현

SimpleMsgBox 팩토리는 C++ 클래스로 구현되어 있으며, 예상하다시피 CSimpleMsgBoxClassFactory로 명명합니다.

class CSimpleMsgBoxClassFactory : public IClassFactory {
public:
    CSimpleMsgBoxClassFactory();
    virtual ~CSimpleMsgBoxClassFactory();

    // IUnknown 메소드
    ULONG AddRef();
    ULONG Release();
    HRESULT QueryInterface(REFIID riid, void** ppv);

    // IClassFactory 메소드
    HRESULT CreateInstance(IUnknown* pUnkOuter, REFIID riid, void ** ppv);
    HRESULT LockServer(BOOL fLock);

protected:
    ULONG m_uRefCount;
};

 

생성자, 소멸자 및 IUnknown서 보았던 예제들과 거의 같게 작동됩니다. 여러분이 예상하듯이 IClassFactory의 메소드이며 새롭게 추가된 LockServer는 비교적 단순합니다.

HRESULT CSimpleMsgBoxClassFactory::LockServer(BOOL fLock) {
    fLock ? g_uDllLockCount++ : g_uDllLockCount--;
    return S_OK;
}

 

지금부터는 흥미로운 내용인 CreateInstance에 대해 살펴보겠습니다. 이 메소드는 새로운 CSimpleMsgBoxImpl 객체를 만드는 역할을 합니다. 이 메소드의 원형과 파라미터에 대해 자세히 보겠습니다.

HRESULT CSimpleMsgBoxClassFactory::CreateInstance(IUnknown * pUnkOuter, REFIID riid, void ** ppv);

 

pUnkOuter는 새 객체가 결합될 때에 한하여 사용됩니다. 그리고 이는 “외부(outer)” COM 객체를 가리키는 포인터입니다. 즉, 새로운 객체를 포함하게 될 기존의 객체입니다. 객체 결함은 본 글의 범위를 벗어나는 주제이므로 우리의 예제 객체는 결합을 지원하지 않을 것입니다.

riidppvQueryInterface에서와 동일하게 사용됩니다. 각각 COM 클라이언트가 요청하고 있는 인터페이스에 대한 IID이고, 인터페이스 포인터를 보관할 수 있는 포인터 크기의 버퍼입니다.

여기 CreateInstance 구현 예가 있습니다. 먼저 파라미터 유효성 검증과 초기화부터 시작합니다.

HRESULT CSimpleMsgBoxClassFactory::CreateInstance(IUnknown * pUnkOuter, REFIID riid, void ** ppv) {
    // 객체 결합을 사용하지 않을 것이므로 pUnkOuter는 항상 NULL이어야 합니다.
    if (pUnkOuter != NULL)
        return CLASS_E_NOAGGREGATION;

    // ppv가 실제 void * 형 포인터를 가리키고 있는지 검사합니다.
    if (IsBadWritePtr(ppv, sizeof(void *)))
        return E_POINTER;

    *ppv = NULL;

 

파라미터가 유효함을 확인하였기 때문에 우리는 새로운 객체를 생성할 수 있습니다.

CSimpleMsgBoxImpl* pMsgbox;

// 새로운 COM 객체를 생성합니다.
pMsgbox = new CSimpleMsgBoxImpl;

if (NULL == pMsgbox) return E_OUTOFMEMORY;

 

마지막으로 우리는 새롭게 생성되는 객체에 대해 클라이언트가 요청한 형식의 인터페이스로 QI를 합니다. QI가 실패하면 객체는 사용할 수 없는 것이므로 우리는 이를 해제합니다.

HRESULT hrRet;

// 클라이언트가 요청한 형식의 인터페이스로 새로 만든 객체에 대해 QI합니다.
hrRet = pMsgbox->QueryInterface(riid, ppv);

// QI가 실패하면 클라이언트가 사용할 수 없는 객체이므로 이를 해제합니다.
// 왜냐하면 객체로부터 원하는 인터페이스 형식을 얻을 수 없기 때문입니다.
if (FAILED(hrRet)) delete pMsgbox;

return hrRet;

 

DllGetClassObject

DllGetClassObject의 내부를 좀 더 자세히 들여다 보겠습니다. 원형은 다음과 같습니다.

HRESULT DllGetClassObject(REFCLSID rclsid, REFIID riid, void ** ppv);

 

rclsid는 COM 클라이언트가 필요로 하는 coclass의 CLSID입니다. 이 함수는 해당 coclass에 대한 클래스팩토리를 반드시 반환해야 합니다.

riidppv는 QI에서 언급한 파라미터와 같습니다. 이 경우 riid는 COM 라이브러리가 요청하는 클래스팩토리 객체에 대한 IID, 즉 IID_IClassFactory입니다.

DllGetClassObject가 새로운 COM 객체로서 클래스 팩토리를 반환하기 때문에, 이 코드는 IClassFactory::CreateInstance와 비슷해 보입니다. 유효성 검사와 초기화로 시작하는 부분을 살펴보겠습니다.

HRESULT DllGetClassObject(REFCLSID rclsid, REFIID riid, void ** ppv) {

    // 클라이언트가 CSimpleMsgBoxImpl에 대한 팩토리를 요청하고 있는지 검사합니다.
    if (!InlineIsEqualGUID(rclsid, __uuidof(CSimpleMsgBoxImpl)))
        return CLASS_E_CLASSNOTAVAILABLE;

    // ppv가 실제 void * 형 버퍼를 가리키고 있는지 검사합니다.
    if ( IsBadWritePtr ( ppv, sizeof(void*) ))
        return E_POINTER;

    *ppv = NULL;

 

먼저 if 구문이 rclsid 파라미터를 검사합니다. 우리의 COM 서버는 단 하나의 coclass만을 가지고 있기 때문에, rclsid는 반드시 CSimpleMsgBoxImpl에 대한 CLSID여야 합니다. __uuidof 연산자는 앞서 __deslcped(uuid()) 선언으로 CSimpleMsgBoxImpl 클래스에 부여된 GUID를 가져오는 작동을 합니다. InlineIsEqualsGUID는 두 개의 GUID가 서로 같은지 여부를 확인하는 인라인 함수입니다.

다음 단계는 클래스팩토리 생성 단계입니다.

CSimpleMsgBoxClassFactory* pFactory;

// 새로운 클래스팩토리를 생성합니다.
pFactory = new CSimpleMsgBoxClassFactory;

if (pFactory == NULL)
        return E_OUTOFMEMORY;

 

이 부분이 CreateInstance와 다소 다른 부분입니다. CreateInstance로 돌아가서, 우리는 QI의 호출이 실패하였을 때 COM 객체를 할당 해제하였습니다. 하지만 이 부분은 그렇지 않습니다.

우리는 스스로를 우리가 만든 COM 객체(클래스팩토리)의 클라이언트라고 생각할 수 있습니다. 그래서 우리는 이 객체의 레퍼런스 카운트를 1로 만들기 위해 AddRef를 호출합니다. 그 다음 우리는 QI를 호출합니다. QI가 성공하면 이 객체에 한번 더 AddRef가 이루어져서 레퍼런스 카운트가 2가 됩니다. QI가 실패하면 레퍼런스 카운트는 1로 감소합니다.

QI가 호출된 후 우리는 클래스팩토리 객체의 사용을 더 이상 안 할 것이기 때문에 Release를 호출합니다. QI가 실패하면 이 클래스팩토리의 레퍼런스카운트가 0이 되어 스스로 할당 해제합니다. 결국 결과는 같습니다.

// 클래스팩토리를 사용하고 있는 동안에는 AddRef()를 한 번 해줍니다.
pFactory->AddRef();

HRESULT hrRet;

// 클래스팩토리에 대해 COM 클라이언트가 요청한 형태의 인터페이스가 있는지 QI합니다.
hrRet = pFactory->QueryInterface(riid, ppv);

// 클래스팩토리의 사용을 끝냈으므로 Release합니다.
pFactory->Release();

return hrRet;

 

QueryInterface 다시 살펴보기

필자는 이전에 QI의 구현을 보인 바 있습니다. 그러나 실전에서는 COM 객체가 꼭 IUnknown만을 구현하지 않기 때문에 클래스팩토리의 QI는 다시 살펴보는 것이 좋습니다. 먼저 우리는 ppv 버퍼가 유효한지 검사하고 이를 초기화합니다.

HRESULT CSimpleMsgBoxClassFactory::QueryInterface(REFIID riid, void ** ppv) {
HRESULT hrRet = S_OK;

    // ppv가 실제로 void * 형 버퍼를 참조하고 있는 지 검사합니다.
    if (IsBadWritePtr(ppv, sizeof(void *)))
        return E_POINTER;

    // 표준 QI 초기화: *ppv를 NULL로 설정합니다.
    *ppv = NULL;

 

그 다음 우리는 riid를 검사하여 해당 클래스팩토리에서 구현하고 있는 IUnknown 또는 IClassFactory 중 하나의 것인지를 확인합니다.

// COM 클라이언트가 우리자 지원할 수 있는 형태의 인터페이스를 요청하고 있다면 *ppv 설정
if (InlineIsEqualGUID(riid, IID_IUnknown)) {
    *ppv = (IUnknown*) this;
} else if (InlineIsEqualGUID(riid, IID_IClassFactory)) {
    *ppv = (IClassFactory *) this;
} else {
    hrRet = E_NOINTERFACE;
}

 

마지막으로 riid가 가리키는 인터페이스가 우리가 지원할 수 있는 인터페이스일 때 우리는 AddRef를 호출하여 인터페이스 포인터의 레퍼런스 카운트를 1만큼 증가시킵니다. 그 다음 인터페이스 포인터를 반환합니다.

// 인터페이스 포인터를 반환할 수 있게 되었다면, AddRef 호출
if (hrRet == S_OK) {
        ((IUnknown *) *ppv)->AddRef();
}

return hrRet;

 

ISimpleMsgBox 구현

우리의 소스 코드는 ISimpleMsgBox가 가진 유일한 메소드인 DoSimpleMsgBox를 가지고 있습니다. 우리는 bsMessageTextTCHAR로 변환하기 위해 마이크로소프트의 확장 클래스인 _bstr_t를 처음으로 사용해 봅니다.

HRESULT CSimpleMsgBoxImpl::DoSimpleMsgBox(HWND hwndParent, BSTR bsMessageText) {
    _bstr_t bsMsg = bsMessageText;
    LPCTSTR szMsg = (TCHAR *) bsMsg;  // 필요하다면 _bstr_t 문자열을 TCHSR로 변환합니다.
    // 변환 후에 우리는 메시지 박스를 보여주고 값을 반환합니다.
    MessageBox(hwndParent, szMsg, _T("Simple Message Box"), MB_OK);
    return S_OK;
}

 

계속 읽기

이전 게시글: COM의 소개(파트 2) – COM 서버의 이면 (7)

다음 게시글: COM의 소개(파트 2) – COM 서버의 이면 (9) [完]

 

카테고리 “API/COM”
more...
COM의 소개(파트 2) - COM 서버의 이면 (7)
COM의 소개(파트 2) – COM 서버의 이면 본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM Part II - Behind the Scenes of a COM Server’을 번역한 것입니다. 원 게시물은 https://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다. 또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전..
API/COM
2020. 10. 8. 15:33

COM의 소개(파트 2) - COM 서버의 이면 (7)

API/COM
2020. 10. 8. 15:33

COM의 소개(파트 2) – COM 서버의 이면

본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM Part II - Behind the Scenes of a COM Server’을 번역한 것입니다.

원 게시물은 https://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다.

또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.

  1. COM의 소개(파트 2) – COM 서버의 이면 (1)
  2. COM의 소개(파트 2) – COM 서버의 이면 (2)
  3. COM의 소개(파트 2) – COM 서버의 이면 (3)
  4. COM의 소개(파트 2) – COM 서버의 이면 (4)
  5. COM의 소개(파트 2) – COM 서버의 이면 (5)
  6. COM의 소개(파트 2) – COM 서버의 이면 (6)
  7. COM의 소개(파트 2) – COM 서버의 이면 (7)
  8. COM의 소개(파트 2) – COM 서버의 이면 (8)
  9. COM의 소개(파트 2) – COM 서버의 이면 (9) [完]

 

COM 객체 만들기 - 클래스팩토리

우리가 COM의 클라이언트 사이드를 보던 때로 돌아가서, 필자는 COM이 COM 객체를 생성하거나 파괴하기 위한 언어 독립적인 과정을 갖는다고 설명하였습니다. COM 클라이언트는 COM 객체를 새로 만들기 위하여 CoCreateInstance를 호출합니다. 지금부터는 우리는 이것을 서버 사이드에서 보도록 하겠습니다.

여러분이 coclass를 구현할 때마다, 여러분은 또한 가장 최초의 coclass 인스턴스 생성을 담당하는 동반 coclass를 작성하게 됩니다. 이 동반 coclass는 해당 coclass의 ‘클래스팩토리(class factory)’라고 부르며 이것의 유일한 목적은 COM 객체를 생성하는 것입니다. 클래스팩토리를 갖는 이유 또한 언어 독립성을 지키기 위함입니다. COM 그 자체는 COM 객체를 생성할 수 없습니다. 왜냐하면 그렇게 하는 것은 언어 독립적(language independent)이지도 않고, 구현 독립적(implementation independent)이지도 않기 때문입니다.

COM 클라이언트가 COM 객체를 생성할 때, COM 라이브러리는 COM 서버에 있는 클래스팩토리에게 이를 요청합니다. 그러면 클래스팩토리는 COM 클라이언트가 반환 받을 수 있는 COM 객체를 생성합니다. 이와 같은 연동 메커니즘이 바로 내보내기되는 함수인 DllGetClassObject입니다.

사족: “클래스팩토리(class factory)”와 “클래스 객체(class object)”는 사실상 같은 대상을 지칭하는 말입니다. 그러나 두 용어 모두 클래스팩토리의 목적을 정확하게 묘사하지는 못하고 있습니다. 왜냐하면 클래스팩토리는 COM 클래스를 만드는 것이 아니라 COM 객체를 만들기 때문입니다. 따라서 “클래스팩토리(class factory)”라는 용어를 접했을 때 여러분은 머릿속으로 “객체 팩토리(object factory)”로 치환해서 생각하는 것이 도움이 될 수 있습니다. (사실 MFC에서는 클래스팩토리 구현체에 COleObjectFactory라는 이름을 붙임으로써 실제로 이를 시전한 바 있습니다.) 그러나 공식적인 용어가 “클래스팩토리”인 만큼, 필자도 본 글에서는 “클래스팩토리” 용어에 따르겠습니다.

COM 라이브러리가 DllGetClassObject를 호출할 때 클라이언트가 요구한 CLSID를 함께 전달합니다. COM 서버는 요청 받은 CLSID에 해당하는 클래스팩토리를 생성하고 이를 전달할 책임이 있습니다. 클래스팩토리는 그 자체로 coclass이고 IClassFactory 인터페이스를 구현하고 있습니다. DllGetClassObject의 작동이 성공하면 COM 서버는 COM 라이브러리에게 IClassFactory의 포인터를 반환합니다. 그러면 IClassFactory의 메소드를 통해 클라이언트가 요구한 COM 객체 인스턴스가 만들어집니다.

IClassFactory 인터페이스는 다음과 같이 생겼습니다.

struct IClassFactory : public IUnknown {
    HRESULT CreateInstance(IUnknown * pUnkOuter, REFIID riid, void ** ppvObject);
    HRESULT LockServer(BOOL fLock);
};

 

CreateInstance는 새로운 COM 객체를 생성하는 메소드입니다. LockServer는 필요 시 COM 라이브러리가 COM 서버에 대한 레퍼런스 카운트를 증감할 수 있게 합니다.

 

계속 읽기

이전 게시글: COM의 소개(파트 2) – COM 서버의 이면 (6)

다음 게시글: COM의 소개(파트 2) – COM 서버의 이면 (8)

 

카테고리 “API/COM”
more...
COM의 소개(파트 2) - COM 서버의 이면 (6)
COM의 소개(파트 2) – COM 서버의 이면 본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM Part II - Behind the Scenes of a COM Server’을 번역한 것입니다. 원 게시물은 https://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다. 또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전..
API/COM
2020. 10. 8. 15:25

COM의 소개(파트 2) - COM 서버의 이면 (6)

API/COM
2020. 10. 8. 15:25

COM의 소개(파트 2) – COM 서버의 이면

본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM Part II - Behind the Scenes of a COM Server’을 번역한 것입니다.

원 게시물은 https://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다.

또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.

  1. COM의 소개(파트 2) – COM 서버의 이면 (1)
  2. COM의 소개(파트 2) – COM 서버의 이면 (2)
  3. COM의 소개(파트 2) – COM 서버의 이면 (3)
  4. COM의 소개(파트 2) – COM 서버의 이면 (4)
  5. COM의 소개(파트 2) – COM 서버의 이면 (5)
  6. COM의 소개(파트 2) – COM 서버의 이면 (6)
  7. COM의 소개(파트 2) – COM 서버의 이면 (7)
  8. COM의 소개(파트 2) – COM 서버의 이면 (8)
  9. COM의 소개(파트 2) – COM 서버의 이면 (9) [完]

 

COM 서버 등록

COM 서버가 작동을 하기 위해서는 무엇보다도 Windows 레지스트리에 적절하게 등록되어 있어야만 합니다. 여러분이 레지스트리 편집기를 통해 HKEY_CLASSES_ROOT\CLSID를 탐색하면 무수히 많은 하위 키들을 보게 되실 것입니다. HKCR\CLSID는 현재 컴퓨터에서 사용 가능한 모든 COM 서버들을 리스트로서 보관하고 있습니다.

대개 DllRegisterServer를 통해 COM 서버가 등록이 되면, 표준적인 레지스트리 포맷에 따라 COM 서버의 GUID에서 따온 새로운 키가 CLSID 하위에 추가됩니다. 레지스트리 포맷의 GUID란 다음과 같은

{067DF822-EAB6-11CF-B56E-00A0244D5087}

처럼 중괄호와 하이픈이 포함된 것입니다. 문자는 대문자도 되고 소문자도 됩니다.

이 레지스트리 키의 기본값은 인간이 읽을 수 있는 형태로 적힌 coclass 이름입니다. 이 값은 Visual C++에서 함께 제공하고 있는 OLE/COM Object Viewer와 같이 사용자 인터페이스에 출력하기 적합한 형태여야 합니다.

GUID 이름으로 된 하위 키의 하위에는 더 많은 정보들이 보관될 수 있습니다. 여러분이 생성해야하는 이러한 레지스트리 키의 종류는 대부분 여러분이 만들고 있는 COM 서버의 종류에 달려 있습니다.

우리가 만들고 있는 간단한 ‘인 프로세스 서버’ 의 목적에 따르면 우리는 InProcServer32라는 이름을 갖는 하나의 레지스트리만 있으면 됩니다.

InProcServer32 레지스트리 키는 두 개의 문자열을 포함하고 있습니다. 하나는 기본값으로서 COM 서버 DLL 파일의 전체 경로를 나타내고, 다른 하나는 스레드 모델을 지정하는 ThreadingModel이라는 값입니다. 스레드 모델은 본 글의 범위를 벗어나므로 싱글 스레드 COM 서버를 만들고자 할 때 사용되는 스레드 모델은 Apartment라고만 알아두시기 바랍니다.

 

계속 읽기

이전 게시글: COM의 소개(파트 2) – COM 서버의 이면 (5)

다음 게시글: COM의 소개(파트 2) – COM 서버의 이면 (7)

 

카테고리 “API/COM”
more...
COM의 소개(파트 2) - COM 서버의 이면 (5)
COM의 소개(파트 2) – COM 서버의 이면 본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM Part II - Behind the Scenes of a COM Server’을 번역한 것입니다. 원 게시물은 https://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다. 또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전..
API/COM
2020. 10. 8. 15:18

COM의 소개(파트 2) - COM 서버의 이면 (5)

API/COM
2020. 10. 8. 15:18

COM의 소개(파트 2) – COM 서버의 이면

본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM Part II - Behind the Scenes of a COM Server’을 번역한 것입니다.

원 게시물은 https://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다.

또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.

  1. COM의 소개(파트 2) – COM 서버의 이면 (1)
  2. COM의 소개(파트 2) – COM 서버의 이면 (2)
  3. COM의 소개(파트 2) – COM 서버의 이면 (3)
  4. COM의 소개(파트 2) – COM 서버의 이면 (4)
  5. COM의 소개(파트 2) – COM 서버의 이면 (5)
  6. COM의 소개(파트 2) – COM 서버의 이면 (6)
  7. COM의 소개(파트 2) – COM 서버의 이면 (7)
  8. COM의 소개(파트 2) – COM 서버의 이면 (8)
  9. COM의 소개(파트 2) – COM 서버의 이면 (9) [完]

 

CoCreateInstance의 내부

필자의 이전 COM 소개글로 돌아가서, 우리는 COM 클라이언트가 COM 객체를 요청할 때 이를 생성하는 CoCreateInstance를 보았습니다. 클라이언트의 관점에서 이는 블랙박스입니다. 단지 적절한 파라미터를 제공하여 CoCreateInstance를 호출하기만 하면, 짜잔! 여러분은 COM 객체를 얻을 수 있습니다. 물론 어떤 흑마법(?)도 포함되어있지 않습니다. COM 서버를 적재하고, 요청한 COM 객체를 생성하고, 그리고 요청한 인터페이스의 형태로 번환해주기까지 잘 정의된 처리과정이 수행될 뿐입니다.

다음은 그 처리과정의 간략한 개요입니다. 몇 가지 낯선 용어들이 있습니다만, 걱정할 필요가 없습니다. 필자는 이후 절에서 모두 다룰 것이기 때문입니다.

1. 클라이언트 프로그램이 원하는 IIDCLSID를 전달하여 CoCreateInstance를 호출합니다.

2. COM 라이브러리는 HKEY\CLASSES_ROOT\CLSID의 하위 키들 중 해당 COM 서버의 CLSID를 찾습니다. 이 하위 키에는 COM 서버의 등록 정보가 들어 있습니다.

3. COM 라이브러리는 COM 서버 DLL의 전체 경로를 읽고 COM 클라이언트의 프로세스 공간 속으로 이 DLL을 적재합니다.

4. COM 라이브러리는 COM 클라이언트가 요청한 coclass를 만들 수 있는 클래스팩토리를 요청하기 위하여 COM 서버에 있는 DllGetClassObject 함수를 호출합니다.

5. 서버는 클래스팩토리를 생성하여 이를 DllGetClassObject를 통해 반환합니다.

6. COM 라이브러리는 클래스팩토리에 있는 CreateInstance를 호출하여 COM 클라이언트가 요청했던 COM 객체를 생성합니다.

7. CoCreateInstance는 COM 객체로부터 COM 클라이언트가 요청했던 인터페이스 포인터를 반환합니다.

 

계속 읽기

이전 게시글: COM의 소개(파트 2) – COM 서버의 이면 (4)

다음 게시글: COM의 소개(파트 2) – COM 서버의 이면 (6)

 

카테고리 “API/COM”
more...
COM의 소개(파트 2) - COM 서버의 이면 (4)
COM의 소개(파트 2) – COM 서버의 이면 본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM Part II - Behind the Scenes of a COM Server’을 번역한 것입니다. 원 게시물은 https://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다. 또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전..
API/COM
2020. 10. 8. 15:07

COM의 소개(파트 2) - COM 서버의 이면 (4)

API/COM
2020. 10. 8. 15:07

COM의 소개(파트 2) – COM 서버의 이면

본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM Part II - Behind the Scenes of a COM Server’을 번역한 것입니다.

원 게시물은 https://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다.

또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.

  1. COM의 소개(파트 2) – COM 서버의 이면 (1)
  2. COM의 소개(파트 2) – COM 서버의 이면 (2)
  3. COM의 소개(파트 2) – COM 서버의 이면 (3)
  4. COM의 소개(파트 2) – COM 서버의 이면 (4)
  5. COM의 소개(파트 2) – COM 서버의 이면 (5)
  6. COM의 소개(파트 2) – COM 서버의 이면 (6)
  7. COM의 소개(파트 2) – COM 서버의 이면 (7)
  8. COM의 소개(파트 2) – COM 서버의 이면 (8)
  9. COM의 소개(파트 2) – COM 서버의 이면 (9) [完]

 

IUnknown부터 시작하여 인터페이스 구현하기

모든 인터페이스는 IUnknown으로부터 파생됨을 떠올려 봅시다. 왜냐하면 IUnknown은 COM 객체의 두 가지 기본 기능인 레퍼런스 카운트와 인터페이스 쿼리를 다루고 있기 때문입니다. 여러분이 coclass를 작성할 때, 여러분은 또은 여러분의 필요에 맞게 IUnknown을 구현해야 합니다. IUnknown만을 구현하는 coclass 예제를 살펴봅시다. 이는 여러분이 작성할 수 있는 coclass 중 가장 간단한 클래스입니다. 우리는 CUnknownImpl이라는 이름을 가진 C++ 클래스에서 IUnknown을 구현할 것입니다. 클래스의 선언은 다음과 같습니다.

class CUnknownImpl : public IUnknown {
public:
    CUnknownImpl(); // 생성자
    virtual ~CUnknownImpl(); // 소멸자

    /* 다음 3개는 IUnknown 인터페이스로부터 유래된 메소드 */
    ULONG AddRef();
    ULONG Release();
    HRESULT QueryInterface(REFIID riid, void ** ppv);

protected:
    UINT m_uRefCount; // COM 객체의 레퍼런스 카운트
};

 

생성자와 소멸자

생성자와 소멸자는 COM 서버의 레퍼런스 카운트를 증감합니다.

extern UINT g_uDllRefCount;

CUnknownImpl::CUnknownImpl() {
    m_uRefCount = 0;
    g_uDllRefCount++;
}

CUnknownImpl::~CUnknownImpl() {
    g_uDllRefCount--;
}

 

생성자는 새로운 COM 객체가 생성될 때 호출됩니다. 그러므로 COM 서버가 메모리에 남아있게 하도록 COM 서버의 레퍼런스 카운트를 1만큼 증가시킵니다. 또한 생성자는 COM 객체의 레퍼런스 카운트를 0으로 초기화합니다. COM 객체가 소멸될 때, 소멸자는 서버의 레퍼런스 카운트를 1만큼 감소시킵니다.

 

AddRef와 Release

이들 두 메소드는 COM 객체의 수명을 제어하는 메소드입니다. AddRef는 간단하게 구현 가능합니다.

ULONG CUnknownImpl::AddRef() {
    return ++m_uRefCount;
}

 

AddRef는 COM 객체의 레퍼런스 카운트를 1만큼 증가시키고, 증가된 값을 반환합니다. 다만 ReleaseAddRef만큼 간단하지는 않습니다.

ULONG CUnknownImpl::Release() {
    ULONG uRet = --m_uRefCount;

    if (m_uRefCount == 0) delete this;
    return uRet;
}

 

COM 객체의 레퍼런스 카운트를 감소시키는 것뿐만 아니라, Release는 더 이상 명시적인 참조가 없는 경우 객체 스스로를 파괴합니다. Release는 또한 갱신된 레퍼런스 카운트를 반환합니다. 다만 이와 같은 Release 구현은 COM 객체가 힙(heap) 영역에 생성되었음을 가정하고 있습니다. 여러분이 COM 객체를 스택이나 전역 범위에 생성하였을 경우 COM 객체가 스스로를 소멸시키려 할 때 모든 것들이 엉망이 되고 맙니다.

이제 여러분의 클라이언트 앱에서 왜 AddRefRelease 메소드가 적절하게 호출되어야 하는지에 대한 이유가 명확해야 합니다. 여러분이 이들 메소드를 정확하게 호출해주지 않는다면 여러분이 사용하고 있는 COM 객체가 너무 빨리 소멸될 수도 있고, 아예 소멸되지 않을 수도 있습니다. COM 객체가 너무 일찍 소멸되어 버리면 이후 COM 서버 전체가 메모리에서 날아가버릴 수 있습니다. 이는 여러분의 어플리케이션이 서버에 접속을 시도할 때 충돌을 야기합니다.

여러분이 멀티스레드 프로그래밍을 해본 경험이 있다면, 여러분은 왜 변수를 증감할 때 InterlockedIncrement, InterlockedDecrement 대신에 단순 증감연산자 ++--를 쓰는지 의아해 하실 것입니다. 싱글 스레드 COM 서버에서 증감연산자 ++--를 사용하는 것은 전적으로 안전합니다. 왜냐하면 클라이언트 어플리케이션이 멀티 스레드이고 메소드 호출이 제각각의 스레드에서 이루어진다고 해도, COM 라이브러리는 이러한 메소드 호출을 직렬화하기 때문입니다. 이것은 어떤 하나의 메소드 호출이 시작되면, 메소드를 호출한 다른 스레드들은 앞서 호출된 메소드 실행이 끝날 때까지 잠겨 있음을 뜻합니다. COM 라이브러리는 우리가 만들고 있는 COM 서버에 한 번에 하나 이상의 스레드가 진입하지 않을 것임을 보장합니다.

 

QueryInterface

QueryInterface, 줄여서 QI는 COM 클라이언트가 하나의 COM 객체에서 다양한 인터페이스들을 요청할 때 사용됩니다.

우리의 예제 coclass는 단지 하나의 인터페이스만을 구현하고 있기 때문에, 우리가 다룰 QI는 매우 단순할 것입니다. QI는 두 가지 파라미터를 받습니다. 하나는 요청하는 인터페이스의 IID이고 다른 하나는 쿼리 작업이 성공하였을 때 인터페이스 포인터를 보관하게 될 포인터 크기의 버퍼입니다.

HRESULT CUnknownImpl::QueryInterface(REFIID riid, void ** ppv) {
    HRESULT hrRet = S_OK;

    // 표준 QI 초기화로서 ppv를 NULL 참조하게 설정합니다.
    *ppv = NULL;

    if (IsQeualIID(riid, IID_IUnknown)) {
    // COM 클라이언트가 요청하는 인터페이스가 이 coclass에서 지원 가능한 형식이면
        *ppv = (IUnknown *)this;
    } else {
        // COM 클라이언트가 요청하는 인터페이스 형식이 이 coclass와 호환되지 않으면
        hrRet = E_NOINTERFACE;
    }

     // 인터페이스 포인터를 반환할 수 있다면 AddRef를 호출합니다.
    if (hrRet == S_OK) {
        ((IUnknown *)*ppv)->AddRef();
    }

    return hrRet;
}

 

예제 소스의 QI에는 다음의 세 가지 작업이 포함되어 있습니다.

1. 반환을 위해 전달된 포인터 파라미터를 NULL로 초기화합니다: *ppv = NULL;

2. COM 클라이언트가 요청한 riid 값이 우리의 coclass가 구현하고 있는 인터페이스의 IID 중에 있는지를 확인합니다: if (IsEqualIID(riid, IID_IUnknown))

3. 요청한 형식의 인터페이스를 우리의 coclass가 구현하고 있다면, COM 객체의 레퍼런스 카운트를 증가시킵니다: ((IUnknown *)*ppv)->AddRef();

 

AddRef를 호출하는 것은 매우 중요합니다. 다음의 문장

*ppv = (IUnknown *)this;

은 COM 객체에 대한 새로운 참조를 형성합니다. 그러므로 우리는 AddRef를 호출함으로써 이 객체에게 새로운 참조 관계가 추가되었음을 알려야만 합니다.

AddRef를 호출하는데 IUnknown * 형으로 캐스팅하는 것이 이상하게 보일 수는 있습니다. 그러나 특별한 coclass의 QI에서 *ppvIUnknown *이 아닐 수 있습니다. 그러므로 캐스트하여 호출하는 습관을 들이는 것이 좋습니다.

이제 우리는 DLL 서버의 내부적인 세부 사항을 일부 다루어 보았습니다, 다음 절에서는 처음으로 돌아가서 COM 클라이언트가 CoCreateInstance를 실행하였을 때 우리가 만들고 있는 COM 서버가 어떻게 사용되는지에 대해 살펴보겠습니다.

 

계속 읽기

이전 게시글: COM의 소개(파트 2) – COM 서버의 이면 (3)

다음 게시글: COM의 소개(파트 2) – COM 서버의 이면 (5)

 

카테고리 “API/COM”
more...
COM의 소개(파트 2) - COM 서버의 이면 (3)
COM의 소개(파트 2) – COM 서버의 이면 본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM Part II - Behind the Scenes of a COM Server’을 번역한 것입니다. 원 게시물은 https://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다. 또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전..
API/COM
2020. 10. 8. 07:48

COM의 소개(파트 2) - COM 서버의 이면 (3)

API/COM
2020. 10. 8. 07:48

COM의 소개(파트 2) – COM 서버의 이면

본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM Part II - Behind the Scenes of a COM Server’을 번역한 것입니다.

원 게시물은 https://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다.

또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.

  1. COM의 소개(파트 2) – COM 서버의 이면 (1)
  2. COM의 소개(파트 2) – COM 서버의 이면 (2)
  3. COM의 소개(파트 2) – COM 서버의 이면 (3)
  4. COM의 소개(파트 2) – COM 서버의 이면 (4)
  5. COM의 소개(파트 2) – COM 서버의 이면 (5)
  6. COM의 소개(파트 2) – COM 서버의 이면 (6)
  7. COM의 소개(파트 2) – COM 서버의 이면 (7)
  8. COM의 소개(파트 2) – COM 서버의 이면 (8)
  9. COM의 소개(파트 2) – COM 서버의 이면 (9) [完]

 

서버 수명 관리

DLL 서버의 특이한 것은, 그들 스스로 메모리에서 얼마나 머물러 있을 것인지를 제어한다는 것입니다. 일반적인 DLL 파일은 수동적이어서, 전적으로 그 DLL을 사용하는 어플리케이션에서 적재할 것인지, 적재 해제할 것인지에 따릅니다. 기술적으로 COM으로 만든 DLL 서버도 근본이 DLL 파일이기 때문에 수동적입니다. 그러나 COM 라이브러리는 서버가 COM 라이브러리에게 적재 해제해도 되는지를 결정할 수 있도록 기회를 주는 메커니즘을 가지고 있습니다.

이 작업은 앞서 설명한 내보내기된 함수 DllCanUnloadNow가 수행합니다. 이 함수의 원형은 다음과 같습니다.

HRESULT DllCanUnloadNow();

 

COM 서버를 사용하던 클라이언트 어플리케이션이 유휴 시간 동안 COM 라이브러리 API인 CoFreeUnusedLibraries를 실행하면, 어플리케이션이 적재하고 있는 모든 DLL 서버에게 하나씩 DllCanUnloadNow 함수를 실행하면서 각각 적재 해제 가능 여부를 확인합니다.

COM 서버가 좀 더 적재되어 있고자 한다면 S_FALSE를 반환하고, 서버가 더 이상 메모리에 남아있을 이유가 없다고 판단하면 S_OK를 반환하여 COM 라이브러리가 자신을 적재 해제하게 합니다.

COM 서버 스스로 메모리에서 적재 해제되어도 좋은지 여부를 확인하는 방법에는 레퍼런스 카운트가 있습니다. 레퍼런스 카운트를 사용한 DllCanUnloadNow는 다음과 같이 구현할 수 있습니다.

extern UINT g_uDllRefCount; // COM 서버가 어플리케이션에 참조된 횟수

HRESULT DllCanUnloadNow() {
    return (g_uDllRefCount > 0) ? S_FALSE : S_OK;
}

 

다음 절에서 레퍼런스 카운트가 어떻게 유지되는지 살펴보겠습니다.

 

계속 읽기

이전 게시글: COM의 소개(파트 2) – COM 서버의 이면 (2)

다음 게시글: COM의 소개(파트 2) – COM 서버의 이면 (4)

 

카테고리 “API/COM”
more...
COM의 소개(파트 2) - COM 서버의 이면 (2)
COM의 소개(파트 2) – COM 서버의 이면 본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM Part II - Behind the Scenes of a COM Server’을 번역한 것입니다. 원 게시물은 https://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다. 또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전..
API/COM
2020. 10. 8. 07:41

COM의 소개(파트 2) - COM 서버의 이면 (2)

API/COM
2020. 10. 8. 07:41

COM의 소개(파트 2) – COM 서버의 이면

본 게시물은 ‘codeproject.com’에 게시된 글 ‘Introduction to COM Part II - Behind the Scenes of a COM Server’을 번역한 것입니다.

원 게시물은 https://www.codeproject.com/Articles/901/Introduction-to-COM-Part-II-Behind-the-Scenes-of-a에 게재되어 있습니다. 최대한 원문에 적힌 의도를 반영하고자 하였으나, 우리말로 읽었을 때 보다 자연스럽게 하고자 부득이 어순과 어휘를 조정한 부분도 있음을 양해 바랍니다.

또한 본 게시물에서 언급하고 있는 예제 소스 코드는 Visual C++ 6.0을 기준으로 작성되어 있기 때문에 후속 버전의 Visual Studio(또는 Visual Studio .NET)에서 자동 생성되는 COM 코드와는 다소 차이가 있음을 감안하고 읽으시기 바랍니다.

  1. COM의 소개(파트 2) – COM 서버의 이면 (1)
  2. COM의 소개(파트 2) – COM 서버의 이면 (2)
  3. COM의 소개(파트 2) – COM 서버의 이면 (3)
  4. COM의 소개(파트 2) – COM 서버의 이면 (4)
  5. COM의 소개(파트 2) – COM 서버의 이면 (5)
  6. COM의 소개(파트 2) – COM 서버의 이면 (6)
  7. COM의 소개(파트 2) – COM 서버의 이면 (7)
  8. COM의 소개(파트 2) – COM 서버의 이면 (8)
  9. COM의 소개(파트 2) – COM 서버의 이면 (9) [完]

 

COM 서버 훑어보기

본 글에서 우리는 가장 간단한 COM 서버인 ‘인 프로세스 서버(in-process server)’ 형태의 서버에 대해 살펴보겠습니다.

‘인 프로세스(in-process)’라는 말은 클라이언트 프로그램의 프로세스 영역에 COM 서버가 적재(load)된다는 뜻입니다. ‘인 프로세스(줄여서 in-proc)’ 서버는 항상 DLL 파일 형태로 존재하고, 클라이언트 프로그램이 설치된 컴퓨터와 같은 컴퓨터에 설치되어야 합니다.

 

인 프로세서 서버는 COM 라이브러리가 사용하기 전에 다음의 두 조건을 만족해야 합니다.

1. HKEY_CLASSES_ROOT\CLSID의 하위 키로 적절하게 등록되어 있어야 합니다.

2. DllGetClassObject라는 이름의 함수를 내보내야(export)합니다.

 

위의 두 조건은 여러분이 인 프로세스 서버를 작동시키기 위하여 필요한 최소한의 조건입니다. HKEY_CLASSES_ROOT\CLSID의 하위 키 이름은 COM 서버의 GUID여야 합니다. 그리고 키는 COM 서버의 위치와 스레드 모델에 대한 값을 반드시 가지고 있어야 합니다.

DllGetClassObject 함수는 COM 라이브러리가 CoCreateInstance API에 따라 작업을 수행할 때 호출되는 함수입니다.

COM 서버는 또한 다음의 세 가지 함수를 내보내야(export) 합니다.

DllCanUnloadNow
COM 서버가 메모리에서 적재 해제될 수 있는지 COM 라이브러리에서 확인하고자 할 때 호출되는 함수입니다.
DllRegisterServer
regsvr32와 같은 설치 유틸리티가 COM 서버를 등록하는 과정에서 호출되는 함수입니다.
DllUnregisterServer
regsvr32와 같은 설치 제거 유틸리티가 COM 서버를 등록 해제할 때 DllRegisterServer가 생성한 레지스트리 키를 삭제하기 위해 호출되는 함수입니다.

 

물론 위와 같은 함수들을 단순히 내보낸다고 해서 되는 것은 아니고, COM 기술 사항을 준수해야 COM 라이브러리와 COM 클라이언트에서 이 COM 서버를 사용할 수 있습니다.

 

계속 읽기

이전 게시글: COM의 소개(파트 2) – COM 서버의 이면 (1)

다음 게시글: COM의 소개(파트 2) – COM 서버의 이면 (3)

 

카테고리 “API/COM”
more...

“API” (48건)