= How to create a list control (info) =
Basic operation:

 . Associating python data with items
 Selecting and manipulating items in wx.ListCtrls (using SetItem* methods) Report-view List controls "why don't my items show up" (missing headers) {{{self.list.InsertColumn( 0, "Items", width=-1)}}}

See Also:

 . [[wxListCtrl ToolTips]]

 . ObjectListView

<<TableOfContents>>

== Associating Python Data with Items (Phoenix) ==
When you need to associate data with an item:

{{{#!python
listctrl.SetItemData(item_by_index, integer_for_item)
}}}
Yep! You get to associate a ''WHOLE'' integer with the item!

To associate more, make a num generator (or use wx.NewIdRef) and keep a dict binding id to whatever data you want to associate.

So, for example:

{{{#!python
id = wx.NewIdRef()
data_dict[id] = some_data_to_associate
listctrl.InsertItem(index_to_place_at, string_name_of_item)
listctrl.SetItemData(index_to_place_at, id)
}}}
So, now you can traverse:

index --> associated data (id) --> some_data_to_associate

== Python Data Mixin ==
I'm surprised I haven't seen one of these yet.

{{{#!python
class ListCtrlPyDataMixin(object):
    def __init__(self):
        import sys

        self.id = -sys.maxint
        self.map = {}

        self.Bind(wx.EVT_LIST_DELETE_ITEM, self.OnDeleteItem)
        self.Bind(wx.EVT_LIST_DELETE_ALL_ITEMS, self.OnDeleteAllItems)

    def SetPyData(self, item, data):
        self.map[self.id] = data
        self.SetItemData(item, self.id)
        self.id += 1

    def SortPyItems(self, fn):
        from functools import wraps

        @wraps(fn)
        def wrapper(a, b):
            return fn(self.map[a], self.map[b])

        self.SortItems(wrapper)

    def OnDeleteItem(self, event):
        try:
            del self.map[event.Data]
        except KeyError:
            pass
        event.Skip()

    def OnDeleteAllItems(self, event):
        self.map.clear()
        event.Skip()
}}}
Have fun. -- ChristopherMonsanto

== Selecting Items Programmatically ==
To select an item programmatically in a wx.ListCtrl (note that this appears to be a bug ?  Shouldn't need to use wx.LIST_STATE_SELECTED for stateMask argument, should be able to use a MASK argument instead ?) :

 . {{{self.SetItemState(ID, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED)}}}

It should be (I think) :

 . {{{self.SetItemState(ID, wx.LIST_STATE_SELECTED, wx.LIST_MASK_STATE)}}}

''Note : I just tried both versions, and the first version works in wxPython 2.5 ; the second version (using LIST_MASK_STATE) does not.''

I think only the first version is correct. The last parameter is a mask of all the states you want to modify, so it must consist only of wx.LIST_STATE_* combinations. The wx.LIST_MASK_STATE is only used in SetItem, where it tells which fields are valid. Seems the docs could be clearer though.

== Retrieve Currently Selected Indices in List Control ==
A common task, there's no pre-built function to accomplish this, so here's a recipe:

{{{#!python
def _getSelectedIndices(self, state =  wx.LIST_STATE_SELECTED):
        indices = []
        lastFound = -1
        while True:
                index = self.GetNextItem(
                        lastFound,
                        wxLIST_NEXT_ALL,
                        state,
                )
                if index == -1:
                        break
                else:
                        lastFound = index
                        indices.append( index )
        return indices
}}}
== wx.ListCtrl's Hello world (Phoenix) ==
It took half an hour for a newbie like me to get this tiny application running. Until I saw that you must call "InsertItem" :-) .

{{attachment:img_sample_one.png}}

