Department of Engineering

IT Services

wxpython: wxWidgets with Python

wxWidgets is a way to write Graphical User Interfaces (GUIs, with buttons, menus, etc) for Win32, Mac OS X, etc from C++, Python, Perl, and C#/.NET. It's used in our IIA Software Engineering Project. Though it's cross-platform, the resulting programs don't look the same on all machines - they follow the look-and-feel of the machine. Whereas other graphics libraries we use at CUED (OpenGL and GLUT) aren't Object-Oriented, wxWidgets is, so you need some awareness of using classes.

Versions

This document briefly introduces wxWidgets 3.0 when used with Python 3, highlighting a few issues that sometimes puzzle newcomers. Knowing which version you're using matters, especially when you reports bugs.

  • On Linux, type "uname -a" to find out the operating system version, or search in Settings.
  • Type python to find the Python version, then type
    import wx
    wx.version()
    
    to find the wxWidgets version.

These examples were developed on "18.04.1-Ubuntu" using "Python 3.7.6" and "4.0.4 gtk2 (phoenix) wxWidgets 3.0.5". Run on different systems these programs will have a different appearance to the screendumps on this page.

Concepts

The basic concepts are much the same as in most graphics systems.

  • Widgets (things - windows, buttons, etc). These have properties like color, size, etc. Some widgets (e.g. frames) can have other widgets inside them. wxGLCanvas is a useful widget that allows you to use OpenGL commands in a window. This lets you use advanced graphics. For simpler drawing you can use the wxDC class.
  • Events (triggering actions - keypresses, window resizing, mouse clicks, etc). When an event happens, your program might want to react to it. You can arrange for particular events in certain widgets to cause a routine to run. These routines are referred to as "callbacks" or "handlers".
  • Sizers - these are layout managers: they organise widgets when they're within other widgets. If for example you want a row of buttons in a panel, you don't have to calculate sizes and coordinates when the main window is resized. If you specify for each button a minimal size, a stretch factor, a border, and what's to the left of it, the sizer routine will manage the re-drawing.

Programs with GUIs often have quite a different organisation to non-GUI programs. What you do is create the widgets and set things up so that the appropriate callbacks are called. Then you call the main event-loop, giving control over to the application. From then on, the program waits for events. When they happen, the program calls the appropriate callback then waits for the next event. Exiting the loop exits from the program.

You'll need to understand about classes and inheritance in order to make the most of the documentation. For example the page about GLCanvas tells you about the extra facilities that GLCanvas offers and shows you what classes it inherits from. What it doesn't say explicitly is that GLCanvas may also have access to the functions offered by the inherited classes which are documented elsewhere.

Identifiers

Each component needs to be given an integer identifier. You can provide explicit values. There are some Standard Event Identifiers - wx.ID_EXIT, etc. Using wx.ID_ANY lets wxWidgets assign an unused identifier to the component automatically.

Writing a program

Here are the main steps. Full code is in the next section

  • Create a wx.App object. This is the top level of your program.
  • Create an object derived from the frame class - this will be the parent of widgets
  • Create widgets, and put them in sizers. Put the sizers in frames or sizers
  • Write the handlers to deal with events
  • Launch the app

Demo 1 - The basics

This program creates a window with a button, some text, and a menu bar. The side_sizer manages the text and the button. The main_sizer manages the side_sizer. Note that you can resize the window, but there's a minimum size.

# Adapted from an example by Dr Gee (CUED)
import wx

class Gui(wx.Frame):

    def __init__(self, title):
        """Initialise widgets and layout."""
        super().__init__(parent=None, title=title, size=(400, 400))

        # Configure the file menu
        fileMenu = wx.Menu()
        menuBar = wx.MenuBar()
        fileMenu.Append(wx.ID_EXIT, "&Exit")
        menuBar.Append(fileMenu, "&File")
        self.SetMenuBar(menuBar)

        # Configure the widgets
        self.text = wx.StaticText(self, wx.ID_ANY, "Some text")
        self.button1 = wx.Button(self, wx.ID_ANY, "Button1")
        # Bind events to widgets
        self.Bind(wx.EVT_MENU, self.OnMenu)
        self.button1.Bind(wx.EVT_BUTTON, self.OnButton1)
        # Configure sizers for layout
        main_sizer = wx.BoxSizer(wx.HORIZONTAL)
        side_sizer = wx.BoxSizer(wx.VERTICAL)
        main_sizer.Add(side_sizer, 1, wx.ALL, 5)

        side_sizer.Add(self.text, 1, wx.TOP, 10)
        side_sizer.Add(self.button1, 1, wx.ALL, 5)

        self.SetSizeHints(300, 300)
        self.SetSizer(main_sizer)

    def OnMenu(self, event):
        """Handle the event when the user selects a menu item."""
        Id = event.GetId()
        if Id == wx.ID_EXIT:
            self.Close(True)
 
    def OnButton1(self, event):
        """Handle the event when the user clicks button1."""
        print ("Button 1 pressed")

