What I want to do
I am trying to make an interactive plot for a Jupyter Notebook. The functions are all written in different files, but their intended use is in interactive notebook sessions. I have a Button widget on a matplotlib figure, which, when clicked, I want to open a file dialog where a user can enter a filename to save the figure to. I am on Mac OSX (Mojave 10.14.6) and Tkinter is giving me major problems (complete system crashes), so I am trying to implement this with PyQt5.
The code
-----------
plotting.py
-----------
from . import file_dialog as fdo
import matplotlib.pyplot as plt
import matplotlib.widgets as wdgts
def plot_stack(stack):
fig, ax = plt.subplots(figsize=(8, 6))
plt.subplots_adjust(bottom=0.25, left=-0.1)
... # plotting happens here
# button for saving
def dosaveframe(event):
fname = fdo.save()
fig.savefig(fname) # to be changed to something more appropriate
savea = plt.axes([0.65, 0.8, 0.15, 0.05], facecolor=axcolor)
saveb = Button(savea, "save frame", hovercolor="yellow")
saveb.on_clicked(dosaveframe)
savea._button = saveb # for persistence
plt.show()
--------------
file_dialog.py
--------------
import sys
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import (QWidget, QFileDialog)
class SaveFileDialog(QWidget):
def __init__(self, text="Save file", types="All Files (*)"):
super().__init__()
self.title = text
self.setWindowTitle(self.title)
self.types = types
self.filename = self.saveFileDialog()
self.show()
def saveFileDialog(self):
options = QFileDialog.Options()
options |= QFileDialog.DontUseNativeDialog
filename, _ = (
QFileDialog.getSaveFileName(self, "Enter filename",
self.types, options=options))
return filename
def save(directory='./', filters="All files (*)"):
"""Open a save file dialog"""
app = QApplication([directory])
ex = SaveFileDialog(types=filters)
return ex.filename
sys.exit(app.exec_())
What is not working
The save dialog opens and it responds to the mouse, but not to the keyboard. The keyboard stays connected to the notebook no matter if I select the little window, so when I press "s" it saves the notebook. As such, the user can not enter a file path. How can I make this work? I have Anaconda, PyQt 5.9.2, matplotlib 3.1.1, jupyter 1.0.0.
I found a very crappy, non-clean solution but it seems to work. For some reason, opening a QFileDialog directly does not allow me to activate it. It opens up behind the active window from where it was called (terminal window or browser in Jupyter Notebook) and does not respond to the keyboard. So the save function in the following block does NOT work as expected on Mac:
from PyQt5.QtWidgets import QApplication, QFileDialog
def save(directory='./', filters="All files (*)"):
app = QApplication([directory])
path, _ = QFileDialog.getSaveFileName(caption="Save to file",
filter=filters,
options=options)
return path
What does work is if the file dialog is opened from a widget. So working with a dummy widget that never shows up on the screen does work for me, at least from the command line:
from PyQt5.QtWidgets import (QApplication, QFileDialog, QWidget)
class DummySaveFileDialogWidget(QWidget):
def __init__(self, title="Save file", filters="All Files (*)"):
super().__init__()
self.title = title
self.filters = filters
self.fname = self.savefiledialog()
def savefiledialog(self):
filename, _ = QFileDialog.getSaveFileName(caption=self.title,
filter=self.filters,
options=options)
return filename
def save(directory='./', filters="All files (*)"):
app = QApplication([directory])
form = DummySaveFileDialogWidget()
return form.fname
If anyone finds a more elegant solution that works let me know
EDIT: this works when it is called from the command line, but still not from a Jupyter Notebook. Also tried this, no success. The file dialog stays behind the browser window and does not respond to the keyboard.
Related
I want to make a custom client for an online flash game
I am on Linux Manjaro KDE
I installed PPAPI using sudo pamac install pepper-flash
the result of this code
from PyQt6.QtCore import QUrl
from PyQt6.QtWidgets import *
from PyQt6.QtGui import *
from PyQt6.QtWebEngineWidgets import *
from PyQt6.QtWebEngineCore import QWebEngineSettings
import sys
sys.argv += ['--webEngineArgs',
"--register-pepper-plugins=/usr/lib/PepperFlash/libpepflashplayer.so;application/x-shockwave-flash",
"--ppapi-flash-path=/usr/lib/PepperFlash/libpepflashplayer.so", '--ppapi-flash-version=32.21.0.153']
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow,self).__init__(*args, **kwargs)
self.browser = QWebEngineView()
self.setCentralWidget(self.browser)
self.browser.settings().setAttribute(QWebEngineSettings.WebAttribute.PluginsEnabled, True)
self.browser.settings().setAttribute(QWebEngineSettings.WebAttribute.JavascriptEnabled, True)
self.browser.setUrl(QUrl("https://webbrowsertools.com/test-flash-player/"))
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
is "This plugin is not supported"
What am I missing ???
once, I managed to run flash on PyQt5 QWebEngineView
but now the web page on PyQt5 is not even rendering. it is blank. I don't know why, it is the same code that worked before.
EDIT:
I managed to get PyQt5 QWebEngineView working again.
see https://stackoverflow.com/a/74325318/10701585
I have a QMainWindow which contains a QPlainTextEdit and a button with clicked even connected. When user finishes text input and press the button, I just want to execute user input for example "1+1". I should get "2", but it is "1+1". Very appreciated for your reply, thanks!
import sys
from PyQt5.QtWidgets import QMainWindow, QPushButton, \
QApplication, QVBoxLayout, QPlainTextEdit, QWidget
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setFixedSize(600, 600)
l_layout = QVBoxLayout()
self.edit = QPlainTextEdit()
self.edit.setFixedSize(400, 300)
self.edit1 = QPlainTextEdit()
self.edit1.setFixedSize(100, 100)
self.btn = QPushButton('Test')
self.btn.clicked.connect(self.press)
l_layout.addWidget(self.edit)
l_layout.addWidget(self.edit1)
l_layout.addWidget(self.btn)
dummy_widget = QWidget()
dummy_widget.setLayout(l_layout)
self.setCentralWidget(dummy_widget)
def press(self):
text = self.edit.toPlainText()
try:
code = """print(text)"""
exec(code)
except Exception as e:
print('not ok!')
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
You should evaluate the input text and then print it, like so:
def press(self):
text = self.edit.toPlainText()
try:
print(eval(text))
except Exception as e:
print('not ok!')
Pay attention, because eval() use can lead to security issues (people executing python code on your app). Make sure your input is sanitized.
shortly said:
I am creating a quick TKinter API and I firstly generate a tk.Canvas
I am embedding a FigureCanvasTkAgg canvas with master = tk.Canvas above
With this I am able to show an image via Matplotlib
Now I want to draw TKinter objects ON TOP of the FigureCanvasTkAgg canvas (e.g. rectangles or buttons)
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!
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.
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").
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.
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.
I have constructed a GUI in QtChooser and the code for it is written in PyQt5. I have a total of 7 tabs in the GUI, each of which are defined in the QMainWindow class of my code. These definitions contain the codes for each TextEdit, LineEdit, PushButtons, RadioButtons, etc.
However, in one the the tabs, I want to embed an external terminal which will open when a particular PushButton is clicked within that tab. I was able to open the Urxvt terminal when the RadioButton is toggled. The issue I'm facing now is to open the terminal specifically in the area of the TextEdit. This is how the original GUI (built in the QtDesigner looks like. I need the terminal to open in the TextEdit below the Output label. But, this is how the terminal opens in the GUI when the code is run
This is a part of the updated code:
from PyQt5 import QtCore, QtGui, QtWidgets, uic
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton, QMessageBox, QAction
from PyQt5.QtCore import QDate, QTime, QDateTime, Qt
import sys
import platform
import os
import subprocess
import time
import re
import textwrap
class EmbTerminal(QtWidgets.QWidget):
def __init__(self, parent=None):
super(EmbTerminal, self).__init__()
self.process = QtCore.QProcess(self)
self.terminal = QtWidgets.QWidget(self)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.terminal)
# Works also with urxvt:
self.process.start('urxvt',['-embed', str(int(self.winId())), '-bg', '#000000', '-fg', '#ffffff'])
self.setFixedSize(539, 308)
class Ui_Dialog(QtWidgets.QMainWindow):
def __init__(self):
super(Ui_Dialog, self).__init__()
#Load GUI from QT5 Designer
uic.loadUi("S1_mainwindow.ui", self)
def openTerminalCheckBox (self):
if self.openTerminalRMachineRadioButton.isChecked():
status = True
self.commandLineRemoteCommandRMachineLineEdit.setDisabled(True)
self.commandlineRemoteCommandRMachineLabel.setDisabled(True)
self.executeRemoteCommandRMachinePushButton.setDisabled(True)
self.remoteMachineOutputLabel.setText("Terminal")
self.outputRMachineTextEdit = QtWidgets.QTabWidget()
self.gridLayout_6.addWidget(self.outputRMachineTextEdit)
self.outputRMachineTextEdit.addTab(EmbTerminal(), "EmbTerminal")
else:
status = False
app = QtWidgets.QApplication(sys.argv) # Create an instance of QtWidgets.QApplication
window = Ui_Dialog()
main = mainWindow()
main.show() # Create an instance of our class
app.exec_()
I need to open the terminal specifically in the QTextEdit which is already been defined in that tab. Do you guys have any suggestions/input?
I've encountered this behaviour intermittently using the Matplotlib NavigationToolbar2Wx in a Matplotlib Figure canvas in a wx.Frame (or wx.Panel). If the Zoom Icon or Pan Icon are selected the icon disappears however a click in the vacant space still toggles the tool. The Icons for the Home, Backward step or Forward step all behave as expected.
Can anyone offer advice on 1. what causes it and 2. how to fix it?
Thanks to joaquin for posting the initial code slightly modified to include the toolbar.
(http://stackoverflow.com/questions/10737459/embedding-a-matplotlib-figure-inside-a-wxpython-panel)
I'm use python 2.6, wxPython 2.9.2.4 osx-carbon (classic) and Matplotlib 1.1.0
Thanks
The code below shows the problem:
#!/usr/bin/env python
# encoding: UTF-8
"""
wxPython and Matplotlib Canvas with Matplotlib Toolbar.py
"""
from numpy import arange, sin, pi
import matplotlib
matplotlib.use('WXAgg')
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from matplotlib.backends.backend_wx import NavigationToolbar2Wx
from matplotlib.figure import Figure
import wx
class CanvasPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.figure = Figure()
self.axes = self.figure.add_subplot(111)
self.canvas = FigureCanvas(self, -1, self.figure)
# Add Matplotlib Toolbar
# Add the Matplotlib Navigation toolBar here
self.toolbar=NavigationToolbar2Wx(self.canvas)
self.toolbar.AddLabelTool(5,'',wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN, wx.ART_TOOLBAR, (32,32)))
#self.Bind(wx.EVT_TOOL, self.NewTitle(), id=5)
self.toolbar.Realize()
# Add to Box Sizer
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.toolbar, 0, wx.LEFT | wx.TOP | wx.GROW)
self.sizer.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW)
self.SetSizer(self.sizer)
self.Fit()
def draw(self):
t = arange(0.0, 3.0, 0.01)
s = sin(2 * pi * t)
self.axes.plot(t, s)
if __name__ == "__main__":
app = wx.PySimpleApp()
fr = wx.Frame(None, title='test',size=(800,600))
panel = CanvasPanel(fr)
panel.draw()
fr.Show()
app.MainLoop()
I can't comment on the causes of this specific issue,
but I was experiencing some problems with non-Agg backend with wxpython 2.9 too (while the code worked ok in 2.8). Replacing the Toolbar with the Agg version fixed such problems for me; e.g.:
from matplotlib.backends.backend_wx import NavigationToolbar2Wx
==>
from matplotlib.backends.backend_wxagg import NavigationToolbar2WxAgg
and adjusting the code accordingly:
self.toolbar=NavigationToolbar2Wx(self.canvas)
==>
self.toolbar = NavigationToolbar2WxAgg(self.canvas)
hth,
vbr