Search code examples
delphidrag-and-dropc++builderolevirtualtreeview

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


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?


Solution

  • 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