Custom Controls in wxPython: Part 1

This is the first part of an ongoing series of articles about making custom UI controls using Python and the wxPython toolkit.

I’ve decided that, in addition to everything else I’m writing, to also write a series of articles about the ins and outs of programming custom controls with Python and the wxPython GUI toolkit. I’ve found most of the resources online to be a good starting point, but nothing that goes into the depth that I needed for several of my projects.

Introduction

Before you begin, you should first familiarize yourself with wxPython. It’s an extension for Python of wxWindows, a cross-platform native GUI toolkit. It makes creating platform-independent GUI applications much easier in my opinion, especially when you couple it with the power of Python.

You should also have a good reason for coming up with a new custom control. The wxPython library comes with quite a large number of UI elements, many of which can handle just about anything you can throw at them.

But maybe they do about 90% of what you need for a specific job and are missing that extra 10% that would make it absolutely perfect. That’s where subclassing as a custom control method comes in handy.

A Word on Terminology

Throughout this article I refer to both widgets and controls. If you come from the world of Microsoft you’re probably more familiar with the term control. If you’re from the Linux/UNIX/OS X world, widget is what you probably know better.

I use both terms interchangeably. I’m not here to start a holy flame war about what word fits better. In my mind they both work.

What’s a Control?

Controls (or widgets) make up a large chunk of any GUI toolkit. Windows, buttons, labels, sliders, text boxes, progress gauges, they’re all controls. A control takes in input in some form and displays it in a (hopefully) user-friendly manner.

Put a bunch of them together in a window and you’ve got the beginnings of a modern GUI application.

Controls communicate with the parent program through events. These are fired off when something happens to the control that warrants it telling the application, and then the application can act accordingly. We’ll get into that later on in the article.

Subclassing

The easiest way to make a custom control in wxPython is to subclass an existing control and modify its behavior. This method works great because you don’t have to write a lot of the base code to establish the control, but rather concentrate on just how you want it to behave.

Subclassing means you derive a new class from an existing control’s class and work on improving it. You can override methods of the base class and introduce your own custom behavior without having to reinvent the wheel. You’re only reinventing a few spokes.

The StatusLabel Control

Let’s get started on our own subclassed control. What shall we make?

For one particular project, I had a whole bunch of drop-down listboxes which led to a lot of confusion. Since the values were essentially on rails it made more sense to present the information in a more friendly manner while also being able to change values easily.

The wx.StaticText control did about 90% of what I needed. It’s also one of the simplest controls you can work with: you set the text value to something and that’s it. Maybe you get the value sometimes, but that’s it.

What if we made it smarter? That is, we let people click on the it to cycle through some known values and fire off an event that its value had changed?

That, in a nutshell, is the StatusLabel control.

Planning

It’s recommended that you have a good idea of what kind of behavior you want your control to have. Who knows, you might find out that someone else has already included it in wxPython and all you have to do is call up some code in the demo to get a good grasp of its functionality. If not, make sure you don’t go in blind.

It’s also a good practice to keep your control’s code in a separate file or files from everything else. For this control, I created a file called StatusLabel.py.

The wx.Window Class

Anything you see drawn on a window in wxPython all comes from a single base class called wx.Window. This class provides much of the default behavior of all the widgets you see on the screen, even if they’re derived much further down the chain.

It’s important to remember this because we’ll come back to it later. For now, nod and understand that if you’ve got a widget you see on the screen, it originated way back with wx.Window.

Deriving The StatusLabel Class

Let’s start off with our __init__ routine for our new class, along with the declaration:

import wx
 
class StatusLabel(wx.StaticText):
    def __init__(self, *args, **kwds):
        wx.StaticText.__init__(self, *args, **kwds)
        self.stateValues = []
        self.stateColors = []
        self.tooltips = []
        self.curState = -1
 
        self.Bind(wx.EVT_ENTER_WINDOW, self.OnMouseIn, self)
        self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseOut, self)
        self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp, self)
        self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp, self)
    # end __init__

The first thing you’ll notice is that we’re creating a new class called StatusLabel that is derived from wx.StaticText. If you look up the documentation on our parent class, you’ll notice that it doesn’t do much at all.

