Question

Hopefully this doesn't fall under "general discussion topic", since I'd like it to be more about resolving these issues in an efficient manner than a giant debate about which general approach to GUI programming is the absolute best.

So I've started some GUI programming with tkinter and long story short my code is getting pretty ugly pretty quickly. I'm trying to create a tile-based map editor for a video game. My main issues seem to be:

  1. the inability of callbacks to return values.
  2. the inability to transfer data between windows easily.

I assume that the reason I see these as issues is because I'm using functions a lot more than I'm using classes. For instance, my "load tileset" window is handled entirely functionally: Clicking the menu option in the main window calls the function that loads the new window. From within that window, I create an open file dialog when looking for the image, and modify the canvas displaying the image when I press the enter key (so that it draws the appropriate grid over the image). function function function.

What looks like really bad practice to me is the inclusion of extra arguments to compensate. For example, when I create a tileset, the instance of the TileSet class created should be sent back to the main window where the appropriate information can be displayed. I have a list of loaded tilesets as a global variable (even more bad practice: Everything dealing with my root window is in the global scope! yay!), and because callback functions don't return values, I pass that list as an argument to my "load tileset window" function, which then passes the argument to the create tileset function (called when you click the appropriate button in the window), where it's actually needed so that I can add my newly created tileset to the list. Passing arguments through a function 'hierarchy' like that seems like a horrible idea. It gets confusing, it's horrible for writing modular code, and just generally seems unnecessary.

My attempt at fixing the problem would be to write a class representing the whole GUI, and custom made window classes (that the GUI class can create and reference) that can actually store relevant data. That should take care of issues with transferring data between windows. Hopefully it would cut down on my gratuitous use of lambda functions in callbacks as well. But I'm wondering: is this the best way? Or at least close? I'd rather not start rewriting and then end up with another system that's just sloppy and confusing in a different way. I know my methods are bad, but I don't really know what the best approach would be. I'm getting a lot of advice on how to do specific things, but none on how to structure the program as a whole. Any help would be greatly appreciated.

Was it helpful?

Solution

It sounds like you're trying to create a GUI that acts procedurally, which won't work. GUIs aren't procedural, their code doesn't run linearly where functions call callbacks which return values. What you're asking isn't unique to tkinter. This is the nature of event based GUI programming -- callbacks can't return anything because the caller is an event rather than a function.

Roughly speaking, you must use a global object of some sort to store your data. Typically this is called the "Model". It can be a global variable, or it might be a database, or it can be an object of some sort. In any case, it must exist "globally"; that is, it must be accessible to the whole GUI.

Often, this access is provided by a third component called a "Controller". It is the interface between the GUI (the "View") and the data (the "Model"). These three components make up what is called the model-view-controller pattern, or MVC.

The model, view and controller don't have to be three different objects. Often, the GUI and the controller are the same object. For small programs this works quite well -- the GUI components talk directly to your data model.

For example, you could have a class that represents a window which inherits from Tkinter.Toplevel. It can have an attribute that represents the data being edited. When the user selects "New" from a main window, it does something like self.tileset = TileSet(filename). That is, it sets the attribute named tileset of the GUI object named self to be an instance of the TileSet class specific to the given filename. Later functions that manipulate the data use self.tileset to access the object. For functions that live outside the main window object (for example, a "save all" function from the main window) you can either pass this object as an argument, or use the window object as the controller, asking it to do something to its tileset.

Here's a brief example:

import Tkinter as tk
import tkFileDialog
import datetime

class SampleApp(tk.Tk):
    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)
        self.windows = []
        menubar = tk.Menu(self)
        self.configure(menu=menubar)
        fileMenu = tk.Menu(self)
        fileMenu.add_command(label="New...", command=self.new_window)
        fileMenu.add_command(label="Save All", command=self.save_all)
        menubar.add_cascade(label="Window", menu=fileMenu)
        label = tk.Label(self, text="Select 'New' from the window menu")
        label.pack(padx=20, pady=40)

    def save_all(self):
        # ask each window object, which is acting both as 
        # the view and controller, to save it's data
        for window in self.windows:
            window.save()

    def new_window(self):
        filename = tkFileDialog.askopenfilename()
        if filename is not None:
            self.windows.append(TileWindow(self, filename))

class TileWindow(tk.Toplevel):
    def __init__(self, master, filename):
        tk.Toplevel.__init__(self, master)
        self.title("%s - Tile Editor" % filename)
        self.filename = filename
        # create an instance of a TileSet; all other
        # methods in this class can reference this
        # tile set
        self.tileset = TileSet(filename)
        label = tk.Label(self, text="My filename is %s" % filename)
        label.pack(padx=20, pady=40)
        self.status = tk.Label(self, text="", anchor="w")
        self.status.pack(side="bottom", fill="x")

    def save(self):
        # this method acts as a controller for the data,
        # allowing other objects to request that the 
        # data be saved
        now = datetime.datetime.now()
        self.status.configure(text="saved %s" % str(now))

class TileSet(object):
    def __init__(self, filename):
        self.data = "..."

if __name__ == "__main__":
    app = SampleApp()
    app.mainloop()
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top