How to Build a Python GUI Application With wxPython

How to Build a Python GUI Application With wxPython

by Mike Driscoll intermediate gui

There are many graphical user interface (GUI) toolkits that you can use with the Python programming language. The big three are Tkinter, wxPython, and PyQt. Each of these toolkits will work with Windows, macOS, and Linux, with PyQt having the additional capability of working on mobile.

A graphical user interface is an application that has buttons, windows, and lots of other widgets that the user can use to interact with your application. A good example would be a web browser. It has buttons, tabs, and a main window where all the content loads.

In this article, you’ll learn how to build a graphical user interface with Python using the wxPython GUI toolkit.

Here are the topics covered:

  • Getting Started with wxPython
  • Definition of a GUI
  • Creating a Skeleton Application
  • Creating a Working Application

Let’s start learning!

Getting Started With wxPython

The wxPython GUI toolkit is a Python wrapper around a C++ library called wxWidgets. The initial release of wxPython was in 1998, so wxPython has been around quite a long time. wxPython’s primary difference from other toolkits, such as PyQt or Tkinter, is that wxPython uses the actual widgets on the native platform whenever possible. This makes wxPython applications look native to the operating system that it is running on.

PyQt and Tkinter both draw their widgets themselves, which is why they don’t always match the native widgets, although PyQt is very close.

This is not to say that wxPython does not support custom widgets. In fact, the wxPython toolkit has many custom widgets included with it, along with dozens upon dozens of core widgets. The wxPython downloads page has a section called Extra Files that is worth checking out.

Here, there is a download of the wxPython Demo package. This is a nice little application that demonstrates the vast majority of the widgets that are included with wxPython. The demo allows a developer to view the code in one tab and run it in a second tab. You can even edit and re-run the code in the demo to see how your changes affect the application.

Installing wxPython

You will be using the latest wxPython for this article, which is wxPython 4, also known as the Phoenix release. The wxPython 3 and wxPython 2 versions are built only for Python 2. When Robin Dunn, the primary maintainer of wxPython, created the wxPython 4 release, he deprecated a lot of aliases and cleaned up a lot of code to make wxPython more Pythonic and easier to maintain.

You will want to consult the following links if you are migrating from an older version of wxPython to wxPython 4 (Phoenix):

The wxPython 4 package is compatible with both Python 2.7 and Python 3.

You can now use pip to install wxPython 4, which was not possible in the legacy versions of wxPython. You can do the following to install it on your machine:

Shell
$ pip install wxpython

Fortunately, the error messages that pip displays are helpful in figuring out what is missing, and you can use the prerequisites section on the wxPython Github page to help you find the information you need if you want to install wxPython on Linux.

There are some Python wheels available for the most popular Linux versions that you can find in the Extras Linux section with both GTK2 and GTK3 versions. To install one of these wheels, you would use the following command:

Shell
$ pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-18.04/ wxPython

Be sure you have modified the command above to match your version of Linux.

Definition of a GUI

As was mentioned in the introduction, a graphical user interface (GUI) is an interface that is drawn on the screen for the user to interact with.

User interfaces have some common components:

  • Main window
  • Menu
  • Toolbar
  • Buttons
  • Text Entry
  • Labels

All of these items are known generically as widgets. There are many other common widgets and many custom widgets that wxPython supports. A developer will take the widgets and arrange them logically on a window for the user to interact with.

Event Loops

A graphical user interface works by waiting for the user to do something. The something is called an event. Events happen when the user types something while your application is in focus or when the user uses their mouse to press a button or other widget.

Underneath the covers, the GUI toolkit is running an infinite loop that is called an event loop. The event loop just waits for events to occur and then acts on those events according to what the developer has coded the application to do. When the application doesn’t catch an event, it effectively ignores that it even happened.

When you are programming a graphical user interface, you will want to keep in mind that you will need to hook up each of the widgets to event handlers so that your application will do something.

There is a special consideration that you need to keep in mind when working with event loops: they can be blocked. When you block an event loop, the GUI will become unresponsive and appear to freeze to the user.

