 |
AutoHotkey Community Let's help each other out
|
| View previous topic :: View next topic |
| Author |
Message |
rulfzid
Joined: 27 Nov 2008 Posts: 62
|
Posted: Thu Apr 23, 2009 7:57 am Post subject: Treeview drag & drop [proof of concept] |
|
|
Here is some code I've been playing around with to make drag and drop worth within a Treeview control. The code is based off of the msdn documentation of drag&drop in TV controls here:
I've documented the script pretty thoroughly, so a lot of the information is in there, but some things of note:
- No drag image - I point out in the comments where the code for the dragimage stuff should go, but decided not to include it because it was being buggy and because I frankly prefer the method I ended up going with (drop target highlighting and insertion marks)
- Drop handling - my focus was on the getting the dragging stuff to work, which I did. What to do when the dragging ends is something I contended with only in a very simple way (just added the dragged node as a child of the drop target). There are other things one could do - vary the drop action depending on what kind of drop target / drag item you were contending with, etc.
Anyway, here's the code:
| Code: | #NoEnv
Gui, +lastfound
hgui := WinExist()
; Set the message handlers
OnMessage( 0x4E, "Treeview_BeginDrag" ) ; WM_NOTIFY
OnMessage( 0x200, "Treeview_Dragging" ) ; WM_MOUSEMOVE
OnMessage( 0x202, "Treeview_EndDrag" ) ; WM_LBUTTONUP
; Add the Treeview
Gui, Add, TreeView, x0 y0 w200 h300 HWNDhTreeview
P1 := TV_Add("Apple", 0, "Expand" )
P1C1 := TV_Add("Apple juice", P1, "Expand" )
P2 := TV_Add("Orange", 0, "Expand" )
P2C1 := TV_Add("Orange juice", P2, "Expand" )
P2C2 := TV_Add("Orange soda", P2, "Expand" )
P2C2C1 := TV_Add("Tang", P2C2, "Expand" )
otherfruits = pear,kiwi,lime,grape,peach
Loop, parse, otherfruits, `,
TV_ADD( A_LoopField, 0 )
; Show the GUI
Gui, Show, w200 h300
return
; ----- End Auto-Execute section -----
guiclose:
exitapp
Treeview_BeginDrag( wParam, lParam )
{
global TV_is_dragging, hTreeview, hDragitem
static TVM_SELECTITEM := 0x110B, TVGN_CARET := 0x9
nmhdr_code := NumGet( lParam + 0, 8, "uint" )
; TVN_BEGINDRAGA = 0xFFFFFE69, TVN_BEGINDRAGW = 0xFFFFFE38
If ( nmhdr_code = 0xFFFFFE69 ) or ( nmhdr_code = 0xFFFFFE38 )
{
; Get the drag item out of the NMTREEVIEW structure
hDragitem := NumGet( lParam + 0, 60, "uint" )
; Select the item before dragging so it's clear what you're dragging
SendMessage( hTreeview, TVM_SELECTITEM, TVGN_CARET, hDragitem )
; If you were so inclined, you could initialize drag image stuff here:
; 1. get a drag image (either by using TVM_SETIMAGE, which requires
; that the Treeview control have been created with an associated
; imagelist) OR just use TVM_GETITEMRECT and fancy gdi-style stuff
; and make one yourself
; 2. add the image to an imagelist and use the imagelist drag functions
; (read up on it at msdn)
; 3. add the appropriate imagelist dragging code in the Treeview_Dragging()
; and Treeview_EndDrag() functions
;
; I initially had some code here that was mostly working but (a) it was
; creating some strange visual artifacts and (b) I kind of prefer just using
; insertion marks and highlighting the drop target.
; Set the dragging flag
TV_is_dragging := 1
}
}
Treeview_Dragging( wParam, lParam )
{
global TV_is_dragging, hTreeview, height
static TVM_HITTEST := 0x1111, TVM_SETINSERTMARK := 0x111A, TVM_GETITEMRECT := 0x1104
static TVM_SELECTITEM := 0x110B, TVGN_DROPHILITE := 0x8, TVM_GETITEMHEIGHT := 0x111C
If TV_is_dragging
{
if not height
height := SendMessage( hTreeview, TVM_GETITEMHEIGHT, 0, 0 )
; Get the mouse location out of lParam
x := lParam & 0xFFFF
y := lParam >> 16
; Create the TVHITTESTINFO struct...
VarSetCapacity( tvht, 16, 0 )
NumPut( x, tvht, 0, "int" ), NumPut( y, tvht, 4, "int" )
; ... to determine whether the pointer is over an item. If it is...
If hitTarget := SendMessage( hTreeview, TVM_HITTEST, 0, &tvht )
{
; ... highlight the item as a drop target, and / or ...
SendMessage( hTreeview, TVM_SELECTITEM, TVGN_DROPHILITE, hitTarget )
; ... if the pointer is in the top or bottom quarter of the item,
; show an insertion mark before or after, respectively.
; This way you can decide whether to make the dragged item
; a child or sibling of the drop target item.
;
; If you really wanted, you could check here to see what kind of
; item hitTarget was and display the insertion mark accordingly.
VarSetCapacity( rcitem, 16, 0 ), NumPut( hitTarget, rcitem )
SendMessage( hTreeview, TVM_GETITEMRECT, 1, &rcitem )
rcitem_top := NumGet( rcitem, 4, "int" )
rcitem_bottom := NumGet( rcitem, 12, "int" )
fAfter := 99 ; just a default that's not 0 or 1
fAfter := ( y - rcitem_top ) < ( height/4 ) ? 0 : ( rcitem_bottom - y) < ( height/4 ) ? 1 : fAfter
If ( fAfter = 99 )
SendMessage( hTreeview, TVM_SETINSERTMARK, 0, 0 ) ; hide insertionmark
Else
SendMessage( hTreeview, TVM_SETINSERTMARK, fAfter, hitTarget ) ; show insertion mark
}
}
}
Treeview_EndDrag( wParam, lParam )
{
global TV_is_dragging, hTreeview, hDragitem
static TVM_SETINSERTMARK := 0x111A, TVM_SELECTITEM := 0x110B, TVGN_DROPHILITE := 0x8
static TVM_HITTEST := 0x1111
If TV_is_dragging
{
; Remove the drop-target highlighting and insertion mark
SendMessage( hTreeview, TVM_SELECTITEM, TVGN_DROPHILITE, 0 )
SendMessage( hTreeview, TVM_SETINSERTMARK, 1, 0 )
; Add code here to handle the moving of the dragged node
; - hDragitem is the handle to the item currently being dragged
; - you can use the code from the WM_MOUSEMOVE to determine
; where the pointer is and where/how the item should be inserted
;
; For the sake of simplicity, this script will always move the
; dragitem to be a child of the drop target
; Get the mouse location out of lParam
x := lParam & 0xFFFF
y := lParam >> 16
; Create the TVHITTESTINFO struct...
VarSetCapacity( tvht, 16, 0 )
NumPut( x, tvht, 0, "int" ), NumPut( y, tvht, 4, "int" )
; ... to determine whether the pointer is over an item.
If hDroptarget := SendMessage( hTreeview, TVM_HITTEST, 0, &tvht )
{
; Only do stuff if the droptarget is different from the drag item
If ( hDragitem != hDroptarget )
{
; To prevent infinite loops, first make sure "parent" isn't actually a
; descendant of node (it's like going back in time and becoming your own
; great-grandfather: no good can come of it)
If not IsParentADescendant( hDragitem, hDroptarget )
{
AddNodeToParent( hDragitem, hDroptarget )
TV_Modify( hDropTarget, "Expand" )
TV_Delete( hDragitem )
}
}
}
; Set the dragging flag and dragitem handle to false
TV_is_dragging := 0, hDragitem := 0
}
}
IsParentADescendant( node, parent )
{
dlist := GetDescendantsList( node )
Loop, parse, dlist, `,
If ( A_LoopField = parent )
return 1
return 0
}
; Wheeeee! Recursion is fun!
GetDescendantsList( node )
{
If ( kid := TV_GetChild( node ) )
{
kids .= kid . "," . GetDescendantsList( kid )
While ( kid := TV_GetNext( kid ) )
kids .= kid . "," . GetDescendantsList( kid )
}
return kids
}
; I recurse! You recurse! We all recurse for Ira Curs! (or something )
AddNodeToParent( node, parent )
{
TV_GetText( t, node)
node_id := TV_Add( t, parent, "Expand" )
If ( kid := TV_GetChild( node ) )
{
AddNodeToParent( kid, node_id )
While ( kid := TV_GetNext( kid ) )
AddNodeToParent( kid, node_id )
}
return node_id
}
SendMessage( hWnd, Msg, wParam, lParam )
{
static SendMessageA
If not SendMessageA
SendMessageA := LoadDllFunction( "user32.dll", "SendMessageA" )
return DllCall( SendMessageA, uint, hWnd, uint, Msg, uint , wParam, uint, lParam )
}
LoadDllFunction( file, function ) {
if !hModule := DllCall( "GetModuleHandle", uint, &file, uint )
hModule := DllCall( "LoadLibrary", uint, &file, uint )
return DllCall("GetProcAddress", uint, hModule, uint, &function, uint)
} |
I welcome comments/questions/criticisms. |
|
| Back to top |
|
 |
