tricostume
tricostume

Reputation: 29

Can I draw TKinter objects on top of an embedded FigureCanvasTkAgg?

shortly said:

Is this possible? Or is there any particular recommendation (i.e. using only one canvas or the other)?

Here some quick code:

import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure

class MyApp(tk.Tk):
  def __init__(self):
    tk.Tk.__init__(self)
    self.canvas = tk.Canvas(self, width=500, height=500, cursor="cross")
    self.canvas.pack(side="top", fill="both", expand=True)

  def draw_image_and_button(self):
    self.figure_obj = Figure()
    a = self.figure_obj.add_axes([0, 0, 1, 1])
    imgplot = a.imshow(some_preloaded_data_array, cmap='gray')
    # create tkagg canvas
    self.canvas_agg = FigureCanvasTkAgg(self.figure_obj, master=self.canvas)
    self.canvas_agg.draw()
    self.canvas_agg.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
    # attempt to draw rectangle
    self.rectangle = self.canvas.create_rectangle(0, 0, 100, 100, fill='red')

if __name__ == "__main__":
    app = MyApp()
    app.draw_image()
    app.mainloop()

I mean I see that the rectangle is being drawn before the image. Maybe its my lack of understanding on how FigureCanvasTkAgg is attached to tk.canvas

Thank you!

Upvotes: 0

Views: 1338

Answers (1)

Francesco
Francesco

Reputation: 513

Ok, this is an app that I recently developed where I have matplotlib widgets and mouse events. You can also have tkinter widgets but I didn't find a way to put them on top of the matplolib canvas. Personally I like matplotlib widgets more than tkinter widgets, so I think it is not too bad.

The only pre-step that you have to take is to modify matplotlib source code because you need pass the canvas to the widget class, while by default the widget takes the figure canvas which will not work when embedding in tk (button would be unresponsive). The modification is actually quite simple, but let's go in order.

  1. Open 'widgets.py' in the matplotlib folder (depending on where you installed it, in my case I have it in "C:\Program Files\Python37\Lib\site-packages\matplotlib").
  2. Go to the class AxesWidget(Widget) (around line 90) and modify the __init__ method with the following code:
def __init__(self, ax, canvas=None):
        self.ax = ax
        if canvas is None:
            self.canvas = ax.figure.canvas
        else:
            self.canvas = canvas
        self.cids = []

As you can see compared to the original code I added a keyword argument canvas=None. In this way the original functionality is mantained, but you can now pass the canvas to the widget.

  1. To have a responsive button on the matplolib canvas that is embedded in tk you now create a widget and you pass the matplolib canvas created with FigureCanvasTkAgg. For example for a Buttonyou would write
from matplotlib.widgets import Button

ok_button = Button(ax_ok_button, 'Ok', canvas=canvas)  # canvas created with FigureCanvasTkAgg

Ok now we have all the functionalities required to have matplolib widgets on the matplolib canvas embedded in tk, plus you can also have mouse and key events, which I guess covers 95% of what you expect from a GUI. Note that if you don't want to modify the original source code you can, of course, create your own class copying AxesWidget class.

You find all the available matplolib widgets here https://matplotlib.org/3.1.1/api/widgets_api.html

Here is a modified version of your app where we put everything together:

import tkinter as tk from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk from matplotlib.figure import Figure from matplotlib.widgets import Button import numpy as np

