PyQT5 slot with Matplotlib has no effect - python-3.x

I'm trying to implement a slot method that will clear a matplotlib figure that is part of QtWidget. I have tried with both Python 3.6 and 3.5 on windows 7 and 10 and i get same behaviour.
My issue is that i can perfectly clear the figure when calling the wipe method from the main block of code or within the class drawing the figure. But whenever this very same method is called as a slot to a PyQT signal, slot is actually activated but the call to fig.clf() does not clear the figure.
The code below shows the issue :
import sys
from PyQt5.QtCore import *
from PyQt5.QtWidgets import QApplication,QMainWindow, QSizePolicy,QWidget,QVBoxLayout, QPushButton
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
class GraphPopulate(FigureCanvas):
def __init__(self, parent=None):
self.fig = Figure(figsize=(6,4))
self.ax = self.fig.add_axes([0.07, 0.16, 0.95, 0.95]) # fraction of figure size #
FigureCanvas.__init__(self, self.fig)
self.setParent(parent)
x = [2000, 2001, 2002, 2003, 2004]
y = [10, 20, 30, 40, 50]
self.plt = self.ax.plot(x, y)
# self.wipe() # OK This one works as well
# self.plt.close(self.fig) # close the app, does not only clear figure
def wipe(self):
print('wipe start')
# del self.fig # close the app on second call, does not only clear figure !!
# self.plt.close(fig) # close the app, does not only clear figure
rc = self.fig.clf()
print('return code from clf() '+ str(rc))
print('wipe stop')
if __name__ == '__main__':
# create main window
app = QApplication(sys.argv)
MainWindow = QMainWindow()
MainWindow.resize(800, 600)
# create widget to be used by MathPlotlib
WidgetMatplot = QWidget(MainWindow)
WidgetMatplot.setGeometry(QRect(10, 40, 500, 500))
# create Push Button with clicked signal linked to GraphPopulate.wipe() slot
button = QPushButton(MainWindow)
button.setText("Push Me !")
# add widget to vertical box layout
hbox = QVBoxLayout(MainWindow)
hbox.addWidget(WidgetMatplot)
hbox.addWidget(button)
g = GraphPopulate(WidgetMatplot)
button.pyqtConfigure(clicked=g.wipe) # NOT OK !!!!!!! g.wipe is triggered as prints statements show
# on console but self.fig.clf() has no effect
MainWindow.show()
# g.wipe() # OK
sys.exit(app.exec_())
I've put comments on statements that if uncommented are actually clearing the figure or closing the whole app.
Looks like the "matplotlib context" is not the same when called from the signa-slot connect than from other calls.
May be i miss something trivial. If so sorry for that but i couldn't find any direction to investigate by myself...so i rely on you guys to spot it out.
Thanks - Thibault

In your code the figure is successfully cleared. You just don't see it because nobody told the figure that it needs to be redrawn.
If you redraw the figure, you'll see that it is indeed empty.
def wipe(self):
self.fig.clf()
self.fig.canvas.draw_idle()

Related

PySide2 updating a graph

