[listbox] Code to move items?

Hello,

I need to let the user move items in a listbox, either through the mouse (drag 'n drop) or through the keyboard (eg. CTRL + up/down arrows).

EditableListBox seems to support that feature but 1) I don’t need to edit the contents and 2) it displays a toolbar.

As for RearrangeCtrl, I found no example.

Would someone have code this newbie could just copy/paste?

Thank you.

import sys,os, wx

class ListBoxFrame(wx.Frame):
  def __init__(self, *args, **kwargs):  
    #wx.Frame.__init__(self, None, -1, 'List Box Example')
    super().__init__(None, -1,title='List Box Example')
    
    self.Centre()

    self.statusbar = self.CreateStatusBar()

    panel = wx.Panel(self, -1)

    sizer = wx.BoxSizer(wx.VERTICAL)

    #create dict, and loop to add to listbox
    lb1 = wx.ListBox(panel, -1, choices=[], style=(wx.LB_SINGLE | wx.LB_ALWAYS_SB))
    sampleList_dict = {'key1': 'value1', 'key2': 'value2', 'item3': 'value3'}
    for k, v in sampleList_dict.items():
      lb1.Append(k, v);

    sizer.Add(lb1,0,wx.ALL,5)

    panel.SetSizer(sizer)

    self.Bind(wx.EVT_LISTBOX, self.EvtListBox, lb1)
        
  def EvtListBox(self, event):
    name = event.GetString()
    data = str(event.GetClientData())
    self.statusbar.SetStatusText(f"{name}: {data}")

app = wx.App()
ListBoxFrame().Show()
app.MainLoop()


Edit: Never mind. The listctrl supports drag 'n drop:

https://wiki.wxpython.org/How%20to%20create%20a%20list%20control%20with%20drag%20and%20drop%20(Phoenix)

I agree that using a ListCtrl would probably suit your requirements better, especially if you prefer drag 'n drop.

However, I did wonder if it was possible to make moving the selected item work in a ListBox using CTRL + UP/DOWN keys.

I found a couple of techniques which almost worked, but noticed that a dotted border kept appearing on one of the items in the ListBox, which the user may find confusing. This appears to be a separate focus selection that is not modified by calls to SetSelection(). Unlike the ListCtrl, the ListBox doesn’t have a Focus() method to programmatically change the item that has the dotted border.

After a lot of experiments, I finally found a version (see below) in which the dotted border did not appear, although I don’t understand how the 2 lines at the end of the MoveItemListBox.__init__() method achieve that result!

I have only tested it using wxPython 4.2.1 + Python 3.10.12 + Linux Mint 21.3, so it may not work on other platforms.

import wx

class MoveItemListBox(wx.ListBox):
    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)
        self.Bind(wx.EVT_CHAR, self.OnChar)

        # This prevents a dotted border from
        # appearing on one of the items
        self.SetFocus()
        self.SetSelection(0)


    def OnChar(self, event):
        pos = self.GetSelection()
        if pos >= 0:
            code = event.GetKeyCode()
            if code == wx.WXK_DOWN and pos < self.GetCount() - 1:
                if event.ControlDown():
                    self.moveItemDown(pos)
                else:
                    self.SetSelection(pos+1)
            elif code == wx.WXK_UP and pos > 0:
                if event.ControlDown():
                    self.moveItemUp(pos)
                else:
                    self.SetSelection(pos-1)


    def moveItemDown(self, pos):
        label = self.GetString(pos)
        self.Delete(pos)
        self.Insert(label, pos+1)
        self.SetSelection(pos+1)


    def moveItemUp(self, pos):
        label = self.GetString(pos)
        self.Delete(pos)
        self.Insert(label, pos-1)
        self.SetSelection(pos-1)


class TestFrame(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, title="Move item in ListBox")
        self.SetSize((370, 400))
        panel = wx.Panel(self)
        items = [f"Item {i+1}" for i in range(20)]
        self.list_box = MoveItemListBox(panel, choices=items, size=(100, 200), style=wx.LB_SINGLE)
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.list_box, 0, wx.EXPAND|wx.RIGHT, 4)
        label = wx.StaticText(panel, label="Move selected item using Ctrl+Up/Down")
        sizer.Add(label, 0, wx.ALIGN_CENTER_VERTICAL, 0)
        panel.SetSizer(sizer)
        self.Layout()


if __name__ == '__main__':
    app = wx.App()
    frame = TestFrame()
    frame.Show()
    app.MainLoop()

EDIT: in the original code you are using the Append() method to include client data with the items, so my moveItemDown() and moveItemUp() methods would need to be modified to also copy that as well.

2 Likes

Hi Richard,
just logged in to say, you’re a bit of a diamond mate!
Elegant and simple.