Using Python Tkinter with XML

This post discusses a simple example of a Python script which creates a GUI to manipulate an XML document. I have published this post as I could not find many complete examples of this functionality elsewhere on the web.

The example is deliberately simplified to emphasise the code required to read from the XML document, create a GUI window in which the data can be edited and finally to write the changes back to the XML document.

The following image shows the window created by the example:

The example is based on the first part of the build options menu of HammerDB. I used HammerDB because it is open source and is written in TCL which uses the same underlying GUI library.

The example was developed on a VM running Python 2.6 and Tkinter 8.5 on Linux 5U4.

I have used the XML Minidom library to manipulate the XML document.

The example XML configuration file (config.xml) is as follows:

<?xml version="1.0" encoding="utf-8"?>
<hammerdb>
  <rdbms>Oracle</rdbms>
  <bm>TPC-C</bm>
  <oracle>
    <service>
      <system_user>SYSTEM</system_user>
      <system_password>PASSWORD</system_password>
      <service_name>PROD</service_name>
    </service>
  </oracle>
</hammerdb>

To reduce the length of the example, I have only used the first handful of elements in the HammerDB configuration file.

The complete example follows. The remainder of this post discusses the example in detail:

#!/usr/bin/python

import xml.dom.minidom

from ttk import Frame, Label,Entry,Button
from Tkinter import Tk, StringVar, BOTH, W, E
import tkMessageBox 

import sys

def printf (format, *args):
        sys.stdout.write (format % args)

def fprintf (fp, format, *args):
        fp.write (format % args)

# get an XML element with specified name
def getElement (parent,name):
    nodeList = []
    if parent.childNodes:
       for node in parent.childNodes:
          if node.nodeType == node.ELEMENT_NODE:
             if node.tagName == name:
                nodeList.append (node) 
    return nodeList[0]

# get value of an XML element with specified name
def getElementValue (parent,name):
    if parent.childNodes:
       for node in parent.childNodes:
          if node.nodeType == node.ELEMENT_NODE:
             if node.tagName == name:
                if node.hasChildNodes:
                   child = node.firstChild
                   return child.nodeValue
    return None

# set value of an XML element with specified name
def setElementValue (parent,name,value):
    if parent.childNodes:
       for node in parent.childNodes:
          if node.nodeType == node.ELEMENT_NODE:
             if node.tagName == name:
                if node.hasChildNodes:
                   child = node.firstChild
                   child.nodeValue = value
    return None