app = wx.App()
gui = Gui("Demo")
gui.Show(True)
app.MainLoop()

The following part of the constructor for Gui may need an explanation.

super().__init__(parent=None, title=title, size=(400, 400))

When a derived object is created, it will by default first call the default constructor of the class it's derived from (the superclass). In this case however, we don't want to call wx.Frame's default constructor, we want to pass some arguments. It's a constructor "with a member initialiser list"

Troubleshooting

In general, if your program's Python is legal, but doesn't work properly

  • Start from an earlier version that works and add a line or two at a time.
  • Check to see if you're using a faultly implementation - the code's supposed to work the same on all platforms, but that's not always so in practise. Commands that are optional on some systems are required on others.
  • If widgets don't appear, or are in the wrong place when you resize the window, check that the widgets are being managed by the appropriate sizers, and the sizers are in turn associated with a parent sizer or frame. Nothing should be unmanaged. Drawing a diagram may help. In the example above
    • side_sizer stacks its contents (text and a button) vertically.
    • main_sizer arranges its contents (side_sizer) horizontally, and is associated with Gui
  • I get messages like (demo1.py:2299): Gtk-WARNING **: 07:47:43.883: Unable to locate theme engine in module_path: "adwaita" when I start a wxWidgets program. They're harmless, and eventually removable.
  • If you get a Segmentation fault (core dumped) message the system may not give you much of a clue about your mistake. The message means that you've managed to make some C++ code crash (which isn't hard to do when using wxWidgets). Check carefully the arguments used in the call that seems to be the cause.

Demo 2 - Toolbars

This extension of the first program adds a toolbar whose final "Exit" icon has a callback, and whose second "open file" icon creates a wxFileDialog to get a "*txt" filename from the user. The icons for the toolbox are supplied with wxWidgets (or, optionally, the operating system)

 

 

 

 

# Adapted from an example by Dr Gee (CUED)
import wx
from wx import ArtProvider

