PyQT5 & QtabWidget - changing values in different tabs - pyqt5

I've build a small PyQt5 application with a QtabWidget as my CentralWidget. Because I wanted to bring some structure into the code I create new tabs by using different classes. This works pretty fine so far.
class Main_window(QtWidgets.QMainWindow):
"""Main Window"""
def __init__(self, parent=None):
"""Initializer"""
super(Main_window, self).__init__(parent)
self.setGeometry(50, 50, 1100, 750)
# Create menu
self.qtMenu3()
self.tabWidget = QtWidgets.QTabWidget()
# sets the tabWidget as the central widget inside the QMainWindow
self.setCentralWidget(self.tabWidget)
self.tab_first = QtWidgets.QWidget()
self.tabWidget.addTab(FirstTab(), 'First')
self.tab_second = QtWidgets.QWidget()
self.tabWidget.addTab(SecondTab(), 'Second')
My SecondTab class looks like this and creates a GroupBox and two QTextEdits
class SecondTab(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle("Groupbox")
layout = QGridLayout()
self.setLayout(layout)
groupbox = QGroupBox("GroupBox Example")
# groupbox.setCheckable(True)
layout.addWidget(groupbox)
# Layout manager QVBox (vertical)
vbox = QVBoxLayout()
groupbox.setLayout(vbox)
# radiobutton = QRadioButton("Radiobutton 1")
# vbox.addWidget(radiobutton)
textEdit_input = QTextEdit()
vbox.addWidget(textEdit_input)
textEdit_output = QTextEdit()
vbox.addWidget(textEdit_output)
The point where I struggle now is that I want to load a txt file and the text should update the empty textEdit_input in my second tab. Because the function should work for multiple tabs I don't want to attach it to my SecondTab class.
How can I properly address QTextEdit in my second tab?

Thanks for the input musicamante, changed the code according to your suggestions and it works.
self.tab_first = FirstTab()
self.tabWidget.addTab(self.tab_first,"Tab 1")
self.tab_second = SecondTab()
self.tabWidget.addTab(self.tab_second,"Tab 2")
and with self.tab_first.textEdit_input I can now access the needed fields.

Related

How to set a hover event handler on a QPushButton in PyQt5

I'm trying to set a hover event on a button to open an options menu implemented as a QDialog. Everything works so far except the hovering: the menu opens when the button is pressed and disappears if any of the options is selected or the mouse is moved away from the dialog.
Now I want to open the window without clicking the button but rather by hovering over it.
I've seen PyQt5 mouse hover functions and How to detect mouse hover event in PySide6 widget but i wasn't able to make it work this way.
My code looks like this:
class ElementWidget(QWidget):
def __init__ (self, p, element):
super().__init__(p)
layout = QHBoxLayout()
label = QLabel(element)
label.setFixedSize(200,39)
self.btn = QPushButton("btn")
self.btn.clicked.connect(self._openOptionsMenu)
self.btn.setFixedSize(50,39)
layout.addWidget(label)
layout.addWidget(self.btn)
self.setLayout(layout)
self.setFixedSize(250,60)
def _openOptionsMenu(self):
self.dlg = selfClosingDialog(self.closeOptionsMenu, parent = self)
self.dlg.setLayout(ElementOptionsLayout(self.closeOptionsMenu))
self.dlg.setWindowFlag(Qt.FramelessWindowHint)
self.dlg.setGeometry(QCursor.pos().x(), QCursor.pos().y() ,100,100)
self.dlg.show()
def closeOptionsMenu(self):
self.dlg.close()
if __name__ == "__main__":
app = QApplication([])
window = QMainWindow()
window.resize(500,400)
wid = ElementWidget(window,"Parabola_0")
window.show()
app.exec_()
with the custom dialog:
class selfClosingDialog(QDialog):
def __init__(self, closeFunc, parent=None):
super().__init__(parent)
self.closeFunc = closeFunc
def leaveEvent(self, event):
self.closeFunc()
The perfect solution would be to replace the clicked-event by some kind of an onHover
I found the answer.
It is not a signal but an event enterEvent that needs to be reimplemented by a subclass of QWidget
class HoverOpenBtn(QPushButton):
def __init__(self, text, openOtionsFunc, parent=None):
super().__init__(text, parent)
self.openFunc = openOtionsFunc
def enterEvent(self, event):
self.openFunc()

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

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.

How to set the order of widgets in QGridLayout

I am trying to put a QPushButton on top of a PlotWidget. My current code always has the plot widget on top of the button. How can I bring the button to the front of the QGridLayout?
Or is there a better way in PyQt5 to overlay widgets?
I have tried using .raise__() and .lower__() neither worked as expected.
As far as I found there is no way in PyQt5 to set a z value
Changing the order of adding the widgets to the QGridLayout had no effect
The PlotWindow class is used in a Stacked widget in a Window controller class as well as some other classes
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.central_widget = QtWidgets.QStackedWidget()
self.setCentralWidget(self.central_widget)
self.plot = PlotWindow(self)
self.central_widget.addWidget(self.plot)
self.central_widget.setCurrentWidget(self.plot)
self.show()
class PlotWindow(QtWidgets.QWidget):
def __init__(self, parent=None):
super(PlotWindow, self).__init__(parent)
plot_frame = pyqtgraph.PlotWidget()
self.connect_btn = QtWidgets.QPushButton("Connect", self)
plot_wrapper_layout = QtWidgets.QGridLayout()
plot_wrapper_layout.addWidget(plot_frame, 0, 0, 12, 12)
plot_wrapper_layout.addWidget(self.connect_btn, 11, 11, 1, 1)
self.setLayout(plot_wrapper_layout)
Expecting: Button to be visible on top of the graph in the bottom right corner
Result: The connect button is hidden behind the plot widget
plot_frame = pyqtgraph.PlotWidget(self). That puts the graph widget before the push button in PlotWindow.children() which changes the order in which they are rendered.
Heikie's comment fixed the issue

Matplot lib does not plot with PyQt

I'm having a problem with PyQt and Mathplotlib.
Here you can find a pseudocode of what I am doing: I have a class "MainWindow" that creates a main window with a menu and an empty mathplotlib graph. When I click on the menu Item, the method "Select" is executed that opens a new Dialog. There is also a method that plots on the grah the content of the global variable Data.
import TeraGui
Data = []
class MainWindow(QMainWindow, TeraGui.Ui_MainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setupUi(self)
self.actionSelect.triggered.connect(self.Select)
# Create the frame with the graphs
self.create_main_frame()
#Plot empty graphs
self.axes.clear()
self.canvas.draw()
def create_main_frame(self):
self.main_frame = QWidget()
# Create the mpl Figure and FigCanvas objects.
# 5x4 inches, 100 dots-per-inch
#
self.dpi = 100
self.fig = Figure((5.0, 4.0), dpi=self.dpi)
self.canvas = FigureCanvas(self.fig)
self.canvas.setParent(self.main_frame)
#
self.axes = self.fig.add_subplot(111)
# Create the navigation toolbar, tied to the canvas
#
self.mpl_toolbar = NavigationToolbar(self.canvas, self.main_frame)
#
# Layout with box sizers
#
vbox = QVBoxLayout()
vbox.addWidget(self.canvas)
vbox.addWidget(self.mpl_toolbar)
self.main_frame.setLayout(vbox)
self.setCentralWidget(self.main_frame)
def Plotting(self):
""" Redraws the figure
"""
print "I am here"
time = Data[0]
sig = Data[]
plot(time, sig)
# clear the axes and redraw the plot anew
#
self.axes.clear()
self.axes.plot(time, sig)
self.canvas.draw()
def Select(self):
dialog = Dialog(self)
dialog.exec_()
Now, if I add in the init method of the MainWindow class these lines:
Global Data
Data = [[1,2,3],[4,5,6]]
self.Plotting()
"I am here" is printed and the plot is correctly displayed into the graph, BUT if I don't add these lines and i try to call Plotting from the Dialog class it doesn't work. "I am here" is plotted but the plot stays empty. In the Dialog class, method "accept" is caled when the "ok" button of a button box is pressed:
class Dialog(QDialog, TeraGui.Ui_SelectFiles):
def __init__(self, parent=None):
super(Dialog, self).__init__(parent)
self.setAttribute(Qt.WA_DeleteOnClose)
self.setupUi(self)
def accept(self):
global Data
Data = [[1,2,3],[4,5,6]]
MainWindow().Plotting()
The Plotting method draws also a separate plot by means of the command "plot(time,sig)". This plot is always showed correctly regardless the way used to call Plotting.
These are my fist tries with PyQt and matplotlib and I am not able to identify the mistake.
The problem is with the line
MainWindow().Plotting()
When you write MainWindow() you are actually creating a new instance of the MainWindow class and calling its Plotting() function, not the one of your existing MainWindow instance. This window is never shown, and since you don't save a reference to it, is subsequently deleted when accept() returns. The only evidence of its existence is the 'i am here' message it writes to the console. This is why you don't see the plot.
In this case, you are setting your MainWindow instance as the parent of dialog through dialog = Dialog(self), so you could access it though a call to parent().
self.parent().Plotting()
You should also consider adding another parameter to the Plotting() function so you can pass your data directly to it instead of having to declare globals everywhere.
def Plotting(self, Data):
...

PySide: Adding Navigation Toolbar to embedded plot changes cell size in QGridLayout

My project has a class GUI that uses PySide to embed Matplotlib's FigureCanvas inside one of two cells in a QGridLayout. The other class is MplGrapher, which originally was an extension of FigureCanvas. At first, everything was working fine except the plot didn't have a Navigation Toolbar. Then I found a tutorial that explained that if you want a Navigation Toolbar with your embedded plot, you need create an instance of it first...so after this, my plot included a toolbar, but it also decreased in size and I can't get it to fill the cell of GUI's QGridLayout. I've tried having the MplGrapher class extend QWidget instead...I've tried creating a sub layout inside MplGrapher that houses the plot and the toolbar...I've tried setting QSizePolicy, but nothing seems to work.
Part of what's making this so difficult for me is that I'm not sure the proper way to create a FigureCanvas, and I'm not sure the proper way to create a NavigationToolbar. There are too many different ways of extending, initializing, and setting the parent of this class, MplGrapher, that I'm loosing track of my test cases to figure the problem out for myself. Adding to the confusion is the fact that all these different test cases only produce one of three results:
a proper sized Plot, but without a NavigationToolbar (unable to revert back to when this happens)
a improperly sized Plot with a NavigationToolbar pic
the QMainWindow of the GUI class disappears (not crashing)
Here's some code:
#FigureCanvas to be embedded in PySide GUI
class MplGrapher(FigureCanvas):
def __init__(self,name,parent):
self.figure = Figure()
self.figure.suptitle(name)
self.parent = parent
super(FigureCanvas, self).__init__(self.figure)
self.initFigure()
def initFigure(self):
self.plt = self.figure.add_subplot(111)
self.mpl_toolbar = NavigationToolbar(self, self.parent)
Here's the another attempt:
# FigureCanvas to be embedded in PySide GUI
class MplGrapher(QWidget):
def __init__(self,name,parent):
self.figure = Figure()
self.figure.suptitle(name)
self.parent = parent
super(MplGrapher, self).__init__(self.parent)
self.initFigure()
def initFigure(self):
self.plt = self.figure.add_subplot(111)
self.mpl_toolbar = NavigationToolbar(self, self.parent)
And another:
# FigureCanvas to be embedded in PySide GUI
class MplGrapher(FigureCanvas):
def __init__(self,name,parent):
self.figure = Figure()
self.figure.suptitle(name)
self.parent = parent
super(MplGrapher, self).__init__(self.figure)
self.initFigure()
def initFigure(self):
self.setParent(self.parent)
self.plt = self.figure.add_subplot(111)
self.mpl_toolbar = NavigationToolbar(self, self.parent)
And another:
# FigureCanvas to be embedded in PySide GUI
class MplGrapher(FigureCanvas):
def __init__(self,name,parent=None):
super(FigureCanvas, self).__init__(Figure())
self.initFigure(name)
def initFigure(self, name):
self.layout = QGridLayout()
self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Preferred)
self.figure = Figure()
self.figure.suptitle(name)
self.canvas = FigureCanvas(self.figure)
self.canvas.setParent(self)
self.plt = self.figure.add_subplot(111)
self.mpl_toolbar = NavigationToolbar(self.canvas, self)
self.layout.addWidget(self.canvas,0,0)
self.layout.addWidget(self.mpl_toolbar,1,0)
Something like this should do what you want.
class MplGrapher(QtGui.QWidget):
def __init__(self,name,parent=None):
super(MplGrapher, self).__init__(parent)
self.initFigure(name)
def initFigure(self, name):
self.figure = Figure()
self.canvas = FigureCanvas(self.figure)
self.plt = self.figure.add_subplot(111)
self.navbar = NavigationToolbar(self.canvas, self)
self.layout = QtGui.QVBoxLayout()
self.layout.addWidget(self.navbar)
self.layout.addWidget(self.canvas)
self.setLayout(self.layout)