ConfigObj + wxPython = Geek Happiness

I recently starting using Michael Foord’s ConfigObj for one of our internal wxPython applications at work. When I wrote my other ConfigObj tutorial, I wanted to show you how I was using ConfigObj with my preferences dialog, but I didn’t want all my posts to include wx. In this article, I’ll just show you how easy it is to add a new preference setting in ConfigObj without wiping out your original ones and how to load and save them with a wxPython dialog. Now, let’s get to it!

First we’ll create a simple controller for creating and accessing our configuration file with ConfigObj:

import configobj
import os
import sys
import wx
from wx.lib.buttons import GenBitmapTextButton

appPath = os.path.abspath(os.path.dirname(os.path.join(sys.argv[0])))
inifile = os.path.join(appPath, "example.ini")

########################################################################
class CloseBtn(GenBitmapTextButton):
    """
    Creates a reusuable close button with a bitmap
    """

    #----------------------------------------------------------------------
    def __init__(self, parent, label="Close"):
        """Constructor"""
        font = wx.Font(16, wx.SWISS, wx.NORMAL, wx.BOLD)
        img = wx.Bitmap(r"%s\images\cancel.png" % appPath)
        GenBitmapTextButton.__init__(self, parent, wx.ID_CLOSE, img, 
                                     label=label, size=(110, 50))
        self.SetFont(font)

#----------------------------------------------------------------------
def createConfig():
    """
    Create the configuration file
    """
    config = configobj.ConfigObj()
    config.filename = inifile
    config['update server'] = "http://www.someCoolWebsite/hackery.php"    
    config['username'] = ""
    config['password'] = ""
    config['update interval'] = 2
    config['agency filter'] = 'include'
    config['filters'] = ""
    config.write()
    
#----------------------------------------------------------------------
def getConfig():
    """
    Open the config file and return a configobj
    """
    if not os.path.exists(inifile):
        createConfig()
    return configobj.ConfigObj(inifile)

This piece of code is pretty straightforward. In the createConfig function, it creates an “example.ini” file in the same directory as the one that this script is run from. The config file gets six fields, but no sections. In the getConfig function, the code checks for the existence of the configuration file and creates it if it does not exist. Regardless, the function returns a ConfigObj object to the caller. We’ll put this script into “controller.py”. Now we’ll subclass the wx.Dialog class to create a preferences dialog.

# -----------------------------------------------------------
# preferencesDlg.py
#
# Created 10/20/2009 by mld
# -----------------------------------------------------------

import controller
import wx
from wx.lib.buttons import GenBitmapTextButton

