I am new to object oriented programming and need assistance getting the value from a listwidget in one class to another class. I have one listwidget called selectedVariables in the MainWindow class. I want the SecondWindow class to be able to get the variables from selectedVariables so the user can select them in the second window to group the data by a certain variable. Ideally, when they click the three dots, a new window pops up with a list of the widgets, but for now I just need to understand how to get the selectedVariable list items and send it to the SecondWindow Class. The code below runs without errors, but has no reference to the selectedVariables list.
I did try layout.addWidget(MainWindow.selectedVariables, 3, 0, 1, 1) and it gave me the error :
Traceback (most recent call last):
File "S:\1WORKING\FINANCIAL ANALYST\Shawn Schreier\Python\Minimum Example.py", line 99, in
mw = MainWindow()
File "S:\1WORKING\FINANCIAL ANALYST\Shawn Schreier\Python\Minimum Example.py", line 49, in init
self.mw = SecondWindow()
File "S:\1WORKING\FINANCIAL ANALYST\Shawn Schreier\Python\Minimum Example.py", line 30, in init
layout.addWidget(MainWindow.selectedVariables, 3, 0, 1, 1)
AttributeError: type object 'MainWindow' has no attribute 'selectedVariables'
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QPushButton, QListWidget, QLineEdit, QTextEdit, QGridLayout, QHBoxLayout, QVBoxLayout, QSizePolicy, QFileDialog, QTabWidget, QCheckBox
import PyQt5.QtGui as qtg
from PyQt5.QtCore import Qt, QSettings
import inspect
from PyQt5 import QtCore
class SecondWindow(QWidget):
def __init__(self):
super().__init__()
layout = QGridLayout()
by = QLabel("By")
byVariables = QLineEdit()
byVariableList = QListWidget()
layout.addWidget(by, 1,0,1,1)
layout.addWidget(byVariables, 2, 0, 1, 1)
byButton = QPushButton("...")
layout.addWidget(byButton, 2, 1, 1, 1)
self.setLayout(layout)
class MainWindow(QWidget):
def __init__(self):
super().__init__()
# Add a title
self.setWindowTitle("GUI Querying Program")
self.layout = QHBoxLayout()
self.setLayout(self.layout)
self.initUI()
self.setButtonConnections()
self.mw = SecondWindow()
def initUI(self):
subLayouts = {}
subLayouts['LeftColumn'] = QGridLayout()
self.layout.addLayout(subLayouts['LeftColumn'],1)
# Buttons
self.buttons = {}
self.buttons['addVariable'] = QPushButton('>')
self.buttons['removeVariable'] = QPushButton('<')
self.buttons['Toolkit'] = QPushButton('Toolkit')
self.variables = QListWidget()
self.selectedVariables = QListWidget()
subLayouts['LeftColumn'].addWidget(self.variables, 7,0,4,1)
subLayouts['LeftColumn'].addWidget(self.selectedVariables, 7,1,4,1)
subLayouts['LeftColumn'].addWidget(self.buttons['addVariable'], 10,0,1,1)
subLayouts['LeftColumn'].addWidget(self.buttons['removeVariable'], 10,1,1,1)
subLayouts['LeftColumn'].addWidget(self.buttons['Toolkit'], 11,1,1,1)
names = ['apple', 'banana', 'Cherry']
self.variables.insertItems(0, names)
def setButtonConnections(self):
self.buttons['addVariable'].clicked.connect(self.add_variable)
self.buttons['removeVariable'].clicked.connect(self.remove_variable)
self.buttons['Toolkit'].clicked.connect(self.show_new_window)
def add_variable(self):
selected_elements=[item.text() for item in self.variables.selectedItems()]
variableItem = self.selectedVariables.insertItems(self.variables.count(),selected_elements)
#self.variablesSearch.clear()
def remove_variable(self):
oldVariable = self.selectedVariables.currentRow()
self.selectedVariables.takeItem(oldVariable)
def show_new_window(self):
self.mw.show()
if __name__ == "__main__":
import sys
app = QApplication([])
mw = MainWindow()
mw.show()
# Run the app
app.exec()
app.quit()
#sys.exit(app.exec())
If you want to copy items, then you should use the clone() function of QListWidgetItem.
Note that you should do the same also in add_variable: while your case is very simple, cloning an item ensures that all its properties (including custom data, like text or background color) are copied, not only its text.
class SecondWindow(QWidget):
def __init__(self):
super().__init__()
layout = QGridLayout(self)
by = QLabel("By")
byVariables = QLineEdit()
self.byVariableList = QListWidget()
byButton = QPushButton("...")
layout.addWidget(by, 0, 0)
layout.addWidget(byVariables, 1, 0)
layout.addWidget(byButton, 2, 1)
layout.addWidget(self.byVariableList, 3, 0, 1, 2)
def setItems(self, items):
self.byVariableList.clear()
for item in items:
self.byVariableList.addItem(item)
class MainWindow(QWidget):
# ...
def add_variable(self):
for item in self.variables.selectedItems():
self.selectedVariables.addItem(item.clone())
def show_new_window(self):
items = []
for i in range(self.selectedVariables.count()):
items.append(self.selectedVariables.item(i).clone())
self.mw.setItems(items)
self.mw.show()
Note that when app.exec() returns, it means that the application has been quit, calling app.quit() after that is pointless. Also: 1. rows and columns of a grid layout are always based on 0-indexes (just like almost anything in computing), so you should add items from 0, not 1; 2. the row/column span always defaults to 1, so there is no point in specifying it unless you specifically want a span greater than 1.
You can try to make the SecondWindow, class a child class. That means, making it inherit your MainWindow methods. Like this
Class SecondWindow(MainWindow):
And from what i understood, from your code the selectedVariables instance is interchangeable according to the utilized methods. So it would be wiser to define it as a instance variable in your constructor
Class MainWindow(Qwidget):
def__init__(self):
self.selectedVariables = QListWidget()
That way, it will be easier for you to inherit independently what action happens to it . Also you wont need to call the method everytime so kinda of a must.
Related
This question already has an answer here:
QLineEdit emits returnPressed when getting focus triggered by other returnPressed singal
(1 answer)
Closed 1 year ago.
I'm developing a GUI, and I have the issue that sometimes, hitting the 'enter' key makes several widgets send their signal. The weirdest part is that sometimes it happens, and sometimes not. The main thing is, I can't guarantee the focus on one and only one QGroupBox at all times.
Here is a somewhat minimal example. If you run it and enter text, then hit 'enter', two functions will be executed (image below).
# -*- coding: utf-8 -*-
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QApplication, QComboBox, QStyleFactory, QDialog, QTextEdit,
QGroupBox, QLabel, QLineEdit, QGridLayout, QPushButton, QVBoxLayout)
import sys
class GrblGUI(QDialog):
class PositionDescriber:
""" Label and widget associated for each axis. Save some writing later """
def __init__(self, labelText, initVal=0.0):
self.posLabel = QLabel(labelText)
self.value = initVal
self.posWidget = QLineEdit(str(self.value))
def __init__(self, parent=None):
""" Initializes the GUI and all widgets within.
Creates the general layout
"""
super(GrblGUI, self).__init__(parent)
self.originalPalette = QApplication.palette()
self.axes = [ self.PositionDescriber("X pos : "),
self.PositionDescriber("Y pos : "),
self.PositionDescriber("Z pos : "),
self.PositionDescriber("A pos : "),
self.PositionDescriber("B pos : ")]
self.size = range(len(self.axes))
self.ports = ["None"]
# Creating widget within panels
self.createConnectToCOM()
self.createPositionControlPanel()
self.createPushButtonsPanel()
self.createMessageHistory()
mainLayout = QGridLayout()
mainLayout.addWidget(self.connectToCOM, 0, 0, 1, 2)
mainLayout.addWidget(self.positionControlPanel, 1, 0)
mainLayout.addWidget(self.pushButtonsPanel, 0, 2, 2, 1)
mainLayout.addWidget(self.messageHistory, 1, 1)
mainLayout.setRowStretch(1, 1)
self.setLayout(mainLayout)
self.setWindowTitle("minimal")
QApplication.setStyle(QStyleFactory.create('Fusion'))
QApplication.setPalette(QApplication.style().standardPalette())
"""
Creation of panels, widgets, and associated layouts
"""
def createConnectToCOM(self):
self.connectToCOM = QGroupBox()
self.availableDevicesScroll = QComboBox()
for item in self.ports:
self.availableDevicesScroll.addItem(item)
connectLabel = QLabel("Connect to device :")
self.updatePushButton = QPushButton("Update")
self.updatePushButton.setDefault(True)
self.connectPushButton = QPushButton("Connect")
self.connectPushButton.setDefault(True)
self.updatePushButton.clicked.connect(self.updateAvailableCOM)
self.connectPushButton.clicked.connect(self.connectToPort)
layout = QGridLayout()
layout.addWidget(connectLabel, 0, 0)
layout.addWidget(self.availableDevicesScroll, 1, 0)
layout.addWidget(self.updatePushButton, 0, 1)
layout.addWidget(self.connectPushButton, 1, 1)
layout.setColumnStretch(0, 1)
self.connectToCOM.setLayout(layout)
def createPositionControlPanel(self):
self.positionControlPanel = QGroupBox("Position Control Panel : ")
for i in self.size:
self.axes[i].posWidget.returnPressed.connect(self.registerInput)
layout = QGridLayout()
for i in self.size:
layout.addWidget(self.axes[i].posLabel, i, 0)
for i in self.size:
layout.addWidget(self.axes[i].posWidget, i, 1)
sendPushButton = QPushButton("Send to pos")
sendPushButton.setDefault(True)
sendPushButton.clicked.connect(self.sendToPos)
layout.addWidget(sendPushButton, len(self.axes), 2, 1, 2)
layout.setRowStretch(6, 1)
self.positionControlPanel.setLayout(layout)
def createPushButtonsPanel(self):
self.pushButtonsPanel = QGroupBox("Things you may want to do : ")
self.homingPushButton = QPushButton("Homing")
self.homingPushButton.setDefault(True)
self.homingPushButton.clicked.connect(self.homing)
recPosPushButton = QPushButton("Record current pos")
recPosPushButton.setDefault(True)
recPosPushButton.clicked.connect(self.recordPosition)
layout = QVBoxLayout()
layout.setSpacing(20)
layout.addWidget(self.homingPushButton)
layout.addWidget(recPosPushButton)
layout.addStretch(1)
self.pushButtonsPanel.setLayout(layout)
def createMessageHistory(self):
self.messageHistory = QGroupBox("Message history : ")
self.textEdit = QTextEdit()
self.textEdit.setReadOnly(True)
self.textEdit.setPlainText("")
layout = QVBoxLayout()
layout.addWidget(self.textEdit)
self.messageHistory.setLayout(layout)
"""
Methods to call
"""
def connectToPort(self):
self.textEdit.append("connectToPort")
def updateAvailableCOM(self):
self.textEdit.append("updateAvailableCOM")
def registerInput(self):
self.textEdit.append("registerInput")
def homing(self):
self.textEdit.append("homing")
def recordPosition(self):
self.textEdit.append("recordPosition")
def sendToPos(self):
self.textEdit.append("sendToPos")
if __name__ == '__main__':
app = QApplication(sys.argv)
gallery = GrblGUI()
gallery.show()
app.exec()
# sys.exit(appctxt.app.exec())
And the result after entering text:
I've tried different thing such as setFocusPolicy(Qt.NoFocus) or setFocus(), but it didn't work. Any ideas?
The problem comes from both the default and autoDefault properties of QPushButton in combination with the usage of QDialog, and it has the following results
if autoDefault is True, a button becomes a possible default button;
the autoDefault property of a QPushButton is False, unless it is/becomes a child (even indirect) of a QDialog;
Events received by a widget that does not accept them are automatically propagated to its parent, up in the parenthood hierarchy, until one widget does accept it or the top level widget is reached.
QLineEdit by default handles the return key press, but does not accept it, which means that it knows that the key has been pressed (since it can emit the returnPressed signal) but will not handle, thus propagating it to the parent.
Considering the above, the unwanted behavior is that the key event is also received by the QDialog, so there are various possibilities, depending on the requirements.
Use QWidget instead of QDialog
This is the easiest choice, but you might still need a QDialog for its features: the exec event loop, the accepted/rejected signals and result interface, or just to simplify the modality
Set the default property to False for all buttons
Obviously, you can set the autoDefault property to False for each button, but an easier solution is to use a cycle that loops over all QPushButton instances, which should be implemented in an override that we know for sure that would be called, like exec or, maybe better, showEvent:
class Dialog(QtWidgets.QDialog)
# ...
def showEvent(self, event):
super().showEvent(event)
if not event.spontaneous():
for btn in self.findChildren(QtWidgets.QPushButton):
if btn.default():
btn.setDefault(False)
if btn.autoDefault():
btn.setAutoDefault(False)
This can become a problem, though, as sometimes you might want to use that feature anyway: for instance, if you have a tab/stacked widget, and you want to avoid the return feature in a page that has a line edit, but not in another one that only has one button (like a wizard).
Ignore the Return key in the dialog
Overriding the keyPressEvent, and call the base implementation only if the key is not return or enter (this has the same problem above, as it completely disable the feature):
class Dialog(QtWidgets.QDialog)
# ...
def keyPressEvent(self, event):
if event.key() not in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
super().keyPressEvent(event)
Accept the key event in the line edit
This is probably a more appropriate approach, as it solves the problem at the source: consider the event as accepted in the QLineEdit if the return/enter key is pressed. This can only be done in a subclass:
class LineEdit(QtWidgets.QLineEdit):
def keyPressEvent(self, event):
super().keyPressEvent(event)
if event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
event.accept()
As #musicamante found out, this is closely related to QLineEdit emits returnPressed when getting focus triggered by other returnPressed singal
The simpliest answer was to inherit from QWidget instead of QDialog, as there won't be default buttons anymore.
May be the use of eventFilter for whole form is the solution in your case:
from QtCore import QEvent
Define eventFilter(self, obj, event) in class GrblGUI and place under Qt.Key_Enter branch the callback you want for enter key press.
Register event filter app.installEventFilter(gallery)
Modified example below (be careful, I switched to PySide2 in example, you can easily switch back to PyQt5 again):
# -*- coding: utf-8 -*-
from PySide2.QtCore import Qt, QEvent # Step 1 - import QEvent
from PySide2.QtWidgets import QApplication, QComboBox, QStyleFactory, QDialog, QTextEdit
from PySide2.QtWidgets import QGroupBox, QLabel, QLineEdit, QGridLayout, QPushButton, QVBoxLayout
import sys
class GrblGUI(QDialog):
class PositionDescriber:
""" Label and widget associated for each axis. Save some writing later """
def __init__(self, labelText, initVal=0.0):
self.posLabel = QLabel(labelText)
self.value = initVal
self.posWidget = QLineEdit(str(self.value))
def __init__(self, parent=None):
""" Initializes the GUI and all widgets within.
Creates the general layout
"""
super(GrblGUI, self).__init__(parent)
self.originalPalette = QApplication.palette()
self.axes = [ self.PositionDescriber("X pos : "),
self.PositionDescriber("Y pos : "),
self.PositionDescriber("Z pos : "),
self.PositionDescriber("A pos : "),
self.PositionDescriber("B pos : ")]
self.size = range(len(self.axes))
self.ports = ["None"]
# Creating widget within panels
self.createConnectToCOM()
self.createPositionControlPanel()
self.createPushButtonsPanel()
self.createMessageHistory()
mainLayout = QGridLayout()
mainLayout.addWidget(self.connectToCOM, 0, 0, 1, 2)
mainLayout.addWidget(self.positionControlPanel, 1, 0)
mainLayout.addWidget(self.pushButtonsPanel, 0, 2, 2, 1)
mainLayout.addWidget(self.messageHistory, 1, 1)
mainLayout.setRowStretch(1, 1)
self.setLayout(mainLayout)
self.setWindowTitle("minimal")
QApplication.setStyle(QStyleFactory.create('Fusion'))
QApplication.setPalette(QApplication.style().standardPalette())
"""
Creation of panels, widgets, and associated layouts
"""
def eventFilter(self, obj, event): # Step 2 - declare QEvent filter
if event.type() == QEvent.KeyPress:
if (event.key() == Qt.Key_Enter or event.key() == Qt.Key_Return):
self.textEdit.append("***DEBUG: Enter pressed")
# Step 2 Place callback you want to process key press
event.accept() # block event propagation to other widgets
return True
return False
def createConnectToCOM(self):
self.connectToCOM = QGroupBox()
self.availableDevicesScroll = QComboBox()
for item in self.ports:
self.availableDevicesScroll.addItem(item)
connectLabel = QLabel("Connect to device :")
self.updatePushButton = QPushButton("Update")
self.updatePushButton.setDefault(True)
self.connectPushButton = QPushButton("Connect")
self.connectPushButton.setDefault(True)
self.updatePushButton.clicked.connect(self.updateAvailableCOM)
self.connectPushButton.clicked.connect(self.connectToPort)
layout = QGridLayout()
layout.addWidget(connectLabel, 0, 0)
layout.addWidget(self.availableDevicesScroll, 1, 0)
layout.addWidget(self.updatePushButton, 0, 1)
layout.addWidget(self.connectPushButton, 1, 1)
layout.setColumnStretch(0, 1)
self.connectToCOM.setLayout(layout)
def createPositionControlPanel(self):
self.positionControlPanel = QGroupBox("Position Control Panel : ")
for i in self.size:
self.axes[i].posWidget.returnPressed.connect(self.registerInput)
layout = QGridLayout()
for i in self.size:
layout.addWidget(self.axes[i].posLabel, i, 0)
for i in self.size:
layout.addWidget(self.axes[i].posWidget, i, 1)
sendPushButton = QPushButton("Send to pos")
sendPushButton.setDefault(True)
sendPushButton.clicked.connect(self.sendToPos)
layout.addWidget(sendPushButton, len(self.axes), 2, 1, 2)
layout.setRowStretch(6, 1)
self.positionControlPanel.setLayout(layout)
def createPushButtonsPanel(self):
self.pushButtonsPanel = QGroupBox("Things you may want to do : ")
self.homingPushButton = QPushButton("Homing")
#self.homingPushButton.setFocusPolicy(Qt.StrongFocus); #!!!
self.homingPushButton.setDefault(True)
self.homingPushButton.clicked.connect(self.homing)
recPosPushButton = QPushButton("Record current pos")
recPosPushButton.setDefault(True)
recPosPushButton.clicked.connect(self.recordPosition)
layout = QVBoxLayout()
layout.setSpacing(20)
layout.addWidget(self.homingPushButton)
layout.addWidget(recPosPushButton)
layout.addStretch(1)
self.pushButtonsPanel.setLayout(layout)
def createMessageHistory(self):
self.messageHistory = QGroupBox("Message history : ")
self.textEdit = QTextEdit()
self.textEdit.setReadOnly(True)
self.textEdit.setPlainText("")
layout = QVBoxLayout()
layout.addWidget(self.textEdit)
self.messageHistory.setLayout(layout)
"""
Methods to call
"""
def connectToPort(self):
self.textEdit.append("connectToPort")
def updateAvailableCOM(self):
self.textEdit.append("updateAvailableCOM")
def registerInput(self):
self.textEdit.append("registerInput")
def homing(self):
self.textEdit.append("homing")
def recordPosition(self):
self.textEdit.append("recordPosition")
def sendToPos(self):
self.textEdit.append("sendToPos")
if __name__ == '__main__':
app = QApplication(sys.argv)
gallery = GrblGUI()
gallery.show()
app.installEventFilter(gallery) # Step 4 - register event filter in app
app.exec_()
Using my First script, I create a layout as I need. Now I want to use this layout in the second script and add widgets to the frame. Face the following problem: Both the First and second program windows will open simultaneously, I want to open/ show my second program window only. How to resolve it?
First script
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QApplication, QFrame, QHBoxLayout, QSizePolicy
class MsgBox111(QWidget):
def __init__(self):
super().__init__()
self.frame_main = QFrame()
self.frame_main.setObjectName("ob_frame_main")
self.frame_main.setStyleSheet("QFrame#ob_frame_main{background-color:green}")
self.frame_left_top = QFrame()
self.frame_left_top.setObjectName("ob_frame_left_top")
self.frame_left_top.setStyleSheet("QFrame#ob_frame_left_top{background-color:skyblue}")
self.frame_left_top.setMinimumSize(600, 340)
self.frame_left_bot = QFrame()
self.frame_left_bot.setObjectName("ob_frame_left_bot")
self.frame_left_bot.setStyleSheet("QFrame#ob_frame_left_bot{background-color:lightgreen}")
self.frame_left_bot.setFixedSize(600, 60)
self.frame_right = QFrame()
self.frame_right.setObjectName("ob_frame_right")
self.frame_right.setStyleSheet("QFrame#ob_frame_right{background-color:rgb(230,220,150)}")
self.frame_right.setFixedSize(200, 405)
self.lay_main = QHBoxLayout()
self.lay_main.setContentsMargins(0, 0, 0, 0)
self.lay_main.setSpacing(8)
self.lay_left = QVBoxLayout()
self.lay_left.setSpacing(8)
self.lay_left_top = QVBoxLayout()
self.lay_left_top.setContentsMargins(0, 0, 0, 0)
self.lay_left_bot = QHBoxLayout()
self.lay_left_bot.setContentsMargins(0, 0, 0, 0)
self.lay_right = QVBoxLayout()
self.lay_right.setContentsMargins(0, 0, 0, 0)
self.lay_overall = QHBoxLayout()
self.lay_overall.setContentsMargins(0, 0, 0, 0)
self.lay_overall.setSpacing(5)
self.lay_left_top = QVBoxLayout(self.frame_left_top)
self.lay_left_bot = QHBoxLayout(self.frame_left_bot)
self.lay_right = QVBoxLayout(self.frame_right)
self.lay_main = QHBoxLayout(self.frame_main)
self.lay_left.addWidget(self.frame_left_top)
self.lay_left.addWidget(self.frame_left_bot)
self.lay_main.addLayout(self.lay_left)
self.lay_main.addWidget(self.frame_right)
self.lay_overall.addWidget(self.frame_main)
self.setLayout(self.lay_overall)
frame_right_sizehint = self.lay_left.sizeHint()
self.frame_right.setFixedSize(260, frame_right_sizehint.height())
self.frame_main.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
Second script
import sys
from PyQt5.QtWidgets import QLineEdit
from layour_sample_001 import *
class MsgBox222(QWidget):
def __init__(self):
super().__init__()
self.ss = MsgBox111()
self.ss.show()
self.tb = QLineEdit()
self.ss.lay_left.addWidget(self.tb)
self.ss.setLayout(self.ss.lay_left)
if __name__ == "__main__":
app = QApplication(sys.argv)
mainwindow = MsgBox222()
mainwindow.show()
sys.exit(app.exec_())
You need to properly subclass.
After calling the __init__ for the superclass, you can access all inherited attributes of that class and instance: you have a fully constructed MsgBox111 instance that you can then extend according to your needs.
class MsgBox222(MsgBox111):
def __init__(self):
super().__init__()
self.tb = QLineEdit()
self.lay_left.addWidget(self.tb)
I strongly suggest you to do some more research and careful studying on the main aspects of OOP, including classes, instances, inheritance, attributes and subclassing, as knowledge of these fundamental topics is mandatory.
I am coding a GUI program now and I have two separate PyQt5 widget objects that need to communicate with each other. I have something that works now (which I have provided a simplified example of below), but I suspect there is a more robust way of doing this that I am hoping to learn about. I will summarize the functionality below, for those that would like a bit of an intro to the code.
TL;DR: Please help me find a better way to use a button click in object 1 to change a variable in object 2 that sends the coordinates of a mouse click in object 2 to object 1 where those coordinates populate two spin boxes.
This first MainWindow class is where the widget objects are defined. The two objects of interest are MainWindow.plotWidget, an instance of the MplFig class, and MainWindow.linePt1, an instance of the LineEndpoint class. Note here that I am able to pass the self.plotWidget as an argument into the LineEndpoint object, but since MainWindow.plotWidget is defined first, I cannot pass self.linePt1 as an argument there.
The functionality I have achieved with these widgets is a button in LineEndpoint (LineEndpoint.chooseBtn) that, when clicked, changes a variable in MplFig (MplFig.waitingForPt) from None to the value of ptNum which is passed as an argument of LineEndpoint (in the case of linePt1, this value is 1). MplFig has button press events tied to the method MplFig.onClick() which, is MplFig.onClick is not None, passes the coordinates of the mouse click to the two QDoubleSpinBox objects in LineEndpoint.ptXSpin and LineEndpoint.ptYSpin. To achieve this, I pass self as the parent argument when I create the MainWIndow.plotWidget object of MplFig. I set the parent as self.parent which allows me to call the LineEndpoint object as self.parent.linePt1, which from there allows me to access the spin boxes.
This seems like a round-a-bout way of doing things and I'm wondering if anybody could suggest a better way of structuring this functionality? I like the method of passing the MplFig object as an argument to the LineEndpoint class as that makes it clear from the init method in the class definition that the LineEndpoint class communicates with the MplFig class. I know I cannot have both classes depend on each other in the same way, but i would love to learn a way of doing this that still makes it clear in the code that the objects are communicating. I am still open to all suggestions though!
from PyQt5.QtWidgets import (
QMainWindow, QApplication, QLabel, QLineEdit, QPushButton, QFileDialog,
QWidget, QHBoxLayout, QVBoxLayout, QMessageBox, QListWidget,
QAbstractItemView, QDoubleSpinBox
)
from PyQt5.QtCore import Qt
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import (
FigureCanvasQTAgg, NavigationToolbar2QT as NavigationToolbar
)
import sys # need sys to pass argv to QApplication
class MplFig(FigureCanvasQTAgg):
def __init__(self, parent):
self.fig = Figure()
super().__init__(self.fig)
self.parent = parent
self.waitingForPt = None
self.fig.canvas.mpl_connect('button_press_event', self.onClick)
self.ax = self.figure.add_subplot(111)
def onClick(self, e):
if self.waitingForPt is not None:
if self.waitingForPt == 1:
lineObj = self.parent.linePt1
roundX = round(e.xdata, lineObj.ptPrec)
roundY = round(e.ydata, lineObj.ptPrec)
print(f'x{self.waitingForPt}: {roundX}, '
f'y{self.waitingForPt}: {roundY}'
)
lineObj.ptXSpin.setValue(roundX)
lineObj.ptYSpin.setValue(roundY)
lineObj.chooseBtn.setStyleSheet(
'background-color: light gray'
)
self.waitingForPt = None
class LineEndpoint(QWidget):
def __init__(self, parent, mplObject, ptNum, *args, **kwargs):
super().__init__(*args, **kwargs)
self.parent = parent
self.mpl = mplObject
self.layout = QVBoxLayout()
row0Layout = QHBoxLayout()
ptXLabel = QLabel(f'X{ptNum}:')
row0Layout.addWidget(ptXLabel)
ptMin = 0
ptMax = 1000
ptStep = 1
self.ptPrec = 2
self.ptXSpin = QDoubleSpinBox()
self.ptXSpin.setSingleStep(ptStep)
self.ptXSpin.setMinimum(ptMin)
self.ptXSpin.setMaximum(ptMax)
self.ptXSpin.setDecimals(self.ptPrec)
row0Layout.addWidget(self.ptXSpin)
ptYLabel = QLabel(f'Y{ptNum}:')
row0Layout.addWidget(ptYLabel)
self.ptYSpin = QDoubleSpinBox()
self.ptYSpin.setMinimum(ptMin)
self.ptYSpin.setMaximum(ptMax)
self.ptYSpin.setSingleStep(ptStep)
self.ptYSpin.setDecimals(self.ptPrec)
row0Layout.addWidget(self.ptYSpin)
self.layout.addLayout(row0Layout)
row1Layout = QHBoxLayout()
self.chooseBtn = QPushButton('Choose on Plot')
self.chooseBtn.clicked.connect(lambda: self.chooseBtnClicked(ptNum))
row1Layout.addWidget(self.chooseBtn)
self.layout.addLayout(row1Layout)
def chooseBtnClicked(self, endpointNum):
print(f'Choosing point {endpointNum}...')
self.chooseBtn.setStyleSheet('background-color: red')
self.mpl.waitingForPt = endpointNum
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setLayouts()
def setLayouts(self):
self.sideBySideLayout = QHBoxLayout()
self.plotWidget = MplFig(self)
self.sideBySideLayout.addWidget(self.plotWidget)
self.linePt1 = LineEndpoint(self, self.plotWidget, 1)
self.sideBySideLayout.addLayout(self.linePt1.layout)
mainContainer = QWidget()
mainContainer.setLayout(self.sideBySideLayout)
self.setCentralWidget(mainContainer)
QApp = QApplication(sys.argv)
win = MainWindow()
win.show()
sys.exit(QApp.exec_())
If you want to transmit information between objects (remember that classes are only abstractions) then you must use signals:
import sys
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
QApplication,
QDoubleSpinBox,
QGridLayout,
QHBoxLayout,
QLabel,
QMainWindow,
QPushButton,
QWidget,
)
from matplotlib.figure import Figure
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
class MplFig(FigureCanvasQTAgg):
clicked = pyqtSignal(float, float)
def __init__(self, parent=None):
super().__init__(Figure())
self.setParent(parent)
self.figure.canvas.mpl_connect("button_press_event", self.onClick)
self.ax = self.figure.add_subplot(111)
def onClick(self, e):
self.clicked.emit(e.xdata, e.ydata)
class LineEndpoint(QWidget):
def __init__(self, ptNum, parent=None):
super().__init__(parent)
ptMin = 0
ptMax = 1000
ptStep = 1
ptPrec = 2
self.ptXSpin = QDoubleSpinBox(
singleStep=ptStep, minimum=ptMin, maximum=ptMax, decimals=ptPrec
)
self.ptYSpin = QDoubleSpinBox(
singleStep=ptStep, minimum=ptMin, maximum=ptMax, decimals=ptPrec
)
self.chooseBtn = QPushButton("Choose on Plot", checkable=True)
self.chooseBtn.setStyleSheet(
"""
QPushButton{
background-color: light gray
}
QPushButton:checked{
background-color: red
}"""
)
lay = QGridLayout(self)
lay.addWidget(QLabel(f"X{ptNum}"), 0, 0)
lay.addWidget(self.ptXSpin, 0, 1)
lay.addWidget(QLabel(f"Y{ptNum}"), 0, 2)
lay.addWidget(self.ptYSpin, 0, 3)
lay.addWidget(self.chooseBtn, 1, 0, 1, 4)
lay.setRowStretch(lay.rowCount(), 1)
#pyqtSlot(float, float)
def update_point(self, x, y):
if self.chooseBtn.isChecked():
self.ptXSpin.setValue(x)
self.ptYSpin.setValue(y)
self.chooseBtn.setChecked(False)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setLayouts()
def setLayouts(self):
self.plotWidget = MplFig()
self.linePt1 = LineEndpoint(1)
self.plotWidget.clicked.connect(self.linePt1.update_point)
mainContainer = QWidget()
lay = QHBoxLayout(mainContainer)
lay.addWidget(self.plotWidget)
lay.addWidget(self.linePt1)
self.setCentralWidget(mainContainer)
QApp = QApplication(sys.argv)
win = MainWindow()
win.show()
sys.exit(QApp.exec_())
I'm working on a Wizard for installing downloaded packages. At a particular point I want to skip the next QWizardPage, depending whether a checkbox on the page before is checked or not.
The page I want to skip is the InstallPackages class and the case when it should be skipped is if the checkbox self.withPipCBox is unchecked.
I know that I have to use QCheckBox.isChecked() for that, but how to use it?
Code:
from PyQt5 import QtWidgets, QtGui, QtCore
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import Qt, QObject, QTimer, QThread, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (QApplication, QFileDialog, QGridLayout, QLabel,
QVBoxLayout, QWizard, QWizardPage, QProgressBar,
QCheckBox, QLineEdit, QGroupBox, QToolButton,
QComboBox, QDialog, QHBoxLayout)
class VenvWizard(QWizard):
"""
Wizard for creating and setting up virtual environments.
"""
def __init__(self):
super().__init__()
self.setWindowTitle("Venv Wizard")
self.resize(535, 430)
self.move(578, 183)
self.setStyleSheet(
"""
QToolTip {
background-color: rgb(47, 52, 63);
border: rgb(47, 52, 63);
color: rgb(210, 210, 210);
padding: 2px;
opacity: 325
}
"""
)
self.addPage(BasicSettings())
self.addPage(InstallPackages())
self.addPage(Summary())
class BasicSettings(QWizardPage):
"""
Basic settings of the virtual environment being created.
"""
def __init__(self):
super().__init__()
folder_icon = QIcon.fromTheme("folder")
self.setTitle("Basic Settings")
self.setSubTitle("This wizard will help you to create and set up "
"a virtual environment for Python 3. ")
interpreterLabel = QLabel("&Interpreter:")
self.interprComboBox = QComboBox()
interpreterLabel.setBuddy(self.interprComboBox)
self.interprComboBox.addItem("---")
venvNameLabel = QLabel("Venv &name:")
self.venvNameLineEdit = QLineEdit()
venvNameLabel.setBuddy(self.venvNameLineEdit)
venvLocationLabel = QLabel("&Location:")
self.venvLocationLineEdit = QLineEdit()
venvLocationLabel.setBuddy(self.venvLocationLineEdit)
selectFolderToolButton = QToolButton()
selectFolderToolButton.setFixedSize(26, 27)
selectFolderToolButton.setIcon(folder_icon)
selectFolderToolButton.setToolTip("Browse")
placeHolder = QLabel()
# the 'options' groupbox
groupBox = QGroupBox("Options")
self.withPipCBox = QCheckBox("Install and update &Pip")
self.sitePackagesCBox = QCheckBox(
"&Make system (global) site-packages dir available to venv")
self.symlinksCBox = QCheckBox(
"Attempt to &symlink rather than copy files into venv")
self.launchVenvCBox = QCheckBox(
"Launch a terminal with activated &venv after installation")
# grid layout
gridLayout = QGridLayout()
gridLayout.addWidget(interpreterLabel, 0, 0, 1, 1)
gridLayout.addWidget(self.interprComboBox, 0, 1, 1, 2)
gridLayout.addWidget(venvNameLabel, 1, 0, 1, 1)
gridLayout.addWidget(self.venvNameLineEdit, 1, 1, 1, 2)
gridLayout.addWidget(venvLocationLabel, 2, 0, 1, 1)
gridLayout.addWidget(self.venvLocationLineEdit, 2, 1, 1, 1)
gridLayout.addWidget(selectFolderToolButton, 2, 2, 1, 1)
gridLayout.addWidget(placeHolder, 3, 0, 1, 2)
gridLayout.addWidget(groupBox, 4, 0, 1, 3)
self.setLayout(gridLayout)
# 'options' groupbox
groupBoxLayout = QVBoxLayout()
groupBoxLayout.addWidget(self.withPipCBox)
groupBoxLayout.addWidget(self.sitePackagesCBox)
groupBoxLayout.addWidget(self.symlinksCBox)
groupBoxLayout.addWidget(self.launchVenvCBox)
groupBox.setLayout(groupBoxLayout)
class InstallPackages(QWizardPage):
"""
Install packages via `pip` into the created virtual environment.
"""
def __init__(self):
super().__init__()
self.setTitle("Install Packages")
self.setSubTitle("Specify the packages which you want Pip to "
"install into the virtual environment.")
# just some content for testing
TestLabel = QLabel("This is a test label:", self)
TestLineEdit = QLineEdit(self)
TestLabel.setBuddy(TestLineEdit)
TestLabel2 = QLabel("This is a test label:", self)
TestLineEdit2 = QLineEdit(self)
TestLabel2.setBuddy(TestLineEdit2)
v_layout = QVBoxLayout(self)
v_layout.addWidget(TestLabel)
v_layout.addWidget(TestLineEdit)
v_layout.addWidget(TestLabel2)
v_layout.addWidget(TestLineEdit2)
self.setLayout(v_layout)
def initializePage(self):
pass
class Summary(QWizardPage):
def __init__(self):
super().__init__()
self.setTitle("Summary")
self.setSubTitle("...............")
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
wizard = VenvWizard()
wizard.show()
sys.exit(app.exec_())
You need to implement the nextId() of the wizard page or the wizard itself (which by default calls currentPage().nextId()).
In the first case, you'll need to keep or get a reference to the two pages, but that can also be done by overriding the nextId() of the instance: it is a virtual function, meaning that it can be overridden in the instance from the wizard itself, allowing you to simplify things a bit, since you've access to the pageId once you add them:
basicSettings = BasicSettings()
self.addPage(basicSettings)
installId = self.addPage(InstallPackages())
summaryId = self.addPage(Summary())
basicSettings.nextId = lambda: installId if basicSettings.interprComboBox.isChecked() else summaryId
You can do almost the same in the nextId() override of the wizard, as long as you keep a reference to the page that contains the checkbox at least.
Here's a slightly different approach to the method shown above:
class VenvWizard(QWizard):
def __init__(self):
super().__init__()
[...]
self.basicSettings = BasicSettings()
self.addPage(self.basicSettings)
[...]
def nextId(self):
if self.currentPage() == self.basicSettings:
# since python bools can behave as integers, you can add
# 0 or 1 to the pageId using the isChecked() result
return self.currentId() + 1 + self.basicSettins.interprComboBox.isChecked()
# if you need the opposite (skip if unchecked) use this instead
#return self.currentId() + 1 + (not self.basicSettins.interprComboBox.isChecked())
return QtWidgets.QWizard.nextId(self)
Btw, the example above will only work for continuous id numbers, meaning that it won't behave properly if you use "non linear" ids with setPage(id, page) instead of the basic addPage(page) (the same goes if you want to skip more than one page, obviously); if that's the case, you'll need a reference to the two page ids:
class VenvWizard(QWizard):
def __init__(self):
super().__init__()
[...]
self.basicSettings = BasicSettings()
self.addPage(self.basicSettings)
self.installId = self.addPage(InstallPackages())
self.summaryId = 20
self.setPage(self.summaryId, Summary())
def nextId(self):
if self.currentPage() == self.basicSettings:
if self.basicSettins.interprComboBox.isChecked():
return self.installId
return self.summaryId
return QtWidgets.QWizard.nextId(self)
PS: please, try to keep examples as minimal as possible. There's really no need to post everything your code contains: it only makes annoying to tell apart between code that's meaningful or useless to the question, discouraging people willing to help from even trying to check out your code.
I am trying to dynamically update a menu with new items when I add new items via the form into Qsettings. For example, if you open my code and click the button and then click "new" it will open a QLineEdit with a button. When the button is clicked the list data gets stored via Qsettings.
I'd like to be able to update the menu somehow to show the items without restarting. I tried some things like calling repaint and update in a few areas with no luck.
Here is my code, I slimmed it down the best that I could for the example.
import functools
import sys
from PyQt5 import QtCore
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QWidget, QPushButton, QHBoxLayout, \
QVBoxLayout, QLineEdit,QApplication, QWidgetAction, QTextBrowser, QAction, QMenu
class MainWindow(QWidget):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.layout = QHBoxLayout()
self.menu_action = QAction(QIcon("icon.png"),"New", self)
self.menu_btn = QPushButton()
self.menu = MyMenu("Menu", self.menu_btn)
self.add_menu = self.menu.addMenu(QIcon("icon.png"), "Menu")
self.add_menu.addAction(self.menu_action)
self.menu_btn.setMenu(self.menu)
self.textBox = QTextBrowser(self)
action = QWidgetAction(self.menu_btn)
action.setDefaultWidget(self.textBox)
self.menu_btn.menu().addAction(action)
settings = QtCore.QSettings('test_org', 'my_app')
self.new_items = settings.value('new_item', [])
print('%s' % self.new_items)
for item in self.new_items:
self.create_action = QAction(QIcon("icon.png"), item[0], self)
self.create_action.setData(item)
self.add_menu.addAction(self.create_action)
self.create_action.triggered.connect(functools.partial(self.menu_clicked, self.create_action))
self.layout.addWidget(self.menu_btn)
self.setLayout(self.layout)
self.menu_action.triggered.connect(self.open_window)
def open_window(self):
self.create_menu_item = Create_Menu_Item()
self.create_menu_item.show()
def menu_clicked(self, item):
itmData = item.data()
print(itmData)
class Create_Menu_Item(QWidget):
def __init__(self, parent=None):
super(Create_Menu_Item, self).__init__(parent)
self.resize(200, 195)
self.layout = QVBoxLayout()
self.form = QLineEdit()
self.btn = QPushButton()
self.layout.addWidget(self.form)
self.layout.addWidget(self.btn)
self.btn.clicked.connect(self.save_new_item)
self.setLayout(self.layout)
def save_new_item(self):
settings = QtCore.QSettings('test_org', 'my_app')
self.new_item = settings.value('new_item', [])
self.new_item.append([self.form.text()])
settings.setValue('new_item', self.new_item)
print(self.new_item)
print('saved!')
class MyMenu(QMenu):
def event(self,event):
if event.type() == QtCore.QEvent.Show:
self.move(self.parent().mapToGlobal(QtCore.QPoint(0,0))-QtCore.QPoint(0,self.height()))
return super(MyMenu,self).event(event)
if __name__ == '__main__':
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec_()
Anyone have any ideas? Thanks.
You can create a for loop to access the list.
Bare in mind it is a list of lists so a nested for loop is neccessary
def save_new_item(self):
settings = QtCore.QSettings('test_org', 'my_app')
self.new_item = settings.value('new_item', [])
self.new_item.append([self.form.text()])
settings.setValue('new_item', self.new_item)
print(self.new_item)
print('saved!')
# Add new menu items..
for x in self.new_item:
for y in x:
print(y)
The above will keep adding the WHOLE list of items every time you add a new item..
To just add the newest item, this is all you need (the last item added to the list)
w.add_menu.addAction(self.new_item[0][-1])
so
def save_new_item(self):
settings = QtCore.QSettings('test_org', 'my_app')
self.new_item = settings.value('new_item', [])
self.new_item.append([self.form.text()])
settings.setValue('new_item', self.new_item)
print(self.new_item)
print('saved!')
#ADD last item in the list
w.add_menu.addAction(self.new_item[0][-1])