class Gui(wx.Frame):
    QuitID=999
    OpenID=998
    def __init__(self, title):
        """Initialise widgets and layout."""
        super().__init__(parent=None, title=title, size=(400, 400))
        QuitID=999
        OpenID=998
        locale = wx.Locale(wx.LANGUAGE_ENGLISH)
        # Configure the file menu
        fileMenu = wx.Menu()
        menuBar = wx.MenuBar()
        fileMenu.Append(wx.ID_EXIT, "&Exit")
        menuBar.Append(fileMenu, "&File")
        self.SetMenuBar(menuBar)
        toolbar=self.CreateToolBar()
        myimage=wx.ArtProvider.GetBitmap(wx.ART_NEW, wx.ART_TOOLBAR)
        toolbar.AddTool(wx.ID_ANY,"New file", myimage)
        myimage=wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN, wx.ART_TOOLBAR)
        toolbar.AddTool(OpenID,"Open file", myimage)
        myimage=wx.ArtProvider.GetBitmap(wx.ART_FILE_SAVE, wx.ART_TOOLBAR)
        toolbar.AddTool(wx.ID_ANY,"Save file", myimage)
        myimage=wx.ArtProvider.GetBitmap(wx.ART_QUIT, wx.ART_TOOLBAR)
        toolbar.AddTool(QuitID,"Quit", myimage)
        toolbar.Bind(wx.EVT_TOOL, self.Toolbarhandler)
        toolbar.Realize()
        self.ToolBar = toolbar
        # Configure the widgets
        self.text = wx.StaticText(self, wx.ID_ANY, "Some text")
        self.button1 = wx.Button(self, wx.ID_ANY, "Button1")
        # Bind events to widgets
        self.Bind(wx.EVT_MENU, self.OnMenu)
        self.button1.Bind(wx.EVT_BUTTON, self.OnButton1)
        # Configure sizers for layout
        main_sizer = wx.BoxSizer(wx.HORIZONTAL)
        side_sizer = wx.BoxSizer(wx.VERTICAL)
        main_sizer.Add(side_sizer, 1, wx.ALL, 5)

        side_sizer.Add(self.text, 1, wx.TOP, 10)
        side_sizer.Add(self.button1, 1, wx.ALL, 5)

        self.SetSizeHints(300, 300)
        self.SetSizer(main_sizer)

    def OnMenu(self, event):
        """Handle the event when the user selects a menu item."""
        Id = event.GetId()
        if Id == wx.ID_EXIT:
            print("Quitting")
            self.Close(True)
 
    def OnButton1(self, event):
        """Handle the event when the user clicks button1."""
        print ("Button 1 pressed")
        
    def Toolbarhandler(self, event): 
        if event.GetId()==self.QuitID:
            print("Quitting")
            self.Close(True)
        if event.GetId()==self.OpenID:
            openFileDialog= wx.FileDialog(self, "Open txt file", "", "", wildcard="TXT files (*.txt)|*.txt", style=wx.FD_OPEN+wx.FD_FILE_MUST_EXIST)
            if openFileDialog.ShowModal() == wx.ID_CANCEL:
               print("The user cancelled") 
               return     # the user changed idea...
            print("File chosen=",openFileDialog.GetPath())
            
app = wx.App()
gui = Gui("Demo")
gui.Show(True)
app.MainLoop()

Finding windows

After creating a window or a button, you may need to access it again from a different function from where it was created. To do this you'll need to store the value returned when the window was created and make that value visible from elsewhere. Alternatively you can use wXwidget's FindWindowById, FindWindowByLabel, or FindWindowByName function to find the window when you need it. The next program shows how to do this.

Demo 3 - Scrollbars

A varient of the wxWindow called wx.ScrolledWindow can be used if you want scrollbars with automatic callbacks. Compared with the previous program the program below has an extra window with a Run button. This extra window has scrollbars that will only appear when the displayed window dimensions are less than the actual window size (in the illustrated situation the displayed width but not the height is too small). This code also shows FindWindowByName in use - if you click on Button 2, the text on Button 1 will change.


# Adapted from an example by Dr Gee (CUED)
import wx

class Gui(wx.Frame):
    def __init__(self, title):
        """Initialise widgets and layout."""
        super().__init__(parent=None, title=title, size=(400, 100))
      
        # Configure the file menu
        fileMenu = wx.Menu()
        menuBar = wx.MenuBar()
        fileMenu.Append(wx.ID_EXIT, "&Exit")
        menuBar.Append(fileMenu, "&File")
        self.SetMenuBar(menuBar)
        button_sizer = wx.BoxSizer(wx.HORIZONTAL)
        # Configure the widgets
        self.text = wx.StaticText(self, wx.ID_ANY, "Some text")
        button_sizer.Add(self.text, 1, wx.TOP+wx.LEFT+wx.RIGHT, 5)
        self.button1 = wx.Button(self, wx.ID_ANY, label="Button 1", name="SomeNameOrOther")

        button_sizer.Add(self.button1, 1, wx.RIGHT, 5)
        self.button2 = wx.Button(self, wx.ID_ANY, "Button2")

        button_sizer.Add(self.button2 , 1, wx.RIGHT, 5)
        # Bind events to widgets
        self.Bind(wx.EVT_MENU, self.OnMenu)
        self.button1.Bind(wx.EVT_BUTTON, self.OnButton1)
        self.button2.Bind(wx.EVT_BUTTON, self.OnButton2)
        # Configure sizers for layout
        main_sizer = wx.BoxSizer(wx.HORIZONTAL)
        side_sizer = wx.BoxSizer(wx.VERTICAL)
        main_sizer.Add(side_sizer, 1, wx.ALL, 5)

        side_sizer.Add(self.text, 1, wx.TOP, 10)
        side_sizer.Add(self.button1, 1, wx.ALL, 5)
        side_sizer.Add(self.button2, 1, wx.ALL, 5)

        self.SetSizeHints(300, 100)
        self.SetSizer(button_sizer)
        controlwin = wx.ScrolledWindow(self, -1, wx.DefaultPosition, wx.DefaultSize, wx.SUNKEN_BORDER|wx.HSCROLL|wx.VSCROLL)
        button_sizer.Add(controlwin,1, wx.EXPAND | wx.ALL, 10)
        button_sizer2 = wx.BoxSizer(wx.VERTICAL)
        controlwin.SetSizer(button_sizer2)
        controlwin.SetScrollRate(10, 10)
        controlwin.SetAutoLayout(True)

        button_sizer2.Add(wx.Button(controlwin, wx.ID_ANY, "Run"), 0, wx.ALL, 10)

    def OnMenu(self, event):
        """Handle the event when the user selects a menu item."""
        Id = event.GetId()
        if Id == wx.ID_EXIT:
            print("Quitting")
            self.Close(True)
 
    def OnButton1(self, event):
        """Handle the event when the user clicks button1."""
        print ("Button 1 pressed")
        
    def OnButton2(self, event):
        """Now find button 1 and change its label."""
        print ("Button 2 pressed")
        tmp=wx.FindWindowByName("SomeNameOrOther")
        if tmp!=None:
            tmp.SetLabel("Button 1 updated")
        
            