Any process that you launch in a GUI that will take longer than a quarter second should probably be launched as a separate thread or process. This will prevent your GUI from freezing and give the user a better user experience.

The wxPython framework has special thread-safe methods that you can use to communicate back to your application to let it know that the thread is finished or to give it an update.

Let’s create a skeleton application to demonstrate how events work.

Creating a Skeleton Application

An application skeleton in a GUI context is a user interface with widgets that don’t have any event handlers. These are useful for prototyping. You basically just create the GUI and present it to your stakeholders for sign-off before spending a lot of time on the backend logic.

Let’s start by creating a Hello World application with wxPython:

Python
import wx

app = wx.App()
frame = wx.Frame(parent=None, title='Hello World')
frame.Show()
app.MainLoop()

In this example, you have two parts: wx.App and the wx.Frame. The wx.App is wxPython’s application object and is required for running your GUI. The wx.App starts something called a .MainLoop(). This is the event loop that you learned about in the previous section.

The other piece of the puzzle is wx.Frame, which will create a window for the user to interact with. In this case, you told wxPython that the frame has no parent and that its title is Hello World. Here is what it looks like when you run the code:

Hello World in wxPython

By default, a wx.Frame will include minimize, maximize, and exit buttons along the top. You won’t normally create an application in this manner though. Most wxPython code will require you to subclass the wx.Frame and other widgets so that you can get the full power of the toolkit.

Let’s take a moment and rewrite your code as a class:

Python
import wx

class MyFrame(wx.Frame):    
    def __init__(self):
        super().__init__(parent=None, title='Hello World')
        self.Show()

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

You can use this code as a template for your application. However, this application doesn’t do very much, so let’s take a moment to learn a little about some of the other widgets you could add.

Widgets

The wxPython toolkit has more than one hundred widgets to choose from. This allows you to create rich applications, but it can also be daunting trying to figure out which widget to use. This is why the wxPython Demo is helpful, as it has a search filter that you can use to help you find the widgets that might apply to your project.

Most GUI applications allow the user to enter some text and press a button. Let’s go ahead and add those widgets:

Python
import wx

class MyFrame(wx.Frame):    
    def __init__(self):
        super().__init__(parent=None, title='Hello World')
        panel = wx.Panel(self)

        self.text_ctrl = wx.TextCtrl(panel, pos=(5, 5))
        my_btn = wx.Button(panel, label='Press Me', pos=(5, 55))

        self.Show()

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

When you run this code, your application should look like this:

Hello World in wxPython with widgets

The first widget you need to add is something called wx.Panel. This widget is not required, but recommended. On Windows, you are actually required to use a Panel so that the background color of the frame is the right shade of gray. Tab traversal is disabled without a Panel on Windows.

When you add the panel widget to a frame and the panel is the sole child of the frame, it will automatically expand to fill the frame with itself.

The next step is to add a wx.TextCtrl to the panel. The first argument for almost all widgets is which parent the widget should go onto. In this case, you want the text control and the button to be on top of the panel, so it is the parent you specify.

You also need to tell wxPython where to place the widget, which you can do by passing in a position via the pos parameter. In wxPython, the origin location is (0,0) which is the upper left corner of the parent. So for the text control, you tell wxPython that you want to position its top left corner 5 pixels from the left (x) and 5 pixels from the top (y).

Then you add your button to the panel and give it a label. To prevent the widgets from overlapping, you need to set the y-coordinate to 55 for the button’s position.

Absolute Positioning

When you provide exact coordinates for your widget’s position, the technique that you used is called absolute positioning. Most GUI toolkits provide this capability, but it’s not actually recommended.

As your application becomes more complex, it becomes difficult to keep track of all the widget locations and if you have to move the widgets around. Resetting all those positions becomes a nightmare.

Fortunately all modern GUI toolkits provide a solution for this, which is what you will learn about next.

Sizers (Dynamic Sizing)

