## page was renamed from AndreaGavana
= Introduction =
Hi Wiki surfer, I am going to describe how to create a custom control in wxPython. In this article's respect, custom control means "owner-drawn".

Even if wxPython ships with a huge number of available widgets, sometimes there is the need to create a new control that performs a particular action or simply looks nicer/more modern in our opinion. That being said, the first place to look at in order to familiarize with custom control design is surely wx.lib, a directory in the wxPython installation tree which contains a large number of owner-drawn widgets.

= Subclassing =
Once we have decided how our widget should roughly look like and which actions it should perform, the first question that should arise is: "my control should be a subclass of what?". In wxWidgets, using C++, you can simply derive your custom class from `wx.Control`, `wx.Panel`, `wx.Window` and so on. However, you may want to deny your personalized widget to accept focus, or do some particular action in methods like `TransferDataFromWindow`. Moreover, if your widget has to be managed by a sizer, you need to be able to override methods like `DoGetBestSize`.

Well, wxPython provides a set of wxPy classes, like `wx.PyControl`, `wx.PyPanel`, `wx.PyWindow` and so on, which are just like their wxWidgets counterparts except they allow some of the more common C++ virtual methods to be overridden in Python derived classes. If you navigate through the wx.lib directory, you will see widgets derived from `wx.PyControl` (stattext.py), `wx.PyPanel` (buttonpanel.py), `wx.PyScrolledWindow` (customtreectrl.py).

= Creating The Widget =
Ok, let's imagine I have decided that I want a new and cool `wx.CheckBox`. I would like this new widget to have a nice checked/unchecked bitmap, and I will take care of drawing it from scratch, handling mouse events, providing it with keyboard support and much more. Obviously, a custom `wx.CheckBox` is composed by a bitmap (which represent the checked/unchecked state) and a label right to it. I am assuming that I already have 4 bitmaps to handle all the possible state combinations of this particular `wx.CheckBox`, namely:

 1. Enabled/Checked
 1. Enabled/Unchecked
 1. Disabled/Checked
 1. Disabled/Unchecked

In the complete source code I will provide, together with a demo, the enabled-state icon are included; the disabled ones are created on the fly using pure Python/wxPython methods.

So, let's start with some code, basically the initialization, and I will try to explain what I am doing.

{{{#!python
import wx

class CustomCheckBox(wx.PyControl):
    """
    A custom class that replicates some of the functionality of wx.CheckBox,
    while being completely owner-drawn with a nice check bitmaps.
    """

    def __init__(self, parent, id=wx.ID_ANY, label="", pos=wx.DefaultPosition,
                 size=wx.DefaultSize, style=wx.NO_BORDER, validator=wx.DefaultValidator,
                 name="CustomCheckBox"):
        """
        Default class constructor.

        @param parent: Parent window. Must not be None.
        @param id: CustomCheckBox identifier. A value of -1 indicates a default value.
        @param label: Text to be displayed next to the checkbox.
        @param pos: CustomCheckBox position. If the position (-1, -1) is specified
                    then a default position is chosen.
        @param size: CustomCheckBox size. If the default size (-1, -1) is specified
                     then a default size is chosen.
        @param style: not used in this demo, CustomCheckBox has only 2 state
        @param validator: Window validator.
        @param name: Window name.
        """

        # Ok, let's see why we have used wx.PyControl instead of wx.Control.
        # Basically, wx.PyControl is just like its wxWidgets counterparts
        # except that it allows some of the more common C++ virtual method
        # to be overridden in Python derived class. For CustomCheckBox, we
        # basically need to override DoGetBestSize and AcceptsFocusFromKeyboard

        wx.PyControl.__init__(self, parent, id, pos, size, style, validator, name)

        # Initialize our cool bitmaps
        self.InitializeBitmaps()

        # Initialize the focus pen colour/dashes, for faster drawing later
        self.InitializeColours()

        # By default, we start unchecked
        self._checked = False

        # Set the spacing between the check bitmap and the label to 3 by default.
        # This can be changed using SetSpacing later.
        self._spacing = 3

        # I assume at the beginning we are not focused
        self._hasFocus = False

        # Ok, set the wx.PyControl label, its initial size (formerly known an
        # SetBestFittingSize), and inherit the attributes from the standard
        # wx.CheckBox
        self.SetLabel(label)
        self.SetInitialSize(size)
        self.InheritAttributes()

        # Bind the events related to our control: first of all, we use a
        # combination of wx.BufferedPaintDC and an empty handler for
        # wx.EVT_ERASE_BACKGROUND (see later) to reduce flicker
        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)

        # Then we want to monitor user clicks, so that we can switch our
        # state between checked and unchecked
        self.Bind(wx.EVT_LEFT_DOWN, self.OnMouseClick)
        if wx.Platform == '__WXMSW__':
            # MSW Sometimes does strange things...
            self.Bind(wx.EVT_LEFT_DCLICK,  self.OnMouseClick)

        # We want also to react to keyboard keys, namely the
        # space bar that can toggle our checked state
        self.Bind(wx.EVT_KEY_UP, self.OnKeyUp)

        # Then, we react to focus event, because we want to draw a small
        # dotted rectangle around the text if we have focus
        # This might be improved!!!
        self.Bind(wx.EVT_SET_FOCUS, self.OnSetFocus)
        self.Bind(wx.EVT_KILL_FOCUS, self.OnKillFocus)
}}}
Ok, excluding the easiest things like assigning the spacing, the check status and the checkbox label, the interesting things happen when we look at the event that I bound to `CustomCheckBox`. If you are going to draw your custom widget from scratch, my suggestion is to use the combination of `wx.BufferedPaintDC` and an empty event handler for the `wx.EVT_ERASE_BACKGROUND` event. `wx.BufferedPaintDC` is a subclass of `wx.BufferedDC` which can be used inside of an `OnPaint()` event handler. Just create an object of this class instead of `wx.PaintDC` and that's all you have to do to (mostly) avoid flicker.

