How to create a list control with drag and drop (Phoenix)

Keywords : ListCtrl, Drag and drop.


Demonstrating :

Tested py3.x, wx4.x and Win10.

Are you ready to use some samples ? ;)

Test, modify, correct, complete, improve and share your discoveries ! (!)


Sample one

img_sample_one.png

Here's how you can use drag-and-drop to reorder a simple list, or to move items from one list to another.

   1 # sample_one.py
   2 
   3 """
   4 
   5 Drag and drop (DnD) demo with listctrl :
   6 - Dragging of multiple selected items.
   7 - Dropping on an empty list.
   8 - Dropping of items on a list with a different number of columns.
   9 - Dropping on a different applications.
  10 
  11 """
  12 
  13 import pickle
  14 import sys
  15 from   random import choice
  16 import wx
  17 
  18 # class MyFrame
  19 # class MyDragList
  20 # class MyListDrop
  21 # class MyApp
  22 
  23 #---------------------------------------------------------------------------
  24 
  25 items = ['Foo', 'Bar', 'Baz', 'Zif', 'Zaf', 'Zof']
  26 
  27 #---------------------------------------------------------------------------
  28 
  29 class MyFrame(wx.Frame):
  30     def __init__(self, parent, id):
  31         wx.Frame.__init__(self, parent, id,
  32                           "Sample one",
  33                           size=(450, 295))
  34 
  35         #------------
  36 
  37         self.SetIcon(wx.Icon('icons/wxwin.ico'))
  38         self.SetMinSize((450, 295))
  39 
  40         #------------
  41 
  42         dl1 = MyDragList(self, style=wx.LC_LIST)
  43         dl1.SetBackgroundColour("#e6ffd0")
  44 
  45         dl2 = MyDragList(self, style=wx.LC_REPORT)
  46         dl2.InsertColumn(0, "Column - 0", wx.LIST_FORMAT_LEFT)
  47         dl2.InsertColumn(1, "Column - 1", wx.LIST_FORMAT_LEFT)
  48         dl2.InsertColumn(2, "Column - 2", wx.LIST_FORMAT_LEFT)
  49         dl2.SetBackgroundColour("#f0f0f0")
  50 
  51         maxs = -sys.maxsize - 1
  52 
  53         for item in items:
  54             dl1.InsertItem(maxs, item)
  55             idx = dl2.InsertItem(maxs, item)
  56             dl2.SetItem(idx, 1, choice(items))
  57             dl2.SetItem(idx, 2, choice(items))
  58 
  59         #------------
  60 
  61         sizer = wx.BoxSizer(wx.HORIZONTAL)
  62 
  63         sizer.Add(dl1, proportion=1, flag=wx.EXPAND)
  64         sizer.Add(dl2, proportion=1, flag=wx.EXPAND)
  65 
  66         self.SetSizer(sizer)
  67         self.Layout()
  68 
  69 #---------------------------------------------------------------------------
  70 
  71 class MyDragList(wx.ListCtrl):
  72     def __init__(self, *arg, **kw):
  73         wx.ListCtrl.__init__(self, *arg, **kw)
  74 
  75         #------------
  76 
  77         self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.StartDrag)
  78 
  79         #------------
  80 
  81         dt = MyListDrop(self)
  82         self.SetDropTarget(dt)
  83 
  84     #-----------------------------------------------------------------------
  85 
  86     def GetItemInfo(self, idx):
  87         """
  88         Collect all relevant data of a listitem, and put it in a list.
  89         """
  90 
  91         l = []
  92         l.append(idx) # We need the original index, so it is easier to eventualy delete it.
  93         l.append(self.GetItemData(idx)) # Itemdata.
  94         l.append(self.GetItemText(idx)) # Text first column.
  95         for i in range(1, self.GetColumnCount()): # Possible extra columns.
  96             l.append(self.GetItem(idx, i).GetText())
  97         return l
  98 
  99 
 100     def StartDrag(self, event):
 101         """
 102         Put together a data object for drag-and-drop _from_ this list.
 103         """
 104 
 105         l = []
 106         idx = -1
 107         while True: # Find all the selected items and put them in a list.
 108             idx = self.GetNextItem(idx, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED)
 109             if idx == -1:
 110                 break
 111             l.append(self.GetItemInfo(idx))
 112 
 113         # Pickle the items list.
 114         itemdata = pickle.dumps(l, 1)
 115         # Create our own data format and use it
 116         # in a Custom data object.
 117         ldata = wx.CustomDataObject("ListCtrlItems")
 118         ldata.SetData(itemdata)
 119         # Now make a data object for the  item list.
 120         data = wx.DataObjectComposite()
 121         data.Add(ldata)
 122 
 123         # Create drop source and begin drag-and-drop.
 124         dropSource = wx.DropSource(self)
 125         dropSource.SetData(data)
 126         res = dropSource.DoDragDrop(flags=wx.Drag_DefaultMove)
 127 
 128         # If move, we want to remove the item from this list.
 129         if res == wx.DragMove:
 130             # It's possible we are dragging/dropping from this list to this list.
 131             # In which case, the index we are removing may have changed...
 132 
 133             # Find correct position.
 134             l.reverse() # Delete all the items, starting with the last item.
 135             for i in l:
 136                 pos = self.FindItem(i[0], i[2])
 137                 self.DeleteItem(pos)
 138 
 139 
 140     def Insert(self, x, y, seq):
 141         """
 142         Insert text at given x, y coordinates --- used with drag-and-drop.
 143         """
 144 
 145         # Find insertion point.
 146         index, flags = self.HitTest((x, y))
 147 
 148         if index == wx.NOT_FOUND: # Not clicked on an item.
 149             if flags & (wx.LIST_HITTEST_NOWHERE|wx.LIST_HITTEST_ABOVE|wx.LIST_HITTEST_BELOW): # Empty list or below last item.
 150                 index = self.GetItemCount() # Append to end of list.
 151             elif self.GetItemCount() > 0:
 152                 if y <= self.GetItemRect(0).y: # Clicked just above first item.
 153                     index = 0 # Append to top of list.
 154                 else:
 155                     index = self.GetItemCount() + 1 # Append to end of list.
 156         else: # Clicked on an item.
 157             # Get bounding rectangle for the item the user is dropping over.
 158             rect = self.GetItemRect(index)
 159 
 160             # If the user is dropping into the lower half of the rect,
 161             # we want to insert _after_ this item.
 162             # Correct for the fact that there may be a heading involved.
 163             if y > rect.y - self.GetItemRect(0).y + rect.height/2:
 164                 index += 1
 165 
 166         for i in seq: # Insert the item data.
 167             idx = self.InsertItem(index, i[2])
 168             self.SetItemData(idx, i[1])
 169             for j in range(1, self.GetColumnCount()):
 170                 try: # Target list can have more columns than source.
 171                     self.SetItem(idx, j, i[2+j])
 172                 except:
 173                     pass # Ignore the extra columns.
 174             index += 1
 175 
 176 #---------------------------------------------------------------------------
 177 
 178 class MyListDrop(wx.DropTarget):
 179     """
 180     Drop target for simple lists.
 181     """
 182     def __init__(self, source):
 183         """
 184         Arguments:
 185         source: source listctrl.
 186         """
 187         wx.DropTarget.__init__(self)
 188 
 189         #------------
 190 
 191         self.dv = source
 192 
 193         #------------
 194 
 195         # Specify the type of data we will accept.
 196         self.data = wx.CustomDataObject("ListCtrlItems")
 197         self.SetDataObject(self.data)
 198 
 199     #-----------------------------------------------------------------------
 200 
 201     # Called when OnDrop returns True.
 202     # We need to get the data and do something with it.
 203     def OnData(self, x, y, d):
 204         """
 205         ...
 206         """
 207 
 208         # Copy the data from the drag source to our data object.
 209         if self.GetData():
 210             # Convert it back to a list and give it to the viewer.
 211             ldata = self.data.GetData()
 212             l = pickle.loads(ldata)
 213             self.dv.Insert(x, y, l)
 214 
 215         # What is returned signals the source what to do
 216         # with the original data (move, copy, etc.)  In this
 217         # case we just return the suggested value given to us.
 218         return d
 219 
 220 #---------------------------------------------------------------------------
 221 
 222 class MyApp(wx.App):
 223     def OnInit(self):
 224 
 225         #------------
 226 
 227         frame = MyFrame(parent=None, id=-1)
 228         self.SetTopWindow(frame)
 229         frame.Show(True)
 230 
 231         return True
 232 
 233 #---------------------------------------------------------------------------
 234 
 235 def main():
 236     app = MyApp(False)
 237     app.MainLoop()
 238 
 239 #---------------------------------------------------------------------------
 240 
 241 if __name__ == "__main__" :
 242     main()


