1

I am making an application with python. It is all working. So far everything is in one source file. You start small, and then it all grows. I got to a point where the code gets pretty hard to understand. So I decided that I needed to split up the code in modules and classes.

I finally got some stuff together to get it all to work. However, I could not find much about making complex GUI's with python. hence using classes to create widget etcetera.

I made a small example application that demonstrates the following:

  1. Split GUI code and action code. In my example the action code is handled by a seperate class, this could also be just a seperate module.
  2. Create custom widgets by subclassing a container, in my example Tkinter.LabelFrame.
  3. Use virtual/custom events, which are propagated, to trigger actions in the main code.
  4. Exchange data with subclasses/widgets

The intention of this post is two fold.

  1. I hope other people can benefit from the struggle that I had to figure this out.
  2. Maybe others can improve the example further.

My example has four source files.

  1. start.py. This module only starts the application, creating an object of the Gui class.

    import main
    
    if __name__ == '__main__':
        title = "Test"
        gui = main.Gui(title)
    
  2. main.py. This module contains the Gui class, and holds the root element of the GUI.

    import Tkinter
    import action
    import widget
    
    class Gui():
        def __init__(self, title):
            self.root = Tkinter.Tk()
            self.root.protocol("WM_DELETE_WINDOW", self.applicationExit)
            self.root.title(title)
    
            #create the action object
            self.process = action.Adder()
    
            #create the input frame
            self.frameIn = widget.Input(self.root)
            self.frameIn.grid(row=0, column=0, padx = 5, pady =5, ipadx = 5, ipady = 5, sticky = Tkinter.N)
    
            #create the output frame
            self.frameOut = widget.Output(self.root)
            self.frameOut.grid(row=1, column=0, padx = 5, pady =5, ipadx = 5, ipady = 5, sticky = Tkinter.N)
    
            #bind events
            self.root.bind("<<input_submit>>", self.__submit)
    
            self.root.mainloop()
    
        def applicationExit(self):
            self.root.destroy()
    
        def __submit(self, event = None):
            value = self.frameIn.getValue()
            result = self.process.addValue(value)
            self.frameOut.outputText.set(result)
    
  3. widget.py. This module contains two custom widgets, which are used in the GUI.

    import Tkinter
    
    class Input(Tkinter.LabelFrame):
        def __init__(self, master):
            Tkinter.LabelFrame.__init__(self, master, text = "Input")
            self.inputText = Tkinter.StringVar()
    
            #create entry box
            self.entInput = Tkinter.Entry(self, textvariable = self.inputText, width = 20,)
            self.entInput.grid(row = 0, column = 0, padx = 5, pady = 2, sticky = Tkinter.N)
    
            #create submite button
            self.btnSubmit = Tkinter.Button(self, text = "Add", width = 10,
                command = self.__handlerSubmitButton)
            self.btnSubmit.grid(row = 1, column = 0, padx = 5, pady = 2, sticky = Tkinter.N)
    
        def getValue(self):
            value = self.inputText.get()
            if value.isdigit():
                return int(value)
            else:
                None
    
        def __handlerSubmitButton(self, event = None):
            self.btnSubmit.event_generate("<<input_submit>>")
    
    class Output(Tkinter.LabelFrame):
        def __init__(self, master):
            Tkinter.LabelFrame.__init__(self, master, text = "Output")
            self.outputText = Tkinter.StringVar()
    
            #create out put label box
            self.lblOutput = Tkinter.Label(self, textvariable = self.outputText, width = 20,
                anchor = Tkinter.E)
            self.lblOutput.grid(row = 0, column = 0, padx = 5, pady = 2, sticky = Tkinter.N)
    
        def setValue(self, value):
            self.outputText.set(value)
    
  4. action.py. This module contains the code that will perform the actual tasks of the application.

    class Adder():
        def __init__(self):
            self.count = 0
    
        def addValue(self, value):
            if value:
                self.count += value
            return self.count
    

Any improvements are very welcome.

martineau
  • 119,623
  • 25
  • 170
  • 301
Eric
  • 195
  • 1
  • 3
  • 7
  • 1
    If your only question is "Maybe others can improve the example further", this would be more appropriate for [code review](http://codereview.stackexchange.com/.) – Kevin Apr 01 '14 at 12:46

1 Answers1

11

Usually, the standard pattern for implementing a Tkinter application is to have some root object called Application or something which extends Tkinter.Frame and which then goes on to create all the widgets which define your interface:

import Tkinter as tk

class Application(tk.Frame):
    
    def __init__(self, root, *args, **kwargs):
        tk.Frame.__init__(self, root, *args, **kwargs)
        ... #do other initialisation
        self.grid() #or pack()

... 

if __name__ == '__main__':
    root = tk.Tk()
    app = Application(root)
    root.mainloop()

The advantage of this technique is two fold:

  • You now possess an object which can trigger Tkinter events and behaviour (since Tkinter has its own hierarchy of widgets) and can also intercept those behaviours using normal class idioms, such as methods
  • Your root class can pass your own message handling scheme (for handling requirement 4 "Exchange data with subclasses/widgets") which can be consistent and orchestrated with the natural hierarchy that your interface will form as you build it.

As an example of the latter point:

class Message(object):
    def __init__(self, kind, data):
        self.kind = kind
        self.data = data

class Application(tk.Frame):
    def __init__(self, root, *args, **kwargs):
        self.widgets = []
        ... #do widget declarations

    def message_downstream(self, message):
        for widget in self.widgets:
            widget.receive_message(message)
    
    def message_upstream(self, message):
        #do some logic based on the message
        ...

class Widget(tk.Button):
    def __init__(self, master, name, *args, **kwargs):
        tk.Button.__init__(self, master, *args, **kwargs)
        self.master = master
        #perhaps set command event to send a message
        self['command'] = lambda: self.message_upstream(Message(self.name, "I Got Clicked"))
    
    def message_downstream(self, message):
        #similar to above
        pass
    
    def message_upstream(self, message):
        self.master.message_upstream(self, message)

This method introduces the Chain of Responsibility pattern into your app since you can now control message flow at any point in the chain (i.e do something or pass it upstream back downstream, but through a different path). Beware though, good application design tries to incorporate the Model View Controller pattern into their code, which could be muddled if you introduce 'controlling' code somewhere in your 'view' code chain of responsibility, causing headaches.

The best way to use a chain of responsibility in a Tkinter hierarchy is to restrict the code to only interface concerns and pass everything else, ie code which modifies data, to some appropriate controller, such as the action classes you referred to.

So why would you use a pattern like above? When your interface interacts with itself in complex ways. An example might be controls in some sub-menu change what is visible in some other frame. The behaviour doesn't really concern or rely on the model, so implementing it as above would work.

I once wrote a code editor for Python which automatically compiled and ran the code in another window as you typed (which actually became annoying), displaying either the code output or which exceptions occurred. I used a chain of responsibility to collect the code from the editor widget and also to send the program output to the output window. I also used it to apply syntax highlighting changes to both windows simultaneously.

martineau
  • 119,623
  • 25
  • 170
  • 301
teletypist
  • 321
  • 4
  • 7