The wxPython toolkit includes sizers, which are used for creating dynamic layouts. They manage the placement of your widgets for you and will adjust them when you resize the application window. Other GUI toolkits will refer to sizers as layouts, which is what PyQt does.

Here are the primary types of sizers that you will see used most often:

  • wx.BoxSizer
  • wx.GridSizer
  • wx.FlexGridSizer

Let’s add a wx.BoxSizer to your example and see if we can make it work a bit more nicely:

Python
import wx

class MyFrame(wx.Frame):    
    def __init__(self):
        super().__init__(parent=None, title='Hello World')
        panel = wx.Panel(self)        
        my_sizer = wx.BoxSizer(wx.VERTICAL)        
        self.text_ctrl = wx.TextCtrl(panel)
        my_sizer.Add(self.text_ctrl, 0, wx.ALL | wx.EXPAND, 5)        
        my_btn = wx.Button(panel, label='Press Me')
        my_sizer.Add(my_btn, 0, wx.ALL | wx.CENTER, 5)        
        panel.SetSizer(my_sizer)        
        self.Show()

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

Here you create an instance of a wx.BoxSizer and pass it wx.VERTICAL, which is the orientation that widgets are added to the sizer.

In this case, the widgets will be added vertically, which means they will be added one at a time from top to bottom. You may also set a BoxSizer’s orientation to wx.HORIZONTAL. When you do that, the widgets would be added from left to right.

To add a widget to a sizer, you will use .Add(). It accepts up to five arguments:

  • window (the widget)
  • proportion
  • flag
  • border
  • userData

The window argument is the widget to be added while proportion sets how much space relative to other widgets in the sizer this particular widget should take. By default, it is zero, which tells wxPython to leave the widget at its default proportion.

The third argument is flag. You can actually pass in multiple flags if you wish as long as you separate them with a pipe character: |. The wxPython toolkit uses | to add flags using a series of bitwise ORs.

In this example, you add the text control with the wx.ALL and wx.EXPAND flags. The wx.ALL flag tells wxPython that you want to add a border on all sides of the widget while wx.EXPAND makes the widgets expand as much as they can within the sizer.

Finally, you have the border parameter, which tells wxPython how many pixels of border you want around the widget. The userData parameter is only used when you want to do something complex with your sizing of the widget and is actually quite rare to see in practice.

Adding the button to the sizer follows the exact same steps. However, to make things a bit more interesting, I went ahead and switched out the wx.EXPAND flag for wx.CENTER so that the button would be centered on-screen.

When you run this version of the code, your application should look like the following:

Hello World in wxPython with Sizers

If you’d like to learn more about sizers, the wxPython documentation has a nice page on the topic.

Adding an Event

While your application looks more interesting visually, it still doesn’t really do anything. For example, if you press the button, nothing really happens.

Let’s give the button a job:

Python
import wx

class MyFrame(wx.Frame):    
    def __init__(self):
        super().__init__(parent=None, title='Hello World')
        panel = wx.Panel(self)        
        my_sizer = wx.BoxSizer(wx.VERTICAL)        
        self.text_ctrl = wx.TextCtrl(panel)
        my_sizer.Add(self.text_ctrl, 0, wx.ALL | wx.EXPAND, 5)        
        my_btn = wx.Button(panel, label='Press Me')
        my_btn.Bind(wx.EVT_BUTTON, self.on_press)
        my_sizer.Add(my_btn, 0, wx.ALL | wx.CENTER, 5)        
        panel.SetSizer(my_sizer)        
        self.Show()

    def on_press(self, event):
        value = self.text_ctrl.GetValue()
        if not value:
            print("You didn't enter anything!")
        else:
            print(f'You typed: "{value}"')

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

The widgets in wxPython allow you to attach event bindings to them so that they can respond to certain types of events.

You want the button to do something when the user presses it. You can accomplish this by calling the button’s .Bind() method. .Bind() takes the event you want to bind to, the handler to call when the event happens, an optional source, and a couple of optional ids.

