翻译《The Old New Thing》 - Dragging a shell object 系列

发布于:2024-05-07 ⋅ 阅读:(27) ⋅ 点赞:(0)

Dragging a shell object, part 1: Getting the IDataObject - The Old New Thing (microsoft.com)icon-default.png?t=N7T8https://devblogs.microsoft.com/oldnewthing/20041206-00/?p=37133Dragging a shell object, part 2: Enabling the Move operation - The Old New Thing (microsoft.com)icon-default.png?t=N7T8https://devblogs.microsoft.com/oldnewthing/20041207-00/?p=37123Dragging a shell object, part 3: Detecting an optimized move - The Old New Thing (microsoft.com)icon-default.png?t=N7T8https://devblogs.microsoft.com/oldnewthing/20041208-00/?p=37093Dragging a shell object, part 4: Adding a prettier drag icon - The Old New Thing (microsoft.com)icon-default.png?t=N7T8https://devblogs.microsoft.com/oldnewthing/20041209-00/?p=37083Dragging a shell object, part 5: Making somebody else do the heavy lifting - The Old New Thing (microsoft.com)icon-default.png?t=N7T8https://devblogs.microsoft.com/oldnewthing/20041210-00/?p=37063

Raymond Chen


拖拽 Shell 对象

译注 Raymond Chen 原文是分为五部分的系列文章,先将其合并后作为一篇文章翻译,便于阅读。


第一部分:获取 IDataObject 对象

