0

I have a class A that contains a matplotlib canvas. This canvas is clickable, i.e. I click on the plot, this calls a function which saves the event.x = self.x. I have another class B that should receive the attributes from class A any time they change. I saw the observer pattern in this post How to trigger function on value change?, and I think this could be what I need but I could not get it to run (see code below).

Both classes sit in two tkk frames, but I guess that is not relevant to the problem.

Explicitly, in this minimal example, I would like to pass the plotFrame.x, plotFrame.y into WorkFrame() class. Every time I click on the plot, I would like to see new values pop up in the little labels to the right!

import Tkinter as tk
import ttk

from matplotlib.backends.backend_tkagg import (
    FigureCanvasTkAgg, NavigationToolbar2TkAgg)

import matplotlib.pyplot as plt
import numpy as np 

class Frame_examples_program():
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("Amazing GUI 5000")
        self.create_widgets()


    def create_widgets(self):
        self.window['padx'] = 10
        self.window['pady'] = 10

        # - - - - - - - - - - - - - - - - - - - - -
        # Frame
        frame1 = ttk.Frame(self.window, relief=tk.RIDGE)
        frame1.grid(row=0, column=0, sticky=tk.E + tk.W + tk.N + tk.S, padx=0, pady=0)

        frame2 = ttk.Frame(self.window, relief=tk.RIDGE)
        frame2.grid(row=1, column=0, sticky=tk.E + tk.W + tk.N + tk.S, padx=0, pady=0)        
        self.plotFrame = self.PlotFrame(frame1, frame2)

        frame3 = ttk.Frame(self.window, relief=tk.RIDGE)
        frame3.grid(row=2, column=2, sticky=tk.E + tk.W + tk.N + tk.S, padx=0, pady=0)   

        self.workFrame = self.WorkFrame(frame3, self.plotFrame)

    class PlotFrame():
        # The plot
        def __init__(self, parent1, parent2):
            self.parent1 = parent1
            self.parent2 = parent2
            self.observers = []
            self.x = 0
            self.y = 0
            canvas = self.plot()
            self.plot_toolbar(canvas)

        def plot(self):
            # the actual plot
            fig, ax = plt.subplots()
            plt.imshow(np.ones((100,100)),picker=True)
            canvas = FigureCanvasTkAgg(fig, self.parent1)
            canvas.mpl_connect('button_press_event', self.onclick)
            return(canvas)

        def plot_toolbar(self, canvas):
            # the tool bar to the plot
            toolbar = NavigationToolbar2TkAgg(canvas, self.parent2)
            toolbar.update()
            canvas.get_tk_widget().grid(row=1, column=1)
            canvas.draw()

        def onclick(self, event):
            # the devilish thing that does nothing!
            self.x = event.x
            self.y = event.y
            self.position()

        @property
        def position(self):
            return(self.x,self.y)

        @position.setter
        def position(self, x, y):
            self.x = x
            self.y = y
            for callback in self.observers:
                self.observers.append(callback)

        def bind_to(self, callback):
            self.observers.append(callback)

    class WorkFrame():
        def __init__(self, parent, plot_frame):
            self.parent =  parent
            self.x = 0
            self.y = 0
            self.plot_frame = plot_frame
            self.plot_frame.bind_to(self.update_position)
            self.display()


        def update_position(self, x, y):
            self.x = x
            self.y = y

        def display(self):
            l_x = tk.Label(self.parent, text ='Xposition: ' + str(self.x))
            l_y = tk.Label(self.parent, text ='Yposition: ' + str(self.y))
            l_x.grid(row = 0,  column=0)
            l_y.grid(row = 0,  column=1)



# Create the entire GUI program
program = Frame_examples_program()

# Start the GUI event loop
program.window.mainloop()

This raises the exception:

File "test_xy_positions.py", line 65, in onclick
    self.position()
TypeError: 'tuple' object is not callable

Which refers to the fact that I return the tuple (self.x,self.y), position(self).

Sebastiano1991
  • 867
  • 1
  • 10
  • 26

2 Answers2

1

I am working with python3 and have changed the imports to work with your original example, but this works beautifully for me, let me know how it works for you. I tried to comment all the places I changed, but may have missed one:

import Tkinter as tk
import ttk

from matplotlib.backends.backend_tkagg import (
    FigureCanvasTkAgg, NavigationToolbar2TkAgg)

import matplotlib.pyplot as plt
import numpy as np