In this example, you bind your button object to the wx.EVT_BUTTON event and tell it to call on_press() when that event gets fired.

An event gets “fired” when the user does the event you have bound to. In this case, the event that you set up is the button press event, wx.EVT_BUTTON.

.on_press() accepts a second argument that you can call event. This is by convention. You could call it something else if you wanted to. However, the event parameter here refers to the fact that when this method is called, its second argument should be an event object of some sort.

Within .on_press(), you can get the text control’s contents by calling its GetValue() method. You then print a string to stdout depending on what the contents of the text control is.

Now that you have the basics out of the way, let’s learn how to create an application that does something useful!

Creating a Working Application

The first step when creating something new is to figure out what you want to create. In this case, I have taken the liberty of making that decision for you. You will learn how to create a MP3 tag editor! The next step when creating something new is to find out what packages can help you accomplish your task.

If you do a Google search for Python mp3 tagging, you will find you have several options:

  • mp3-tagger
  • eyeD3
  • mutagen

I tried out a couple of these and decided that eyeD3 had a nice API that you could use without getting bogged down with the MP3’s ID3 specification. You can install eyeD3 using pip, like this:

Shell
$ pip install eyed3

When installing this package on macOS, you may need to install libmagic using brew. Windows and Linux users shouldn’t have any issues installing eyeD3.

Designing the User Interface

When it comes to designing an interface, it’s always nice to just kind of sketch out how you think the user interface should look.

You will need to be able to do the following:

  • Open up one or more MP3 files
  • Display the current MP3 tags
  • Edit an MP3 tag

Most user interfaces use a menu or a button for opening files or folders. You can go with a File menu for this. Since you will probably want to see tags for multiple MP3 files, you will need to find a widget that can do this in a nice manner.

Something that is tabular with columns and rows would be ideal because then you can have labeled columns for the MP3 tags. The wxPython toolkit has a few widgets that would work for this, with the top two being the following:

  • wx.grid.Grid
  • wx.ListCtrl

You should use wx.ListCtrl in this case as the Grid widget is overkill, and frankly it is also quite a bit more complex. Finally, you need a button to use to edit a selected MP3’s tag.

Now that you know what you want, you can draw it up:

MP3 Editor in wxPython

The illustration above gives us an idea of how the application should look. Now that you know what you want to do, it’s time to code!

Creating the User Interface

There are many different approaches when it comes to writing a new application. For example, do you need to follow the Model-View-Controller design pattern? How do you split up the classes? One class per file? There are many such questions, and as you get more experienced with GUI design, you’ll know how you want to answer them.

In your case, you really only need two classes:

  • A wx.Panel class
  • A wx.Frame class

You could argue for creating a controller type module as well, but for something like this, you really do not need it. A case could also be made for putting each class into its own module, but to keep it compact, you will create a single Python file for all of your code.

Let’s start with imports and the panel class:

Python
import eyed3
import glob
import wx

class Mp3Panel(wx.Panel):    
    def __init__(self, parent):
        super().__init__(parent)
        main_sizer = wx.BoxSizer(wx.VERTICAL)
        self.row_obj_dict = {}

        self.list_ctrl = wx.ListCtrl(
            self, size=(-1, 100), 
            style=wx.LC_REPORT | wx.BORDER_SUNKEN
        )
        self.list_ctrl.InsertColumn(0, 'Artist', width=140)
        self.list_ctrl.InsertColumn(1, 'Album', width=140)
        self.list_ctrl.InsertColumn(2, 'Title', width=200)
        main_sizer.Add(self.list_ctrl, 0, wx.ALL | wx.EXPAND, 5)        
        edit_button = wx.Button(self, label='Edit')
        edit_button.Bind(wx.EVT_BUTTON, self.on_edit)
        main_sizer.Add(edit_button, 0, wx.ALL | wx.CENTER, 5)        
        self.SetSizer(main_sizer)

    def on_edit(self, event):
        print('in on_edit')

    def update_mp3_listing(self, folder_path):
        print(folder_path)