Let's take a closer look at the wx.EVT_PAINT handler:

{{{#!python
    def OnPaint(self, event):
        """ Handles the wx.EVT_PAINT event for CustomCheckBox. """

        # If you want to reduce flicker, a good starting point is to
        # use wx.BufferedPaintDC.
        dc = wx.BufferedPaintDC(self)

        # It is advisable that you don't overcrowd the OnPaint event
        # (or any other event) with a lot of code, so let's do the
        # actual drawing in the Draw() method, passing the newly
        # initialized wx.BufferedPaintDC
        self.Draw(dc)


    def Draw(self, dc):
        """
        Actually performs the drawing operations, for the bitmap and
        for the text, positioning them centered vertically.
        """

        # Get the actual client size of ourselves
        width, height = self.GetClientSize()

        if not width or not height:
            # Nothing to do, we still don't have dimensions!
            return

        # Initialize the wx.BufferedPaintDC, assigning a background
        # colour and a foreground colour (to draw the text)
        backColour = self.GetBackgroundColour()
        backBrush = wx.Brush(backColour, wx.SOLID)
        dc.SetBackground(backBrush)
        dc.Clear()

        if self.IsEnabled():
            dc.SetTextForeground(self.GetForegroundColour())
        else:
            dc.SetTextForeground(wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT))

        dc.SetFont(self.GetFont())

        # Get the text label for the checkbox, the associated check bitmap
        # and the spacing between the check bitmap and the text
        label = self.GetLabel()
        bitmap = self.GetBitmap()
        spacing = self.GetSpacing()

        # Measure the text extent and get the check bitmap dimensions
        textWidth, textHeight = dc.GetTextExtent(label)
        bitmapWidth, bitmapHeight = bitmap.GetWidth(), bitmap.GetHeight()

        # Position the bitmap centered vertically
        bitmapXpos = 0
        bitmapYpos = (height - bitmapHeight)/2

        # Position the text centered vertically
        textXpos = bitmapWidth + spacing
        textYpos = (height - textHeight)/2

        # Draw the bitmap on the DC
        dc.DrawBitmap(bitmap, bitmapXpos, bitmapYpos, True)

        # Draw the text
        dc.DrawText(label, textXpos, textYpos)

        # Let's see if we have keyboard focus and, if this is the case,
        # we draw a dotted rectangle around the text (Windows behavior,
        # I don't know on other platforms...)
        if self.HasFocus():
            # Yes, we are focused! So, now, use a transparent brush with
            # a dotted black pen to draw a rectangle around the text
            dc.SetBrush(wx.TRANSPARENT_BRUSH)
            dc.SetPen(self._focusIndPen)
            dc.DrawRectangle(textXpos, textYpos, textWidth, textHeight)


    def OnEraseBackground(self, event):
        """ Handles the wx.EVT_ERASE_BACKGROUND event for CustomCheckBox. """

        # This is intentionally empty, because we are using the combination
        # of wx.BufferedPaintDC + an empty OnEraseBackground event to
        # reduce flicker
        pass
}}}
What did I do? Well, I have just asked the control to provide its width and height, the suitable check bitmap (depending on the control state), and then I simply positioned the bitmap centered vertically, the text beside it. In case the custom widget has focus from keyboard, I draw a thin dotted rectangle around the checkbox label. You surely have noted that the method `OnEraseBackground()` is empty.