Drugwash
Joined: 07 Sep 2008 Posts: 921 Location: Ploiesti, RO
|
Posted: Thu Apr 23, 2009 10:19 am Post subject: |
|
|
One innocent question: what happens if the script this function is supposed to go with, already has its own OnMessage handlers that clash with your function's?
What I've been working on the past few days is Hotkey/KeyWait alternatives for Win9x (since the built-in functions don't work on such systems and I'm running 98SE) and I've also used OnMessage, but tried to make it into generic handlers.
What I mean is, the OnMessage functions would set a flag for each particular message received and the rest of the functions/script would act according to those flags.
What I couldn't resolve is passing a dynamic variable from function to function without creating a set of global variables. Tried to use NumPut/NumGet to save/retrieve the variable addresses but failed.
Anyway, I hope this helps somehow. The idea is good. |
|
| Back to top |
|
 |
rulfzid
Joined: 27 Nov 2008 Posts: 62
|
Posted: Thu Apr 23, 2009 11:17 am Post subject: |
|
|
@Drugwash
The initial way I handled this was to (not sure if i'm using terminology correctly, but you'll get the idea) subclass the window procedure, a la:
| Code: | ; Set the message handler
TVWindowProcAddress := RegisterCallback("TVWindowProc")
WindowProcOld := DllCall("SetWindowLong" , UInt , hGui
, Int , -4 ; GWL_WNDPROC
, Int , TVWindowProcAddress
, UInt) |
And the the TVWindowProc function would look like:
| Code: | TVWindowProc(hwnd, uMsg, wParam, lParam)
{
Critical 100
global WindowProcOld, hTreeview, TV_is_dragging, hGui
; WM_NOTIFY := 0x4E
If ( uMsg = 0x4E )
{
nmhdr_code := NumGet( lParam + 0, 8, "uint" )
; TVN_BEGINDRAGA = 0xFFFFFE69, TVN_BEGINDRAGW = 0xFFFFFE38
If ( nmhdr_code = 0xFFFFFE69 ) or ( nmhdr_code = 0xFFFFFE38 )
Treeview_BeginDrag( hTreeview, lParam )
}
; WM_MOUSEMOVE := 0x200
If ( uMsg = 0x200 )
{
If TV_is_dragging
Treeview_Dragging( hGui, hTreeview, lParam & 0xFFFF, lParam >> 16 )
}
; WM_LBUTTONUP := 0x202
If ( uMsg = 0x202 )
{
If TV_is_dragging
Treeview_EndDrag( hTreeview )
}
; Pass all unhandled events to the original WindowProc.
return DllCall("CallWindowProcA", UInt, WindowProcOld, UInt, hwnd, UInt, uMsg, UInt, wParam, UInt, lParam)
} |
Now, that probably won't just work if you plug it in to the script as is, because I've been changing things, but it should give you the general idea. And I did (at one point) have a mostly working script using that method. |
|
| Back to top |
|
 |