Sample two

img_sample_two.png

This striped drag list allows for dragging and dropping within itself while maintaining its appearance.

While not used in the example stub, it'll also stripe itself on an insert event (like from a pop-up file dialog) or on a delete event.

   1 # sample_two.py
   2 
   3 """
   4 
   5 Drag and Drop with a striped drag list.
   6 
   7 """
   8 
   9 import wx
  10 
  11 # class MyFrame
  12 # class MyDragListStriped
  13 # class MyApp
  14 
  15 #---------------------------------------------------------------------------
  16 
  17 firstNameList = ["Ben", "Bruce", "Clark", "Dick", "Tom", "Jerry", "John"]
  18 lastNameList  = ["Grimm","Wayne", "Kent", "Grayson", "Pete", "Black", "Martin"]
  19 superNameList = ["30", "20", "40", "56", "26", "32", "89"]
  20 
  21 #---------------------------------------------------------------------------
  22 
  23 class MyFrame(wx.Frame):
  24     def __init__(self, parent, id):
  25         wx.Frame.__init__(self, parent, id,
  26                           "Sample two (drag list striped)",
  27                           size=(400, 200))
  28 
  29         #------------
  30 
  31         self.SetIcon(wx.Icon('icons/wxwin.ico'))
  32         self.SetMinSize((400, 200))
  33 
  34         #------------
  35 
  36         dls = MyDragListStriped(self, style=wx.LC_REPORT|wx.LC_SINGLE_SEL)
  37         dls.InsertColumn(0, "First name", wx.LIST_FORMAT_LEFT, 125)
  38         dls.InsertColumn(1, "Last name", wx.LIST_FORMAT_LEFT, 125)
  39         dls.InsertColumn(2, "Age", wx.LIST_FORMAT_LEFT, 130)
  40         dls.SetBackgroundColour("#f0f0f0")
  41 
  42         for index in range(len(firstNameList)):
  43             dls.InsertItem(index, firstNameList[index])
  44             dls.SetItem(index, 1, lastNameList[index])
  45             dls.SetItem(index, 2, superNameList[index])
  46 
  47         #------------
  48 
  49         sizer = wx.BoxSizer(wx.HORIZONTAL)
  50 
  51         sizer.Add(dls, proportion=1, flag=wx.EXPAND)
  52 
  53         self.SetSizer(sizer)
  54         self.Layout()
  55 
  56         #------------
  57 
  58         dls._onStripe()
  59 
  60 #---------------------------------------------------------------------------
  61 
  62 class MyDragListStriped(wx.ListCtrl):
  63     def __init__(self, *arg, **kw):
  64         wx.ListCtrl.__init__(self, *arg, **kw)
  65 
  66         #------------
  67 
  68         self.Bind(wx.EVT_LIST_BEGIN_DRAG, self._onDrag)
  69         self.Bind(wx.EVT_LIST_ITEM_SELECTED, self._onSelect)
  70         self.Bind(wx.EVT_LEFT_UP,self._onMouseUp)
  71         self.Bind(wx.EVT_LEFT_DOWN, self._onMouseDown)
  72         self.Bind(wx.EVT_LEAVE_WINDOW, self._onLeaveWindow)
  73         self.Bind(wx.EVT_ENTER_WINDOW, self._onEnterWindow)
  74         self.Bind(wx.EVT_LIST_INSERT_ITEM, self._onInsert)
  75         self.Bind(wx.EVT_LIST_DELETE_ITEM, self._onDelete)
  76 
  77         #------------
  78         # Variables.
  79         #------------
  80         self.IsInControl = True
  81         self.startIndex = -1
  82         self.dropIndex = -1
  83         self.IsDrag = False
  84         self.dragIndex = -1
  85 
  86     #-----------------------------------------------------------------------
  87 
  88     def _onLeaveWindow(self, event):
  89         """
  90         ...
  91         """
  92 
  93         self.IsInControl = False
  94         self.IsDrag = False
  95         event.Skip()
  96 
  97 
  98     def _onEnterWindow(self, event):
  99         """
 100         ...
 101         """
 102 
 103         self.IsInControl = True
 104         event.Skip()
 105 
 106 
 107     def _onDrag(self, event):
 108         """
 109         ...
 110         """
 111 
 112         CURSOR_ARROW = wx.Cursor('cursor/arrow.cur', wx.BITMAP_TYPE_CUR)
 113         self.SetCursor(wx.Cursor(CURSOR_ARROW))
 114 
 115         self.IsDrag = True
 116         self.dragIndex = event.Index
 117         event.Skip()
 118         pass
 119 
 120 
 121     def _onSelect(self, event):
 122         """
 123         ...
 124         """
 125 
 126         self.startIndex = event.Index
 127         event.Skip()
 128 
 129 
 130     def _onMouseUp(self, event):
 131         """
 132         Purpose : to generate a dropIndex.
 133         Process : check self.IsInControl, check self.IsDrag, HitTest, compare HitTest value
 134         The mouse can end up in 5 different places :
 135         - Outside the Control,
 136         - On itself,
 137         - Above its starting point and on another item,
 138         - Below its starting point and on another item,
 139         - Below its starting point and not on another item.
 140         """
 141 
 142         self.SetCursor(wx.Cursor(wx.CURSOR_ARROW))
 143 
 144         if self.IsInControl == False:       # 1. Outside the control : Do Nothing.
 145             self.IsDrag = False
 146         else:                               # In control but not a drag event : Do Nothing.
 147             if self.IsDrag == False:
 148                 pass
 149             else:                           # In control and is a drag event : Determine Location.
 150                 self.hitIndex = self.HitTest(event.GetPosition())
 151                 self.dropIndex = self.hitIndex[0]
 152                 # Drop index indicates where the drop location is; what index number.
 153                 #---------
 154                 # Determine dropIndex and its validity.
 155                 #--------
 156                 if self.dropIndex == self.startIndex or self.dropIndex == -1:    # 2. On itself or below control : Do Nothing.
 157                     pass
 158                 else:
 159                     #----------
 160                     # Now that dropIndex has been established do 3 things :
 161                     # 1. gather item data
 162                     # 2. delete item in list
 163                     # 3. insert item & it's data into the list at the new index
 164                     #----------
 165                     dropList = []         # Drop List is the list of field values from the list control.
 166                     thisItem = self.GetItem(self.startIndex)
 167                     for x in range(self.GetColumnCount()):
 168                         dropList.append(self.GetItem(self.startIndex, x).GetText())
 169                     thisItem.SetId(self.dropIndex)
 170                     self.DeleteItem(self.startIndex)
 171                     self.InsertItem(thisItem)
 172                     for x in range(self.GetColumnCount()):
 173                         self.SetItem(self.dropIndex, x, dropList[x])
 174             #------------
 175             # I don't know exactly why, but the mouse event MUST
 176             # call the stripe procedure if the control is to be successfully
 177             # striped. Every time it was only in the _onInsert, it failed on
 178             # dragging index 3 to the index 1 spot.
 179             #-------------
 180             # Furthermore, in the load button on the wxFrame that this lives in,
 181             # I had to call the _onStripe directly because it would occasionally fail
 182             # to stripe without it. You'll notice that this is present in the example stub.
 183             # Someone with more knowledge than I probably knows why...and how to fix it properly.
 184             #-------------
 185         self._onStripe()
 186         self.IsDrag = False
 187         event.Skip()
 188 
 189 
 190     def _onMouseDown(self, event):
 191         """
 192         ...
 193         """
 194 
 195         self.IsInControl = True
 196         event.Skip()
 197 
 198 
 199     def _onInsert(self, event):
 200         """
 201         Sequencing on a drop event is:
 202         wx.EVT_LIST_ITEM_SELECTED
 203         wx.EVT_LIST_BEGIN_DRAG
 204         wx.EVT_LEFT_UP
 205         wx.EVT_LIST_ITEM_SELECTED (at the new index)
 206         wx.EVT_LIST_INSERT_ITEM
 207         """
 208 
 209         # this call to onStripe catches any addition to the list; drag or not.
 210         self._onStripe()
 211         self.dragIndex = -1
 212         event.Skip()
 213 
 214 
 215     def _onDelete(self, event):
 216         """
 217         ...
 218         """
 219 
 220         self._onStripe()
 221         event.Skip()
 222 
 223 
 224     def _onStripe(self):
 225         """
 226         ...
 227         """
 228 
 229         if self.GetItemCount() > 0:
 230             for x in range(self.GetItemCount()):
 231                 if x % 2 == 0:
 232                     self.SetItemBackgroundColour(x, wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DLIGHT))
 233                 else:
 234                     self.SetItemBackgroundColour(x, wx.WHITE)
 235 
 236 #---------------------------------------------------------------------------
 237 
 238 class MyApp(wx.App):
 239     def OnInit(self):
 240 
 241         #------------
 242 
 243         frame = MyFrame(parent=None, id=-1)
 244         self.SetTopWindow(frame)
 245         frame.Show(True)
 246 
 247         return True
 248 
 249 #---------------------------------------------------------------------------
 250 
 251 def main():
 252     app = MyApp(False)
 253     app.MainLoop()
 254 
 255 #---------------------------------------------------------------------------
 256 
 257 if __name__ == "__main__" :
 258     main()


Download source

source.zip


Additional Information

Link :

http://jak-o-shadows.users.sourceforge.net/python/wxpy/dblistctrl.html

http://wxpython-users.1045709.n5.nabble.com/Example-of-Database-Interaction-td2361801.html

http://www.kitebird.com/articles/pydbapi.html

https://dabodev.com/

https://www.pgadmin.org/download/

https://github.com/1966bc/pyggybank

https://sourceforge.net/projects/pyggybank/

- - - - -

https://wiki.wxpython.org/TitleIndex

https://docs.wxpython.org/


Thanks to

??? (sample_one.py coding), ??? (sample_two.py coding), the wxPython community...


About this page

Date(d/m/y) Person (bot) Comments :

14/03/20 - Ecco (Created page and updated examples for wxPython Phoenix).


Comments

- blah, blah, blah...

How to create a list control with drag and drop (Phoenix) (last edited 2020-12-13 14:08:37 by Ecco)

NOTE: To edit pages in this wiki you must be a member of the TrustedEditorsGroup.