Generating a Dialog from a File

A few days ago, I wrote an article about using ConfigObj with wxPython. The first question I was asked about the article regarded using a configuration file to generate the dialog. I thought this was an interesting idea, so I took a stab at implementing that functionality. Personally I think it would be probably be better to just create the dialog using XRC and use ConfigObj to help manage which dialog file is loaded that way. However, this was an intriguing exercise to me and I think you’ll find it enlightening too.

DISCLAIMER: This is a total hack and may or may not serve you needs. I give various suggestions for expanding the example though, so I hope it’s helpful!

Now that that’s out of the way, let’s create a super simple configuration file. We’ll call it “config.ini” for convenience’s sake:

config.ini

[Labels]
server = Update Server:
username = Username:
password = Password:
update interval = Update Interval:
agency = Agency Filter:
filters = ""

[Values]
server = http://www.someCoolWebsite/hackery.php
username = ""
password = ""
update interval = 2
agency_choices = Include all agencies except, Include all agencies except, Exclude all agencies except
filters = ""

This configuration file has two sections: Labels and Values. The Labels section has the labels we will use to create wx.StaticText controls with. The Values section has some sample values we can use for the corresponding text control widgets and one combo box. Note that the agency_choices field is a list. The first item in the list will be the default option in the combo box and the other two items are the real contents of the widget.

Now let’s take a look at the code that will build the dialog:

preferencesDlg.py

import configobj
import wx

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

    #----------------------------------------------------------------------
    def __init__(self):
        """
        Initialize the dialog
        """
        wx.Dialog.__init__(self, None, wx.ID_ANY, 'Preferences', size=(550,300))
        self.createWidgets()
        
    #----------------------------------------------------------------------
    def createWidgets(self):
        """
        Create and layout the widgets in the dialog
        """
        lblSizer = wx.BoxSizer(wx.VERTICAL)
        valueSizer = wx.BoxSizer(wx.VERTICAL)
        btnSizer = wx.StdDialogButtonSizer()
        colSizer = wx.BoxSizer(wx.HORIZONTAL)
        mainSizer = wx.BoxSizer(wx.VERTICAL)
        
        iniFile = "config.ini"
        self.config = configobj.ConfigObj(iniFile)
        
        labels = self.config["Labels"]
        values = self.config["Values"]
        self.widgetNames = values
        font = wx.Font(12, wx.SWISS, wx.NORMAL, wx.BOLD)
        
        for key in labels:
            value = labels[key]
            lbl = wx.StaticText(self, label=value)
            lbl.SetFont(font)
            lblSizer.Add(lbl, 0, wx.ALL, 5)
            
        for key in values:
            print key
            value = values[key]
            if isinstance(value, list):
                default = value[0]
                choices = value[1:]
                cbo = wx.ComboBox(self, value=value[0],
                                  size=wx.DefaultSize, choices=choices, 
                                  style=wx.CB_DROPDOWN|wx.CB_READONLY, 
                                  name=key)
                valueSizer.Add(cbo, 0, wx.ALL, 5)
            else:
                txt = wx.TextCtrl(self, value=value, name=key)
                valueSizer.Add(txt, 0, wx.ALL|wx.EXPAND, 5)
                
        saveBtn = wx.Button(self, wx.ID_OK, label="Save")
        saveBtn.Bind(wx.EVT_BUTTON, self.onSave)
        btnSizer.AddButton(saveBtn)
        
        cancelBtn = wx.Button(self, wx.ID_CANCEL)
        btnSizer.AddButton(cancelBtn)
        btnSizer.Realize()
        
        colSizer.Add(lblSizer)
        colSizer.Add(valueSizer, 1, wx.EXPAND)
        mainSizer.Add(colSizer, 0, wx.EXPAND)
        mainSizer.Add(btnSizer, 0, wx.ALL | wx.ALIGN_RIGHT, 5)
        self.SetSizer(mainSizer)
        
    #----------------------------------------------------------------------
    def onSave(self, event):
        """
        Saves values to disk
        """
        for name in self.widgetNames:
            widget = wx.FindWindowByName(name)
            if isinstance(widget, wx.ComboBox):
                selection = widget.GetValue()
                choices = widget.GetItems()
                choices.insert(0, selection)
                self.widgetNames[name] = choices
            else:
                value = widget.GetValue()
                self.widgetNames[name] = value
        self.config.write()
        self.EndModal(0)
        
########################################################################
class MyApp(wx.App):
    """"""

    #----------------------------------------------------------------------
    def OnInit(self):
        """Constructor"""
        dlg = PreferencesDialog()
        dlg.ShowModal()
        dlg.Destroy()
        
        return True
        
if __name__ == "__main__":
    app = MyApp(False)
    app.MainLoop()

