How to make Tkinter GUI thread safe? - python

I have written a piece of code where I have a simple GUI with an canvas. On this canvas I draw a Matplot. The Matplot is updated every second with data from an SQ Lite DB which I fill with some fake Sensor information (just for testing at the moment).
My Problem was that the redrawing of the canvas causes my window/gui to lag every second. I even tried to update the plot in another thread. But even there I get an lag.
With my newest Code i got most of my things working. Threading helps to prevent my GUI/Window from freezing while the Canvas is updated.
The last thing I miss is to make it Thread safe.
This is the message I get:
RuntimeError: main thread is not in main loop
Here is my newest working code with threading:
from tkinter import *
import random
from random import randint
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import time
import threading
from datetime import datetime
continuePlotting = False
def change_state():
global continuePlotting
if continuePlotting == True:
continuePlotting = False
else:
continuePlotting = True
def data_points():
yList = []
for x in range (0, 20):
yList.append(random.randint(0, 100))
return yList
def app():
# initialise a window and creating the GUI
root = Tk()
root.config(background='white')
root.geometry("1000x700")
lab = Label(root, text="Live Plotting", bg = 'white').pack()
fig = Figure()
ax = fig.add_subplot(111)
ax.set_ylim(0,100)
ax.set_xlim(1,30)
ax.grid()
graph = FigureCanvasTkAgg(fig, master=root)
graph.get_tk_widget().pack(side="top",fill='both',expand=True)
# Updated the Canvas
def plotter():
while continuePlotting:
ax.cla()
ax.grid()
ax.set_ylim(0,100)
ax.set_xlim(1,20)
dpts = data_points()
ax.plot(range(20), dpts, marker='o', color='orange')
graph.draw()
time.sleep(1)
def gui_handler():
change_state()
threading.Thread(target=plotter).start()
b = Button(root, text="Start/Stop", command=gui_handler, bg="red", fg="white")
b.pack()
root.mainloop()
if __name__ == '__main__':
app()
Here the idea without a thread:
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import tkinter as tk
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import sqlite3
from datetime import datetime
from random import randint
class MainApplication(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
root.update_idletasks()
f = Figure(figsize=(5,5), dpi=100)
x=1
ax = f.add_subplot(111)
line = ax.plot(x, np.sin(x))
def animate(i):
# Open Database
conn = sqlite3.connect('Sensor_Data.db')
c = conn.cursor()
# Create some fake Sensor Data
NowIs = datetime.now()
Temperature = randint(0, 100)
Humidity = randint(0, 100)
# Add Data to the Database
c = conn.cursor()
# Insert a row of data
c.execute("insert into Sensor_Stream_1 (Date, Temperature, Humidity) values (?, ?, ?)",
(NowIs, Temperature, Humidity))
# Save (commit) the changes
conn.commit()
# Select Data from the Database
c.execute("SELECT Temperature FROM Sensor_Stream_1 LIMIT 10 OFFSET (SELECT COUNT(*) FROM Sensor_Stream_1)-10")
# Gives a list of all temperature values
x = 1
Temperatures = []
for record in c.fetchall():
Temperatures.append(str(x)+','+str(record[0]))
x+=1
# Setting up the Plot with X and Y Values
xList = []
yList = []
for eachLine in Temperatures:
if len(eachLine) > 1:
x, y = eachLine.split(',')
xList.append(int(x))
yList.append(int(y))
ax.clear()
ax.plot(xList, yList)
ax.set_ylim(0,100)
ax.set_xlim(1,10)
ax.grid(b=None, which='major', axis='both', **kwargs)
label = tk.Label(root,text="Temperature / Humidity").pack(side="top", fill="both", expand=True)
canvas = FigureCanvasTkAgg(f, master=root)
canvas.get_tk_widget().pack(side="left", fill="both", expand=True)
root.ani = animation.FuncAnimation(f, animate, interval=1000)
if __name__ == "__main__":
root = tk.Tk()
MainApplication(root).pack(side="top", fill="both", expand=True)
root.mainloop()
Here is my DB Schema:
CREATE TABLE `Sensor_Stream_1` (
`Date` TEXT,
`Temperature` INTEGER,
`Humidity` INTEGER
);

Your GUI process must not run in any thread. only the dataacquisition must be threaded.
When needed , the data acquired are transfered to the gui process (or the gui process notified from new data available) . I may need to use a mutex to share data resource between acquisition thread and gui (when copying)
the mainloop will look like :
running = True
while running:
root.update()
if data_available:
copydata_to_gui()
root.quit()

I had the same problem with tkinter and using pypubsub events was my solution.
As comments above suggested, you have to run your calculation in another thread, then send it to the gui thread.
import time
import tkinter as tk
import threading
from pubsub import pub
lock = threading.Lock()
class MainApplication(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
self.label = tk.Label(root, text="Temperature / Humidity")
self.label.pack(side="top", fill="both", expand=True)
def listener(self, plot_data):
with lock:
"""do your plot drawing things here"""
self.label.configure(text=plot_data)
class WorkerThread(threading.Thread):
def __init__(self):
super(WorkerThread, self).__init__()
self.daemon = True # do not keep thread after app exit
self._stop = False
def run(self):
"""calculate your plot data here"""
for i in range(100):
if self._stop:
break
time.sleep(1)
pub.sendMessage('listener', text=str(i))
if __name__ == "__main__":
root = tk.Tk()
root.wm_geometry("320x240+100+100")
main = MainApplication(root)
main.pack(side="top", fill="both", expand=True)
pub.subscribe(main.listener, 'listener')
wt = WorkerThread()
wt.start()
root.mainloop()

This function is called every second, and it is outside the normal refresh.
def start(self,parent):
self.close=False
self.Refresh(parent)
def Refresh(self,parent):
'''your code'''
if(self.close == False):
frame.after( UpdateDelay*1000, self.Refresh, parent)
The function is called alone, and everything that happens inside it does not block the normal operation of the interface.

Related

How can i avoid making two tkinter windows?

I have made a GUI in which on user values a line is drawn, and when you click on the line another window opens where you can select something. And when you press ok it should print on the terminal. I think because i am creating a new tkinter window when i click on the line, i am unable to retrieve the user selection.How can i solve this problem? Your help is greatly appreciated.
import matplotlib
matplotlib.use('TkAgg')
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from matplotlib import style
import tkinter as tk
from tkinter import ttk
from tkinter import *
import random
import numpy as np
LARGE_FONT = ('Verdana',12)
style.use('ggplot')
from matplotlib import pyplot as plt
class PageOne(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
label = tk.Label(self,text='Experiment', font = LARGE_FONT)
label.pack(pady=10,padx=10)
self.adding_widgets()
button2= ttk.Button(self,text='Validate', command=self.draw)
button2.pack()
button3= ttk.Button(self,text='Erase', command=self.dlet)
button3.pack()
self.lines_pts=[]
self.f1= Figure(figsize=(5,5),dpi=100)
self.b=self.f1.add_subplot(1,1,1)
self.b.set_xlim([0,10])
self.b.set_ylim([0,10])
self.canvas = FigureCanvasTkAgg(self.f1, self)
self.canvas.draw()
self.canvas.get_tk_widget().pack(side=tk.TOP,fill=tk.BOTH,expand=True)
self.f1.canvas.mpl_connect('pick_event', self.on_pick)
self.lines =[]
def adding_widgets(self,*args):
labfram=ttk.LabelFrame(self,width=100,height=100,text='Enter Member Coordinates',labelanchor=N)
labfram.pack()
l1=ttk.Label(labfram,text='x0')
l1.pack(side='left')
self.x0=Entry(labfram,width=10,)
self.x0.pack(side='left')
l2=ttk.Label(labfram,text='y0')
l2.pack(side='left')
self.y0=Entry(labfram,width=10)
self.y0.pack(side='left')
l3=ttk.Label(labfram,text='x1')
l3.pack(side='left')
self.x1=Entry(labfram,width=10)
self.x1.pack(side='left')
l4=ttk.Label(labfram,text='y1')
l4.pack(side='left')
self.y1=Entry(labfram,width=10)
self.y1.pack(side='left')
def draw(self):
p0 = float(self.x0.get()), float(self.y0.get())
p1 = float(self.x1.get()), float(self.y1.get())
self.lines_pts.append((p0,p1))
for p0,p1 in self.lines_pts:
x0, y0, x1, y1 = *p0, *p1
X = x0,x1
Y = y0,y1
ax = self.b.plot(X,Y, 'r', linewidth=4,picker=5 )
self.lines.append(ax)
self.canvas.draw()
def dlet(self):
self.b.lines.remove(self.b.lines[-1])
self.canvas.draw()
def on_pick(self,event):
w=Tk()
w.title('Channel Select')
w.geometry('250x50')
n = StringVar()
ch = ttk.Combobox(w, width = 15 , textvariable = n)
ch['values'] = ('CH1','CH2','CH3','CH4','CH5','CH6','CH7','CH8','CH9','CH10')
ch.grid(column=1,row=0)
ch_label = ttk.Label(w,text='Select Your Channel')
ch_label.grid(column=0,row=0)
ch_button = ttk.Button(w,text='OK',command=lambda: print ('value is:'+ n.get()))
ch_button.grid(column=1,row=1)
for line in self.lines:
line = event.artist
xdata, ydata = line.get_data()
ind = event.ind
print('on pick line:', np.array([xdata[ind], ydata[ind]]).T)
app = PageOne()
app.mainloop()
It's here do you really want to create a new window?
def on_pick(self,event):
w=Tk()
w.title('Channel Select')
w.geometry('250x50')
Take note that when you do this w=Tk() you are creating an instance of an object a NEW OBJECT which is Tk() so that's why it creates a new window. What is your goal are you really trying to pop up a window whenever you click cause if not you I think you can remove this.
----UPDATE---
So the reason why your window keeps popping out is because on your function you create an instance of an object with it. Then I tried putting the w=Tk() away from the function but still it would create or show that object cause it is within the mainloop.
Alternative solution is you can check if once that window exists or its state is normal then you can just do a focus.
Here is the code that I've added only on your on_pick function. I also added on your __init__ method a self.w = None to just set the w variable to None initially.
OVERALL this are only the changes made
def on_pick(self,event):
try:
if self.w.state() == "normal":
self.w.focus()
except BaseException as on_pick_error:
self.w=Toplevel()
self.w.title('Channel Select')
self.w.geometry('250x50')
n = StringVar()
ch = ttk.Combobox(self.w, width = 15 , textvariable = n)
ch['values'] = ('CH1','CH2','CH3','CH4','CH5','CH6','CH7','CH8','CH9','CH10')
ch.grid(column=1,row=0)
ch_label = ttk.Label(self.w,text='Select Your Channel')
ch_label.grid(column=0,row=0)
ch_button = ttk.Button(self.w,text='OK',command=lambda: print ('value is:'+ n.get()))
ch_button.grid(column=1,row=1)
for line in self.lines:
line = event.artist
xdata, ydata = line.get_data()
ind = event.ind
print('on pick line:', np.array([xdata[ind], ydata[ind]]).T)
You might wonder what this does, it just checks if the state of your self.w which happens to be your window that pops out, it just checks if its state is equal to "normal" meaning that it is active, then when it is it will just do a .focus() and focus on that current window. Else it would create a new window which is this self.w=Toplevel() and so on.
try:
if self.w.state() == "normal":
self.w.focus()
except BaseException as on_pick_error:
self.w=Toplevel()
self.w.title('Channel Select')
self.w.geometry('250x50')
Why is it Toplevel instead of Tk?
I would suggest to have it Toplevel however it is up to you to decide since Toplevel and Tk I think might just have the same properties but do note they are not the same so it's up to you.

Putting an continuously updating animated plot with other components inside a tkinter gui

I want to show a plot within one of the pages in my GUI. The data plotted, theData will constantly change. Whenever theData is changed, the value of valueChangedtheData is written to 1. The mapping of time and data is here:
xsize=100
xdata,ydata = [],[]
def data_gen():
t = data_gen.t
global theData
global valueChangedtheData
while True:
if (valueChangedtheData == 1):
valueChangedtheData = 0;
t+=0.1
val=float(theData);
if val>1000:
continue
yield t, val
else: pass
def animate(data):
t, val = data
if t>-1:
xdata.append(t)
ydata.append(val)
if t>xsize: # Scroll to the left.
a.set_xlim(t-xsize, t)
line.set_data(xdata, ydata)
return line,
def on_close_figure(event):
sys.exit(0)
data_gen.t = -1
f = plt.figure()
f.canvas.mpl_connect('close_event', on_close_figure)
#f = Figure(figsize=(5,5), dpi=100)
a = f.add_subplot(111)
line, = a.plot([], [], lw=2)
a.set_ylim(0, 250)
a.set_xlim(0, xsize)
a.grid()
I've defined my the container of my GUI as such:
class Gui(Tk):
def __init__(self, *args, **kwargs):
Tk.__init__(self, *args, **kwargs)
container = Frame(self)
container.pack(side="top", fill = "both", expand = TRUE)
container.grid_rowconfigure(0, weight = 1)
global theData;
self.MyReading = StringVar()
self.frames={}
for F in (StartPage, PageOne):
frame = F(container, self)
self.frames[F] = frame
frame.grid(row = 0, column = 0, sticky = "nsew")
self.show_frame(StartPage)
def show_frame(self, cont):
frame = self.frames[cont]
frame.tkraise()
and the page showing the plot:
class StartPage(Frame): #The Graphical Page
def __init__(self, parent, controller):
Frame.__init__(self,parent)
label = Label(self, text="StartPage")
label.pack()
label1 = Label(self, textvariable = controller.theData)
label1.pack
canvas = FigureCanvasTkAgg(f, self)
canvas.draw() #changed from show to draw
canvas.get_tk_widget().pack(side=BOTTOM, fill=BOTH, expand=True)
toolbar = NavigationToolbar2Tk(canvas, self)
toolbar.update()
canvas._tkcanvas.pack(side=TOP, fill=BOTH, expand=True)
And in order to start the animation and Gui:
root = Gui()
update_reading()
ani = animation.FuncAnimation(f, animate, data_gen, blit = False, interval=100, repeat = False)
root.mainloop()
With update_reading() updating the label:
def update_reading():
global theData
global valueChangedtheData
theData = randint(1,20) #This is just an example of the changing value
print(theData)
valueChangedtheData = 1;
root.MyReading.set(str(theData));
root.after(100,update_reading)
However, after adding the canvas on the page, all of the labels that rely on the variable classes would refuse to shop-up, including the value for theData but the plot is graphing. Also, labels that show images would also refuse to show-up. After commenting the canvas, data mapping and animation, the label would appear back. Am I missing an important initializing code? Also, during plotting, there is a considerable "sluggishness" happening in the gui window. Could this be alleviated through a better code writing? Thanks
Imports:
from tkinter import *
import matplotlib
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
import matplotlib.animation as animation
import matplotlib.pyplot as plt
import numpy as np
from matplotlib import style
from random import randint
EDIT: Added missed and necessary code
EDIT2: Included all the imports

Multiple GUIs communicate back and forth

I have two GUIs and I want these guis to be able communicate together. I used Matlab in the past and in Matlab I was using addlistener and basically communicate between multiple guis. I am new to python and I want when I am clicking on the show button on my second gui it update the axes on my first gui. Basically, plot the image on the other gui based on the path I choose on another.
Here is the image for better understanding
Here is the code:
from tkinter import *
from PIL import Image, ImageTk
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import
FigureCanvasTkAgg
import PySimpleGUI as sg
import tkinter.filedialog as fdialog
from natsort import natsorted
import os
import cv2
class MyCanvas(Canvas):
def __init__(self, parent=None, img=None, *parms, **kparms):
Canvas.__init__(self, parent, *parms, **kparms)
self._width = 20;
self._height = 10;
self._starting_drag_position = ()
self.config(width=self._width, height=self._height, bg='white')
self._draw_some_example_objects()
self.pack(fill=BOTH, expand=YES)
def _draw_some_example_objects(self):
self.fig = Figure()
gs = self.fig.add_gridspec(5, 2)
self.axis= self.fig.add_subplot(gs[0:4, 0])
self.canvas = FigureCanvasTkAgg(self.fig, master=self)
self.canvas.get_tk_widget().pack(side="top", fill='both', expand=True)
colors = dict(outline="black")
class MyGUI(Tk):
def __init__(self, *args, **kwargs):
Tk.__init__(self, *args, **kwargs)
self.title("Drag canvas with mouse")
self.geometry("700x700")
"""For some reason menu should be added here"""
self.menuBar = Menu(master=self)
self.filemenu = Menu(self.menuBar, tearoff=0)
self.filemenu.add_command(label="listview!", command=self.list)
self.menuBar.add_cascade(label="File", menu=self.filemenu)
self.config(menu=self.menuBar)
self._addWidgets()
def _addWidgets(self):
my_canvas = MyCanvas(self)
def list(self):
listView(self)
def listView(self):
sg.ChangeLookAndFeel('GreenTan')
dir = fdialog.askdirectory()
filesList = os.listdir(dir)
filesList = natsorted(filesList)
layout = [
[sg.Listbox(values=(filesList), size=(60, 30), key='_IN_')],
[sg.Button('Show')]
]
window = sg.Window('Everything bagel', default_element_size=(40, 1), grab_anywhere=False).Layout(layout)
while True:
event, values = window.Read()
if event is None or event == 'Exit':
break
print(dir + values.get('_IN_')[0])
if __name__ == '__main__':
MyGUI().mainloop()`
Take a look at this demo program.
Drop your code into the section where it says:
#------------------------ PASTE YOUR MATPLOTLIB CODE HERE ----------------------
Make sure your drawing is in the variable 'fig'. It will create a window with GUI options and your Matplotlib plot embedded in it.

Python TKinter GUI updates from multiple threads -- is it thread-safe? Some example code

I've been doing some research on this one issue I've been having with TKinter, which is updating the GUI based on data from other threads. As many have suggested online, I resorted to using the queue polling strategy with the self.root.after() method.
This works pretty well, but I have a problem in which I need to update matplotlib plots embedded in a TKinter GUI, and doing so "deactivates" / draws focus away from / perhaps blocks the other aspects of the GUI. Specifically, if I'm typing something in a TKinter entry and the matplotlib figure updates, the cursor is no longer in the entry and my typing process has been interrupted.
To solve this issue, I figured I'd try to update TKinter in a separate thread altogether. Now, I have seen online many people who claim that TKinter is not thread-safe, but I have also seen others who say it is thread-safe. I went ahead and made an example program, which seems to run pretty well: the cursor can remain in the entry even when the plot updates. But what I'd like to know is, is this program safe? Or is it susceptible to failures, random or predictable? What can I do to have a thread-safe GUI?
Here's my example code:
import Tkinter as tk
import threading
import Queue
import datetime
import math
import matplotlib
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
from matplotlib.figure import Figure
import matplotlib.gridspec as gridspec
gui_queue = Queue.Queue()
GUI_THEME_COLOR = 'lightblue'
##################################################
class MainGUI:
def __init__(self, root):
self.root = root
self.root.title('TKinter GUI with Threaded Updates')
self.root.minsize(width=800, height=300)
self.root.config(background=GUI_THEME_COLOR)
self.big_frame = tk.Frame(master=root)
self.big_frame.pack(fill=tk.BOTH, expand=tk.YES, padx=10, pady=10)
self.button1 = tk.Button(master=self.big_frame, text='Button 1', command=self.button1_command)
self.button1.grid(row=0, column=0)
self.entry1 = tk.Entry(master=self.big_frame)
self.entry1.bind('<Return>', self.entry1_event)
self.entry1.grid(row=1, column=0)
def entry1_event(self, event):
self.button1.config(text=str(self.entry1.get()))
self.entry1.delete(0, tk.END)
def button1_command(self):
print 'Button 1 clicked'
class GUIManipulator(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
self.app = None
self.start_time = datetime.datetime.utcnow()
self.sec_checker = 1
self.num_loops = 0
self.x_list = []
self.y_list = []
def run(self):
print 'Starting GUIManipulator thread...'
while gui_queue.empty(): # Wait here until we receive the MainGUI instance
pass
self.app = gui_queue.get()
while True:
diff_time = (datetime.datetime.utcnow() - self.start_time).total_seconds()
floor_diff_time = math.floor(diff_time)
if floor_diff_time >= self.sec_checker: # Configure button1 text every second
self.app.button1.config(text=str(floor_diff_time))
self.sec_checker += 1
self.plot_figure()
def plot_figure(self):
self.num_loops += 1
self.x_list.append(self.num_loops)
self.y_list.append(math.sin(self.num_loops/5.0))
if self.num_loops == 1:
self.fig1 = Figure(figsize=(12, 6), facecolor=GUI_THEME_COLOR)
self.fig1.suptitle('Figure 1',
fontsize=14,
fontweight='bold')
self.gs = gridspec.GridSpec(2, 2)
self.plot1 = self.fig1.add_subplot(self.gs[:, 0])
self.plot1.set_title('Plot 1')
self.plot1.set_xlabel('x')
self.plot1.set_ylabel('sin(x)', labelpad=-10)
self.plot1.grid(True)
if self.num_loops > 1:
self.plot1.cla()
self.plot1.plot(self.x_list, self.y_list)
if self.num_loops == 1:
self.canvas = FigureCanvasTkAgg(self.fig1, master=self.app.big_frame)
self.canvas.draw()
self.canvas.get_tk_widget().grid(row=2, column=0)
else:
self.canvas.draw()
def main():
root = tk.Tk()
app = MainGUI(root)
gui_queue.put(app)
gui_manipulator_thread = GUIManipulator()
gui_manipulator_thread.daemon = True
gui_manipulator_thread.start()
root.mainloop()
if __name__ == '__main__':
main()

Matplotlib event and replotting

I want to be able to create a plot, press one button or another depending on what the plot shows, and then plot the following object. However, I am having some trouble wih it: it seems I can't make it "wait" untill a button is pressed. Also, I am wondering if it would be possible to pass some parameters to the press_event, like a path to save something.
Here is the scheme of the program in case it helps. Thanks a lot in advance!
# event definition
def ontype(event):
if event.key == '1':
do stuff 1
plt.savefig(...)
plt.clf()
elif event.key == '2':
do stuff 2
plt.savefig(...)
plt.clf()
elif event.key == '3':
do stuff 3
plt.savefig(...)
plt.clf()
# main program
...stuff
create figure
plt.show()
plt.gcf().canvas.mpl_connect('key_press_event',ontype)
You must call plt.gcf().canvas.mpl_connect('key_press_event',ontype) before plt.show(). In non-interactive mode, the execution waits at plt.show() until the plot-window is closed.
import pylab as plt
# event definition
def ontype(event):
if event.key == '1':
print "1"
elif event.key == '2':
print "2"
elif event.key == '3':
print "3"
# main program
plt.plot([1,6,3,8,7])
plt.gcf().canvas.mpl_connect('key_press_event',ontype)
plt.show()
Alternatively, replace in your sample plt.show() to plt.ion(), which enables interactive mode. But it depends on your specific needs which solution you prefer.
Edit
New example using Tkinter
import random
import matplotlib
matplotlib.use('TkAgg')
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
from matplotlib.figure import Figure
try:
import Tkinter as Tk
except ImportError:
import tkinter as Tk
import tkMessageBox
class PlotClassifier(Tk.Tk):
def __init__(self, plot_generator, arguments, classes, classification_callback, *args, **kwargs):
Tk.Tk.__init__(self, *args, **kwargs)
self.title("Plot classifier, working on %i plots" % len(arguments))
#self.label = Tk.Label(text="Plot classifier, working on %i plots" % len(arguments))
#self.label.pack(padx=10, pady=10)
self._plot_generator = plot_generator
self._arguments = arguments
self._classes = [str(x) for x in classes]
self._classification_callback = classification_callback
self._setup_gui()
def _setup_gui(self):
#self.columnconfigure(0, minsize=100, weight=2)
#self.columnconfigure(1, minsize=500, weight=8)
f = Figure()
self._ax = f.add_subplot(111)
buttons_frame = Tk.Frame(self)
buttons_frame.pack(side=Tk.TOP, fill=Tk.BOTH, expand=True)
buttons_class = []
for i, cls in enumerate(self._classes):
buttons_class.append(Tk.Button(master=buttons_frame, text=cls,
command=lambda x=i: self.button_classification_callback(self._current_args, x)))
buttons_class[-1].pack(side=Tk.LEFT)
button_quit = Tk.Button(master=buttons_frame, text='Quit', command=self.destroy)
button_quit.pack(side=Tk.RIGHT) #.grid(row=0,column=0)
self._canvas = FigureCanvasTkAgg(f, master=self)
self._canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) #.grid(row=0, column=1, rowspan=3) #
self._canvas.show()
toolbar = NavigationToolbar2TkAgg( self._canvas, self )
toolbar.pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) #.grid(row=3, column=1) #
toolbar.update()
def button_classification_callback(self, args, class_idx):
self._classification_callback(args, self._classes[class_idx])
self.classify_next_plot()
def classify_next_plot(self):
try:
self._current_args = self._arguments.pop(0)
self._ax.cla()
self._plot_generator(self._ax, *self._current_args)
self._canvas.draw()
except IndexError:
tkMessageBox.showinfo("Complete!", "All plots were classified")
self.destroy()
def create_plot(ax, factor):
ax.plot([(i*factor) % 11 for i in range(100)])
def announce_classification(arguments, class_):
print arguments, class_
if __name__ == "__main__":
classes = ["Class %i"%i for i in range(1, 6)]
arguments_for_plot = [[random.randint(1,10)] for x in range(10)]
root = PlotClassifier(create_plot, arguments_for_plot, classes, classification_callback=announce_classification)
root.after(50, root.classify_next_plot)
root.mainloop()
The class takes as arguments:
* a callback to create each plot
* a list of lists of arguments for each plot to generate (might each be an empty list)
* a list of class-names. For each class, a button is created
* a callback that is called each time a classification has been performed
Any feedback would be appreciated.
*EDIT 2 *
For your comment, a slightly modified version. For every iteration of the loop, a new window is opened
import random
import matplotlib
matplotlib.use('TkAgg')
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
from matplotlib.figure import Figure
try:
import Tkinter as Tk
except ImportError:
import tkinter as Tk
import tkMessageBox
class PlotClassifier(Tk.Tk):
def __init__(self, plot_generator, arguments, classes, *args, **kwargs):
Tk.Tk.__init__(self, *args, **kwargs)
self.title("Plot classifier")
self._plot_generator = plot_generator
self._arguments = arguments
self._classes = [str(x) for x in classes]
self.class_ = None
self._setup_gui()
def _setup_gui(self):
#self.columnconfigure(0, minsize=100, weight=2)
#self.columnconfigure(1, minsize=500, weight=8)
f = Figure()
self._ax = f.add_subplot(111)
buttons_frame = Tk.Frame(self)
buttons_frame.pack(side=Tk.TOP, fill=Tk.X, expand=True)
buttons_class = []
for i, cls in enumerate(self._classes):
buttons_class.append(Tk.Button(master=buttons_frame, text=cls,
command=lambda x=i: self.button_classification_callback(x)))
buttons_class[-1].pack(side=Tk.LEFT)
button_quit = Tk.Button(master=buttons_frame, text='Quit', command=self.destroy)
button_quit.pack(side=Tk.RIGHT) #.grid(row=0,column=0)
self._canvas = FigureCanvasTkAgg(f, master=self)
self._canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) #.grid(row=0, column=1, rowspan=3) #
self._canvas.show()
toolbar = NavigationToolbar2TkAgg( self._canvas, self )
toolbar.pack(side=Tk.TOP, fill=Tk.BOTH, expand=1) #.grid(row=3, column=1) #
toolbar.update()
def button_classification_callback(self, class_idx):
self.class_ = self._classes[class_idx]
self.destroy()
def classify_plot(self):
self._ax.cla()
self._plot_generator(self._ax, *self._arguments)
self._canvas.draw()
self.mainloop()
return self.class_
def create_plot(ax, factor):
ax.plot([(i*factor) % 11 for i in range(100)])
if __name__ == "__main__":
classes = ["Class %i"%i for i in range(1, 6)]
arguments_for_plot = [[random.randint(1,10)] for x in range(10)]
for args in arguments_for_plot:
classifier = PlotClassifier(create_plot, args, classes)
class_ = classifier.classify_plot()
print args, class_
if class_ is None:
break
This helps to fit into your own for-loop, but you still have to give a function to do the plotting after the GUI was created.

Categories