WildByDesign Posted January 30 Posted January 30 Just in case you guys want to test what I have implemented so far, I've created a branch shell-dodragdrop for the Files Au3 project for testing this stuff. If you want to use it as a testbed for playing with any of your drag and drop code, it can make for a good testbed. For the purpose of testing, I only have DROPEFFECT_COPY and DROPEFFECT_LINK enabled for the drag and drop. I have tested it with DROPEFFECT_MOVE as well and it works great since the OS makes the logical decisions on whether to copy, move, etc. But for the sake of no surprises, I have removed DROPEFFECT_MOVE for testing. Drag and drop from the TreeView and ListView are both working great to external programs like File Explorer. Drag and drop to (or within) Files Au3 is not working yet because I don't know how to do the drop target interfaces or RegisterDragDrop function. I am going to try to play around with RegisterDragDrop today though. MattyD 1
MattyD Posted January 30 Posted January 30 (edited) Ok, work in progress - here are the bones of a DropTarget Object. (for receiving files) I'm stashing the incoming pDataObject internally in our DropTarget when the mouse enters the listview. At some point we need to GetData() from that to extract the file names. If we want to drop items halfway down, I assume we're meant to LVM_HITTEST to determine which LV item we're currently hovering over. Then it'll be a LVM_SETINSERTMARK to display the insertion mark. Edit: In hindsight, I probably should've removed the WS_EX_ACCEPTFILES style for this demo! (and our WM_DROPFILES handler by association). Edit 2: snipped attachment to claw back space! Updated example further down... Edited January 31 by MattyD WildByDesign 1
WildByDesign Posted January 30 Posted January 30 1 hour ago, MattyD said: Ok, work in progress - here are the bones of a DropTarget Object. (for receiving files) This is really great progress, Matty. So much of the older code for drag and drop here in the forum was made for x86 at the time. Your work (and @jugador) is going to benefit many users by giving proper methods for modern Windows OS. This help thread has ended up being very beneficial. 1 hour ago, MattyD said: Edit: In hindsight, I probably should've removed the WS_EX_ACCEPTFILES style for this demo! (and our WM_DROPFILES handler by association). Good point. We wont be needing those older techniques anymore. This is really quite exciting. Regarding IDropTargetHelper interface and drag images: Quote The IDragSourceHelper and IDropTargetHelper interfaces are exposed by the drag-image manager object to allow the IDropTarget interface to use custom drag images. To use either of these interfaces, you must create an in-process server drag-image manager object by calling CoCreateInstance with a class identifier (CLSID) of CLSID_DragDropHelper. Get interface pointers using standard Component Object Model (COM) procedures. Hopefully this isn't something that would be limited by AutoIt lack of threads and such. Fingers crossed. 🤞 I like how we can control on our end whether to allow move, copy, etc.
jugador Posted January 30 Posted January 30 Updated version of GetDataObjectOfFile_C. Added IDragSourceHelper which solves the icon visual glitch. expandcollapse popup#include <WinAPIShellEx.au3> Func GetDataObjectOfFile_C(ByRef $sPath) If UBound($sPath) = 0 Then Return 0 Local $tIID_IDataObject = _WinAPI_GUIDFromString($sIID_IDataObject) Local $pIDataObject = __SHCreateDataObject($tIID_IDataObject, 0, 0, 0, 0) If Not $pIDataObject Then Return 0 Local $CLSID_DragDropHelper = "{4657278A-411B-11D2-839A-00C04FD918D0}" Local $IID_IDragSourceHelper = "{DE5BF786-477A-11D2-839D-00C04FD918D0}" Local $tag_IDragSourceHelper = "InitializeFromBitmap hresult(ptr;ptr);InitializeFromWindow hresult(hwnd;ptr;ptr);" Local $oDragHelper = ObjCreateInterface($CLSID_DragDropHelper, $IID_IDragSourceHelper, $tag_IDragSourceHelper) If Not IsObj($oDragHelper) Then _Release($pIDataObject) Return 0 Endif Local Const $tag_IDataObject = _ "GetData hresult(ptr;ptr*);" & _ "GetDataHere hresult(ptr;ptr*);" & _ "QueryGetData hresult(ptr);" & _ "GetCanonicalFormatEtc hresult(ptr;ptr*);" & _ "SetData hresult(ptr;ptr;bool);" & _ "EnumFormatEtc hresult(dword;ptr*);" & _ "DAdvise hresult(ptr;dword;ptr;dword*);" & _ "DUnadvise hresult(dword);" & _ "EnumDAdvise hresult(ptr*);" Local $oIDataObject = ObjCreateInterface($pIDataObject, $sIID_IDataObject, $tag_IDataObject) If Not IsObj($oIDataObject) Then _Release($pIDataObject) Return 0 Endif Local $tFORMATETC, $tSTGMEDIUM __Fill_tag_FORMATETC($tFORMATETC) __Fill_tag_STGMEDIUM($tSTGMEDIUM, $sPath) $oIDataObject.SetData(DllStructGetPtr($tFORMATETC), DllStructGetPtr($tSTGMEDIUM), 1) Local $hBitmap = __GetFileIcon_as_Bitmap($sPath[0]) Local $tSHDRAGIMAGE = DllStructCreate("int sizeX; int sizeY; int ptX; int ptY; ptr hbmp; uint crColorKey") __CreateDragImageStruct($tSHDRAGIMAGE, $hBitmap, 96, 96) $oDragHelper.InitializeFromBitmap(DllStructGetPtr($tSHDRAGIMAGE), $pIDataObject) _AddRef($pIDataObject) Return $pIDataObject EndFunc Func __CreateDragImageStruct(Byref $tSHDRAGIMAGE, $hBitmap, $iWidth, $iHeight) DllStructSetData($tSHDRAGIMAGE, "sizeX", $iWidth) DllStructSetData($tSHDRAGIMAGE, "sizeY", $iHeight) DllStructSetData($tSHDRAGIMAGE, "ptX", 0) DllStructSetData($tSHDRAGIMAGE, "ptY", 0) DllStructSetData($tSHDRAGIMAGE, "hbmp", $hBitmap) DllStructSetData($tSHDRAGIMAGE, "crColorKey", 0x00FF00FF) EndFunc Func __GetFileIcon_as_Bitmap($sPath) Local $tSHFILEINFO = DllStructCreate($tagSHFILEINFO) _WinAPI_ShellGetFileInfo($sPath, BitOR($SHGFI_ICON, $SHGFI_USEFILEATTRIBUTES, $SHGFI_LARGEICON), 0, $tSHFILEINFO) Local $hIcon = DllStructGetData($tSHFILEINFO, 'hIcon') Local $hBitmap = __IconToBitmap($hIcon, 96) _WinAPI_DestroyIcon($hIcon) Return $hBitmap EndFunc Func __IconToBitmap($hIcon, $iSize = 32) Local $hDestDC = _WinAPI_GetDC(0) Local $hMemDC = _WinAPI_CreateCompatibleDC($hDestDC) Local $hBitmap = _WinAPI_CreateCompatibleBitmap($hDestDC, $iSize, $iSize) Local $hOldObj = _WinAPI_SelectObject($hMemDC, $hBitmap) _WinAPI_DrawIconEx($hMemDC, 0, 0, $hIcon, $iSize, $iSize) _WinAPI_SelectObject($hMemDC, $hOldObj) _WinAPI_DeleteDC($hMemDC) _WinAPI_ReleaseDC(0, $hDestDC) Return $hBitmap EndFunc Func __Fill_tag_FORMATETC(Byref $tFORMATETC) Local Const $CF_HDROP = 15 Local Const $TYMED_HGLOBAL = 1 $tFORMATETC = DllStructCreate("ushort cfFormat; ptr ptd; uint dwAspect; int lindex; uint tymed") DllStructSetData($tFORMATETC, "cfFormat", $CF_HDROP) DllStructSetData($tFORMATETC, "dwAspect", 1) DllStructSetData($tFORMATETC, "lindex", -1) DllStructSetData($tFORMATETC, "tymed", $TYMED_HGLOBAL) EndFunc Func __Fill_tag_STGMEDIUM(Byref $tSTGMEDIUM, Byref $aFiles) Local Const $CF_HDROP = 15 Local Const $TYMED_HGLOBAL = 1 Local $sFileList = "" For $i = 0 To UBound($aFiles) - 1 $sFileList &= $aFiles[$i] & Chr(0) Next $sFileList &= Chr(0) Local $iSize = 20 + (StringLen($sFileList) * 2) Local $hGlobal = DllCall("kernel32.dll", "ptr", "GlobalAlloc", "uint", 0x2042, "ulong_ptr", $iSize)[0] Local $pLock = DllCall("kernel32.dll", "ptr", "GlobalLock", "ptr", $hGlobal)[0] Local $tDROPFILES = DllStructCreate("dword pFiles; int x; int y; bool fNC; bool fWide", $pLock) DllStructSetData($tDROPFILES, "pFiles", 20) DllStructSetData($tDROPFILES, "fWide", True) Local $tPaths = DllStructCreate("wchar[" & StringLen($sFileList) & "]", $pLock + 20) DllStructSetData($tPaths, 1, $sFileList) DllCall("kernel32.dll", "bool", "GlobalUnlock", "ptr", $hGlobal) $tSTGMEDIUM = DllStructCreate("uint tymed; ptr hGlobal; ptr pUnkForRelease") DllStructSetData($tSTGMEDIUM, "tymed", $TYMED_HGLOBAL) DllStructSetData($tSTGMEDIUM, "hGlobal", $hGlobal) DllStructSetData($tSTGMEDIUM, "pUnkForRelease", 0) EndFunc Func __SHCreateDataObject($tIID_IDataObject, $ppidlFolder = 0, $cidl = 0, $papidl = 0, $pdtInner = 0) Local $aRes = DllCall("shell32.dll", "long", "SHCreateDataObject", _ "ptr", $ppidlFolder, _ "uint", $cidl, _ "ptr", $papidl, _ "ptr", $pdtInner, _ "struct*", $tIID_IDataObject, _ "ptr*", 0) If @error Then Return SetError(1, 0, $aRes[0]) Return $aRes[6] EndFunc MattyD and WildByDesign 2
WildByDesign Posted January 30 Posted January 30 2 hours ago, jugador said: Updated version of GetDataObjectOfFile_C. Added IDragSourceHelper which solves the icon visual glitch. This works perfectly. Thank you.
MattyD Posted January 31 Posted January 31 (edited) Thanks @jugador , I'll add in the helper at some point. This is today's effort: A couple of fixups, and Drag/Drop between TreeViews and ListViews - featuring insertion marks. Edit: re-uploaded example - items weren't always being inserted at insertion mark Edit 2: afternoon update! - Included helper for the DropTarget, so we now get a fancy image going from Explorer -> Control. Edit 3: Using SHDoDragDrop instead of DoDragDrop seems to automatically create our drag images when going from control -> Wherever... and as mentioned earlier, it allows us to knock off our custom DropSource object too (unless @WildByDesign you're still planning to hook its methods?). I'll probably circle back to the multi-file thing next. I realise we have a method, but Ideally I'd like to generate a IDataObject for a list of files rather than manually setting up the hdrop. So I'll see how far I get with that. _Demo2.zip Edited January 31 by MattyD argumentum 1
WildByDesign Posted January 31 Posted January 31 7 hours ago, MattyD said: Edit 2: afternoon update! - Included helper for the DropTarget, so we now get a fancy image going from Explorer -> Control. This is really exciting. It works very well and the OS is showing the nice drag image (high DPI too) plus the tooltip that shows Copy, Move, etc. which is helpful. 7 hours ago, MattyD said: Edit 3: Using SHDoDragDrop instead of DoDragDrop seems to automatically create our drag images when going from control -> Wherever... and as mentioned earlier, it allows us to knock off our custom DropSource object too (unless @WildByDesign you're still planning to hook its methods?). I'm glad that you switched to SHDoDragDrop because it has worked much better in all of my testing regarding drag images. I was hooking into the GiveFeedback, DragEnterTarget and DragLeaveTarget of the DropSource with good success. But I am pretty confident that we can get rid of it and let the OS manage that part. Since you are able to use the new DropTarget for DoInsertMark successfully, I should actually be able to hook into that instead. In my use case, I would be doing drag and drop into directories. So instead of using InsertMark, I would be getting the TreeView or ListView item under the cursor and, if it is a directory, ensure that drophilite is applied to whichever directory is currently hot during drag. And only for our window handle. Any other target windows will have their own methods, of course. By the way, in the SHDoDragDrop function, you could do the following: Local $aCall = DllCall("Shell32.dll", "long", "SHDoDragDrop", "hwnd", Null, "ptr", $pDataObj, "ptr", Null, "dword", $iOKEffects, "ptr*", 0) Since our window is not providing the drag image (the OS is), we can just put Null for hWnd since it is not really getting used anyway. And same for putting Null for $pDropSource since the OS is doing it. It seems to work either way just the same. Issue: The one and only issue that I noticed that is a bit problematic is that the very first drag image always (almost always) seems to be blank. And every drag after that shows the correct drag image. It seems to be only when the drag originates from our window/control. MattyD 1
MattyD Posted January 31 Posted January 31 4 minutes ago, WildByDesign said: first drag image always (almost always) seems to be blank yeah I've noticed this is a bit sporadic - I wouldn't say the "first one" specifically is more troublesome for me though. I found items from explorer randomly lose their image too (even when our app isn't involved), so not sure how fixable this will be. For explorer, I think I can mainly reproduce it by dragging items fast outside their window - but its a bit hit and miss.
WildByDesign Posted January 31 Posted January 31 (edited) I think I have an idea. Some Explorer++ source: link // DropTargetInternal // Note that, as described above, this window is registered as a drop target only so that drag // images are shown consistently. Dropping items isn't supported at all. DWORD MainWindow::DragEnter(IDataObject *dataObject, DWORD keyState, POINT pt, DWORD effect) { UNREFERENCED_PARAMETER(dataObject); UNREFERENCED_PARAMETER(keyState); UNREFERENCED_PARAMETER(pt); UNREFERENCED_PARAMETER(effect); return DROPEFFECT_NONE; They register the whole GUI likely to prevent this issue. And it seems they return DROPEFFECT_NONE for the main GUI handle, and likely return different effects for the ListView and TreeView. EDIT: Added code comment from their main window: // The main window is registered as a drop target only so that the drag image will be // consistently shown when an item is being dragged. Edited January 31 by WildByDesign MattyD 1
MattyD Posted January 31 Posted January 31 (edited) worth a shot Edit - Wait, are they just eluding to the cursor turning into a "no" icon, rather than the pretty pic when outside a drop target (within the gui)? If its just that I probably wont bother! Edited January 31 by MattyD WildByDesign 1
WildByDesign Posted January 31 Posted January 31 10 minutes ago, MattyD said: Edit - Wait, are they just eluding to the cursor turning into a "no" icon, rather than the pretty pic when outside a drop target (within the gui)? If its just that I probably wont bother! // The main window is registered as a drop target only so that the drag image will be // consistently shown when an item is being dragged. For the drag image to be shown, the // relevant IDropTargetHelper methods need to be called during the drag. To do that, the window // under the mouse needs to be registered as a drop target. // Rather than having to register every window, the top-level window can simply be registered // instead. That way, it will act as a fallback if there isn't a more specific child window // registered. It sounds like they are specifically referring to the drag images not showing consistently.
MattyD Posted January 31 Posted January 31 I read "consistently shown" as their image never disappears because they're always over a droptarget. It just gets a little "no symbol" added off the bottom right of the cursor. In ours, the image disappears entirely and we replace the cursor. I'm betting this probably won't help us if the image is broken at the initial drag - but I guess we should look at it anyway from an aethetics point of view...
WildByDesign Posted January 31 Posted January 31 I just found the initial commit related to it. It took me a bit because the code searches weren't coming up with it. It describes better what was happening and how it was fixed. His comments are always super thorough. Commit: link Quote Register main window as drop target The main window doesn't actually accept any drop items, but this has the advantage that a drag item is show consistently when a drag is taking place. Previously, if a drag item had an image, the image wouldn't always be shown. If the item was dragged over a window that wasn't registered as a drop target, the image would revert to just a cursor. This meant that, as you dragged an item over different child windows: - The image would be shown and the cursor would indicate that a drop was possible. - The image would be shown and the cursor would indicate that a drop wasn't possible. - The image wouldn't be shown and the cursor would indicate that a drop wasn't possible. By registering the main window as a drop target (and calling the appropriate IDropTargetHelper methods), the image is always shown, meaning that there's more consistency in the case where you can't actually perform a drop. MattyD 1
WildByDesign Posted January 31 Posted January 31 6 minutes ago, MattyD said: I'm betting this probably won't help us if the image is broken at the initial drag - but I guess we should look at it anyway from an aethetics point of view... I am honestly not too sure. I mean, it only seems to happen to me on the first drag. Every day after that works consistently for me. So I'm not certain why that is.
WildByDesign Posted January 31 Posted January 31 @MattyD I finally got a chance to sit down and add your latest code to the Files Au3 project for testing. I especially wanted to test the new drop target stuff. The weird thing is that when dragging from the TreeView or ListView, the drag image is working 100% of the time. I was expecting it to not work on the first drag. I'm using your GetDataObjectOfFile function from a previous demo. It looks like it has changed recently, so that means I am using a slightly older one. And my ListView is using @jugador's GetDataObjectOfFile_B function for multiple selection. I just tested it with your most recent GetDataObjectOfFile right now while testing and it still worked 100% of the time. This is very strange. I'll try to see why it fails on the first drag for your Demo GUI.
WildByDesign Posted February 1 Posted February 1 In the Files Au3 project, I've got the DropTargetObject stuff working mostly. For visual feedback to the user (and similar to File Explorer and Explorer++), I use _GUICtrlListView_SetHotItem when dragging items over the ListView and set the item state for DROPHILITE for the TreeView. Visually, everything looks fantastic. But the I realized... "Oh man, I need to be providing info from the DropTargetObject back somehow." For example, let's assume that were are dragging a single file over a bunch of folders. While dragging over the folders, the tooltip portion of the drag image (eg. Copy to Downloads) would normally update with the folder name that is under the cursor during the drag. Also, the folder (full path) would need to be provided from the DropTargetObject for the drop. At least that is my understanding of it. I can get the full path from the TreeView or ListView item being dragged. But I don't know how to have that path provided back to the drag manager. Does anyone know how to do that?
MattyD Posted February 1 Posted February 1 OK its not too bad, it looks like we update the CFSTR_DROPDESCRIPTION entry in the dataobject. I've added a crude example in the DragOver method on the droptarget. Obviously we should change the type/message based on the returned drageffect etc. _Demo2.zip WildByDesign 1
jugador Posted February 1 Posted February 1 (edited) Tried coding IDropTarget with my limited knowledge. Hope it will work. expandcollapse popup#Include "DragDrop.au3" #include <GuiListView.au3> #Include <WindowsConstants.au3> #include <GUIConstantsEx.au3> Global $idListview __Example_A() Func __Example_A() Local $hMGui = GUICreate("listview items", 350, 250) $idListview = GUICtrlCreateListView("col1", 20, 20, 300, 200, BitOR($LVS_REPORT, $LVS_SHOWSELALWAYS)) __RegisterDragDrop(GUICtrlGetHandle($idListview)) GUISetState(@SW_SHOW) GUIRegisterMsg($WM_NOTIFY, "WM_NOTIFY") GUIRegisterMsg($WM_DROP, "WM_DROPFILES_FUNC") While 1 Switch GUIGetMsg() Case $GUI_EVENT_CLOSE ExitLoop EndSwitch WEnd __RevokeDragDrop(GUICtrlGetHandle($idListview)) __Cancel_IDropTarget() GUIDelete($hMGui) EndFunc Func WM_NOTIFY($hWnd, $Msg, $wParam, $lParam) #forceref $hWnd, $Msg, $wParam Local $tNMHDR = DllStructCreate($tagNMHDR, $lParam) Local $hWndFrom = $tNMHDR.hWndFrom Local $iCode = $tNMHDR.Code If $hWndFrom = GUICtrlGetHandle($idListview) Then Switch $iCode Case $LVN_BEGINDRAG, $LVN_BEGINRDRAG Local $sIndices = _GUICtrlListView_GetSelectedIndices($hWndFrom) If $sIndices <> "" Then Local $aSelected = StringSplit($sIndices, "|") Local $aMultipleFiles[$aSelected[0]] For $i = 1 To $aSelected[0] $aMultipleFiles[$i - 1] = _GUICtrlListView_GetItemText($hWndFrom, Int($aSelected[$i]), 0) Next ;~~~~~~~~ Drag and Drop Local $pIDataObject = __Fill_IDataObject($aMultipleFiles) If IsPtr($pIDataObject) Then Local $pIDropSource = __Initiate_DropSource() If $pIDropSource <> 0 Then __SHDoDragDrop(0, $pIDataObject, $pIDropSource, BitOR($DROPEFFECT_COPY, $DROPEFFECT_LINK)) Endif __Initiate_DropSource() __Release($pIDataObject) Endif ;~~~~~~~~ Drag and Drop EndIf Return 0 EndSwitch EndIf Return $GUI_RUNDEFMSG EndFunc Func WM_DROPFILES_FUNC($hWnd, $Msg, $wParam, $lParam) #forceref $hWnd, $Msg, $wParam, $lParam Switch $wParam Case GUICtrlGetHandle($idListview) Local $aDropFiles = __GetFilesFrom_IDataObject($lParam) If IsArray($aDropFiles) Then For $i = 0 To UBound($aDropFiles) - 1 GUICtrlCreateListViewItem($aDropFiles[$i], $idListview) Next Endif Case Else EndSwitch EndFunc DragDrop.au3 Edited February 2 by jugador WildByDesign 1
WildByDesign Posted February 1 Posted February 1 @MattyD I figured out how to fix the issue with the drag icons now showing initially. There were some rare times where it would not show as well after the first drag. Not sure why it fixes it 100%, but it seems to need DPI scaling set. Throw this at the top and see if it helps: ; DPI awareness DllCall('user32.dll', "uint", "SetThreadDpiAwarenessContext", @AutoItX64 ? "int64" : "int", -2)
WildByDesign Posted February 2 Posted February 2 2 hours ago, jugador said: Tried coding IDropTarget with my limited knowledge. Hope it will work. Thank you for sharing this. I tested it earlier when you posted it and it works well. Now I'm checking out some of the code and learning a lot from your techniques. Particularly, I really like the way that you get the multiple ListView items and the way that you handle the drop and drop directly in the WM_NOTIFY function. Very creative. I'm going to read more of it now. Everybody has different techniques with their coding and it's interesting to learn from more people. So I appreciate your help and time with this and for sharing your code. jugador 1
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now