class Frame_examples_program(object):
    def __init__(self):
        self.window = tk.Tk()
        self.window.title("Amazing GUI 5000")
        self.create_widgets()


    def create_widgets(self):
        self.window['padx'] = 10
        self.window['pady'] = 10

        # - - - - - - - - - - - - - - - - - - - - -
        # Frame
        frame1 = ttk.Frame(self.window, relief=tk.RIDGE)
        frame1.grid(row=0, column=0, sticky=tk.E + tk.W + tk.N + tk.S, padx=0, pady=0)

        frame2 = ttk.Frame(self.window, relief=tk.RIDGE)
        frame2.grid(row=1, column=0, sticky=tk.E + tk.W + tk.N + tk.S, padx=0, pady=0)
        self.plotFrame = self.PlotFrame(frame1, frame2)

        frame3 = ttk.Frame(self.window, relief=tk.RIDGE)
        frame3.grid(row=2, column=2, sticky=tk.E + tk.W + tk.N + tk.S, padx=0, pady=0)

        self.workFrame = self.WorkFrame(frame3, self.plotFrame)

    class PlotFrame(object):
        # The plot
        def __init__(self, parent1, parent2):
            self.parent1 = parent1
            self.parent2 = parent2
            self.observers = []
            self.x = 0
            self.y = 0
            canvas = self.plot()
            self.plot_toolbar(canvas)

        def plot(self):
            # the actual plot
            fig, ax = plt.subplots()
            plt.imshow(np.ones((100, 100)), picker=True)
            canvas = FigureCanvasTkAgg(fig, self.parent1)
            canvas.mpl_connect('button_press_event', self.onclick)
            return canvas

        def plot_toolbar(self, canvas):
            # the tool bar to the plot
            toolbar = NavigationToolbar2TkAgg(canvas, self.parent2)
            toolbar.update()
            canvas.get_tk_widget().grid(row=1, column=1)
            canvas.draw()

        def onclick(self, event):
            # Here I am now setting the position
            self.set_new_position(event.x, event.y)

        def set_new_position(self, x, y):
            self.x = x
            self.y = y
            for callback in self.observers:
                # Here I am now calling the methods that have been captured so far
                # and passing them the arguments of x, y to do with as they please
                callback(self.x, self.y)

        def bind_to(self, callback):
            self.observers.append(callback)

    class WorkFrame():
        def __init__(self, parent, plot_frame):
            self.parent =  parent
            self.x = 0
            self.y = 0
            self.plot_frame = plot_frame
            self.plot_frame.bind_to(self.update_position)
            self.display()

        def update_position(self, x, y):
            self.x = x
            self.y = y
            # Here I have added the requirement to run the display code again
            # after an update
            self.display()

        def display(self):
            l_x = tk.Label(self.parent, text ='Xposition: ' + str(self.x))
            l_y = tk.Label(self.parent, text ='Yposition: ' + str(self.y))
            l_x.grid(row = 0,  column=0)
            l_y.grid(row = 0,  column=1)



# Create the entire GUI program
program = Frame_examples_program()

# Start the GUI event loop
program.window.mainloop()
ThePoetCoder
  • 182
  • 8
  • 1
    As a note: This will return the event coordinates with respect to the matpoltlib figure. Usually one would want the coordinates of the image shown. For this convert figure coordinates using: `self.x, self.y = event.inaxes.transData.inverted().transform((event.x, event.y))` in `onclick` – Sebastiano1991 Jun 04 '18 at 07:20
0

Just a shot from the hip, have you tried removing the @property decorator from above the position method? Then the function would be callable instead of a property, which is not callable.

ThePoetCoder
  • 182
  • 8
  • This then leads to: `@position.setter AttributeError: 'function' object has no attribute 'setter'` it needs the property decorator for the setter thing to work... – Sebastiano1991 Jun 03 '18 at 02:08
  • Could you just remove that too then? – ThePoetCoder Jun 03 '18 at 02:10
  • This leads to `self.position()` lacking the `self.x, self.y`. Adding that into the function call than crashes everything. Even kate :@ – Sebastiano1991 Jun 03 '18 at 02:17
  • Then how about leaving the properties where they are and just removing the parenthesis from the onclick method's call to the position property? Edit: What are you doing with the position property within the onclick method anyway? It returns the values and you seem to do nothing with them. I would think the last line (and perhaps only one needed) of the onclick method should be you setting the position property "self.position = event.x, event.y" – ThePoetCoder Jun 03 '18 at 02:51
  • Hi again, this code snippet follows the linked example. Simply setting the `def onclick: self.x = event.x self.y = event.y` does not crosstalk to the `WorkFrame` class. I, of course, could make the WorkFrame a subclass of PlotFrame and call it on the click events, but I wanted to write good code for once ;). But I may be completely off with my observer pattern idea, please educate me! – Sebastiano1991 Jun 03 '18 at 06:09
  • Went through it & got it working, posted a new answer to keep these comments intact, check out my other answer – ThePoetCoder Jun 03 '18 at 12:43
  • Thanks, perfect solution! I wonder if you could expand why the example that I first got that observer patter scheme from used decorates? Is there any advantage? – Sebastiano1991 Jun 03 '18 at 23:07
  • 1
    I think that the use of properties there was ok but unnecessary, the biggest take-aways would be calling the functions that were appended/binded and then calling the display function from those functions. You had all the pieces and just needed to get them talking right to each other. If you ever get stuck on a tkinter problem, I've found it best to walk step by step, maybe even with print statements at the beginning of each method, to guarantee the flow is going how you expect – ThePoetCoder Jun 03 '18 at 23:39