How to create a list control with drag and drop (Phoenix)
Keywords : ListCtrl, Drag and drop.
Contents
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
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
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
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://www.pgadmin.org/download/
https://github.com/1966bc/pyggybank
https://sourceforge.net/projects/pyggybank/
- - - - -
https://wiki.wxpython.org/TitleIndex
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...