{{{#!python
# sample_one.py

import wx

# class MyFrame
# class MyApp

#---------------------------------------------------------------------------

data = [("World", "Python", "Welcome"),
        ("wxWidgets", "Data", "Items"),
        ("ListCtrl", "Report", "Border"),
        ("Index", "Column", "Insert"),
        ("Width", "Header", "Image")]

#---------------------------------------------------------------------------

class MyFrame(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, -1,
                          "List control report",
                          size=(380, 220))

        id = wx.NewIdRef()
        self.list = wx.ListCtrl(self, id,
                                style=wx.LC_REPORT|
                                      wx.SUNKEN_BORDER)

        # Add some columns
        self.list.InsertColumn(0, "Data #1")
        self.list.InsertColumn(1, "Data #2")
        self.list.InsertColumn(2, "Data #3")

        # Add the rows
        for item in data:
            index = self.list.InsertItem(self.list.GetItemCount(), item[0])
            for col, text in enumerate(item[1:]):
                self.list.SetItem(index, col+1, text)

        # Set the width of the columns
        self.list.SetColumnWidth(0, 120)
        self.list.SetColumnWidth(1, 120)
        self.list.SetColumnWidth(2, 120)

#---------------------------------------------------------------------------

class MyApp(wx.App):
    def OnInit(self):

        #------------

        frame = MyFrame()
        self.SetTopWindow(frame)
        frame.Show(True)

        return True

#---------------------------------------------------------------------------

def main():
    app = MyApp(False)
    app.MainLoop()

#---------------------------------------------------------------------------

if __name__ == "__main__" :
    main()
}}}
== Changing fonts in a ListCtrl (Phoenix) ==
Supposing you have a List Control, and you want to set some items to, say, bold. That's all - no changing anything else about the font, so the face remains the same as the other items. (Bear in mind the face will most likely be the default face as set by the system, and hence we don't know what it is.)

Because I (Dave Cridland) am terminally stupid, I didn't figure out how to do this, and couldn't find any documentation. In case anyone else is trying to do the same thing, and can't figure it out either, here's what I did:

{{attachment:img_sample_two.png}}

{{{#!python
# sample_two.py

import wx

# class MyFancyListCtrl
# class MyFrame
# class MyApp

#---------------------------------------------------------------------------

class MyFancyListCtrl(wx.ListCtrl):
    def __init__(self, parent, id):
        wx.ListCtrl.__init__(self, parent, id,
                             style=wx.LC_REPORT|
                                   wx.LC_VIRTUAL|
                                   wx.SUNKEN_BORDER)

        # Add some columns
        self.InsertColumn(0, "Data #1")
        self.InsertColumn(1, "Data #2")
        self.InsertColumn(2, "Data #3")

        # Set the width of the columns
        self.SetColumnWidth(0, 120)
        self.SetColumnWidth(1, 120)
        self.SetColumnWidth(2, 120)

        self.SetItemCount(10)

        # We setup our pretty attribute once, here.
        self._funky_attr = wx.ListItemAttr()

        # This now contains default attributes.
        # We can set the text colour to be different :
        self._funky_attr.SetTextColour(wx.BLUE)

        # But we can't do either of :
        # self._funky_attr.GetFont().SetWeight(wx.BOLD)
        # (Above fails silently - probably GetFont() returns a copy).
        # new_font = wx.Font()
        # (No default constructor in wxPython).
        # But we can extract the default font :
        new_font = self._funky_attr.GetFont()

        # Set the weight on it :
        new_font.SetWeight(wx.BOLD)

        # Then put it back.
        self._funky_attr.SetFont(new_font)
        # self._funky_attr is now ready to be returned by OnGetItemAttr()
        # (since this happens to be virtual).

        self.Bind(wx.EVT_LIST_ITEM_SELECTED, self.OnItemSelected)
        self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnItemActivated)
        self.Bind(wx.EVT_LIST_ITEM_DESELECTED, self.OnItemDeselected)

    #-----------------------------------------------------------------------

    def OnItemSelected(self, event):
        self.currentItem = event.Index
        print('OnItemSelected: "%s", "%s", "%s", "%s"\n' %
              (self.currentItem,
               self.GetItemText(self.currentItem),
               self.GetColumnText(self.currentItem, 1),
               self.GetColumnText(self.currentItem, 2)))


    def OnItemActivated(self, event):
        self.currentItem = event.Index
        print("OnItemActivated: %s\nTopItem: %s\n" %
              (self.GetItemText(self.currentItem), self.GetTopItem()))


    def OnItemDeselected(self, evt):
        print("OnItemDeselected: %s" % evt.Index)


    def GetColumnText(self, index, col):
        item = self.GetItem(index, col)
        return item.GetText()


    def OnGetItemText(self, item, col):
        return "Item %d, column %d" % (item, col)


    def OnGetItemAttr(self, item):
        if item % 2 == 1:
            return self._funky_attr
        else:
            return None

#---------------------------------------------------------------------------

class MyFrame(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, -1,
                          "Fancy list control",
                          size=(400, 240))

        id = wx.NewIdRef()
        self.list = MyFancyListCtrl(self, id)

#---------------------------------------------------------------------------

class MyApp(wx.App):
    def OnInit(self):

        #------------

        frame = MyFrame()
        self.SetTopWindow(frame)
        frame.Show(True)

        return True

#---------------------------------------------------------------------------

def main():
    app = MyApp(False)
    app.MainLoop()

#---------------------------------------------------------------------------

if __name__ == "__main__" :
    main()
}}}
Simply changing the font of a specific item :