class Application (Frame):

  def __init__(self, parent):

      # initialize frame
      Frame.__init__(self,parent)

      # set root as parent
      self.parent = parent

      # read and parse XML document
      DOMTree = xml.dom.minidom.parse ("config.xml")

      # create attribute for XML document
      self.xmlDocument = DOMTree.documentElement

      # get value of "rdbms" element
      self.database = StringVar()
      self.database.set (getElementValue (self.xmlDocument,"rdbms"))

      # get value of "benckmark" element
      self.benchmark = StringVar()
      self.benchmark.set (getElementValue (self.xmlDocument,"bm"))

      # create attribute for "oracle" element
      self.xmlOracle = getElement (self.xmlDocument,"oracle")
     
      # create attribute for "service" element 
      self.xmlService = getElement (self.xmlOracle,"service")

      # get value of "system_user" element
      self.systemUser = StringVar()
      self.systemUser.set (getElementValue (self.xmlService,"system_user"))

      # get value of "system_password" element
      self.systemPassword = StringVar()
      self.systemPassword.set (getElementValue (self.xmlService,"system_password"))

      # get value of "service_name" element
      self.serviceName = StringVar()
      self.serviceName.set (getElementValue (self.xmlService,"service_name"))

      # initialize UI
      self.initUI()

  def initUI(self):
      # set frame title
      self.parent.title ("HammerDB")

      # pack frame
      self.pack (fill=BOTH, expand=1)

      # configure grid columns
      self.columnconfigure (0, pad=3)
      self.columnconfigure (1, pad=3)

      # configure grid rows
      self.rowconfigure (0, pad=3)
      self.rowconfigure (1, pad=3)
      self.rowconfigure (2, pad=3)
      self.rowconfigure (3, pad=3)
      self.rowconfigure (4, pad=3)
      self.rowconfigure (6, pad=3)

      # database
      label1 = Label (self,text = "Database: ")
      label1.grid (row=0,column=0,sticky=W)

      entry1 = Entry (self,width=30,textvariable = self.database)
      entry1.grid (row=0,column=1)

      # bench mark
      label2 = Label (self,text = "Benckmark : ")
      label2.grid (row=1,column=0,sticky=W)

      entry2 = Entry (self,width=30,textvariable = self.benchmark)
      entry2.grid (row=1,column=1)

      # service name
      label3 = Label (self,text = "Service Name : ")
      label3.grid (row=2,column=0,sticky=W)

      entry3 = Entry (self,width=30,textvariable = self.serviceName)
      entry3.grid (row=2,column=1)

      # system user
      label4 = Label (self,text = "System User : ")
      label4.grid (row=3,column=0,sticky=W)

      entry4 = Entry (self,width=30,textvariable = self.systemUser)
      entry4.grid (row=3,column=1)

      # system user password
      label5 = Label (self,text = "System User Password : ")
      label5.grid (row=4,column=0,sticky=W)

      entry5 = Entry (self,width=30,textvariable = self.systemPassword)
      entry5.grid (row=4,column=1)

      # blank line
      label6 = Label (self,text = "")
      label6.grid (row=5,column=0,sticky=E+W)

      # create OK button 
      button1 = Button (self, text="OK", command=self.onOK)
      button1.grid (row=6,column=0,sticky=E)

      # create Cancel button
      button2 = Button (self, text="Cancel", command=self.onCancel)
      button2.grid (row=6,column=1,sticky=E)

  def onOK(self): 

      # set values in xml document
      setElementValue (self.xmlDocument,"rdbms",self.database.get())
      setElementValue (self.xmlDocument,"bm",self.benchmark.get())
      setElementValue (self.xmlService,"system_user",self.systemUser.get())
      setElementValue (self.xmlService,"system_password",self.systemPassword.get())
      setElementValue (self.xmlService,"service_name",self.serviceName.get())

      # open XML file
      f = open ("config.xml","w")

      # set xml header
      fprintf (f,'<?xml version="1.0" encoding="utf-8"?>\n')

      # write XML document to XML file
      self.xmlDocument.writexml (f)

      # close XML file
      f.close ()

      # show confirmation message
      tkMessageBox.showerror ("Message","Configuration updated successfully")

      # exit program
      self.quit();

  def onCancel(self): 

      # exit program
      self.quit();

def main():

   # initialize root object
   root = Tk()

   # set size of frame
   root.geometry ("410x160+300+300")

   # call object
   app = Application (root)

   # enter main loop
   root.mainloop()

# if this is the main thread then call main() function
if __name__ == '__main__':
   main ()

The above code is discussed in the following sections:

There are several XML libraries available in Python including Minidom and ElementTree. In this example I have used Minidom which is shipped as part of the base Python release on Linux.

Within Minidom, I have only used the Node class, not the Element, Attribute etc classes. In particular I have avoided using the Element.getElementsByTagName() method as it appears not to support some XML document schema designs.

import xml.dom.minidom

The next set of declarations are for the GUI elements used by ttk and Tkinter. Tkinter (Tk interface) is based on ttk (Themed tk)

from ttk import Frame, Label,Entry,Button
from Tkinter import Tk, StringVar, BOTH, W, E

Alternatively all classes could be imported from Tkinter as follows:

from Tkinter import Tk, Frame, Label,Entry,Button, StringVar, BOTH, W, E

The tkMessageBox is a Python widget that is used to display messages in a popup window. The class is imported separately from Tkinter

import tkMessageBox 