########################################################################
class PreferencesDialog(wx.Dialog):
    """
    Creates and displays a preferences dialog that allows the user to
    change some settings.
    """

    #----------------------------------------------------------------------
    def __init__(self):
        """
        """
        wx.Dialog.__init__(self, None, wx.ID_ANY, 'Preferences', size=(550,300))
        appPath = controller.appPath
        
        # ---------------------------------------------------------------------
        # Create widgets
        font = wx.Font(12, wx.SWISS, wx.NORMAL, wx.BOLD)
        serverLbl = wx.StaticText(self, wx.ID_ANY, "Update Server:")
        self.serverTxt = wx.TextCtrl(self, wx.ID_ANY, "")
        self.serverTxt.Disable()
        
        usernameLbl = wx.StaticText(self, wx.ID_ANY, "Username:")
        self.usernameTxt = wx.TextCtrl(self, wx.ID_ANY, "")
        self.usernameTxt.Disable()
        
        passwordLbl = wx.StaticText(self, wx.ID_ANY, "Password:")
        self.passwordTxt = wx.TextCtrl(self, wx.ID_ANY, "", style=wx.TE_PASSWORD)
        self.passwordTxt.Disable()
        
        updateLbl = wx.StaticText(self, wx.ID_ANY, "Update Interval:")
        self.updateTxt = wx.TextCtrl(self, wx.ID_ANY, "")
        minutesLbl = wx.StaticText(self, wx.ID_ANY, "minutes")
        
        agencyLbl = wx.StaticText(self, wx.ID_ANY, "Agency Filter:")
        choices = ["Include all agencies except", "Exclude all agencies except"]
        self.agencyCbo = wx.ComboBox(self, wx.ID_ANY, "Include all agencies except",
                                     None, wx.DefaultSize, choices, wx.CB_DROPDOWN|wx.CB_READONLY)
        self.agencyCbo.SetFont(font)
        self.filterTxt = wx.TextCtrl(self, wx.ID_ANY, "")
        
        img = wx.Bitmap(r"%s/images/filesave.png" % appPath)
        saveBtn = GenBitmapTextButton(self, wx.ID_ANY, img, "Save", size=(110, 50))
        saveBtn.Bind(wx.EVT_BUTTON, self.savePreferences)
        cancelBtn = controller.CloseBtn(self, label="Cancel")
        cancelBtn.Bind(wx.EVT_BUTTON, self.onCancel)
        
        widgets = [serverLbl, usernameLbl, passwordLbl, updateLbl, agencyLbl, minutesLbl,
                   self.serverTxt, self.usernameTxt, self.passwordTxt, self.updateTxt,
                   self.agencyCbo, self.filterTxt, saveBtn, cancelBtn]
        for widget in widgets:
            widget.SetFont(font)
        
        # ---------------------------------------------------------------------
        # layout widgets
        mainSizer = wx.BoxSizer(wx.VERTICAL)
        updateSizer = wx.BoxSizer(wx.HORIZONTAL)
        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
        prefSizer = wx.FlexGridSizer(cols=2, hgap=5, vgap=5)
        prefSizer.AddGrowableCol(1)
        
        prefSizer.Add(serverLbl, 0, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL)
        prefSizer.Add(self.serverTxt, 0, wx.EXPAND)
        prefSizer.Add(usernameLbl, 0, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL)
        prefSizer.Add(self.usernameTxt, 0, wx.EXPAND)
        prefSizer.Add(passwordLbl, 0, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL)
        prefSizer.Add(self.passwordTxt, 0, wx.EXPAND)
        prefSizer.Add(updateLbl, 0, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL)
        updateSizer.Add(self.updateTxt, 0, wx.RIGHT, 5)
        updateSizer.Add(minutesLbl, 0, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL)
        prefSizer.Add(updateSizer)
        prefSizer.Add(agencyLbl, 0, wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL)
        prefSizer.Add(self.agencyCbo, 0, wx.EXPAND)
        prefSizer.Add((20,20))
        prefSizer.Add(self.filterTxt, 0, wx.EXPAND)
        
        mainSizer.Add(prefSizer, 0, wx.EXPAND|wx.ALL, 5)
        btnSizer.Add(saveBtn, 0, wx.ALL, 5)
        btnSizer.Add(cancelBtn, 0, wx.ALL, 5)
        mainSizer.Add(btnSizer, 0, wx.ALL | wx.ALIGN_RIGHT, 10)
        self.SetSizer(mainSizer)
                
        # ---------------------------------------------------------------------
        # load preferences
        self.loadPreferences()
        
    #----------------------------------------------------------------------
    def loadPreferences(self):
        """
        Load the preferences and fill the text controls
        """
        config = controller.getConfig()
        updateServer = config['update server']
        username = config['username']
        password = config['password']
        interval = config['update interval']
        agencyFilter = config['agency filter']
        filters = config['filters']
        
        self.serverTxt.SetValue(updateServer)
        self.usernameTxt.SetValue(username)
        self.passwordTxt.SetValue(password)
        self.updateTxt.SetValue(interval)
        self.agencyCbo.SetValue(agencyFilter)
        self.filterTxt.SetValue(filters)
        
    #----------------------------------------------------------------------
    def onCancel(self, event):
        """
        Closes the dialog
        """
        self.EndModal(0)
        
    #----------------------------------------------------------------------
    def savePreferences(self, event):
        """
        Save the preferences
        """
        config = controller.getConfig()
        
        config['update interval'] = self.updateTxt.GetValue()
        config['agency filter'] = str(self.agencyCbo.GetValue())
        data = self.filterTxt.GetValue()
        if "," in data:
            filters = [i.strip() for i in data.split(',')]
        elif " " in data:
            filters = [i.strip() for i in data.split(' ')]
        else:
            filters = [data]
        text = ""
        for f in filters:
            text += " " + f
        text = text.strip()
        config['filters'] = text
        config.write()
        
        dlg = wx.MessageDialog(self, "Preferences Saved!", 'Information',  
                               wx.OK|wx.ICON_INFORMATION)
        dlg.ShowModal()        
        self.EndModal(0)
        
if __name__ == "__main__":
    app = wx.PySimpleApp()
    dlg = PreferencesDialog()
    dlg.ShowModal()
    dlg.Destroy()

The code above creates a basic preferences dialog and loads its configuration from a file using ConfigObj. You can see how that works by reading the code in the loadPreferences method. The only other piece that we care about is how the code saves the preferences when the user changes them. For that, we need to look at the savePreferences method. This is a pretty straightforward method in that all it does is grab the various values from the widgets using wx’s specific getter functions. There’s also a conditional that does some minor checking on the filter field. The main reason for that is that in my original program, I use a space as the delimiter and the program needed to convert commas and such to spaces. This code is still a work in progress though as it does not cover all the cases that a user could enter.

Anyway, once we have the values inside the ConfigObj’s dict-like interface, we write the ConfigObj instance’s data to file. Then the program displays a simple dialog to let the user know that’s saved.

Now, let’s say that our program’s specifications change such that we need to add or remove a preference. All that is required to do so is to add or delete it in the configuration file. ConfigObj will pick up the changes and we just need to remember to add or remove the appropriate widgets in our GUI. One of the best things about ConfigObj is that it won’t reset the data in your file, it will just add the changes as appropriate. Give it a try and find out just how easy it is!

Note: All code tested on Windows XP with Python 2.5, ConfigObj 4.6.0, and Validate 1.0.0.

Downloads

2 thoughts on “ConfigObj + wxPython = Geek Happiness”

  1. Finally an example that brings together Python, a GUI toolkit and ConfigObj (or ConfigParser, does not matter).

    I’ve been thinking about this as well, and I’m asking you: wouldn’t it be possible to generate the preferences panel from the COnfigFile itself automagically? In other words, the program reads the config file, and then generates the labels and entry fields?

    In a first version, you might create just string fields, but then one could also try to guess the type of value…

    What do you think? Let me know via email,
    Axel

  2. Finally an example that brings together Python, a GUI toolkit and ConfigObj (or ConfigParser, does not matter).

    I’ve been thinking about this as well, and I’m asking you: wouldn’t it be possible to generate the preferences panel from the COnfigFile itself automagically? In other words, the program reads the config file, and then generates the labels and entry fields?

    In a first version, you might create just string fields, but then one could also try to guess the type of value…

    What do you think? Let me know via email,
    Axel

Comments are closed.