{{{#!python
# Get the item at a specific index :
item = self.myListControl.GetItem(index)
# Get its font, change it, and put it back :
font = item.GetFont()
font.SetWeight(wx.FONTWEIGHT_BOLD)
item.SetFont(font)
# This does the trick:
self.myListControl.SetItem(item)
}}}
== Unselecting an item programmatically (Phoenix) ==
It took me (Werner Bruhin) some searching, as always when one figures it out it is easy!

{{{#!python
# Selecting as shown in the demo
self.SetItemState(item, wx.LIST_STATE_SELECTED, wx.LIST_STATE_SELECTED)

# Unselecting
self.SetItemState(item, 0, wx.LIST_STATE_SELECTED)
}}}
== Type Ahead Mixin ==
Windows (and presumably other OS do as well) provides Type Ahead functionality by default in list controls.  But not if they are Virtual.  I had a need to provide Type Ahead for some virtual controls in my app, so I knocked up this Mixin class for wxListCtrl objects.  Happily it works with both Virtual and Normal list controls which means if you use it in your app then all your list controls can have the same Type Ahead behaviour.

There are some things I've tried to make configurable; the type ahead timeout, case sensitivity to the search, special keycodes to avoid during typeahead, and I've provided methods that you could override to change the default behaviour that I've coded (it was almost on a whim as I didn't really like the behaviour that Windows was providing, it seemed, weird). To help understand what's going on I've tried to document the code to explain the bits I think are weird.

One thing that is notable is that the default windows type ahead doesn't seem to recognise the space character, with some playing about with EVT_KEY_DOWN and EVT_CHAR events I've made my type ahead deal with spaces.  Which is nice.

Anyway, on with the code (please excuse the verbosity of my variable and method names :) ).