class MyApp(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        self.canvas = tk.Canvas(self, width=500, height=500, cursor="cross")
        self.canvas.pack(side="top", fill="both", expand=True)

    def draw_image_and_button(self):
        self.figure_obj = Figure()
        self.ax = self.figure_obj.add_subplot()
        self.figure_obj.subplots_adjust(bottom=0.25)
        some_preloaded_data_array = np.zeros((600,600))
        imgplot = self.ax.imshow(some_preloaded_data_array, cmap='gray')
        # create tkagg canvas
        self.canvas_agg = FigureCanvasTkAgg(self.figure_obj, master=self.canvas)
        self.canvas_agg.draw()
        self.canvas_agg.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
        # add matplolib toolbar
        toolbar = NavigationToolbar2Tk(self.canvas_agg, self.canvas)
        toolbar.update()
        self.canvas_agg._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
        # add matplolib widgets
        self.ax_ok_B = self.figure_obj.add_subplot(position=[0.2, 0.2, 0.1, 0.03]) # axes position doesn't really matter here because we have the resize event that adjusts widget position
        self.ok_B = Button(self.ax_ok_B, 'Ok', canvas=self.canvas_agg)
        # add tkinter widgets (outside of the matplolib canvas)
        button = tk.Button(master=self, text="Quit", command=self._quit)
        button.pack(side=tk.BOTTOM)
        # Connect to Events
        self.ok_B.on_clicked(self.ok)
        self.canvas_agg.mpl_connect('button_press_event', self.press)
        self.canvas_agg.mpl_connect('button_release_event', self.release)
        self.canvas_agg.mpl_connect('resize_event', self.resize)
        self.canvas_agg.mpl_connect("key_press_event", self.on_key_press)
        self.protocol("WM_DELETE_WINDOW", self.abort_exec)

    def abort_exec(self):
        print('Closing with \'x\' is disabled. Please use quit button')

    def _quit(self):
        print('Bye bye')
        self.quit()
        self.destroy()

    def ok(self, event):
        print('Bye bye')
        self.quit()
        self.destroy()

    def press(self, event):
        button = event.button
        print('You pressed button {}'.format(button))
        if event.inaxes == self.ax and event.button == 3:
            self.xp = int(event.xdata)
            self.yp = int(event.ydata)
            self.cid = (self.canvas_agg).mpl_connect('motion_notify_event',
                                                            self.draw_line)
            self.pltLine = Line2D([self.xp, self.xp], [self.yp, self.yp])

    def draw_line(self, event):
        if event.inaxes == self.ax and event.button == 3:
            self.yd = int(event.ydata)
            self.xd = int(event.xdata)
            self.pltLine.set_visible(False)
            self.pltLine = Line2D([self.xp, self.xd], [self.yp, self.yd], color='r')
            self.ax.add_line(self.pltLine)
            (self.canvas_agg).draw_idle()

    def release(self, event):
        button = event.button
        (self.canvas_agg).mpl_disconnect(self.cid)
        print('You released button {}'.format(button))

    def on_key_press(self, event):
        print("you pressed {}".format(event.key))

    # Resize event is needed if you want your widget to move together with the plot when you resize the window
    def resize(self, event):
        ax_ok_left, ax_ok_bottom, ax_ok_right, ax_ok_top = self.ax.get_position().get_points().flatten()
        B_h = 0.08 # button width
        B_w = 0.2 # button height
        B_sp = 0.08 # space between plot and button
        self.ax_ok_B.set_position([ax_ok_right-B_w, ax_ok_bottom-B_h-B_sp, B_w, B_h])
        print('Window was resized')


if __name__ == "__main__":
    app = MyApp()
    app.draw_image_and_button()
    app.mainloop()

Ok let's see the functionalities of this app:

  • Press a key on the keyboard → print the pressed key
  • Press a mouse button → print the pressed button (1 = left, 2 = wheel, 3 = right)
  • Release a mouse button → print the released button
  • Press the right button on any point on the plot and draw a line while keeping the mouse button down
  • Press ok or quit to close the application
  • Pressing 'x' to close the window is disabled.
  • Resize the window → Plot and widgets scales accordingly

I also took the liberty to add the classic matplotlib toolbar for other functionalities like zooming.

Note that the image plot is added with add_suplot() method which adds the resizing functionality. In this way when you resize the window the plot scales accordingly.

Most of the things I implemented you also find them on the official tutorial from matplotlib on how to embed in tk (https://matplotlib.org/3.1.3/gallery/user_interfaces/embedding_in_tk_sgskip.html).

Let me know if this answers your question. I wanted to share it because I actually developed something very similar a few days ago.

Upvotes: 0

Related Questions