Our custom control is now perfectly drawn, but we still don't know what to do to react to mouse and keyboard events. What we want the control to do is just to change its state (checked/unchecked) when the user click with the left mouse button or when he hits the spacebar and we have keyboard focus. About mouse events:

{{{#!python
    def OnMouseClick(self, event):
        """ Handles the wx.EVT_LEFT_DOWN event for CustomCheckBox. """

        if not self.IsEnabled():
            # Nothing to do, we are disabled
            return

        self.SendCheckBoxEvent()
        event.Skip()


    def SendCheckBoxEvent(self):
        """ Actually sends the wx.wxEVT_COMMAND_CHECKBOX_CLICKED event. """

        # This part of the code may be reduced to a 3-liner code
        # but it is kept for better understanding the event handling.
        # If you can, however, avoid code duplication; in this case,
        # I could have done:
        #
        # self._checked = not self.IsChecked()
        # checkEvent = wx.CommandEvent(wx.wxEVT_COMMAND_CHECKBOX_CLICKED,
        #                              self.GetId())
        # checkEvent.SetInt(int(self._checked))
        if self.IsChecked():

            # We were checked, so we should become unchecked
            self._checked = False

            # Fire a wx.CommandEvent: this generates a
            # wx.wxEVT_COMMAND_CHECKBOX_CLICKED event that can be caught by the
            # developer by doing something like:
            # MyCheckBox.Bind(wx.EVT_CHECKBOX, self.OnCheckBox)
            checkEvent = wx.CommandEvent(wx.wxEVT_COMMAND_CHECKBOX_CLICKED,
                                         self.GetId())

            # Set the integer event value to 0 (we are switching to unchecked state)
            checkEvent.SetInt(0)

        else:

            # We were unchecked, so we should become checked
            self._checked = True

            checkEvent = wx.CommandEvent(wx.wxEVT_COMMAND_CHECKBOX_CLICKED,
                                         self.GetId())

            # Set the integer event value to 1 (we are switching to checked state)
            checkEvent.SetInt(1)

        # Set the originating object for the event (ourselves)
        checkEvent.SetEventObject(self)

        # Watch for a possible listener of this event that will catch it and
        # eventually process it
        self.GetEventHandler().ProcessEvent(checkEvent)

        # Refresh ourselves: the bitmap has changed
        self.Refresh()
}}}
As you can see, the procedure is quite simple: wxPython detects the mouse click and call the event handler. We just query the status of our control (checked/unchecked), and we fire a `wx.wxEVT_COMMAND_CHECKBOX_CLICKED`, assign the event value based on the control state, and watch for a possible listener of this event (look at the demo, it is very simple).

Now, for keyboard event handling:

{{{#!python
    def OnKeyUp(self, event):
        """ Handles the wx.EVT_KEY_UP event for CustomCheckBox. """

        if event.GetKeyCode() == wx.WXK_SPACE:
            # The spacebar has been pressed: toggle our state
            self.SendCheckBoxEvent()
            event.Skip()
            return

        event.Skip()
}}}
Here, we simply check if the user has pressed the spacebar. In this case, we call `SendCheckBoxEvent` which actually fires the event.

To complete the event handling review, let's look at the focus events, which are quite simple:

{{{#!python
    def OnSetFocus(self, event):
        """ Handles the wx.EVT_SET_FOCUS event for CustomCheckBox. """

        self._hasFocus = True

        # We got focus, and we want a dotted rectangle to be painted
        # around the checkbox label, so we refresh ourselves
        self.Refresh()

    def OnKillFocus(self, event):
        """ Handles the wx.EVT_KILL_FOCUS event for CustomCheckBox. """

        self._hasFocus = False

        # We lost focus, and we want a dotted rectangle to be cleared
        # around the checkbox label, so we refresh ourselves
        self.Refresh()
}}}
Nothing really special here, we just redraw ourselves when the focus event is fired because we either want to paint a dotted focus rectangle (we have focus) or we want to remove it (we lost focus).