{{{#!python
from wxPython.wx import *

class TypeAheadListCtrlMixin:
    """  A Mixin Class that does type ahead scrolling for list controls.
         Assumes that the wxListCtrl it is mixed into is sorted (it's using
         a binary search to find the correct item).

         I wrote this because on Windows there was no type ahead when you
         used a virtual list control, and then couldn't be bohered deciphering
         the default windows typeahead for non-virtual list controls.  So I
         made it work for both virtual and non-virtual controls so that all
         my list controls would have the same functionality.

         Things you can change programatically:
         * expand or contract the list of keycodes that stop the type ahead
         * expand or contract the list of keycodes that are allowed to be
           used inside typeahead, but won't start it (e.g. space which normally
           acts as an Activation Key)
         * change the timeout time (init param, defaults to 500 milliseconds)
         * change the sensitivity of the search (init param, defaults to caseinsensitive)
         Things you can change in the class that you mix this into:
         * override the following methods:
           - currentlySelectedItemFoundByTypeAhead
           - currentlySelectedItemNotFoundByTypeAhead
           - newItemFoundByTypeAhead
           - nothingFoundByTypeAhead
           changing these changes the behaviour of the typeahead in various stages.
           See doc comments on methods.

        Written by Murray Steele (muz at h-lame dot com)
    """

    # These Keycodes are the ones that if we detect them we will cancel the current
    # typeahead state.
    stopTypeAheadKeyCodes = [
     WXK_BACK,
     WXK_TAB,
     WXK_RETURN,
     WXK_ESCAPE,
     WXK_DELETE,
     WXK_START,
     WXK_LBUTTON,
     WXK_RBUTTON,
     WXK_CANCEL,
     WXK_MBUTTON,
     WXK_CLEAR,
     WXK_PAUSE,
     WXK_CAPITAL,
     WXK_PRIOR,
     WXK_NEXT,
     WXK_END,
     WXK_HOME,
     WXK_LEFT,
     WXK_UP,
     WXK_RIGHT,
     WXK_DOWN,
     WXK_SELECT,
     WXK_PRINT,
     WXK_EXECUTE,
     WXK_SNAPSHOT,
     WXK_INSERT,
     WXK_HELP,
     WXK_F1,
     WXK_F2,
     WXK_F3,
     WXK_F4,
     WXK_F5,
     WXK_F6,
     WXK_F7,
     WXK_F8,
     WXK_F9,
     WXK_F10,
     WXK_F11,
     WXK_F12,
     WXK_F13,
     WXK_F14,
     WXK_F15,
     WXK_F16,
     WXK_F17,
     WXK_F18,
     WXK_F19,
     WXK_F20,
     WXK_F21,
     WXK_F22,
     WXK_F23,
     WXK_F24,
     WXK_NUMLOCK,
     WXK_SCROLL]
    # These are the keycodes that we have to catch in evt_key_down, not evt_char
    # By the time they get to evt_char then the OS has looked at them and gone,
    # hey, this keypress means do something (like pressing space acts as an ACTIVATE
    # key in a list control.
    catchInKeyDownIfDuringTypeAheadKeyCodes = [
     WXK_SPACE
    ]
    # These are the keycodes that we will allow during typeahead, but won't allow to start
    # the type ahead process.
    dontStartTypeAheadKeyCodes = [
     WXK_SHIFT,
     WXK_CONTROL,
     WXK_MENU #ALT Key, ALT Gr generates both WXK_CONTROL and WXK_MENU.
     ]
    dontStartTypeAheadKeyCodes.extend(catchInKeyDownIfDuringTypeAheadKeyCodes)

    def __init__(self, typeAheadTimeout = 500, casesensitive = False, columnToSearch = 0):
        # Do most work in the char handler instead of keydown.
        # This means we get the correct keycode for the key pressed as it should
        # appear on screen, rather than all uppercase or "default" us keyboard
        # punctuation.
        # However there are things that we need to catch in key_down to stop
        # them getting sent to the underlying windows control and generating
        # other events (notably I'm talking about the SPACE key which generates
        # an ACTIVATE event in these list controls).
        EVT_KEY_DOWN(self, self.OnTypeAheadKeyDown)
        EVT_CHAR(self, self.OnTypeAheadChar)
        timerId = wxNewId()
        self.typeAheadTimer = wxTimer(self,timerId)
        EVT_TIMER(self,timerId,self.OnTypeAheadTimer)
        self.clearTypeAhead()
        self.typeAheadTimeout = typeAheadTimeout
        self.columnToSearch = columnToSearch
        if not casesensitive:
            self._GetItemText = lambda idx: self.GetItem(idx,self.columnToSearch).GetText().lower()
            self._GetKeyCode = lambda keycode: chr(keycode).lower()
        else:
            self._GetItemText = lambda idx: self.GetItem(idx,self.columnToSearch).GetText()
            self._GetKeyCode = chr

    def OnTypeAheadKeyDown(self, event):
        keycode = event.GetKeyCode()
        if keycode in self.stopTypeAheadKeyCodes:
            self.clearTypeAhead()
        elif self.typeAhead == None:
            if keycode in self.dontStartTypeAheadKeyCodes:
                self.clearTypeAhead()
        else:
            if keycode in self.catchInKeyDownIfDuringTypeAheadKeyCodes:
                self.OnTypeAheadChar(event)
                return
        event.Skip()

    def OnTypeAheadChar(self, event):
        # stop the timer, to make sure that it doesn't fire in the middle of
        # doing this and screw up by None-ifying the typeAhead string.
        # TODO: Yes some kind of lock around a typeAheadState object
        # that contained typeAhead, lastTypeAheadFoundAnything and lastTypeAhead
        # would be better...
        self.typeAheadTimer.Stop()
        keycode = event.GetKeyCode()
        if keycode in self.stopTypeAheadKeyCodes:
            self.clearTypeAhead()
            event.Skip()
            return
        else:
            if self.typeAhead == None:
                if keycode in self.dontStartTypeAheadKeyCodes:
                    self.clearTypeAhead()
                    event.Skip()
                    return
                else:
                    self.typeAhead = self._GetKeyCode(keycode)
            else:
                self.typeAhead += self._GetKeyCode(keycode)
            self.doTypeAhead()
            # This timer is used to nullify the typeahead after a while
            self.typeAheadTimer.Start(self.typeAheadTimeout,wxTIMER_ONE_SHOT)

    def inTypeAhead(self):
        return self.typeAhead != None

    def currentlySelectedItemFoundByTypeAhead(self, idx):
        """This method is called when the typeahead string matches
           the text of the currently selected item.  Put code here if
           you want to have something happen in this case.
           NOTE: Method only called if there was a currently selected item.

           idx refers to the index of the currently selected item."""
        # we don't do anything as we've already selected the thing we want
        pass

    def currentlySelectedItemNotFoundByTypeAhead(self, idx):
        """This method is called when the typeahead string matches
           an item that isn't the currently selected one.  Put code
           here if you want something to happen to the currently
           selected item.
           NOTE: use newItemFoundByTypeAhead for doing something to
           the newly matched item.
           NOTE: Method only called if there was a currently selected item.

           idx referes to the index of the currently selected item."""
        # we deselect it.
        self.SetItemState(idx, 0, wxLIST_STATE_SELECTED)
        self.SetItemState(idx, 0, wxLIST_STATE_FOCUSED)

    def newItemFoundByTypeAhead(self, idx):
        """This is called when the typeahead string matches
           an item that isn't the currently selected one.  Put
           code here if you want something to happen to the newly
           found item.
           NOTE: use currentlySelectedItemNotFoundByTypeAhead for
           doing something to the previously selected item.

           idx refers to the index of the newly matched item."""
        # we select it and make sure it is focused
        self.SetItemState(idx, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED)
        self.SetItemState(idx, wxLIST_STATE_FOCUSED, wxLIST_STATE_FOCUSED)
        self.EnsureVisible(idx)

    def nothingFoundByTypeAhead(self, idx):
        """This method is called when the typeahead string doesn't
           match any items.  Put code here if you want something to
           happen in this case.

           idx refers to the index of the currently selected item
           or -1 if nothing was selected."""
        # don't do anything here, what could we do?
        pass

    def doTypeAhead(self):
        curselected = -1
        if self.lastTypeAheadFoundSomething:
            curselected = self.lastTypeAhead
        else:
            curselected = self.GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)
        min = 0
        if curselected != -1:
            term_name = self._GetItemText(curselected)
            if term_name.startswith(self.typeAhead):
                self.currentlySelectedItemFoundByTypeAhead(curselected)
                self.lastTypeAheadFoundAnything = True
                self.lastTypeAhead = curselected
                return #We don't want this edgecase falling through

        new_idx = self.binary_search(self.typeAhead,min)
        if new_idx != -1:
            if new_idx != curselected and curselected != -1:
                self.currentlySelectedItemNotFoundByTypeAhead(curselected)
            self.newItemFoundByTypeAhead(new_idx)
            self.lastTypeAheadFoundAnything = True
            self.lastTypeAhead = new_idx
        else:
            self.nothingFoundByTypeAhead(curselected)
            self.lastTypeAheadFoundAnything = False
            self.lastTypeAhead = -1

    # NOTE: Originally from ASPN. Augmented.
    def binary_search(self, t, min = 0):
        min = min;
        max = self.GetItemCount() - 1
        while True:
            if max < min:
                return self.doEdgeCase(m, t)
            m = (min + max) / 2
            cur_term = self._GetItemText(m)
            if cur_term < t:
                min = m + 1
            elif cur_term > t:
                max = m - 1
            else:
                return m

    def doEdgeCase(self, m, t):
        """ This method makes sure that if we don't find the typeahead
         as an actual string, then we will return the first item
         that starts with the typeahead string (if there is one)"""
        before = self._GetItemText(max(0,m-1))
        this = self._GetItemText(m)
        after = self._GetItemText(min(self.GetItemCount()-1,m+1))
        if this.startswith(t):
            return m
        elif before.startswith(t):
            return max(0,m-1)
        elif after.startswith(t):
            return min(self.GetItemCount()-1,m+1)
        else:
            return -1

    def clearTypeAhead(self):
        self.typeAhead = None
        self.lastTypeAheadFoundSomething = False
        self.lastTypeAheadIdx = -1

    def OnTypeAheadTimer(self, event):
        self.clearTypeAhead()
}}}
== Customizing ColumnSorterMixin to Sort Dates ==
This code augments wx.lib.mixins.listctrl.ColumnSorterMixin so that it can sort dates that match the date_re regular expression listed at the top of the code.  You need to modify another section of the code to transform your date into the YYYYMMDD format so that the dates can be easily sorted numerically.  Alternatively, you could use the datetime module and store and sort date objects.