Here, you import the eyed3 package, Python’s glob package, and the wx package for your user interface. Next, you subclass wx.Panel and create your user interface. You need a dictionary for storing data about your MP3s, which you can name row_obj_dict.

Then you create a wx.ListCtrl and set it to report mode (wx.LC_REPORT) with a sunken border (wx.BORDER_SUNKEN). The list control can take on a few other forms depending on the style flag that you pass in, but the report flag is the most popular.

To make the ListCtrl have the correct headers, you will need to call .InsertColumn() for each column header. You then supply the index of the column, its label, and how wide in pixels the column should be.

The last step is to add your Edit button, an event handler, and a method. You can create the binding to the event and leave the method that it calls empty for now.

Now you should write the code for the frame:

Python
class Mp3Frame(wx.Frame):    
    def __init__(self):
        super().__init__(parent=None,
                         title='Mp3 Tag Editor')
        self.panel = Mp3Panel(self)
        self.Show()

if __name__ == '__main__':
    app = wx.App(False)
    frame = Mp3Frame()
    app.MainLoop()

This class is much simpler than the first one in that all you need to do is set the title of the frame and instantiate the panel class, Mp3Panel. When you are all done, your user interface should look like this:

wxPython MP3 Tag Editor

The user interface looks almost right, but you don’t have a File menu. This makes it impossible to add MP3s to the application and edit their tags!

Let’s fix that now.

Make a Functioning Application

The first step in making your application work is to update the application so that it has a File menu because then you can add MP3 files to your creation. Menus are almost always added to the wx.Frame class, so that is the class you need to modify.

Let’s learn how to add a menu bar to our application:

Python
class Mp3Frame(wx.Frame):

    def __init__(self):
        wx.Frame.__init__(self, parent=None, 
                          title='Mp3 Tag Editor')
        self.panel = Mp3Panel(self)
        self.create_menu()
        self.Show()

    def create_menu(self):
        menu_bar = wx.MenuBar()
        file_menu = wx.Menu()
        open_folder_menu_item = file_menu.Append(
            wx.ID_ANY, 'Open Folder', 
            'Open a folder with MP3s'
        )
        menu_bar.Append(file_menu, '&File')
        self.Bind(
            event=wx.EVT_MENU, 
            handler=self.on_open_folder,
            source=open_folder_menu_item,
        )
        self.SetMenuBar(menu_bar)

    def on_open_folder(self, event):
        title = "Choose a directory:"
        dlg = wx.DirDialog(self, title, 
                           style=wx.DD_DEFAULT_STYLE)
        if dlg.ShowModal() == wx.ID_OK:
            self.panel.update_mp3_listing(dlg.GetPath())
        dlg.Destroy()

Here, you add a call to .create_menu() within the class’s constructor. Then in .create_menu() itself, you will create a wx.MenuBar instance and a wx.Menu instance.

To add a menu item to a menu, you call the menu instance’s .Append() and pass it the following:

  • A unique identifier
  • The label for the new menu item
  • A help string

Next, you need to add the menu to the menubar, so you will need to call the menubar’s .Append(). It takes the menu instance and the label for menu. This label is a bit odd in that you called it &File instead of File. The ampersand tells wxPython to create a keyboard shortcut of Alt+F to open the File menu using just your keyboard.

To create an event binding, you will need to call self.Bind(), which binds the frame to wx.EVT_MENU. When you use self.Bind() for a menu event, you need to not only tell wxPython which handler to use, but also which source to bind the handler to.

Finally, you must call the frame’s .SetMenuBar() and pass it the menubar instance for it to be shown to the user.

Now that you have the menu added to your frame, let’s go over the menu item’s event handler, which is reproduced again below:

Python
def on_open_folder(self, event):
    title = "Choose a directory:"
    dlg = wx.DirDialog(self, title, style=wx.DD_DEFAULT_STYLE)
    if dlg.ShowModal() == wx.ID_OK:
        self.panel.update_mp3_listing(dlg.GetPath())
    dlg.Destroy()