2004年12月6日

        Shell 提供了 `IDataObject` 对象,你只需负责拖拽操作。(这是五部分系列的开篇。)

        从一个基础模板程序开始,引入之前文章中的 `GetUIObjectOfFile` 函数,并把 `CoInitialize` 和 `CoUninitialize` 替换为 `OleInitialize` 和 `OleUninitialize`,因为我们接下来要使用的是完整的 OLE 功能,而不仅仅是 COM。

        要启动拖放操作,首先需要一个拖放源:`

class CDropSource : public IDropSource {
public:
  // *** IUnknown 接口 ***
  STDMETHODIMP QueryInterface(REFIID riid, void **ppv);
  STDMETHODIMP_(ULONG) AddRef();
  STDMETHODIMP_(ULONG) Release();

  // *** IDropSource 接口 ***
  STDMETHODIMP QueryContinueDrag(BOOL fEscapePressed, DWORD grfKeyState);
  STDMETHODIMP GiveFeedback(DWORD dwEffect);

  CDropSource() : m_cRef(1) { }
private:
  ULONG m_cRef;
};

// ...(此处省略了 QueryInterface, AddRef, Release, QueryContinueDrag 和 GiveFeedback 的实现细节)

        可以看到,这个拖放源类非常基础。即便是那些本应有趣的方法,在这里也显得平淡无奇。

        `IDropSource::QueryContinueDrag` 方法几乎是标准的模板代码。如果按下了 Escape 键,则取消拖放操作;如果鼠标按钮被释放,则结束操作;否则继续操作。

        `IDropSource::GiveFeedback` 方法更是简单,它仅返回 `DRAGDROP_S_USEDEFAULTCURSORS`,表明我们希望使用默认的拖放反馈。

        信不信由你,有了这些,我们就已经具备了拖拽文件所需的一切。

void OnLButtonDown(HWND hwnd, BOOL fDoubleClick,
                   int x, int y, UINT keyFlags) {
  IDataObject *pdto;
  // 在实际应用中,你当然不会使用固定的文件路径。
  if (SUCCEEDED(GetUIObjectOfFile(hwnd,
                    L"C:\\Windows\\clock.avi",
                    IID_IDataObject, (void**)&pdto))) {
    IDropSource *pds = new CDropSource();
    if (pds) {
      DWORD dwEffect;
      DoDragDrop(pdto, pds, DROPEFFECT_COPY | DROPEFFECT_LINK,
                 &dwEffect);
      pds->Release();
    }
    pdto->Release();
  }
}

// ...(此处省略了消息处理宏 HANDLE_MSG 的调用)

        要拖拽一个对象,你需要两样东西:一个数据对象和一个拖放源。我们已经创建了拖放源,而数据对象则由 Shell 提供。接下来,只需通过调用 `DoDragDrop` 函数来启动拖放操作。

        请注意,我们指定了允许的操作为 `DROPEFFECT_COPY` 和 `DROPEFFECT_LINK`。我们明确禁止了 `DROPEFFECT_MOVE`,因为本程序没有提供一个类似文件夹的界面;用户不会预期拖放操作会导致移动文件。

        下一篇文章,我们将探讨如何添加移动操作的支持。


第二部分:支持移动操作

2004年12月7日

        比方说,不管出于什么原因,我们确实想在拖放程序中支持 "移动"。不过,让我们用某个抓取文件来代替 clock.avi。在你不介意丢失的地方创建一个文件,比方说 C:\throwaway.txt。

        对 `OnLButtonDown` 函数进行如下修改:

void OnLButtonDown(HWND hwnd, BOOL fDoubleClick,
                   int x, int y, UINT keyFlags) {
  IDataObject *pdto;
  if (SUCCEEDED(GetUIObjectOfFile(hwnd,
                    L"C:\\throwaway.txt",
                    IID_IDataObject, (void**)&pdto))) {
    IDropSource *pds = new CDropSource();
    if (pds) {
      DWORD dwEffect;
      if (DoDragDrop(pdto, pds,
                 DROPEFFECT_COPY | DROPEFFECT_LINK | DROPEFFECT_MOVE,
                 &dwEffect) == DRAGDROP_S_DROP) {
        if (dwEffect & DROPEFFECT_MOVE) {
          DeleteFile(TEXT("C:\\throwaway.txt"));
        }
      }
      pds->Release();
    }
    pdto->Release();
  }
}

        为了避免硬编码路径的问题,我们可以将程序修改为处理通过命令行传入的路径。

        这是一段纯粹为了演示而添加的代码,可能会分散你对文章主题的注意力。我个人不喜欢接收到的示例程序中有大量与所展示技术无关的代码,这会让我不得不在代码中寻找那些真正重要的部分。

#include <shellapi.h>
LPWSTR *g_argv;
LPCWSTR g_pszTarget;
// ...(此处省略了 OnLButtonDown 和 InitApp 的实现细节)

        现在,我们允许移动操作,需要检查操作结果是否为 `DROPEFFECT_MOVE`,这表示拖放目标希望执行移动操作,但只完成了复制对象的部分;需要通过删除原始对象来完成移动操作。

        注意,`DROPEFFECT_MOVE` 并不表示拖放目标已经执行了移动操作。它实际上是在告诉你,拖放目标希望你删除原始对象。如果拖放目标能够直接删除(或移动)原始对象,那么你就不会得到 `DROPEFFECT_MOVE`。

        (一个特殊情况是,如果用户将对象拖拽到一个“回收站”图标上,其目的是销毁任何拖放到它上面的东西。在这种情况下,回收站会在没有复制对象的情况下返回 `DROPEFFECT_MOVE`,结果就是对象被删除。`DROPEFFECT_MOVE` 一个更恰当的名称可能是 `DROPEFFECT_DELETEORIGINAL`。)

        如果数据对象代表一个文件,Shell 通常能够很好地处理将文件移动到目标位置而不是复制后再要求你删除原始对象。你通常只会在数据对象代表非文件对象时得到 `DROPEFFECT_MOVE`,因为那时 Shell 不知道如何删除原始对象。

        但如果你想要确切地知道操作是否为移动操作,而不考虑拖放目标是否进行了优化处理?

        我们将在下一篇文章中探讨这个问题。

顺便提一句,如果你移动了临时文件,别忘了将其移回原位置,这样你就可以再次运行示例程序了!


第三部分:检测优化的移动操作

2004年12月8日

        我们正在考虑如何检测拖放操作是否在概念上导致了移动操作,即使 `DROPEFFECT_MOVE` 被优化掉了。

        如果拖放目标是 Shell,你可以查询数据对象的 `CFSTR_PERFORMEDDROPEFFECT` 来了解实际执行的效果是什么。

void OnLButtonDown(HWND hwnd, BOOL fDoubleClick,
                   int x, int y, UINT keyFlags) {
  …
        if (dwEffect & DROPEFFECT_MOVE) {
          DeleteFileW(wszPath);
        }
        CheckPerformedEffect(hwnd, pdto);
  …
}

// ……(此处省略了 CheckPerformedEffect 函数的实现细节)

        当然,我们还需要 `CheckPerformedEffect` 函数来执行这一检查。

void CheckPerformedEffect(HWND hwnd, IDataObject *pdto) {
  FORMATETC fe = {
     (CLIPFORMAT)RegisterClipboardFormat(CFSTR_PERFORMEDDROPEFFECT),
     NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
  STGMEDIUM stgm;
  if (SUCCEEDED(pdto->GetData(&fe, &stgm))) {
    if ((stgm.tymed & TYMED_HGLOBAL) &&
        GlobalSize(stgm.hGlobal) >= sizeof(DWORD)) {
       DWORD *pdw = (DWORD*)GlobalLock(stgm.hGlobal);
       if (pdw) {
         if (*pdw == DROPEFFECT_MOVE) {
            MessageBox(hwnd, TEXT(“已移动”), TEXT(“提示”), MB_OK);
         }
         GlobalUnlock(stgm.hGlobal);
       }
    }
    ReleaseStgMedium(&stgm);
  }
}

        如果项目被拖放到 Shell 窗口上,拖放目标会在数据对象中设置 `CFSTR_PERFORMEDDROPEFFECT` 剪贴板格式名下的数据。数据以 `HGLOBAL` 中的 `DWORD` 形式存在,其值是在任何优化开始之前的实际拖放效果。

        在这里,我们检查是否执行了 `DROPEFFECT_MOVE`,并在确实移动时显示一个特别的消息。


第四部分:添加一个更美观的拖拽图标

2004年12月9日

        你可能已经注意到,当前的拖拽反馈视觉效果相当简陋,仅显示一个方框,可能带有一个加号或箭头,你甚至无法辨认正在拖拽的是什么。

        让我们来改进这一点,通过拖拽文件的图标来实现。

        为此,我们需要将拖拽图像添加到数据对象中。

void OnLButtonDown(HWND hwnd, BOOL fDoubleClick, int x, int y, UINT keyFlags) {
  IDataObject *pdto;
  if (SUCCEEDED(GetDataObjectOfFileWithCuteIcon(
                hwnd, g_pszTarget, &pdto))) {
     IDropSource *pds = new CDropSource();
     …

        这个新的 `GetDataObjectOfFileWithCuteIcon`函数负责创建数据对象,并将一个吸引人的图标附加到它上面。

HRESULT GetDataObjectOfFileWithCuteIcon(HWND hwnd,
 LPCWSTR pszPath, IDataObject **ppdto) {
  HRESULT hr = GetUIObjectOfFile(hwnd, pszPath,
                    IID_IDataObject, (void**)ppdto);
  if (SUCCEEDED(hr)) {
    IDragSourceHelper *pdsh;
    if (SUCCEEDED(CoCreateInstance(CLSID_DragDropHelper, NULL, CLSCTX_ALL,
                                   IID_IDragSourceHelper, (void**)&pdsh))) {
      SHDRAGIMAGE sdi;
      if (CreateDragImage(pszPath, &sdi)) {
        pdsh->InitializeFromBitmap(&sdi, *ppdto);
        DeleteObject(sdi.hbmpDragImage);
      }
      pdsh->Release();
    }
  }
  return hr;
}

// ……(此处省略了 CreateDragImage 函数的实现细节)

        我们利用 Shell 拖放辅助对象将位图附加到数据对象。该辅助对象要求数据对象能够接受任意的数据块,幸运的是,标准的 Shell 数据对象已经支持这一点。

        生成拖拽图像的过程并不令人愉快,而且你也不会从这个函数中学到什么。它只是一个必须完成的任务。

BOOL CreateDragImage(LPCWSTR pszPath, SHDRAGIMAGE *psdi) {
  psdi->hbmpDragImage = NULL;
  SHFILEINFOW sfi;
  HIMAGELIST himl = (HIMAGELIST)
    SHGetFileInfoW(pszPath, 0, &sfi, sizeof(sfi), SHGFI_SYSICONINDEX);
  if (himl) {
    int cx, cy;
    ImageList_GetIconSize(himl, &cx, &cy);
    psdi->sizeDragImage.cx = cx;
    psdi->sizeDragImage.cy = cy;
    psdi->ptOffset.x = cx;
    psdi->ptOffset.y = cy;
    psdi->crColorKey = CLR_NONE;
    HDC hdc = CreateCompatibleDC(NULL);
    if (hdc) {
      psdi->hbmpDragImage = CreateBitmap(cx, cy, 1, 32, NULL);
      if (psdi->hbmpDragImage) {
        HBITMAP hbmPrev = SelectBitmap(hdc, psdi->hbmpDragImage);
        ImageList_Draw(himl, sfi.iIcon, hdc, 0, 0, ILD_NORMAL);
        SelectBitmap(hdc, hbmPrev);
      }
      DeleteDC(hdc);
    }
  }
  return psdi->hbmpDragImage != NULL;
}

        创建拖拽图像时,我们调用 SHGetFileInfo 函数获取代表文件的图标的图像列表句柄和图标索引。图像列表中的图标尺寸用于设置 SHDRAGIMAGE 结构中的位图尺寸和光标位置(我们将光标放在图像的右下角)。由于我们创建的是一个具有 Alpha 混合的位图,因此不需要颜色键。最后,我们创建一个内存设备上下文(DC)来容纳 ARGB 位图,以便在其中绘制图标。

        如果你运行这个程序,你将看到文本文件的图标在屏幕上被拖拽时的效果。

        在下一篇文章中,我们将探讨如何让别人为你承担一些繁重的工作。


第五部分:让别人为你承担繁重的工作

2004年12月10日

        创建拖拽图像确实需要一些工作。不过,幸运的是,列表视图(ListView)控件愿意为我们分担一些工作。

        我们不再需要 OnLButtonDown 函数(以及相应的 HANDLE_MESSAGE 宏)。相反,我们将让列表视图控件负责所有的呈现工作。

BOOL OnCreate(HWND hwnd, LPCREATESTRUCT lpcs) {
  g_hwndChild = CreateWindow(WC_LISTVIEW, NULL,
                             WS_CHILD | WS_VISIBLE | LVS_ICON |
                             LVS_SHAREIMAGELISTS, // 13 Dec 添加的标志
                             0, 0, 0, 0,
                             hwnd, (HMENU)1, g_hinst, 0);
  if (!g_hwndChild) return FALSE;
  SHFILEINFOW sfi;
  HIMAGELIST himl = (HIMAGELIST)
    SHGetFileInfoW(g_pszTarget, 0, &sfi, sizeof(sfi),
                   SHGFI_SYSICONINDEX |
                   SHGFI_DISPLAYNAME | SHGFI_LARGEICON);
  if (!himl) return FALSE;
  ListView_SetImageList(g_hwndChild, himl, LVSIL_NORMAL);
  LVITEM item;
  item.iSubItem = 0;
  item.mask = LVIF_TEXT | LVIF_IMAGE;
  item.pszText = sfi.szDisplayName;
  item.iImage = sfi.iIcon;
  if (ListView_InsertItem(g_hwndChild, &item) < 0)
    return FALSE;
  return TRUE;
}

// ……(此处省略了 OnBeginDrag 和 OnNotify 的实现细节)

        现在,我们让列表视图控件来管理图标、文本以及其他所有相关的用户界面元素。我们还可以让它来管理拖拽图像。

void OnBeginDrag(HWND hwnd, NMLISTVIEW *plv) {
  IDataObject *pdto;
  if (SUCCEEDED(GetUIObjectOfFile(hwnd, g_pszTarget,
                   IID_IDataObject, (void**)&pdto))) {
    IDragSourceHelper *pdsh;
    if (SUCCEEDED(CoCreateInstance(CLSID_DragDropHelper, NULL,
                    CLSCTX_ALL, IID_IDragSourceHelper, (void**)&pdsh))) {
      pdsh->InitializeFromWindow(g_hwndChild, &plv->ptAction, pdto);
      pdsh->Release();
    }
    IDropSource *pds = new CDropSource();
    if (pds) {
      DWORD dwEffect;
      if (DoDragDrop(pdto, pds, DROPEFFECT_MOVE |
                     DROPEFFECT_COPY | DROPEFFECT_LINK,
                     &dwEffect) == DRAGDROP_S_DROP &&
          (dwEffect & DROPEFFECT_MOVE)) {
        DeleteFileW(g_pszTarget);
      }
      pds->Release();
    }
    pdto->Release();
  }
}

LRESULT OnNotify(HWND hwnd, int idCtrl, NMHDR *pnm) {
  if (idCtrl == 1) {
    NMLISTVIEW *plv;
    switch (pnm->code) {
    case LVN_BEGINDRAG:
      plv = (NMLISTVIEW*)pnm;
      OnBeginDrag(hwnd, plv);
      break;
    }
  }
  return 0;
}

// ……(此处省略了消息处理宏 HANDLE_MSG 的调用)

        我们不再直接检测拖拽操作,而是让列表视图控件来处理,并等待 LVN_BEGINDRAG 通知。在接收到通知后,我们获取要拖拽文件的数据对象,并让列表视图通过其窗口句柄创建拖拽图像。

        列表视图控件负责生成拖拽图像并将其设置到数据对象中。在特定情况下,这可能是一个更简便的方法,尤其是当你在列表视图中启用了多选功能时,使用 IDragSourceHelper::InitializeFromWindow 方法可以大大节省工作量,因为列表视图会负责生成你在资源管理器中拖拽多个文件时看到的径向渐变 Alpha 通道。

        你可能会注意到列表视图生成的图标周围有一些颜色边缘,这是因为我们使用的是公共控件的版本 5,它对 Alpha 通道的支持不是很完美。如果你升级到版本 6,你会发现边缘消失了,图标看起来也更加精致。

        这就是关于如何启动拖放操作的全部内容。接下来,我们将回到每天一个主题的讨论。