The only important things that are now missing are the overridden base class virtual methods, the ones that make wxPy controls so special. In order to behave correctly (in my opinion), `CustomCheckBox` needs to override the following methods:

{{{#!python
    def AcceptsFocusFromKeyboard(self):
        """Overridden base class virtual."""

        # We can accept focus from keyboard, obviously
        return True


    def AcceptsFocus(self):
        """ Overridden base class virtual. """

        # It seems to me that wx.CheckBox does not accept focus with mouse
        # but please correct me if I am wrong!
        return False

    def GetDefaultAttributes(self):
        """
        Overridden base class virtual.  By default we should use
        the same font/colour attributes as the native wx.CheckBox.
        """

        return wx.CheckBox.GetClassDefaultAttributes()

    def ShouldInheritColours(self):
        """
        Overridden base class virtual.  If the parent has non-default
        colours then we want this control to inherit them.
        """

        return True
}}}
And, last but not least, the most fundamental one, `DoGetBestSize`: you need to override it if you want your control to be correctly managed by sizers:

{{{#!python
    def DoGetBestSize(self):
        """
        Overridden base class virtual.  Determines the best size of the control
        based on the label size, the bitmap size and the current font.
        """

        # Retrieve our properties: the text label, the font and the check
        # bitmap
        label = self.GetLabel()
        font = self.GetFont()
        bitmap = self.GetBitmap()

        if not font:
            # No font defined? So use the default GUI font provided by the system
            font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)

        # Set up a wx.ClientDC. When you don't have a dc available (almost
        # always you don't have it if you are not inside a wx.EVT_PAINT event),
        # use a wx.ClientDC (or a wx.MemoryDC) to measure text extents
        dc = wx.ClientDC(self)
        dc.SetFont(font)

        # Measure our label
        textWidth, textHeight = dc.GetTextExtent(label)

        # Retrieve the check bitmap dimensions
        bitmapWidth, bitmapHeight = bitmap.GetWidth(), bitmap.GetHeight()

        # Get the spacing between the check bitmap and the text
        spacing = self.GetSpacing()

        # Ok, we're almost done: the total width of the control is simply
        # the sum of the bitmap width, the spacing and the text width,
        # while the height is the maximum value between the text width and
        # the bitmap width
        totalWidth = bitmapWidth + spacing + textWidth
        totalHeight = max(textHeight, bitmapHeight)

        best = wx.Size(totalWidth, totalHeight)

        # Cache the best size so it doesn't need to be calculated again,
        # at least until some properties of the window change
        self.CacheBestSize(best)

        return best
}}}
In this method, I simply initialize a `wx.ClientDC`, and I use `GetTextExtent` to calculate the text extent of the checkbox label. Then, the total width of the control is simply the text width plus the spacing plus the bitmap width, while the total height is simply the maximum between the text height and the bitmap height. If you correctly implement the `DoGetBestSize` method, your control will be correctly laid out by sizers.

Ok, if you followed me till now, it means you have a lot of spare time ;-)

What's still missing? Oh, a couple of methods typical for `wx.CheckBox`, but nothing special:

{{{#!python
    def GetValue(self):
        """
        Returns the state of CustomCheckBox, True if checked, False
        otherwise.
        """

        return self._checked

    def IsChecked(self):
        """
        This is just a maybe more readable synonym for GetValue: just as the
        latter, it returns True if the CustomCheckBox is checked and False
        otherwise.
        """

        return self._checked

    def SetValue(self, state):
        """
        Sets the CustomCheckBox to the given state. This does not cause a
        wx.wxEVT_COMMAND_CHECKBOX_CLICKED event to get emitted.
        """

        self._checked = state

        # Refresh ourselves: the bitmap has changed
        self.Refresh()
}}}
We're done. Our custom control is ready to be tested with a demo :-)) . You can find the source code, demo and the icons in this attachment:

[[attachment:CustomCheckBox.zip|CustomCheckBox]]

= Conclusions =
The custom control we designed is very simple, but I hope to have given enough information on how you can actually build your custom widget. As always, if you got new ideas about new widgets, please contact me ;-)

For the wxPython gurus: Please correct my sluggishness in coding, there might be something I have overlooked or misinterpreted. And please don't shoot me if you think that the code is not "Pythonic" (whatever that means).

wxPython rules ;-)

Andrea.

