从 VirtualTreeView 拖放到 shell(Ole 拖放)

Drag and drop from VirtualTreeView to shell (Ole drag and drop)

我正在尝试从 VirtualTreeView 拖放以在 shell 中创建文件(从 VirtualTreeView 拖放至文件资源管理器或桌面文件夹中的文件夹)。

我只找到了相反的例子(shell 到 VirtualTreeView),但我找不到任何这样做的例子。帮忙?

在 Windows 中执行任何拖放操作都涉及创建一个 IDataObject,并将该对象提供给 Windows。

Virtual Treeview 为您处理大量繁重的工作,创建一个为您实现 IDataObject 的对象。当您需要帮助填充它时,树就会引发事件。

当通过复制粘贴或拖放传递 "file-like" 内容时,您需要向 IDataObject 添加两种剪贴板格式:

  • CF_FILEDESCRIPTOR
  • CF_FILECONTENTS

除了支持虚拟树本身会添加的格式外,您还可以选择表示支持更多的剪贴板格式。

OnGetUserClipboardFormats 事件

在这个事件中,您有机会向 IDataObject 树将要创建的 IDataObject 添加其他剪贴板格式:

procedure TForm1.lvAttachmentsGetUserClipboardFormats(Sender: TBaseVirtualTree;
  var Formats: TFormatEtcArray);
var
    i: Integer;
begin
    //Add formats for CF_FILEDESCRIPTOR and CF_FILECONTENTS
    i := Length(Formats);
    SetLength(Formats, i + 1);
    Formats[i].cfFormat := CF_FILEDESCRIPTOR;
    Formats[i].ptd := nil;
    Formats[i].dwAspect := DVASPECT_CONTENT;
    Formats[i].lindex := -1;
    Formats[i].tymed := TYMED_HGLOBAL;

    i := Length(Formats);
    SetLength(Formats, i + 1);
    Formats[i].cfFormat := CF_FILECONTENTS;
    Formats[i].ptd := nil;
    Formats[i].dwAspect := DVASPECT_CONTENT;
    Formats[i].lindex := 0;
    Formats[i].tymed := TYMED_ISTREAM;
end;

然后树会将 IDataObject 作为拖放操作的一部分提供给 shell。

稍后,用户将项目拖放到的应用程序将枚举 IDataObject 中的所有格式,例如:

  • CF_HTML ("HTML Format")
  • CFSTR_FILEDESCRIPTOR ("FileGroupDescriptorW")
  • CFSTR_FILECONTENTS ("FileContents")
  • CF_ENHMETAFILE

它会看到 IDataObject 包含 FileDescriptorFileContents.

然后,接收应用程序将要求 IDataObject 实际发出数据。 (这 "delayed-rendering" 是一件好事,这意味着您的源应用程序实际上不必读取任何内容,除非它确实被请求)。

OnRenderOleData 事件

这是虚拟树意识到其 IDataObject 已被要求呈现某些内容的事件,它需要您最终呈现该实际内容。

这两种剪贴板格式的总体思路是:

  • CF_FILEDESCRIPTOR 可让您 return 描述类似文件的记录(例如文件名、文件大小、创建日期、最后修改日期、最后访问日期)
  • CF_FILECONTENTS 让你 return 一个包含实际文件内容的 IStream
procedure TForm1.lvAttachmentsRenderOLEData(Sender: TBaseVirtualTree; const FormatEtcIn: tagFORMATETC;
  out Medium: tagSTGMEDIUM; ForClipboard: Boolean; var Result: HRESULT);
var
    global: HGLOBAL;
    stm: IStream;
begin
    if FormatEtcIn.cfFormat = CF_FILEDESCRIPTOR then
    begin
        global := GetAttachmentFileDescriptorsFromListView(lvAttachments, ForClipboard);
        if global = 0 then
            Exit;
        ZeroMemory(@Medium, SizeOf(Medium));
        Medium.tymed := TYMED_HGLOBAL;
        Medium.hGlobal := global;
        Result := S_OK;
    end
    else if FormatEtcIn.cfFormat = CF_FILECONTENTS then
    begin
        ZeroMemory(@Medium, SizeOf(Medium));
        Medium.tymed := TYMED_ISTREAM;
        Result := GetAttachmentStreamFromListView(lvAttachments, ForClipboard, FormatEtcIn.lindex, stm);
        if Failed(Result) then
            Exit;
        Medium.stm := Pointer(stm);
        IUnknown(Medium.stm)._AddRef;
        Result := S_OK;
    end;
end;

第一个辅助函数创建一个 FILE_DESCRIPTOR 对象数组,并将它们复制到 HGLOBAL 分配的内存中:

function GetAttachmentFileDescriptorsFromListView(Source: TVirtualStringTree; ForClipboard: Boolean): HGLOBAL;
var
    i: Integer;
    nCount: Integer;
    nodes: TNodeArray;
    descriptors: TFileDescriptorDynArray; 
    data: TAttachment;
begin
    Result := 0;

   if ForClipboard then
      nodes := Source.GetSortedCutCopySet(False)
   else
      nodes := Source.GetSortedSelection(False);

   if Length(nodes) = 0 then
      Exit;

   nCount := 0;
   for i := 0 to Length(nodes) - 1 do
   begin
        //Get the file thing from this node
        data := GetNodeDataFromNode(nodes[i]);
        if not Assigned(data) then
            Continue;

        //Increase the size of our descriptors array by one
        Inc(nCount);
        SetLength(Descriptors, nCount);

        //Fill in the next descriptor
        descriptors[nCount-1] := data.ToWindowsFileDescriptor;
   end;

   Result := FileDescriptorsToHGLOBAL(descriptors);
end;

第二个助手将你的类似文件的二进制内容复制到 IStream:

function GetAttachmentStreamFromListView(Source: TVirtualStringTree; ForClipboard: Boolean; lindex: Integer; var stm: IStream): HResult;
var
    nodes: TNodeArray;
    data: TAttachment;
begin
   Result := E_FAIL;

   if ForClipboard then
      nodes := Source.GetSortedCutCopySet(False)
   else
      nodes := Source.GetSortedSelection(False);

   if Length(nodes) = 0 then
      Exit;

   if (lIndex < Low(Nodes)) or (lIndex > High(Nodes)) then
    begin
      Result := DV_E_LINDEX;
      Exit;
   end;

   //Get the file thing from this node
   data := GetNodeDataFromNode(nodes[i]);
   if not Assigned(data) then
      Continue;

    //Fetch the content into a IStream wrapped memory stream
    stm := data.GetStream(nil);
    Result := S_OK;
end;

你的附件对象,无论它是什么,都必须知道:

  • 如何将自己表示为 TFileDescriptor
  • 如何 return 将内容作为 IStream