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 ()