Since you want the user to choose a folder that contains MP3s, you will want to use wxPython’s wx.DirDialog. The wx.DirDialog allows the user to only open directories.

You can set the dialog’s title and various style flags. To show the dialog, you will need to call .ShowModal(). This will cause the dialog to show modally, which means that the user won’t be able to interact with your main application while the dialog is shown.

If the user presses the dialog’s OK button, you can get the user’s path choice via the dialog’s .GetPath(). You will want to pass that path to your panel class, which you can do here by calling the panel’s .update_mp3_listing().

Finally you need to close the dialog. To close a dialog, the recommended method is to call its .Destroy().

Dialogs do have a .Close() method, but that basically just hides the dialog, and it will not destroy itself when you close your application, which can lead to weird issues such as your application now shutting down properly. It’s simpler to call .Destroy() on the dialog to prevent this issue.

Now let’s update your Mp3Panel class. You can start by updating .update_mp3_listing():

Python
def update_mp3_listing(self, folder_path):
    self.current_folder_path = folder_path
    self.list_ctrl.ClearAll()

    self.list_ctrl.InsertColumn(0, 'Artist', width=140)
    self.list_ctrl.InsertColumn(1, 'Album', width=140)
    self.list_ctrl.InsertColumn(2, 'Title', width=200)
    self.list_ctrl.InsertColumn(3, 'Year', width=200)

    mp3s = glob.glob(folder_path + '/*.mp3')
    mp3_objects = []
    index = 0
    for mp3 in mp3s:
        mp3_object = eyed3.load(mp3)
        self.list_ctrl.InsertItem(index, 
            mp3_object.tag.artist)
        self.list_ctrl.SetItem(index, 1, 
            mp3_object.tag.album)
        self.list_ctrl.SetItem(index, 2, 
            mp3_object.tag.title)
        mp3_objects.append(mp3_object)
        self.row_obj_dict[index] = mp3_object
        index += 1

Here you set the current directory to the specified folder and then you clear the list control. This keeps the list control fresh and only showing the MP3s that you are currently working on. That also means that you need to re-insert all the columns again.

Next, you’ll want to take the folder that was passed in and use Python’s glob module to search for MP3 files.

Then you can loop over the MP3s and turn them into eyed3 objects. You can do this by calling the .load() of eyed3. Assuming that the MP3s have the appropriate tags already, you can then add the artist, album, and title of the MP3 to the list control.

Interestingly, the method of adding a new row to a list control object is by calling .InsertItem() for the first column and SetItem() for all the subsequent columns.

The last step is to save off your MP3 object to your Python dictionary, row_obj_dict.

Now you need to update the .on_edit() event handler so that you can edit an MP3’s tags:

Python
def on_edit(self, event):
    selection = self.list_ctrl.GetFocusedItem()
    if selection >= 0:
        mp3 = self.row_obj_dict[selection]
        dlg = EditDialog(mp3)
        dlg.ShowModal()
        self.update_mp3_listing(self.current_folder_path)
        dlg.Destroy()

The first thing you need to do is get the user’s selection by calling the list control’s .GetFocusedItem().

If the user has not selected anything in the list control, it will return -1. Assuming that the user did select something, you will want to extract the MP3 object from your dictionary and open a MP3 tag editor dialog. This will be a custom dialog that you will use to edit the artist, album, and title tags of the MP3 file.

As usual, show the dialog modally. When the dialog closes, the last two lines in .on_edit() will execute. These two lines will update the list control so it displays the current MP3 tag information that the user just edited and destroy the dialog.

Creating an Editing Dialog

The final piece of the puzzle is creating an MP3 tag editing dialog. For brevity, we will skip sketching out this interface as it is a series of rows that contains labels and text controls. The text controls should have the existing tag information pre-populated within them. You can create a label for the text controls by creating instances of wx.StaticText.

When you need to create a custom dialog, the wx.Dialog class is your friend. You can use that to design the editor:

