1
votes

I am trying to drag and drop from VirtualTreeView to create a file in shell (drag and drop from VirtualTreeView to a folder in File Explorer or desktop folder).

I only found example of doing the opposite (shell to VirtualTreeView), but I cannot find any example for doing that. Help?

1

1 Answers

3
votes

Doing any drag-drop operations in Windows involves creating an IDataObject, and giving that object to Windows.

The Virtual Treeview handles a lot of that grunt-work for you, creating an object that implements IDataObject for you. The tree then raises events when you need to help populate it.

When passing "file-like" things through a copy-paste or a drag-drop, you are require to add two clipboard formats to the IDataObject:

  • CF_FILEDESCRIPTOR, and
  • CF_FILECONTENTS

In addition to support for formats that the virtualtree itself will add, you can choose to indicate support for more clipboard format.

OnGetUserClipboardFormats Event

This is the event where you are given a chance to add additional clipboard formats to the IDataObject that the tree will be creating:

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;

The tree will then given the IDataObject to the shell as part of the drag-drop operation.

Later, an application that the user dropped items onto will enumerate all formats in the IDataObject, e.g.:

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

And it will see that the IDataObject contains FileDescriptor and FileContents.

The receiving application will then ask the IDataObject to actually cough up data. (This "delayed-rendering" is a good thing, it means your source application doesn't actually have to read any content unless it actually gets requested).

OnRenderOleData Event

This is the event where the virtual tree realizes its IDataObject has been asked to render something, and it needs you to finally render that actual content.

The general idea with these two clipboard formats is:

  • CF_FILEDESCRIPTOR lets you return a record that describes the file-like thing (e.g. filename, file size, created date, last modified date, last accessed date)
  • CF_FILECONTENTS lets you return an IStream that contains the actual file contents
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;

The first helper function creates an array of FILE_DESCRIPTOR objects, and copies them to a HGLOBAL allocated memory:

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;

The second helper copies your file-like thing's binary contents to an 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;

Your attachment object, whatever it is has to know:

  • how to represent itself as a TFileDescriptor
  • how to return the contents as an IStream