In __init__, our first order of business is to call the initializer of our parent class, passing any relevant arguments to it. This ensures that we have a clean creation of our own control.

The lists we have set will store all of the values the text can have, colors of that text, and even some tooltips for each value if we wish to use them. The instance variable self.curState keeps track of which one of those values we’re currently using. This will come in handy later, should we want to query the control for it.

Now we get into the custom behavior. When we call the self.Bind() method notice we’re asking to listen to events we generate inside of the control. That’s because we pass self as the third argument to Bind(). This is perfectly legal and acceptable.

Also notice that these are not events that wx.StaticText uses. Neither is Bind(). That’s because wx.StaticText derives from wx.Window. And since we’re deriving from wx.StaticText, so do we. This means we can also access those events from wx.Window (and any class in-between really) to make our custom control behave like we want.

For the StatusLabel control, we want to listen (known as event binding) for left and right mouse clicks, and also let us know when the user has moved the mouse in and out of the control so we can provide some visual feedback that they can do something.

Custom Events

Obviously the StaticText control doesn’t do much and says even less to any apps that use it. If we want our StatusLabel class to report changes, we’ll have to make some of our own events. That means we can have our app bind and listen for events the same as those that come with wxPython.

For our StatusLabel control, we’ll only need 3 custom events to send out:

  • When the user’s mouse moves on the label
  • When the user’s mouse moves off the label
  • When the user makes a change to the value by left- or right-clicking with the mouse

In practice you may just need the last event that tells the application that something has happened. The other two can be safely ignored, as we’ll see that the StatusLabel control handles that part just fine on its own.

Creating a new event is a two-step process (and two lines also, thankfully). First you must create an object through the wx.NewEventType() method. From this, you can bind it to your own event constant, which will be used by the calling application.

Here are our events:

myEVT_STATUS_LABEL_CHANGED = wx.NewEventType()
EVT_STATUS_LABEL_CHANGED = wx.PyEventBinder(myEVT_STATUS_LABEL_CHANGED, 1)
 
myEVT_LABEL_MOUSE_IN = wx.NewEventType()
EVT_LABEL_MOUSE_IN = wx.PyEventBinder(myEVT_LABEL_MOUSE_IN, 1)
 
myEVT_LABEL_MOUSE_OUT = wx.NewEventType()
EVT_LABEL_MOUSE_OUT = wx.PyEventBinder(myEVT_LABEL_MOUSE_OUT, 1)

Now we have some events defined. If you’ve used wxPython before, you’ll know that an event passes an object to the application. Even though StatusLabel might not look like it needs one, an event class can help ease the passing of important information back to the application.

Let’s define a small event class for this control, one that will pass the application the current state or value that the StatusLabel has:

class StatusLabelEvent(wx.PyCommandEvent):
    def __init__(self, evtType, id):
        wx.PyCommandEvent.__init__(self, evtType, id)
        self._state = -1
 
    def GetState(self):
        return self._state
# end class

We derive our event class from wx.PyCommandEvent, which is another one of those root classes like wx.Window. We have 1 class variable called self._state, which we’ll set when we fire off events.

Now that we have that out of the way, let’s look at making this control do something.

Control Logic

Now that we’ve defined a few events we want to listen for, as well as our own custom events we want to send up to the app, we should write some event handlers to deal with how the control should behave.

    def OnMouseIn(self, event):
        self.SetCursor(wx.StockCursor(wx.CURSOR_HAND))
        evt = StatusLabelEvent(myEVT_LABEL_MOUSE_IN, self.GetId())
        evt._state = self.curState
        self.GetEventHandler().ProcessEvent(evt)
    # end OnMouseIn
 
    def OnMouseOut(self, event):
        self.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT))
        evt = StatusLabelEvent(myEVT_LABEL_MOUSE_OUT, self.GetId())
        evt._state = self.curState
        self.GetEventHandler().ProcessEvent(evt)
    # end OnMouseOut
 
    def OnLeftUp(self, event):
        self.curState += 1
        if self.curState <= len(self.stateValues):
            self.curState = 0
 
        self.SetState(self.curState)
 
        evt = StatusLabelEvent(myEVT_STATUS_LABEL_CHANGED, self.GetId())
        evt._state = self.curState
        self.GetEventHandler().ProcessEvent(evt)
    # end OnLeftUp
 
    def OnRightUp(self, event):
        self.curState -= 1
        if self.curState < 0:
            self.curState = len(self.stateValues) - 1
 
        self.SetState(self.curState)
        evt = StatusLabelEvent(myEVT_STATUS_LABEL_CHANGED, self.GetId())
        evt._state = self.curState
        self.GetEventHandler().ProcessEvent(evt)
    # end OnRightUp