{{{#!python
import wx
import wx.lib.mixins.listctrl
import locale
import re

# Change this to your settings
date_re = re.compile("(\d{2})-(\d{2})-(\d{4})")

#----------------------------------------------------------------------------

class CustColumnSorterMixin(wx.lib.mixins.listctrl.ColumnSorterMixin):
    def __init__(self, numColumns):
        wx.lib.mixins.listctrl.ColumnSorterMixin(self, numColumns)

    def GetColumnSorter(self):
        return self.CustColumnSorter

    def CustColumnSorter(self, key1, key2):
        col = self._col
        ascending = self._colSortFlag[col]
        item1 = self.itemDataMap[key1][col]
        item2 = self.itemDataMap[key2][col]

        alpha = date_re.match(item1)
        beta =  date_re.match(item2)
        if alpha and beta:
            # Change these from your settings to YYYYMMDD
            item1 = alpha.group(3)+alpha.group(1)+alpha.group(2)
            item2 =  beta.group(3)+ beta.group(1)+ beta.group(2)

            item1 = int(item1)
            item2 = int(item2)

        #--- Internationalization of string sorting with locale module
        if type(item1) == type('') or type(item2) == type(''):
            cmpVal = locale.strcoll(str(item1), str(item2))
        else:
            cmpVal = cmp(item1, item2)
        #---

        # If the items are equal then pick something else to make the sort value unique
        if cmpVal == 0:
            cmpVal = cmp(*self.GetSecondarySortValues(col, key1, key2))

        if ascending:
            return cmpVal
        else:
            return -cmpVal
}}}
== Drag and Drop with lists - 1 ==
Here's how you can use drag-and-drop to reorder a simple list, or to move items from one list to another.