app = wx.App()
gui = Gui("Demo")
gui.Show(True)
app.MainLoop()

Demo 4 - Canvas with scrollbars

There are various ways of doing this. I don't know if the method used here is best. Because of the way ShowScrollbars is used, the horizontal scrollbar will always be visible. The vertical scrollbar will disappear if the window's big enough.

import wx
import wx.glcanvas as wxcanvas
from OpenGL import GL, GLUT

class MyGLCanvas(wxcanvas.GLCanvas):

    def __init__(self, parent,id,pos,size):
        """Initialise canvas properties and useful variables."""
        super().__init__(parent, -1,pos=pos,size=size,
                         attribList=[wxcanvas.WX_GL_RGBA,
                                     wxcanvas.WX_GL_DOUBLEBUFFER,
                                     wxcanvas.WX_GL_DEPTH_SIZE, 16, 0])
        GLUT.glutInit()
        self.init = False
        self.context = wxcanvas.GLContext(self)

        # Initialise variables for panning
        self.pan_x = 0
        self.pan_y = 0

        # Initialise variables for zooming
        self.zoom = 1

        # Bind events to the canvas
        self.Bind(wx.EVT_PAINT, self.on_paint)
        self.Bind(wx.EVT_SIZE, self.on_size)

    def init_gl(self):
        """Configure and initialise the OpenGL context."""
        size = self.GetClientSize()
        self.SetCurrent(self.context)
        GL.glDrawBuffer(GL.GL_BACK)
        GL.glClearColor(1.0, 1.0, 1.0, 0.0)
        GL.glViewport(0, 0, size.width, size.height)
        GL.glMatrixMode(GL.GL_PROJECTION)
        GL.glLoadIdentity()
        GL.glOrtho(0, size.width, 0, size.height, -1, 1)
        GL.glMatrixMode(GL.GL_MODELVIEW)
        GL.glLoadIdentity()
        GL.glTranslated(self.pan_x, self.pan_y, 0.0)
        GL.glScaled(self.zoom, self.zoom, self.zoom)

    def render(self, text):
        """Handle all drawing operations."""
        self.SetCurrent(self.context)
        if not self.init:
            # Configure the viewport, modelview and projection matrices
            self.init_gl()
            self.init = True

        # Clear everything
        GL.glClear(GL.GL_COLOR_BUFFER_BIT)

        # Draw specified text at position (10, 10)
        self.render_text(text, 10, 10)

        # Draw a sample signal trace
        GL.glColor3f(0.0, 0.0, 1.0)  # signal trace is blue
        GL.glBegin(GL.GL_LINE_STRIP)
        for i in range(10):
            x = (i * 20) + 10
            x_next = (i * 20) + 30
            if i % 2 == 0:
                y = 75
            else:
                y = 100
            GL.glVertex2f(x, y)
            GL.glVertex2f(x_next, y)
        GL.glEnd()

        # We have been drawing to the back buffer, flush the graphics pipeline
        # and swap the back buffer to the front
        GL.glFlush()
        self.SwapBuffers()

    def on_paint(self, event):
        """Handle the paint event."""
        self.SetCurrent(self.context)
        if not self.init:
            # Configure the viewport, modelview and projection matrices
            self.init_gl()
            self.init = True

        size = self.GetClientSize()
        text = "".join(["Canvas redrawn on paint event, size is ",
                        str(size.width), ", ", str(size.height)])
        self.render(text)

    def on_size(self, event):
        """Handle the canvas resize event."""
        # Forces reconfiguration of the viewport, modelview and projection
        # matrices on the next paint event
        self.init = False

    def render_text(self, text, x_pos, y_pos):
        """Handle text drawing operations."""
        GL.glColor3f(0.0, 0.0, 0.0)  # text is black
        GL.glRasterPos2f(x_pos, y_pos)
        font = GLUT.GLUT_BITMAP_HELVETICA_12

        for character in text:
            if character == '\n':
                y_pos = y_pos - 20
                GL.glRasterPos2f(x_pos, y_pos)
            else:
                GLUT.glutBitmapCharacter(font, ord(character))