The OnMouseIn() and OnMouseOut() event handlers are mirrors of each other. One changes the mouse cursor to a hand when it is placed over the control. This is a good visual cue to the user that they can click the control. When they move the mouse cursor off the control, it is set back to normal.

The next three lines of each of these methods are almost exactly the same. They fire off two of our custom events to wxPython, which handles all of the messy details of getting it to the application. Notice that we send not just which event it should fire off (called myEVT_*) but a StatusLabelEvent object containing the current value.

OnLeftUp() and OnRightUp() handle moving through the list of values in the appropriate direction, and then calling the SetState() method, which we haven’t defined yet, but I bet you can guess what it does pretty easily:

    def SetState(self, state):
        self.SetLabel(self.stateValues[state])
        if state - 1 < len(self.stateColors):
            try:
                self.SetForegroundColour(self.stateColors[state])
            except:
                self.SetForegroundColour(wx.Colour(0,0,0))
 
        if state - 1 < len(self.tooltips):
            try:
                self.SetToolTipString(self.tooltips[state])
            except:
                self.SetToolTipString('')
 
        self.Update()
        self.curState = state
    # end SetState

By putting all of this into a separate method, we’ve allowed it to be callable by the application programmatically without firing off a stray event we don’t want to catch twice.

At the end of this method we call self.Update(), another one of those niceties given to use by wx.Window and overridden by wx.StaticText. This method forces the control to be redrawn, in case there is some lag. Finally, we set the value of self.curState to the state that is passed in, as this is the new value we must abide by, especially when the user clicks with their mouse.

Getters and Setters

To wrap up the code for the control, let’s put a few getters and setters in just to make our lives (and possibly other programmers’) lives easier.

    def SetTooltips(self, tips):
        self.tooltips = tips
    # end SetTooltips
 
    def GetState(self):
        return self.curState
    # end GetState
 
    def SetStateText(self, vals):
        self.stateValues = vals
    # end SetStateText
 
    def SetStateColors(self, colors):
        self.stateColors = colors
    # end SetStateColors

These methods accept lists as their argument. You should do a little bit of error-checking here and possibly throw and exception if the types don’t match (more on that in another article). I’ve left that out of here so you can concentrate on just the main code of the control.

Testbed

Now it’s time to test our control out and see if it works. Before you run off and make a new file, however, consider putting a small testbed app at the bottom of this file and set it up so it only runs when fired off from the interpreter at the command line. This is a good way to keep your tests right there with your controls.

class TestAppFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        kwds["style"] = wx.DEFAULT_FRAME_STYLE
        wx.Frame.__init__(self, *args, **kwds)
        self.panel = wx.Panel(self, -1)
 
        self.ctrl = StatusLabel(self.panel, -1, "MY LABEL")
        self.ctrl.SetStateText(["UNSIGNED","SIGNED", "IN PROCESS"])
        self.ctrl.SetStateColors( [wx.Colour(0,0,0), wx.Colour(0, 139, 0), wx.Colour(33, 70, 243) ] )
        self.ctrl.SetState(0)
 
        self.Bind(EVT_LABEL_MOUSE_IN, self.OnLabelIn, self.ctrl) 
        self.Bind(EVT_LABEL_MOUSE_OUT, self.OnLabelOut, self.ctrl) 
        self.Bind(EVT_STATUS_LABEL_CHANGED, self.OnStatusChange, self.ctrl) 
 
        self.__set_properties()
        self.__do_layout()
 
    def __set_properties(self):
        self.SetTitle("Testbed Framework")
        self.SetSize( (800, 600) )
        self.ctrl.SetFont(wx.Font(12, wx.DEFAULT, wx.NORMAL, wx.BOLD, 0, ""))
 
    def __do_layout(self):
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.panel, 0, wx.ALL|wx.EXPAND, 0)
        self.SetSizer(sizer)
        sizer.Fit(self)
        self.Layout()
    # end __do_layout
 
    def OnLabelIn(self, event):
        print "OnLabelIn..."
 
    def OnLabelOut(self, event):
        print "OnLabelOut..."
 
    def OnStatusChange(self, event):
        print "State changed to %d" % event.GetState()
 
