코딩캣: 코딩하는 고양이.
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' 카테고리의 다른 글
더 보기...
태그 : 
댓글