I have been knocking my head against the wall on the following issue for quite some times now and need some fresh pair of eyes to help me out.
In Qt Designer I created a tab with a QComboBox (to select a feature), a QPushButton (to instruct the plotting of the feature) and a QWidget (plot area, called mywidget). The whole code is largely inspired from various codes found on SO.
In main.py I connected the QPushButton to the following function (defined within my QtApp class):
def launchGraph(self):
df1 = ... #data from a data source
self.mywidget.figure = Figure()
self.mywidget.canvas = FigureCanvas(self.mywidget.figure)
self.mywidget.toolbar = NavigationToolbar(self.mywidget.canvas, self)
self.mywidget.graphLayout = QtWidgets.QVBoxLayout()
self.mywidget.graphLayout.addWidget(self.mywidget.canvas)
self.mywidget.graphLayout.addWidget(self.mywidget.toolbar)
self.mywidget.setLayout(self.mywidget.graphLayout)
ax1f1 = self.mywidget.figure.add_subplot(111)
ax1f1.clear()
ax1f1.xaxis.set_major_formatter(mdates.DateFormatter('%b%-y'))
ax1f1.plot(df1['x'], df1['y'], linewidth=1, color='blue')
ax1f1.set(title='My Little Graph')
self.mywidget.canvas.draw()
The issue is that when I launched my window, select a feature and click the button, the correct graph is being shown. If I changed the feature and click the plot button, nothing happens. I did print the feature of the combobox and it prints the correct up-to-date value from the combobox however the graph is not replaced/updated. I also added a test-variable isgraph and used self.mywidget.figure.clear() but no success neither. canvas.repaint() doesn't update the graph neither. It feels like I need to use a test-variable to check whether a graph is there or not and if yes then I need to clen up the content of mywidget. But that seems overcomplicated for this issue (?)
For info I import the following:
from gui import main
from PySide2 import QtWidgets, QtCore, QtGui
from matplotlib.figure import Figure
import matplotlib.dates as mdates
from matplotlib.dates import DateFormatter
from matplotlib.backends.backend_qt5agg import (FigureCanvasQTAgg as FigureCanvas,
NavigationToolbar2QT as NavigationToolbar)
Edit:
Here is the minimal/adapted full code:
from gui import main
from PySide2 import QtWidgets, QtCore, QtGui
from matplotlib.figure import Figure
import matplotlib.dates as mdates
from matplotlib.dates import DateFormatter
from matplotlib.backends.backend_qt5agg import (FigureCanvasQTAgg as
FigureCanvas, NavigationToolbar2QT as NavigationToolbar)
class MyQtApp(main.Ui_MainWindow, QtWidgets.QMainWindow):
def __init__(self):
super(MyQtApp, self).__init__()
self.setupUi(self)
self.graphBtn.clicked.connect(self.launchGraph)
self.show()
def launchGraph(self):
if self.mycb.currrentText() == 'feature1':
df1 = ... #data from a data source
else: (#== feature2)
df1 = ... #some other data
self.mywidget.figure = Figure()
self.mywidget.canvas = FigureCanvas(self.mywidget.figure)
self.mywidget.toolbar =
NavigationToolbar(self.mywidget.canvas, self)
self.mywidget.graphLayout = QtWidgets.QVBoxLayout()
self.mywidget.graphLayout.addWidget(self.mywidget.canvas)
self.mywidget.graphLayout.addWidget(self.mywidget.toolbar)
self.mywidget.setLayout(self.mywidget.graphLayout)
ax = self.mywidget.figure.add_subplot(111)
ax.clear()
ax.plot(df1['x'], df1['y'])
self.mywidget.canvas.draw()
In Qt Designer (file main.ui comnverted into. main.py), I placed:
- one combobox, called mycb and having 2 values: [feature1, feature2]
- one push button, called graphBtn
- a simple and empty QWidget called mywidget
The problem is most likely, that when you run the launchGraph after the initial run, the function creates another ax1f1 underneath the initial one. Therefore the initial one keeps on showing and no errors are displayed.
In this particular case, you want to keep working with the initial ax1f1 instead of re-declaring another one.
Something like this could fix the problem:
def launchGraph(self):
if self.mycb.currrentText() == 'feature1':
df1 = ['some_data'] #data from a data source
else: (#== feature2)
df1 = ['some_other_data'] #some other data
self.mywidget.figure = Figure()
self.mywidget.canvas = FigureCanvas(self.mywidget.figure)
self.mywidget.toolbar = NavigationToolbar(self.mywidget.canvas, self)
self.mywidget.graphLayout = QtWidgets.QVBoxLayout()
self.mywidget.graphLayout.addWidget(self.mywidget.canvas)
self.mywidget.graphLayout.addWidget(self.mywidget.toolbar)
self.mywidget.setLayout(self.mywidget.graphLayout)
try:
self.ax1f1.clear()
except:
self.a1f1 = self.mywidget.figure.add_subplot(111)
self.ax1f1.clear()
self.ax1f1.xaxis.set_major_formatter(mdates.DateFormatter('%b%-y'))
self.ax1f1.plot(df1['x'], df1['y'], linewidth=1, color='blue')
self.ax1f1.set(title='My Little Graph')
self.mywidget.canvas.draw()

Python pyplot: Handling double-click event catches also the first click event

I wrote a code that handles different events for both - mouse single-click and double click.
The problem is that each time that user double-click the mouse it triggers also single-click and after that trigger the double-click event.
I want that double-click will trigger only one event!! the double-click event.
Any suggestions?
Thanks
As linked by #ThomasKühn, the answer is to create a software debounce. There are several ways to go about it, and the solution probably depends on your application (are you using a GUI, what backend, etc.) To be as agnostic as possible, I've implemented my solution using a one-shot thread from the threading module.
import threading
import matplotlib.pyplot as plt
DEBOUNCE_DUR = 0.25
t = None
def on_press(event):
global t
if t is None:
t = threading.Timer(DEBOUNCE_DUR, on_singleclick, [event])
t.start()
if event.dblclick:
t.cancel()
on_dblclick(event)
def on_dblclick(event):
global t
print("You double-clicked", event.button, event.xdata, event.ydata)
t = None
def on_singleclick(event):
global t
print("You single-clicked", event.button, event.xdata, event.ydata)
t = None
fig, ax = plt.subplots()
cid = fig.canvas.mpl_connect('button_press_event', on_press)
plt.show()
As I was looking for a solution for a embedded Matplotlib canvas in Tkinter, I came up for another solution (since the accepted answer doesn't work quite well in that case) here is a codesnipped which will hopefully help others struggling with tkinter and matplotlib:
def _on_click_debounce(self,event):
if self._job is None:
self._job = root.after(self.DEBOUNCE_DUR, lambda: self._on_click(event))
if event.dblclick:
root.after_cancel(self._job)
self._job = None
self._on_dblclick(event)
def _on_dblclick(self,event):
print('dblclick!')
def _on_click(self,event):
print('singleclick!')
self._job = None
The function _on_click_debounce gets handed over to the matplotlib eventhandling (fig.canvas.mpl_connect('button_press_event', _on_click_debounce)) and root is just the global root = tkinter.Tk() of tkinter

How can I receive a corresponding percentage when progressbar is clicked python3 gtk3

I am writing a very simple and lightweight remote control for Kodi in python3 and GTK3. I was already able to add the progress bar that gets updated as playback progresses, but I would like to be able to click on a specific part of the progress bar and seek to corresponding part of the video.
Can someone please help me or at least orient me a little how can I achieve it? For now I am working with percentage, as it is much more simple than working with time.
The only thing I need for now is to get 0.33 as output when I click on 1/3 of the progress bar (obviously on the whole length of the progress bar accordingly)
I guess I have to be missing something very simple, but I cannot find what it is. I found one example of using EventBox, but it only informed about a mouse movement gesture, and was not able to give the specific number.
The easiest solution probably is to switch to the Gtk.Scale, because it already supports everything you need. A simple scale that prints it value on change would look like this (in python 2.7):
class GtkWindow(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title="Dialog Example")
min_val = 0.0
max_val = 100.0
adjustment = Gtk.Adjustment.new(0.0, min_val, max_val, 1.0, 10.0, 10.0)
scale = Gtk.Scale.new(Gtk.Orientation.HORIZONTAL, adjustment)
scale.connect("value-changed", self.on_value_changed)
self.add(scale)
def on_value_changed(self, scale):
print scale.get_value()
win = GtkWindow()
win.connect("delete-event", Gtk.main_quit)
win.show_all()
Gtk.main()
Finally I came up with an answer for python3 and GTK3.
I posted the whole code here: https://github.com/elpraga/kodi-cli
I will post the relevant bits here:
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject
import subprocess
...
self.progressbar = Gtk.ProgressBar(text="Kodi is not playing at the moment")
self.progresseventbox = Gtk.EventBox()
self.progressbar.set_show_text(True)
...
grid.attach(self.youtube_entry, 0, 0, 4, 1)
grid.attach_next_to(self.progresseventbox, self.youtube_entry, Gtk.PositionType.BOTTOM, 4, 1)
self.progresseventbox.add(self.progressbar)
...
self.progresseventbox.connect("button-press-event", self.on_mouse_click)
...
def on_mouse_click(self, widget, event):
width = self.progresseventbox.get_allocated_width()
percentage = event.x / width * 100
percentage = str(percentage)
output = (subprocess.check_output(["kodi-cli", "-g", percentage]))
# Let's get some response from kodi-cli
output = (subprocess.check_output(["kodi-cli", "-g"]))
output = output.decode('ascii')
# http://stackoverflow.com/questions/1798465/python-remove-last-3-characters-of-a-string
self.output_text.set_text(output)
...
Screenshot of the clickable progress bar I used after all

Cache overflowing while showing an image stream using python 3 on a Raspberry Pi 2 Model B running Jessie and up to date

I’m very new to python. I’m working on a ‘proof of concept’ piece of code; using PiCamera on a Raspberry Pi running Jessie.
I’ve based my code on a tutorial code from: https://pythonprogramming.net/tkinter-adding-text-images/
Once you hit the button to show the image, the code starts PiCamera and starts to get capture_continuous, passes it to a stream, applies crosshairs to it.
It works mostly well… but after a bit over two minutes, the disk drive lights up and it starts to slow drastically. Once I get the program to break, everything is fine. I’ve looked at a couple of logs, but I can’t for the life of me find out what cache is overflowing or why. I’ve tried a bunch of different ways and tried to leave those in as comments. I suspected it had something to do with having to clear the image in tkinter, but even that doesn’t seem to work and makes the video flash unevenly.
Any help would be great! I’ve started to explore using opencv instead. Still installing that.
Thanks!
The code:
# Simple enough, just import everything from tkinter.
from tkinter import *
import picamera
import picamera.array
import time
import threading
import io
import numpy as np
from PIL import Image, ImageTk
# Here, we are creating our class, Window, and inheriting from the Frame
# class. Frame is a class from the tkinter module. (see Lib/tkinter/__init__)
class Window(Frame):
# Create an array representing a 1280x720 image of
# a cross through the center of the display. The shape of
# the array must be of the form (height, width, color)
# Define settings upon initialization. Here you can specify
def __init__(self, master=None):
# parameters that you want to send through the Frame class.
Frame.__init__(self, master)
#reference to the master widget, which is the tk window
self.master = master
#with that, we want to then run init_window, which doesn't yet exist
self.init_window()
#Creation of init_window
def init_window(self):
# changing the title of our master widget
self.master.title("GUI")
# allowing the widget to take the full space of the root window
self.pack(fill=BOTH, expand=1)
# creating a menu instance
menu = Menu(self.master)
self.master.config(menu=menu)
# create the file object)
file = Menu(menu)
# adds a command to the menu option, calling it exit, and the
# command it runs on event is client_exit
file.add_command(label="Exit", command=self.client_exit)
#added "file" to our menu
menu.add_cascade(label="File", menu=file)
# create the file object)
edit = Menu(menu)
# adds a command to the menu option, calling it exit, and the
# command it runs on event is client_exit
edit.add_command(label="Show Img", command=self.showImg)
edit.add_command(label="Show Text", command=self.showText)
#added "file" to our menu
menu.add_cascade(label="Edit", menu=edit)
self.trim_running_bool = False
def showImg(self):
self.trim_running_bool = True
trim_thrd_thread = threading.Thread(target=self._cam_thread_def)
trim_thrd_thread.start()
self.update_idletasks()
def _cam_thread_def(self):
img_stream = io.BytesIO()
frame_count = 0
with picamera.PiCamera() as camera:
camera.resolution = (400, 300)
## while True: ### tried it this way too
for xxx in range(0,900):
img_stream = io.BytesIO()
frame_count = frame_count + 1
print(frame_count," ", xxx)
if self.trim_running_bool == False:
print("break")
break
camera.capture(img_stream, 'jpeg', use_video_port=True)
img_stream.seek(0)
img_load = Image.open(img_stream)
for xl_line in range(0,196,4):
img_load.putpixel((xl_line, 149), (xl_line, 0, 0))
xll=xl_line+2
img_load.putpixel((xl_line, 150), (xl_line, xl_line, xl_line))
img_load.putpixel((xl_line, 151), (xl_line, 0, 0))
(xl_line)
for xr_line in range(208,400,4):
clr = 400 - xr_line
img_load.putpixel((xr_line, 149), (clr, 0, 0))
img_load.putpixel((xr_line, 150), (clr, clr, clr))
img_load.putpixel((xr_line, 151), (clr, 0, 0))
(xr_line)
for yt_line in range(0,146,4):
clrt = int(yt_line * 1.7)
img_load.putpixel((199, yt_line), (clrt, 0, 0))
img_load.putpixel((200, yt_line), (clrt, clrt, clrt))
img_load.putpixel((201, yt_line), (clrt, 0, 0))
(yt_line)
for yb_line in range(158,300,4):
clrb = int((300 - yb_line) * 1.7)
img_load.putpixel((199, yb_line), (clrb, 0, 0))
img_load.putpixel((200, yb_line), (clrb, clrb, clrb))
img_load.putpixel((201, yb_line), (clrb, 0, 0))
(yb_line)
img_render = ImageTk.PhotoImage(img_load)
# labels can be text or images
img = Label(self, image=img_render)
img.image = img_render
img.place(x=0, y=0)
self.update_idletasks()
img_stream.seek(0)
img_stream.truncate(0)
# tried these:
## img_stream.flush()
## print("flushed ", img_stream)
## print("2nd ",img_stream)
## del img_load
##
##
## rawCapture.truncate(0)
##
## rawCapture.seek(0)
## rawCapture.truncate(0)
## del render
## img.image = None
## foregnd_image = None
(xxx)
pass
def showText(self):
text = Label(self, text="Hey there good lookin!")
text.pack()
def client_exit(self):
self.trim_running_bool = False
exit()
# root window created. Here, that would be the only window, but
# you can later have windows within windows.
root = Tk()
root.geometry("400x300")
#creation of an instance
app = Window(root)
#mainloop
root.mainloop()
Each time through your loop you are creating a new image object and a new label, as well as some other objects. That is a memory leak, since you never destroy the old image or old label.
Generally speaking, you should create exactly one label, then use the_label.configure(image=the_image) every time through the loop. With that, you don't need to create new labels or call place on it.
Even better, since a label automatically updates when the associated image changes, you only need to change the the bits that are in the image object itself and the label should update automatically.
The simplest solution is to move image creation to a function so that all of those objects you are creating are local objects that can get automatically garbage collected when the function returns.
The first step is to create a single label and single image in your main thread:
class Window(Frame):
def __init__(self, master=None):
...
self.image = PhotoImage(width=400, height=300)
self.label = Label(self, image=self.image)
...
Next, create a function that copies new data into the image. Unfortunately, tkinter's implementation of the copy method doesn't support the full power of the underlying image object. A workaround is described here: http://tkinter.unpythonic.net/wiki/PhotoImage#Copy_a_SubImage.
Note: the workaround example is a general purpose workaround that uses more arguments than we need. In the following example we can omit many of the arguments. The documentation for the underlying tk photo object copy method is here: http://tcl.tk/man/tcl8.5/TkCmd/photo.htm#M17
The implementation would look something like this (I'm guessing; I don't have a good way to test it):
def new_image(self):
# all your code to create the new image goes here...
...
img_render = ImageTk.PhotoImage(img_load)
# copy the new image bits to the existing image object
self.tk.call(self.image, 'copy', img_render)
Finally, your loop to update the image would be much simpler:
while True:
self.new_image()
# presumeably there's some sort of sleep here so you're
# not updating the image faster than the camera can
# capture it.
I don't know how fast new_image can run. If it can run in 200ms or less you don't even need threads. Instead, you can use after to run that function periodically.
Note: I haven't worked much with Tkinter photo images in a long time, and I have no good way to test this. Use this as a guide, rather than as a definitive solution.

Incorrect behaviour of print() when executed from within a QTDialog window in Spyder

I am working on a very simple interface to explore/graph csv files. My aim is ultimately to explore, not to build software as I am not a developer, more of a "desperate user" :-)
I am leveraging the code found in this example
These are my first steps both in Python and in GUI, so I tend to put print messages in my calls so that I can more or less track what is happening. And this is where I found a strange behavior if I run the code from within Spyder.
import sys
import os
from PyQt4 import QtGui
import pandas as pd
from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar
import matplotlib.pyplot as plt
# QtGui.QDialog
class Window(QtGui.QDialog):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
# a figure instance to plot on
self.figure = plt.figure()
# this is the Canvas Widget that displays the `figure`
# it takes the `figure` instance as a parameter to __init__
self.canvas = FigureCanvas(self.figure)
# this is the Navigation widget
# it takes the Canvas widget and a parent
self.toolbar = NavigationToolbar(self.canvas, self)
# Just some extra button to mess around
self.button= QtGui.QPushButton('Push Me')
self.button.clicked.connect(self.do_print)
# set the layout
layout = QtGui.QVBoxLayout()
layout.addWidget(self.toolbar)
layout.addWidget(self.canvas)
layout.addWidget(self.button)
self.setLayout(layout)
def do_print(self):
print('Hello World!!')
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
main = Window()
main.show()
sys.exit(app.exec_())
The strange behavior is that if I push the button once, nothing happens on the Ipython console. By the second time I push, then two "Hello World!" printouts appear.
If, on the other hand, I just launch my script from within a Windows Shell:
python my_simple_test.py
Then everything works as expected.
What am I then doing wrong from within Spyder?
Thanks,
Michele
IPython buffers stdout a bit differently from a terminal. When something is printed, it looks at how long it has been since it last flushed the buffer, and if it's longer than some threshold, it flushes it again. So the second time you click the button, it flushes stdout, and you see both outputs.
You can force it to flush immediately like this:
print('Hello World!!')
sys.stdout.flush()

Resources