= How to create a list control with drag and drop (Phoenix) = '''Keywords :''' ListCtrl, Drag and drop. <<TableOfContents>> -------- = 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 == {{attachment: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. {{{#!python # sample_one.py """ Drag and drop (DnD) demo with listctrl : - Dragging of multiple selected items. - Dropping on an empty list. - Dropping of items on a list with a different number of columns. - Dropping on a different applications. """ import pickle import sys from random import choice import wx # class MyFrame # class MyDragList # class MyListDrop # class MyApp #--------------------------------------------------------------------------- items = ['Foo', 'Bar', 'Baz', 'Zif', 'Zaf', 'Zof'] #--------------------------------------------------------------------------- class MyFrame(wx.Frame): def __init__(self, parent, id): wx.Frame.__init__(self, parent, id, "Sample one", size=(450, 295)) #------------ self.SetIcon(wx.Icon('icons/wxwin.ico')) self.SetMinSize((450, 295)) #------------ dl1 = MyDragList(self, style=wx.LC_LIST) dl1.SetBackgroundColour("#e6ffd0") dl2 = MyDragList(self, style=wx.LC_REPORT) dl2.InsertColumn(0, "Column - 0", wx.LIST_FORMAT_LEFT) dl2.InsertColumn(1, "Column - 1", wx.LIST_FORMAT_LEFT) dl2.InsertColumn(2, "Column - 2", wx.LIST_FORMAT_LEFT) dl2.SetBackgroundColour("#f0f0f0") maxs = -sys.maxsize - 1 for item in items: dl1.InsertItem(maxs, item) idx = dl2.InsertItem(maxs, item) dl2.SetItem(idx, 1, choice(items)) dl2.SetItem(idx, 2, choice(items)) #------------ sizer = wx.BoxSizer(wx.HORIZONTAL) sizer.Add(dl1, proportion=1, flag=wx.EXPAND) sizer.Add(dl2, proportion=1, flag=wx.EXPAND) self.SetSizer(sizer) self.Layout() #--------------------------------------------------------------------------- class MyDragList(wx.ListCtrl): def __init__(self, *arg, **kw): wx.ListCtrl.__init__(self, *arg, **kw) #------------ self.Bind(wx.EVT_LIST_BEGIN_DRAG, self.StartDrag) #------------ dt = MyListDrop(self) self.SetDropTarget(dt) #----------------------------------------------------------------------- def GetItemInfo(self, idx): """ Collect all relevant data of a listitem, and put it in a list. """ l = [] l.append(idx) # We need the original index, so it is easier to eventualy delete it. l.append(self.GetItemData(idx)) # Itemdata. l.append(self.GetItemText(idx)) # Text first column. for i in range(1, self.GetColumnCount()): # Possible extra columns. l.append(self.GetItem(idx, i).GetText()) return l def StartDrag(self, event): """ Put together a data object for drag-and-drop _from_ this list. """ l = [] idx = -1 while True: # Find all the selected items and put them in a list. idx = self.GetNextItem(idx, wx.LIST_NEXT_ALL, wx.LIST_STATE_SELECTED) if idx == -1: break l.append(self.GetItemInfo(idx)) # Pickle the items list. itemdata = pickle.dumps(l, 1) # Create our own data format and use it # in a Custom data object. ldata = wx.CustomDataObject("ListCtrlItems") ldata.SetData(itemdata) # Now make a data object for the item list. data = wx.DataObjectComposite() data.Add(ldata) # Create drop source and begin drag-and-drop. dropSource = wx.DropSource(self) dropSource.SetData(data) res = dropSource.DoDragDrop(flags=wx.Drag_DefaultMove) # If move, we want to remove the item from this list. if res == wx.DragMove: # It's possible we are dragging/dropping from this list to this list. # In which case, the index we are removing may have changed... # Find correct position. l.reverse() # Delete all the items, starting with the last item. for i in l: pos = self.FindItem(i[0], i[2]) self.DeleteItem(pos) def Insert(self, x, y, seq): """ Insert text at given x, y coordinates --- used with drag-and-drop. """ # Find insertion point. index, flags = self.HitTest((x, y)) if index == wx.NOT_FOUND: # Not clicked on an item. if flags & (wx.LIST_HITTEST_NOWHERE|wx.LIST_HITTEST_ABOVE|wx.LIST_HITTEST_BELOW): # Empty list or below last item. index = self.GetItemCount() # Append to end of list. elif self.GetItemCount() > 0: if y <= self.GetItemRect(0).y: # Clicked just above first item. index = 0 # Append to top of list. else: index = self.GetItemCount() + 1 # Append to end of list. else: # Clicked on an item. # Get bounding rectangle for the item the user is dropping over. rect = self.GetItemRect(index) # If the user is dropping into the lower half of the rect, # we want to insert _after_ this item. # Correct for the fact that there may be a heading involved. if y > rect.y - self.GetItemRect(0).y + rect.height/2: index += 1 for i in seq: # Insert the item data. idx = self.InsertItem(index, i[2]) self.SetItemData(idx, i[1]) for j in range(1, self.GetColumnCount()): try: # Target list can have more columns than source. self.SetItem(idx, j, i[2+j]) except: pass # Ignore the extra columns. index += 1 #--------------------------------------------------------------------------- class MyListDrop(wx.DropTarget): """ Drop target for simple lists. """ def __init__(self, source): """ Arguments: source: source listctrl. """ wx.DropTarget.__init__(self) #------------ self.dv = source #------------ # Specify the type of data we will accept. self.data = wx.CustomDataObject("ListCtrlItems") self.SetDataObject(self.data) #----------------------------------------------------------------------- # Called when OnDrop returns True. # We need to get the data and do something with it. def OnData(self, x, y, d): """ ... """ # Copy the data from the drag source to our data object. if self.GetData(): # Convert it back to a list and give it to the viewer. ldata = self.data.GetData() l = pickle.loads(ldata) self.dv.Insert(x, y, l) # What is returned signals the source what to do # with the original data (move, copy, etc.) In this # case we just return the suggested value given to us. return d #--------------------------------------------------------------------------- class MyApp(wx.App): def OnInit(self): #------------ frame = MyFrame(parent=None, id=-1) self.SetTopWindow(frame) frame.Show(True) return True #--------------------------------------------------------------------------- def main(): app = MyApp(False) app.MainLoop() #--------------------------------------------------------------------------- if __name__ == "__main__" : main() }}} -------- == Sample two == {{attachment: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. {{{#!python # sample_two.py """ Drag and Drop with a striped drag list. """ import wx # class MyFrame # class MyDragListStriped # class MyApp #--------------------------------------------------------------------------- firstNameList = ["Ben", "Bruce", "Clark", "Dick", "Tom", "Jerry", "John"] lastNameList = ["Grimm","Wayne", "Kent", "Grayson", "Pete", "Black", "Martin"] superNameList = ["30", "20", "40", "56", "26", "32", "89"] #--------------------------------------------------------------------------- class MyFrame(wx.Frame): def __init__(self, parent, id): wx.Frame.__init__(self, parent, id, "Sample two (drag list striped)", size=(400, 200)) #------------ self.SetIcon(wx.Icon('icons/wxwin.ico')) self.SetMinSize((400, 200)) #------------ dls = MyDragListStriped(self, style=wx.LC_REPORT|wx.LC_SINGLE_SEL) dls.InsertColumn(0, "First name", wx.LIST_FORMAT_LEFT, 125) dls.InsertColumn(1, "Last name", wx.LIST_FORMAT_LEFT, 125) dls.InsertColumn(2, "Age", wx.LIST_FORMAT_LEFT, 130) dls.SetBackgroundColour("#f0f0f0") for index in range(len(firstNameList)): dls.InsertItem(index, firstNameList[index]) dls.SetItem(index, 1, lastNameList[index]) dls.SetItem(index, 2, superNameList[index]) #------------ sizer = wx.BoxSizer(wx.HORIZONTAL) sizer.Add(dls, proportion=1, flag=wx.EXPAND) self.SetSizer(sizer) self.Layout() #------------ dls._onStripe() #--------------------------------------------------------------------------- class MyDragListStriped(wx.ListCtrl): def __init__(self, *arg, **kw): wx.ListCtrl.__init__(self, *arg, **kw) #------------ self.Bind(wx.EVT_LIST_BEGIN_DRAG, self._onDrag) self.Bind(wx.EVT_LIST_ITEM_SELECTED, self._onSelect) self.Bind(wx.EVT_LEFT_UP,self._onMouseUp) self.Bind(wx.EVT_LEFT_DOWN, self._onMouseDown) self.Bind(wx.EVT_LEAVE_WINDOW, self._onLeaveWindow) self.Bind(wx.EVT_ENTER_WINDOW, self._onEnterWindow) self.Bind(wx.EVT_LIST_INSERT_ITEM, self._onInsert) self.Bind(wx.EVT_LIST_DELETE_ITEM, self._onDelete) #------------ # Variables. #------------ self.IsInControl = True self.startIndex = -1 self.dropIndex = -1 self.IsDrag = False self.dragIndex = -1 #----------------------------------------------------------------------- def _onLeaveWindow(self, event): """ ... """ self.IsInControl = False self.IsDrag = False event.Skip() def _onEnterWindow(self, event): """ ... """ self.IsInControl = True event.Skip() def _onDrag(self, event): """ ... """ CURSOR_ARROW = wx.Cursor('cursor/arrow.cur', wx.BITMAP_TYPE_CUR) self.SetCursor(wx.Cursor(CURSOR_ARROW)) self.IsDrag = True self.dragIndex = event.Index event.Skip() pass def _onSelect(self, event): """ ... """ self.startIndex = event.Index event.Skip() def _onMouseUp(self, event): """ Purpose : to generate a dropIndex. Process : check self.IsInControl, check self.IsDrag, HitTest, compare HitTest value The mouse can end up in 5 different places : - Outside the Control, - On itself, - Above its starting point and on another item, - Below its starting point and on another item, - Below its starting point and not on another item. """ self.SetCursor(wx.Cursor(wx.CURSOR_ARROW)) if self.IsInControl == False: # 1. Outside the control : Do Nothing. self.IsDrag = False else: # In control but not a drag event : Do Nothing. if self.IsDrag == False: pass else: # In control and is a drag event : Determine Location. self.hitIndex = self.HitTest(event.GetPosition()) self.dropIndex = self.hitIndex[0] # Drop index indicates where the drop location is; what index number. #--------- # Determine dropIndex and its validity. #-------- if self.dropIndex == self.startIndex or self.dropIndex == -1: # 2. On itself or below control : Do Nothing. pass else: #---------- # Now that dropIndex has been established do 3 things : # 1. gather item data # 2. delete item in list # 3. insert item & it's data into the list at the new index #---------- dropList = [] # Drop List is the list of field values from the list control. thisItem = self.GetItem(self.startIndex) for x in range(self.GetColumnCount()): dropList.append(self.GetItem(self.startIndex, x).GetText()) thisItem.SetId(self.dropIndex) self.DeleteItem(self.startIndex) self.InsertItem(thisItem) for x in range(self.GetColumnCount()): self.SetItem(self.dropIndex, x, dropList[x]) #------------ # I don't know exactly why, but the mouse event MUST # call the stripe procedure if the control is to be successfully # striped. Every time it was only in the _onInsert, it failed on # dragging index 3 to the index 1 spot. #------------- # Furthermore, in the load button on the wxFrame that this lives in, # I had to call the _onStripe directly because it would occasionally fail # to stripe without it. You'll notice that this is present in the example stub. # Someone with more knowledge than I probably knows why...and how to fix it properly. #------------- self._onStripe() self.IsDrag = False event.Skip() def _onMouseDown(self, event): """ ... """ self.IsInControl = True event.Skip() def _onInsert(self, event): """ Sequencing on a drop event is: wx.EVT_LIST_ITEM_SELECTED wx.EVT_LIST_BEGIN_DRAG wx.EVT_LEFT_UP wx.EVT_LIST_ITEM_SELECTED (at the new index) wx.EVT_LIST_INSERT_ITEM """ # this call to onStripe catches any addition to the list; drag or not. self._onStripe() self.dragIndex = -1 event.Skip() def _onDelete(self, event): """ ... """ self._onStripe() event.Skip() def _onStripe(self): """ ... """ if self.GetItemCount() > 0: for x in range(self.GetItemCount()): if x % 2 == 0: self.SetItemBackgroundColour(x, wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DLIGHT)) else: self.SetItemBackgroundColour(x, wx.WHITE) #--------------------------------------------------------------------------- class MyApp(wx.App): def OnInit(self): #------------ frame = MyFrame(parent=None, id=-1) self.SetTopWindow(frame) frame.Show(True) return True #--------------------------------------------------------------------------- def main(): app = MyApp(False) app.MainLoop() #--------------------------------------------------------------------------- if __name__ == "__main__" : main() }}} -------- = Download source = [[attachment: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...