AutoHotkey Community

It is currently May 26th, 2012, 5:39 pm

All times are UTC [ DST ]




Post new topic Reply to topic  [ 7 posts ] 
Author Message
PostPosted: April 23rd, 2009, 8:57 am 
Offline

Joined: November 27th, 2008, 9:44 am
Posts: 62
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.


Report this post
Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: April 23rd, 2009, 11:19 am 
Offline
User avatar

Joined: September 8th, 2008, 12:26 am
Posts: 1048
Location: Ploieşti, RO
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.


Report this post
Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: April 23rd, 2009, 12:17 pm 
Offline

Joined: November 27th, 2008, 9:44 am
Posts: 62
@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.


Report this post
Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: April 23rd, 2009, 12:59 pm 
Offline
User avatar

Joined: September 8th, 2008, 12:26 am
Posts: 1048
Location: Ploieşti, RO
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. ;)


Report this post
Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: June 3rd, 2009, 8:20 pm 
Offline

Joined: August 7th, 2007, 8:36 pm
Posts: 8
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.


Report this post
Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: November 25th, 2010, 10:43 pm 
Offline

Joined: May 16th, 2010, 2:38 pm
Posts: 185
thanks guys, this is really great example


Report this post
Top
 Profile  
Reply with quote  
 Post subject:
PostPosted: December 14th, 2010, 10:20 am 
Offline
User avatar

Joined: September 8th, 2008, 12:26 am
Posts: 1048
Location: Ploieşti, RO
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.


Report this post
Top
 Profile  
Reply with quote  
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 7 posts ] 

All times are UTC [ DST ]


Who is online

Users browsing this forum: XX0 and 14 guests


You can post new topics in this forum
You can reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Powered by phpBB® Forum Software © phpBB Group