class Gui(wx.Frame):
    def __init__(self, title):
        """Initialise widgets and layout."""
        super().__init__(parent=None, title=title, size=(300, 200))

        # Configure the file menu
        fileMenu = wx.Menu()
        menuBar = wx.MenuBar()
        fileMenu.Append(wx.ID_EXIT, "&Exit")
        menuBar.Append(fileMenu, "&File")
        self.SetMenuBar(menuBar)
        self.scrollable = wx.ScrolledCanvas(self, wx.ID_ANY )
        self.scrollable.SetSizeHints(200, 200)
        self.scrollable.ShowScrollbars(wx.SHOW_SB_ALWAYS,wx.SHOW_SB_DEFAULT)
        self.scrollable.SetScrollbars(20, 20, 15, 10)
        # Configure the widgets
        self.text = wx.StaticText(self, wx.ID_ANY, "Some text")
        self.run_button = wx.Button(self, wx.ID_ANY, "Run")

        # Bind events to widgets
        self.Bind(wx.EVT_MENU, self.on_menu)
        self.run_button.Bind(wx.EVT_BUTTON, self.on_run_button)

        # Configure sizers for layout
        main_sizer = wx.BoxSizer(wx.HORIZONTAL)
        side_sizer = wx.BoxSizer(wx.VERTICAL)
       
        main_sizer.Add(side_sizer, 1, wx.ALL, 5)
        self.canvas = MyGLCanvas(self.scrollable, wx.ID_ANY, wx.DefaultPosition,  wx.Size(300,200))
        self.canvas.SetSizeHints(500, 500)
        side_sizer.Add(self.text, 1, wx.TOP, 10)
        side_sizer.Add(self.run_button, 1, wx.ALL, 5)
        main_sizer.Add(self.scrollable, 1,  wx.EXPAND+wx.TOP, 5)
        self.SetSizeHints(200, 200)
        self.SetSizer(main_sizer)
       
    def on_menu(self, event):
        """Handle the event when the user selects a menu item."""
        Id = event.GetId()
        if Id == wx.ID_EXIT:
            self.Close(True)

 
    def on_run_button(self, event):
        """Handle the event when the user clicks the run button."""
        text = "Run button pressed."
        self.canvas.render(text)

app = wx.App()
gui = Gui("Demo")
gui.Show(True)
app.MainLoop()

There are various other ways to have scrollbars, and different versions of wxpython offer different options. Consequently the documentation can be confusing - e.g. I'm still unsure whether wx.SHOW_SB_ALWAYS and wx.ALWAYS_SHOW_SB are the same thing. Some windows will automatically deal with scroll-related events. In such situations think SetScrollbar() (which explicitly sets sxrollbar features) might struggle unless the default behaviour is turned off.

Sources of wxWidgets information

You'll need to find some on-line information. Make sure that the wxWidgets release you're using matches what the documentation describes (on Linux type "wx-config --release" to see what's installed). Here are some suggestions