3

I have a moderately complex GUI that I'm building for interacting with and observing some simulations. I would like to be able to continue to refactor and add features as the project progresses. For this reason, I would like as loose as possible a coupling between different widgets in the application.

My application is structured something like this:

import tkinter as tk

class Application(tk.Tk):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.instance_a = ClassA(self)
        self.instance_b = ClassB(self)
        # ... #

class ClassA(tk.Frame):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # ... #

class ClassB(tk.Frame):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # ... #

def main():
    application = Application()
    application.mainloop()

if __name__ == '__main__':
    main()

I would like to be able to perform some action in one widget (such as selecting an item in a Treeview widget or clicking on part of a canvas) which changes the state of the other widget.

One way to do this is to have the following code in class A:

self.bind('<<SomeEvent>>', self.master.instance_b.callback())

With the accompanying code in class B:

def callback(self): print('The more that things change, ')

The problem that I have with this approach is that class A has to know about class B. Since the project is still a prototype, I'm changing things all the time and I want to be able to rename callback to something else, or get rid of widgets belonging to class B entirely, or make instance_a a child of some PanedWindow object (in which case master needs to be replaced by winfo_toplevel()).

Another approach is to put a method inside the application class which is called whenever some event is triggered:

class Application(tk.Tk):
    # ... #
    def application_callback():
        self.instance_b.callback()

and modify the bound event in class A:

self.bind('<<SomeEvent>>', self.master.application_callback())

This is definitely easier to maintain, but requires more code. It also requires the application class to know about the methods implemented in class B and where instance_b is located in the hierarchy of widgets. In a perfect world, I would like to be able to do something like this:

# in class A:
self.bind('<<SomeEvent>>', lambda _: self.event_generate('<<AnotherEvent>>'))

# in class B:
self.bind('<<AnotherEvent>>', callback)

That way, if I perform an action in one widget, the second widget would automatically know to to respond in some way without either widget knowing about the implementation details of the other. After some testing and head-scratching, I came to the conclusion that this kind of behavior is impossible using tkinter's events system. So, here are my questions:

  1. Is this desired behavior really impossible?
  2. Is it even a good idea?
  3. Is there a better way of achieving the degree of modularity that I want?
  4. What modules/tools can I use in place of tkinter's built-in event system?
castle-bravo
  • 1,389
  • 15
  • 34

1 Answers1

0

My code in answer avoids the issue of class A having to know about internals of class B by calling methods of a handler object. In the following code methods in class Scanner do not need to know about the internals of a ScanWindow instance. The instance of a Scanner class contains a reference to an instance of a handler class, and communicates with the instance of ScannerWindow through the methods of Handler class.

# this class could be replaced with a class inheriting 
# a Tkinter widget, threading is not necessary
class Scanner(object):
    def __init__(self, handler, *args, **kw):
        self.thread = threading.Thread(target=self.run)
        self.handler = handler

    def run(self):
        while True:
            if self.handler.need_stop():
                break

            img = self.cam.read()
            self.handler.send_frame(img)

class ScanWindow(tk.Toplevel):
    def __init__(self, parent, *args, **kw):
        tk.Toplevel.__init__(self, master=parent, *args, **kw)

        # a reference to parent widget if virtual events are to be sent
        self.parent = parent
        self.lock = threading.Lock()
        self.stop_event = threading.Event()
        self.frames = []

    def start(self):
        class Handler(object):
            # note self and self_ are different
            # self refers to the instance of ScanWindow
            def need_stop(self_):
                return self.stop_event.is_set()

            def send_frame(self_, frame):
                self.lock.acquire(True)
                self.frames.append(frame)
                self.lock.release()

                # send an event to another widget 
                # self.parent.event_generate('<<ScannerFrame>>', when='tail')

            def send_symbol(self_, data):
                self.lock.acquire(True)
                self.symbols.append(data)
                self.lock.release()

                # send an event to another widget
                # self.parent.event_generate('<<ScannerSymbol>>', when='tail')

        self.stop_event.clear()
        self.scanner = Scanner(Handler())
Community
  • 1
  • 1
J.J. Hakala
  • 6,136
  • 6
  • 27
  • 61
  • I would like some clarification to help understand the essential features of your answer better. Is use of the threading module absolutely necessary (for non OpenCV applications)? What functions are bound to the scanner events that you're generating? Which object is the parent in those lines? – castle-bravo Aug 04 '16 at 17:10
  • Thank you for this interesting pattern, but I don't see how this accomplishes the kind of modularity that I described in my question. The `Scanner` class calls a method of `Handler`, which is a class defined within the scope of `ScanWindow`. Correct me if I'm wrong, but I think that this is more-or-less equivalent to calling a method of `ScanWindow` directly. `Scanner` may not know about `ScanWindow`, but only through sleight of hand. The goal was to trigger an event in one widget from any other widget in the hierarchy, with only the event name in common. – castle-bravo Aug 04 '16 at 18:05