Drugwash
Joined: 07 Sep 2008 Posts: 921 Location: Ploiesti, RO
|
Posted: Thu Apr 23, 2009 11:59 am Post subject: |
|
|
Ah, this may still be better than "stealing" mouse movement messages.
I must confess though, that my understanding of subclassing is quite limited (being just a nosy amateur, not a programmer). Hopefully the titans of AHK will chime in with some good, working suggestions.  |
|
| Back to top |
|
 |
Charon the Hand
Joined: 07 Aug 2007 Posts: 8
|
Posted: Wed Jun 03, 2009 7:20 pm Post subject: |
|
|
Very well done, rulfzid; I'm glad you shared this. I was banging my head against a wall trying to write these functions myself and couldn't wrap my brain around the recursion. My add node functions were ludicrously long and filled with arcane symbols to denote depth in the tree. Ugh. All replaced by a few simple lines, thank you very much.
As a side note, I've gotten reports from some of my users that my program doesn't behave well when there is a message handler, so I rewrote the drag section to just use a while loop. It works seemingly identically and makes me feel like I've done some work. This function is fired by the treeview g-label if A_EventInfo = D (drag attempt). Maybe this could help Drugwash a few posts up, since no message handlers are needed for this version.
| Code: | TV_DragAndDrop(DraggedID)
{
global RootID, TVHwnd
If (DraggedID = RootID) ;root cannot be dragged
Return
WinGetPos,,,,WinHeight, ahk_id%TVHwnd%
while GetKeyState("LButton") ;while dragging, highlight the items we pass
{ ;and scroll up or down at the top/bottom of window
MouseGetPos, ThisX, ThisY
VarSetCapacity( tvht, 16, 0 )
NumPut( ThisX, tvht, 0, "int" ), NumPut( ThisY-60, tvht, 4, "int" )
;determine whether the pointer is over an item. If it is, highlight the item as a drop target.
If hitTarget := DllCall( "SendMessageA", uint, TVHwnd, uint, 0x1111, uint , 0, uint, &tvht)
DllCall("SendMessageA", uint, TVHwnd, uint, 0x110B, uint, 0x08, uint, hitTarget)
If (ThisY > WinHeight+57)
Send {WheelDown}
else If (ThisY < 68)
Send {WheelUp}
Sleep, 50
}
DllCall("SendMessageA", uint, TVHwnd, uint, 0x110B, uint, 0x08, uint, 0)
TV_Modify(DroppedID)
DroppedID := hitTarget
If (DroppedID = DraggedID)
return
If (TV_IsDescendant(DraggedID, DroppedID)) ;do not drop item on itself or descendant
return
TV_AddNodeToParent(DraggedID, DroppedID)
TV_Delete(DraggedID)
TV_Modify(DroppedID, "Expand")
Return
} |
Thank you again, Char. |
|
| Back to top |
|
 |
Deo
Joined: 16 May 2010 Posts: 172
|
Posted: Thu Nov 25, 2010 9:43 pm Post subject: |
|
|
| thanks guys, this is really great example |
|
| Back to top |
|
 |
Drugwash
Joined: 07 Sep 2008 Posts: 921 Location: Ploiesti, RO
|
Posted: Tue Dec 14, 2010 9:20 am Post subject: |
|
|
| I've added this functionality to SmartGUI Creator Mod but it's not at all optimized for StdLib functionality. It does however implement drag image but it's by no means complete, nor bug-free. May still be a source of inspiration for others, though. |
|
| Back to top |
|
 |
|
|
You can post new topics in this forum You can reply to topics in this forum
|
Powered by phpBB © 2001, 2005 phpBB Group
|