-----
I hope it's clear that to create a truly custom control all the visible features must not only be drawn, but their positions be calculated first. Sizers can only be used with predefined controls! Since this custom control has only two visible items, this is not very difficult to do. DCs are used to copy bitmaps onto a container control such as a Panel. This is easy to do by using dc.DrawBitmap() function. Use of DrawBitmap() relieves the designer from having to use dc.Blit() which requires another dc tool be created just for the bitmap, itself.

Since Andrea has done the bulk of the hard work, I've taken a shot at improving the design a bit. The most obvious problem, to me, is that CustomCheckBox doesn't vertically auto-center its text label properly when the font is changed in any way. Large text labels get cut off at the bottom and right sides because the control doesn't adjust its own size properly. Perhaps there's something that I don't yet know about resizing a wx.PyControl. The mouse-over dotted outline doesn't work at all, at least not on MS Windows 7. The entire control's background color should change in background color, not just the text label's background.Finally, only the actual checkbox area of the control should accept clicks, not the entire control.

After some tweaks of Andrea's original code to get the control to resize itself I noticed  that the control would redraw itself properly only when I  manually changed the size of the top-level Frame. I've used a  simple "hack" that works by applying that principle to "bump" the Frame's (or wx.Dialog's)  size and then restore it. This repaints the CustomCheckBoxMod control, but also gets all the demo app's sizers to adjust to any new text label font size change. This hack fails if the app Frame is maximized because resizes are ignored (on MSW, at least).

{{{#!python
    def SetLabel( self, labelText ) :

        self.labelText = labelText    # Change the control's text label string
        self.Redraw()
}}}
{{{#!python
    def Redraw( self ) :
        """
        Resizing and redrawing of self. This algorithm is a "hack" !
        This hack fails if the top level Frame or Dialog is maximized.
        """

        # SetInitialSize() acts like "SetSize()"
        self.InvalidateBestSize()
        self.SetInitialSize( self.FindMinDimensions() )     # self.FindMinDimensions() call sets self.minWid, self.minHgt

        # Force re-layout of self. HACK !  HACK !  HACK !
        # There really must be a better way, not just a different way
        #   to accomplish auto-resizing and auto-repainting. I wonder what that is...
        topParent = self.GetTopLevelParent()
        topWid, topHgt = topParent.GetClientSize()

        topParent.SetClientSize( (topWid-1, topHgt) )   # "Bump" the top level Frame size
        topParent.SetClientSize( (topWid,   topHgt) )   # Reset the size

        self.Refresh()      # Repaint self.

    #end def
}}}
It may very well be possible that this new control misbehaves on *nix and Mac platforms - I can't test that myself, so let me know!

RDP  - pascor(at)verizon(dot)net 2011-09-01

[[attachment:CustomCheckBoxMod_Demo.zip]]

Except in special circumstances controls should not force their parents to resize, or even to redraw.  This is a poor design and goes contrary with how the rest of wx works.  Instead, when something in the control changes that would change its best size then it should simply call `self.InvalidateBestSize`, `self.Refresh` if needed,  and leave it up to the programmer to trigger a relayout or a resize of the parent or other windows further up the containment hierarchy.  --RobinDunn

It's more than just a shame that a wx custom control doesn't have the ability auto-manage itself without resorting to 1) using the top-level window size bump hack, or 2) putting the burden on the app designer to monitor the custom control's events (How ?!) and force a relayout and redraw in yet another app event handler. This seems to be an important underlying wxWidgets omission, not wxPython's. -- Ray Pasco

There's nothing to monitor.  The programmer writes the code that does `someWidget.SetLabel` or `someWidget.SetFont` or whatever, and if that will make a difference to the layout that is managing the position and size of someWidget then he just needs to also add a call to `self.Layout()` or perhaps `self.Parent.Layout()` at the same time.  If the layout is more complex then more may need to be done, or if the programmer wants the top-level parent to resize then he can do something like call its `Fit()` method.  The point is that the widget can't know all the situations it will be used in nor if changes in its own best size should always cause a relayout of the parent or a `Fit` of the top-level grandparent or even something totally different.  If a widget is going to be a one-off and only used in one place and is never ever going to be used anywhere else, including in some other window in the same application, the sure, it can do whatever the hell it wants to its parents or other widgets that are not even related to it.  But if you want a well behaved general purpose widget that can be used virtually anywhere in your own code or used by other developers, just like a `wx.Button` can be, then it just should not do those types of things.  --RobinDunn

-----

= Thanks =
Many thanks for the info. It has been used in Fonty Python to create a custom text label control. You rule ;-) \d