Python
class EditDialog(wx.Dialog):    
    def __init__(self, mp3):
        title = f'Editing "{mp3.tag.title}"'
        super().__init__(parent=None, title=title)        
        self.mp3 = mp3        
        self.main_sizer = wx.BoxSizer(wx.VERTICAL)        
        self.artist = wx.TextCtrl(
            self, value=self.mp3.tag.artist)
        self.add_widgets('Artist', self.artist)        
        self.album = wx.TextCtrl(
            self, value=self.mp3.tag.album)
        self.add_widgets('Album', self.album)        
        self.title = wx.TextCtrl(
            self, value=self.mp3.tag.title)
        self.add_widgets('Title', self.title)        
        btn_sizer = wx.BoxSizer()
        save_btn = wx.Button(self, label='Save')
        save_btn.Bind(wx.EVT_BUTTON, self.on_save)        
        btn_sizer.Add(save_btn, 0, wx.ALL, 5)
        btn_sizer.Add(wx.Button(
            self, id=wx.ID_CANCEL), 0, wx.ALL, 5)
        self.main_sizer.Add(btn_sizer, 0, wx.CENTER)        
        self.SetSizer(self.main_sizer)

Here you want to start off by sub-classing wx.Dialog and giving it a custom title based on the title of the MP3 that you are editing.

Next you can create the sizer you want to use and the widgets. To make things easier, you can create a helper method called .add_widgets() for adding the wx.StaticText widgets as rows with the text control instances. The only other widget here is the Save button.

Let’s write the add_widgets method next:

Python
    def add_widgets(self, label_text, text_ctrl):
        row_sizer = wx.BoxSizer(wx.HORIZONTAL)
        label = wx.StaticText(self, label=label_text,
                              size=(50, -1))
        row_sizer.Add(label, 0, wx.ALL, 5)
        row_sizer.Add(text_ctrl, 1, wx.ALL | wx.EXPAND, 5)
        self.main_sizer.Add(row_sizer, 0, wx.EXPAND)

add_widgets() takes the label’s text and the text control instance. It then creates a horizontally oriented BoxSizer.

Next you will create an instance of wx.StaticText using the passed-in text for its label parameter. You will also set its size to be 50 pixels wide and the default height is set with a -1. Since you want the label before the text control, you will add the StaticText widget to your BoxSizer first and then add the text control .

Finally, you want to add the horizontal sizer to the top level vertical sizer. By nesting the sizers in each other, you can design complex applications.

Now you will need to create the on_save() event handler so that you can save your changes:

Python
    def on_save(self, event):
        self.mp3.tag.artist = self.artist.GetValue()
        self.mp3.tag.album = self.album.GetValue()
        self.mp3.tag.title = self.title.GetValue()
        self.mp3.tag.save()
        self.Close()

Here you set the tags to the contents of the text controls and then call the eyed3 object’s .save(). Finally, you call the .Close() of the dialog. The reason you call .Close() here instead of .Destroy() is that you already call .Destroy() in the .on_edit() of your panel subclass.

Now your application is complete!

Conclusion

You learned a lot about wxPython in this article. You became familiar with the basics of creating GUI applications using wxPython.

You now know more about the following:

  • How to work with some of wxPython’s widgets
  • How events work in wxPython
  • How absolute positioning compares with sizers
  • How to create a skeleton application

Finally you learned how to create a working application, an MP3 tag editor. You can use what you learned in this article to continue to enhance this application or perhaps create an amazing application on your own.

The wxPython GUI toolkit is robust and full of interesting widgets that you can use to build cross-platform applications. You are limited by only your imagination.

Further Reading

If you would like to learn more about wxPython, you can check out some of the following links:

For more information on what else you can do with Python, you might want to check out What Can I Do with Python? If you’d like to learn more about Python’s super(), then Supercharge Your Classes With Python super() may be just right for you.

You can also download the code for the MP3 tag editor application that you created in this article if you want to study it more in depth.

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Mike Driscoll

Mike has been programming in Python for over a decade and loves writing about Python!

» More about Mike

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.


Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!