As it stands, item data will be lost.  Maybe in version 2!  - JohnFouhy

{{{#!python
""" DnD demo with listctrl. """

import wx

class DragList(wx.ListCtrl):
    def __init__(self, *arg, **kw):
        if 'style' in kw and (kw['style']&wx.LC_LIST or kw['style']&wx.LC_REPORT):
            kw['style'] |= wx.LC_SINGLE_SEL
        else:
            kw['style'] = wx.LC_SINGLE_SEL|wx.LC_LIST

        wx.ListCtrl.__init__(self, *arg, **kw)

        self.Bind(wx.EVT_LIST_BEGIN_DRAG, self._startDrag)

        dt = ListDrop(self._insert)
        self.SetDropTarget(dt)

    def _startDrag(self, e):
        """ Put together a data object for drag-and-drop _from_ this list. """

        # Create the data object: Just use plain text.
        data = wx.PyTextDataObject()
        idx = e.GetIndex()
        text = self.GetItem(idx).GetText()
        data.SetText(text)

        # 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.
            pos = self.FindItem(idx, text)
            self.DeleteItem(pos)

    def _insert(self, x, y, text):
        """ Insert text at given x, y coordinates --- used with drag-and-drop. """

        # Clean text.
        import string
        text = filter(lambda x: x in (string.letters + string.digits + string.punctuation + ' '), text)

        # Find insertion point.
        index, flags = self.HitTest((x, y))

        if index == wx.NOT_FOUND:
            if flags & wx.LIST_HITTEST_NOWHERE:
                index = self.GetItemCount()
            else:
                return

        # 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.
        if y > rect.y + rect.height/2:
            index += 1

        self.InsertStringItem(index, text)

class ListDrop(wx.PyDropTarget):
    """ Drop target for simple lists. """

    def __init__(self, setFn):
        """ Arguments:
         - setFn: Function to call on drop.
        """
        wx.PyDropTarget.__init__(self)

        self.setFn = setFn

        # specify the type of data we will accept
        self.data = wx.PyTextDataObject()
        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():
            self.setFn(x, y, self.data.GetText())

        # 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

if __name__ == '__main__':
    items = ['Foo', 'Bar', 'Baz', 'Zif', 'Zaf', 'Zof']

    class MyApp(wx.App):
        def OnInit(self):
            self.frame = wx.Frame(None, title='Main Frame')
            self.frame.Show(True)
            self.SetTopWindow(self.frame)
            return True

    app = MyApp(redirect=False)
    dl1 = DragList(app.frame)
    dl2 = DragList(app.frame)
    sizer = wx.BoxSizer()
    app.frame.SetSizer(sizer)
    sizer.Add(dl1, proportion=1, flag=wx.EXPAND)
    sizer.Add(dl2, proportion=1, flag=wx.EXPAND)
    for item in items:
        dl1.InsertStringItem(99, item)
        dl2.InsertStringItem(99, item)
    app.frame.Layout()
    app.MainLoop()
}}}
The content on this page came from ListAndTreeControls, which was split up into this page and TreeControls .