# end of class MyFrame
 
 
if __name__ == "__main__":
    app = wx.PySimpleApp(0)
    wx.InitAllImageHandlers()
    frame_1 = TestAppFrame(None, -1, "")
    app.SetTopWindow(frame_1)
    frame_1.Show()
    app.MainLoop()

Now you can call your file with Python and test it out without having to write out your test app over and over.

Source Code

Here is the full source code for StatusLabel.py:

# StatusLabel.py -- Multi-state label that changes on left- and right-click of mouse
import wx
 
myEVT_STATUS_LABEL_CHANGED = wx.NewEventType()
EVT_STATUS_LABEL_CHANGED = wx.PyEventBinder(myEVT_STATUS_LABEL_CHANGED, 1)
 
myEVT_LABEL_MOUSE_IN = wx.NewEventType()
EVT_LABEL_MOUSE_IN = wx.PyEventBinder(myEVT_LABEL_MOUSE_IN, 1)
 
myEVT_LABEL_MOUSE_OUT = wx.NewEventType()
EVT_LABEL_MOUSE_OUT = wx.PyEventBinder(myEVT_LABEL_MOUSE_OUT, 1)
 
 
class StatusLabelEvent(wx.PyCommandEvent):
    def __init__(self, evtType, id):
        wx.PyCommandEvent.__init__(self, evtType, id)
        self._state = -1
 
    def GetState(self):
        return self._state
# end class
 
class StatusLabel(wx.StaticText):
    def __init__(self, *args, **kwds):
        wx.StaticText.__init__(self, *args, **kwds)
        self.stateValues = []
        self.stateColors = []
        self.tooltips = []
        self.curState = -1
 
        self.Bind(wx.EVT_ENTER_WINDOW, self.OnMouseIn, self)
        self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseOut, self)
        self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp, self)
        self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp, self)
    # end __init__
 
    def OnMouseIn(self, event):
        self.SetCursor(wx.StockCursor(wx.CURSOR_HAND))
        evt = StatusLabelEvent(myEVT_LABEL_MOUSE_IN, self.GetId())
        evt._state = self.curState
        self.GetEventHandler().ProcessEvent(evt)
    # end OnMouseIn
 
    def OnMouseOut(self, event):
        self.SetCursor(wx.StockCursor(wx.CURSOR_DEFAULT))
        evt = StatusLabelEvent(myEVT_LABEL_MOUSE_OUT, self.GetId())
        evt._state = self.curState
        self.GetEventHandler().ProcessEvent(evt)
    # end
 
    def OnLeftUp(self, event):
        self.curState += 1
        if self.curState >= len(self.stateValues):
            self.curState = 0
 
        self.SetState(self.curState)
 
        evt = StatusLabelEvent(myEVT_STATUS_LABEL_CHANGED, self.GetId())
        evt._state = self.curState
        self.GetEventHandler().ProcessEvent(evt)
    # end OnLeftUp
 
    def OnRightUp(self, event):
        self.curState -= 1
        if self.curState < 0:
            self.curState = len(self.stateValues) - 1
 
        self.SetState(self.curState)
        evt = StatusLabelEvent(myEVT_STATUS_LABEL_CHANGED, self.GetId())
        evt._state = self.curState
        self.GetEventHandler().ProcessEvent(evt)
    # end OnRightUp
 
    def SetTooltips(self, tips):
        self.tooltips = tips
    # end SetTooltips
 
    def GetState(self):
        return self.curState
    # end GetState
 
    def SetStateText(self, vals):
        self.stateValues = vals
    # end SetStateText
 
    def SetStateColors(self, colors):
        self.stateColors = colors
    # end SetStateColors
 
    def SetState(self, state):
        self.SetLabel(self.stateValues[state])
        if state - 1 < len(self.stateColors):
            try:
                self.SetForegroundColour(self.stateColors[state])
            except:
                self.SetForegroundColour(wx.Colour(0,0,0))
 
        if state - 1 < len(self.tooltips):
            try:
                self.SetToolTipString(self.tooltips[state])
            except:
                self.SetToolTipString('')
 
        self.Update()
        self.curState = state
    # end SetState
 