To start, we subclass a wx.Dialog and all its createWidgets method. This method will read our config file and use the data therein to create the display. Once the config is read, we loop over the keys in the Labels section and create static text controls as needed. Next, we loop over the values in the other section and use a conditional to check the type of widget. In this case, we only care about wx.TextCtrl and wx.Combobox. This is where ConfigObj helps since it actually can typecast some of the entries in our configuration file. If you use a configspec, you can get even more granular and that may be the way you’ll want to go to extend this tutorial. Note that for the text controls and combo box, I set the name field. This is important for saving the data, which we’ll be seeing in just a moment.

Anyway, in both loops, we use vertical BoxSizers to hold our widgets. You may want to swap this for a GridBagSizer or FlexGridSizer for your specialized interface. I personally really like BoxSizers. I also used a StdDialogButtonSizer for the buttons at the suggestion of Steven Sproat (Whyteboard). If you use the correct standard ids for the buttons, this sizer will place them in the right order in a cross-platform way. It’s quite handy, although it doesn’t take many arguments. Also note that the documentation for this sizer implies you can specify the orientation, but you really cannot. I spoke with Robin Dunn (creator of wxPython) about this issue on IRC and he said that epydoc was grabbing the wrong docstring.

The next method that we care about is onSave. Here is where we save whatever the user has entered. Earlier in the program, I grabbed the widget names from the configuration and we loop over those now. We call wx.FindWindowByName to find the widget by name. Then we use isinstance again to check what kind of widget we have. Once that’s done, we grab the value that the widget holds using GetValue and assign that value to the correct field in our configuration. When the loop finishes, we write the data to disk. Immediate improvement alert: I have no validation here at all! This is something for you to do to extend this example. The last step is to call EndModal(0) to close the dialog and, in turn, the application.

Now you know the basics of generating a dialog from a configuration file. I think using some kind of dictionary with widget type names (probably in strings) might be an easy way to make this script work with other widgets. Use your imagination and let me know what you come up with.

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

Further Reading

Downloads

8 thoughts on “Generating a Dialog from a File”

  1. And the immediate benefit of this is that you can add new preferences to the .ini file, and your dialog gets re-built with the most up-to-date “view”. This is actually really cool, because the ini file acts as meta data and avoids data duplication.

    You could even go as far as creating separate ConfigObj sections for each preference, containing things such as widget type (checkbox/radio), label, tooltip etc

    Taking actions such as updating the GUI based on the new user’s preferences can easily be done in the OnSave method by maintaining a copy of the (application’s) configuration file in the Preference class, and checking its new values against the original config file. You could, for example change the font by doing:

    if old_config[‘font’] != new_config[‘font’]:
    self.update_font() # or something

    if config[‘show_statusbar’]:
    self.show_statusbar()
    else:
    self.hide_statusbar()

    if config[‘show_toolbar’]:
    self.show_toolbar()
    else:
    self.hide_toolbar()

    hope that makes sense 🙂

  2. And the immediate benefit of this is that you can add new preferences to the .ini file, and your dialog gets re-built with the most up-to-date “view”. This is actually really cool, because the ini file acts as meta data and avoids data duplication.

    You could even go as far as creating separate ConfigObj sections for each preference, containing things such as widget type (checkbox/radio), label, tooltip etc

    Taking actions such as updating the GUI based on the new user’s preferences can easily be done in the OnSave method by maintaining a copy of the (application’s) configuration file in the Preference class, and checking its new values against the original config file. You could, for example change the font by doing:

    if old_config[‘font’] != new_config[‘font’]:
    self.update_font() # or something

    if config[‘show_statusbar’]:
    self.show_statusbar()
    else:
    self.hide_statusbar()

    if config[‘show_toolbar’]:
    self.show_toolbar()
    else:
    self.hide_toolbar()

    hope that makes sense 🙂

  3. Hi, Mike!

    Thanx for following up on my feedback so quickly. I like your solution.

    The original idea behind this was that typically, as a pythonista, you have a couple of useful programs that are called from the command line (be it Linux, some other flavor of Unix, or Windows). But if you are on a system with a graphical ui, you want to offer the user the same functionality but with more comfort.

    One could subclass the ConfigObj, and this subclass either just goes ahead and reads the config file, or — if in a UI environment — displays the preferences panel, and returns as soon as the user has clicked on ok. And/or one combines it with an OptionParser (for the non-ui case) that allows the user to override the preferences from the config file.

    ideas over ideas 🙂

    But thanx again!
    Axel

  4. Did you write this yourself? Very fascinating, either way. Couldn’t agree me — enjoy visiting your site, Thanks for your site and post – really like this one! Professional article – enjoy your site! Good information. Keep up the good work. I could not have said it better!

Comments are closed.