The next declarations define equivalents of the C printf and fprintf functions. I normally include these functions in a separate library.

import sys

def printf (format, *args):
        sys.stdout.write (format % args)

def fprintf (fp, format, *args):
        fp.write (format % args)

The getElement method traverses the list of child nodes for a specfied parent node. If the child is an element and the tag matches a specified name then the node is added to a list. The first element of the node list is returned. Note that this function works with this example as a child node will always be found, but the function is not robust.

# get an XML element with specified name
def getElement (parent,name):
    nodeList = []
    if parent.childNodes:
       for node in parent.childNodes:
          if node.nodeType == node.ELEMENT_NODE:
             if node.tagName == name:
                nodeList.append (node) 
    return nodeList[0]

The getElementValue method also traverses the list of child nodes for the specified parent node searching for elements with tags that match the specified name. If the element has children then the value of the first child is returned. Again this method makes assumptions about the presence and type of the child and is therefore not robust.

# get value of an XML element with specified name
def getElementValue (parent,name):
    if parent.childNodes:
       for node in parent.childNodes:
          if node.nodeType == node.ELEMENT_NODE:
            if node.tagName == name:
                if node.hasChildNodes:
                   child = node.firstChild
                   return child.nodeValue
    return None

The setElementValue method is similar to getElementValue, except that it updates the value of the child node with the value passed as an argument.

# set value of an XML element with specified name
def setElementValue (parent,name,value):
    if parent.childNodes:
       for node in parent.childNodes:
          if node.nodeType == node.ELEMENT_NODE:
            if node.tagName == name:
                if node.hasChildNodes:
                   child = node.firstChild
                   child.nodeValue = value
    return None

The Application class contains the application logic including reading, updating and writing the XML document and creating / managing the GUI.

class Application (Frame):

The __init__ method is called when a new instance of the class is created

  def __init__(self, parent):

The first step is to initialize the frame

      # initialize frame
      Frame.__init__(self,parent)

The parent of the class (in this case root) is stored as an attribute for later use

      # set root as parent
      self.parent = parent

The XML document ("config.xml") is parsed and stored in a DOM tree

      # read and parse XML document
      DOMTree = xml.dom.minidom.parse ("config.xml")

An attribute is created for the XML document

      # create attribute for XML document
      self.xmlDocument = DOMTree.documentElement

The variables for the oracle elements - rdbms and benchmark are initialized

      # get value of "rdbms" element
      self.database = StringVar()
      self.database.set (getElementValue (self.xmlDocument,"rdbms"))

      # get value of "benchmark" element
      self.benchmark = StringVar()
      self.benchmark.set (getElementValue (self.xmlDocument,"bm"))

An attribute is created for the "oracle" element

      # create attribute for "oracle" element
      self.xmlOracle = getElement (self.xmlDocument,"oracle")

An attribute is created for the "service" element

     
      # create attribute for "service" element 
      self.xmlService = getElement (self.xmlOracle,"service")

In the next section the variables for the service elements - system_user, system_password and service_name are initialised using getElementValue to read them from the xmlService element

     
      # get value of "system_user" element
      self.systemUser = StringVar()
      self.systemUser.set (getElementValue (self.xmlService,"system_user"))

      # get value of "system_password" element
      self.systemPassword = StringVar()
      self.systemPassword.set (getElementValue (self.xmlService,"system_password"))

      # get value of "service_name" element
      self.serviceName = StringVar()
      self.serviceName.set (getElementValue (self.xmlService,"service_name"))

Finally the UI is initialized by calling the initUI procedure

      # initialize UI
      self.initUI()

The initUI() method is probably redundant in this example, but is typically used to initialize the user interface

  def initUI(self):

The first step sets the title of the applicaation frame

      # set frame title
      self.parent.title ("HammerDB")

The frame is initialized using the Pack layout manager. If this statement is omitted, the frame will still be drawn, but will appear as an empty grey box.

      # pack frame
      self.pack (fill=BOTH, expand=1)

The grid layout manager will be used for the rest of the UI initialization.

First the two columns are created:

      # configure grid columns
      self.columnconfigure (0, pad=3)
      self.columnconfigure (1, pad=3)

Next the six columns are created:

      # configure grid rows
      self.rowconfigure (0, pad=3)
      self.rowconfigure (1, pad=3)
      self.rowconfigure (2, pad=3)
      self.rowconfigure (3, pad=3)
      self.rowconfigure (4, pad=3)
      self.rowconfigure (6, pad=3)

Each variable has a label and and entry

The label is left-justified using the sticky=w parameter

      # database
      label1 = Label (self,text = "Database: ")
      label1.grid (row=0,column=0,sticky=W)

      entry1 = Entry (self,width=30,textvariable = self.database)
      entry1.grid (row=0,column=1)

      # bench mark
      label2 = Label (self,text = "Benckmark : ")
      label2.grid (row=1,column=0,sticky=W)

      entry2 = Entry (self,width=30,textvariable = self.benchmark)
      entry2.grid (row=1,column=1)

      # service name
      label3 = Label (self,text = "Service Name : ")
      label3.grid (row=2,column=0,sticky=W)

      entry3 = Entry (self,width=30,textvariable = self.serviceName)
      entry3.grid (row=2,column=1)

      # system user
      label4 = Label (self,text = "System User : ")
      label4.grid (row=3,column=0,sticky=W)

      entry4 = Entry (self,width=30,textvariable = self.systemUser)
      entry4.grid (row=3,column=1)

      # system user password
      label5 = Label (self,text = "System User Password : ")
      label5.grid (row=4,column=0,sticky=W)

      entry5 = Entry (self,width=30,textvariable = self.systemPassword)
      entry5.grid (row=4,column=1)

A label is used to insert a blank line to create a space in the grid between the variables and buttons.

      # blank line
      label6 = Label (self,text = "")
      label6.grid (row=5,column=0,sticky=E+W)

An OK button is created which calls the onOk() method when clicked

      # create OK button 
      button1 = Button (self, text="OK", command=self.onOK)
      button1.grid (row=6,column=0,sticky=E)

Finally a Cancel button is created which calls the onCancel() method when clicked

      # create Cancel button
      button2 = Button (self, text="Cancel", command=self.onCancel)
      button2.grid (row=6,column=1,sticky=E)

The onOK method is called when the OK button is clicked. It writes the new variable settings back to the XML document.

  def onOK(self): 

The first step sets the current values of the variables in the XML document

      # set values in xml document
      setElementValue (self.xmlDocument,"rdbms",self.database.get())
      setElementValue (self.xmlDocument,"bm",self.benchmark.get())
      setElementValue (self.xmlService,"system_user",self.systemUser.get())
      setElementValue (self.xmlService,"system_password",self.systemPassword.get())
      setElementValue (self.xmlService,"service_name",self.serviceName.get())

The XML document is written back to the XML file using the writexml method. There is probably a more elegant way of setting the XML header

      # open XML file
      f = open ("config.xml","w")

      # set xml header
      fprintf (f,'<?xml version="1.0" encoding="utf-8"?>\n')

      # write XML document to XML file
      self.xmlDocument.writexml (f)

      # close XML file
      f.close ()

A confirmation message is shown using the tkMessageBox widget

      # show confirmation message
      tkMessageBox.showerror ("Message","Configuration updated successfully")

Finally we can quit the application

      # exit program
      self.quit();

The onCancel method is called when the Cancel button is clicked. It exits the application without saving any changes

  def onCancel(self): 

      # exit program
      self.quit();

The main () function is called to initialize the root object, set the size of the frame, create an Application object and then enter the main loop until the application exits

def main():

   # initialize root object
   root = Tk()

   # set size of frame
   root.geometry ("410x160+300+300")

   # call object
   app = Application (root)

   # enter main loop
   root.mainloop()

If this is the main thread then call the main () function:

if __name__ == '__main__':
   main ()