# end class
 
 
class TestAppFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        kwds["style"] = wx.DEFAULT_FRAME_STYLE
        wx.Frame.__init__(self, *args, **kwds)
        self.panel = wx.Panel(self, -1)
 
        self.ctrl = StatusLabel(self.panel, -1, "MY LABEL")
        self.ctrl.SetStateText(["UNSIGNED","SIGNED", "IN PROCESS"])
        self.ctrl.SetStateColors( [wx.Colour(0,0,0), wx.Colour(0, 139, 0), wx.Colour(33, 70, 243) ] )
        self.ctrl.SetState(0)
 
        self.Bind(EVT_LABEL_MOUSE_IN, self.OnLabelIn, self.ctrl) 
        self.Bind(EVT_LABEL_MOUSE_OUT, self.OnLabelOut, self.ctrl) 
        self.Bind(EVT_STATUS_LABEL_CHANGED, self.OnStatusChange, self.ctrl) 
 
        self.__set_properties()
        self.__do_layout()
 
    def __set_properties(self):
        self.SetTitle("Testbed Framework")
        self.SetSize( (800, 600) )
        self.ctrl.SetFont(wx.Font(12, wx.DEFAULT, wx.NORMAL, wx.BOLD, 0, ""))
 
    def __do_layout(self):
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.panel, 0, wx.ALL|wx.EXPAND, 0)
        self.SetSizer(sizer)
        sizer.Fit(self)
        self.Layout()
    # end __do_layout
 
    def OnLabelIn(self, event):
        print "OnLabelIn..."
 
    def OnLabelOut(self, event):
        print "OnLabelOut..."
 
    def OnStatusChange(self, event):
        print "State changed to %d" % event.GetState()
 
# end of class MyFrame
 
 
if __name__ == "__main__":
    app = wx.PySimpleApp(0)
    wx.InitAllImageHandlers()
    frame_1 = TestAppFrame(None, -1, "")
    app.SetTopWindow(frame_1)
    frame_1.Show()
    app.MainLoop()

Conclusion

I hope you found this article useful. If there is sufficient feedback to do so, I will write some more in this series covering more advanced topics of custom control programming with wxPython.

5 thoughts on “Custom Controls in wxPython: Part 1

    1. Most of my programming articles are based on software development for filmmaking. This one in particular is based on some lessons learned from a larger software package that is coming out on the market soon. Filmmaking and computers are joined at the hip. I think it helps to be a little more savvy tech-wise, especially the up-and-comers and indies who have grown up with the Internet.

  1. Thanks for publishing this article! As a relative Python newbie, it was much more comprehensible than the other custo control articles I have seen.

    I’d like to create a similar multi-state custom control, except using different bitmaps to display state changes in a custom button. Would you have any recommendations on how to go about this?
    Thanks much!
    Kevin

    1. If I’m reading your question right, you want to have a button with a bitmap inside of it, and change the bitmap based on state? If that’s the case, you can use the BitmapButton control that ships with wxPython. If that isn’t quite what you’re looking for, let me know and I’ll see if I can help.

  2. Devin thanks for this time saving tutorials on custom control, i have always like such kind of tutorials easy to follow and always subclassing from the real class you want, i hope to do the same with buttons THANKS THANKS THANKS!!!!!!

Comments are closed.

%d bloggers like this: