Related
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.
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_()
I am struggling to add a side menu to my application.
I have a QMainWindow instance to which I was hoping to add a QDrawer object and achieve an effect similar to this sample.
Unfortunately, it seems that PySide2 only provides QMenu, QTooltip and QDialog widgets which inherit from the Popup class, and QDrawer is nowhere to be found. However, using a Drawer tag in a QML file works just fine. Shouldn't it be possible to also create an instance of QDrawer programmatically?
As another try, I tried to load a Drawer instance from a QML file and attach it to my QMainWindow. Unfortunately I can't quite understand what should I specify as parent, what should I wrap it in, what parameters should I use etc. - any advice would be appreciated (although I would much rather create and configure it programatically).
My goal is to create a QMainWindow with a toolbar, central widget and a QDrawer instance as a side navigation menu (such as in this sample). Can you please share some examples or explain what to do?
One possible solution is to implement a Drawer using Qt Widgets, the main feature is to animate the change of width for example using a QXAnimation, the other task is to set the anchors so that it occupies the necessary height. A simple example is the one shown in the following code:
import os
from PySide2 import QtCore, QtGui, QtWidgets
class Drawer(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedWidth(0)
self.setContentsMargins(0, 0, 0, 0)
# self.setFixedWidth(0)
self._maximum_width = 0
self._animation = QtCore.QPropertyAnimation(self, b"width")
self._animation.setStartValue(0)
self._animation.setDuration(1000)
self._animation.valueChanged.connect(self.setFixedWidth)
self.hide()
#property
def maximum_width(self):
return self._maximum_width
#maximum_width.setter
def maximum_width(self, w):
self._maximum_width = w
self._animation.setEndValue(self.maximum_width)
def open(self):
self._animation.setDirection(QtCore.QAbstractAnimation.Forward)
self._animation.start()
self.show()
def close(self):
self._animation.setDirection(QtCore.QAbstractAnimation.Backward)
self._animation.start()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowFlag(QtCore.Qt.FramelessWindowHint)
central_widget = QtWidgets.QWidget()
self.setCentralWidget(central_widget)
self.tool_button = QtWidgets.QToolButton(
checkable=True, iconSize=QtCore.QSize(36, 36)
)
content_widget = QtWidgets.QLabel(alignment=QtCore.Qt.AlignCenter)
content_widget.setText("Content")
content_widget.setStyleSheet("background-color: green")
lay = QtWidgets.QVBoxLayout(central_widget)
lay.setSpacing(0)
lay.setContentsMargins(0, 0, 0, 0)
lay.addWidget(self.tool_button)
lay.addWidget(content_widget)
self.resize(640, 480)
self.drawer = Drawer(self)
self.drawer.move(0, self.tool_button.sizeHint().height())
self.drawer.maximum_width = 200
self.drawer.raise_()
content_lay = QtWidgets.QVBoxLayout()
content_lay.setContentsMargins(0, 0, 0, 0)
label = QtWidgets.QLabel(alignment=QtCore.Qt.AlignCenter)
label.setText("Content\nDrawer")
label.setStyleSheet("background-color: red")
content_lay.addWidget(label)
self.drawer.setLayout(content_lay)
self.tool_button.toggled.connect(self.onToggled)
self.onToggled(self.tool_button.isChecked())
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.onCustomContextMenuRequested)
#QtCore.Slot()
def onCustomContextMenuRequested(self):
menu = QtWidgets.QMenu()
quit_action = menu.addAction(self.tr("Close"))
action = menu.exec_(QtGui.QCursor.pos())
if action == quit_action:
self.close()
#QtCore.Slot(bool)
def onToggled(self, checked):
if checked:
self.tool_button.setIcon(
self.style().standardIcon(QtWidgets.QStyle.SP_MediaStop)
)
self.drawer.open()
else:
self.tool_button.setIcon(
self.style().standardIcon(QtWidgets.QStyle.SP_MediaPlay)
)
self.drawer.close()
def resizeEvent(self, event):
self.drawer.setFixedHeight(self.height() - self.drawer.pos().y())
super().resizeEvent(event)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.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.
So I'm trying to get a grip on Qt (more specifically, Pyqt), and I want to create a simple feedback form. It should have
a title
a name ('author')
a message
a send and a cancel button
Let's try without the buttons, first (the App class just provides a button to create a popup. the question concerns the Form class below it):
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QMainWindow, QDesktopWidget,\
QHBoxLayout, QVBoxLayout, QGridLayout,\
QPushButton, QLabel,QLineEdit, QTextEdit,\
qApp
from PyQt5.QtGui import QIcon
class App(QMainWindow):
def __init__(self):
super().__init__()
self.title = 'PyQt5 Layout Demo'
self.popup = None
self.initUI()
def initUI(self):
self.setWindowTitle(self.title)
self.setWindowIcon(QIcon('imgs/python3.png'))
formButton = QPushButton("show form")
formButton.clicked.connect(self.showPopup)
formBox = QHBoxLayout()
formBox.addWidget(formButton)
formBox.addStretch(1)
vbox = QVBoxLayout()
vbox.addLayout(formBox)
vbox.addStretch(1)
# self.setLayout(vbox) # would work if this was a QWidget
# instead, define new central widget
window = QWidget()
window.setLayout(vbox)
self.setCentralWidget(window)
self.center(self)
self.show()
#staticmethod
def center(w: QWidget):
qr = w.frameGeometry() # get a rectangle for the entire window
# center point = center of screen resolution
cp = QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp) # move center of rectangle to cp
w.move(qr.topLeft()) # move top-left point of window to top-let point of rectangle
def showPopup(self):
if self.popup is None:
self.popup = Form(self)
self.popup.setGeometry(10, 10, 300, 400)
self.center(self.popup)
self.popup.show()
class Form(QWidget):
def __init__(self, main):
super().__init__()
self.initUI()
self.main = main
def initUI(self):
self.setWindowTitle('Feedback')
self.setWindowIcon(QIcon('imgs/python3.png'))
title = QLabel('Title')
author = QLabel('Author')
message = QLabel('Message')
titleEdit = QLineEdit()
authorEdit = QLineEdit()
messageEdit = QTextEdit()
grid = QGridLayout()
grid.setSpacing(10)
grid.addWidget(title, 1, 0)
grid.addWidget(titleEdit,1, 1)
grid.addWidget(author, 2, 0)
grid.addWidget(authorEdit,2, 1)
grid.addWidget(message, 3, 0)
grid.addWidget(messageEdit, 4, 0, 6, 0)
self.setLayout(grid)
# probably should delegate to self.main, but bear with me
def send(self):
self.main.popup = None
self.hide()
def cancel(self):
self.hide()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
Ok, looks about right. There's a bit too much spacing in-between the line edits and the text edit, but since I want to add some buttons below it, that should be be a problem.
So I add:
sendBtn = QPushButton("send")
cancelBtn = QPushButton("cancel")
sendBtn.clicked.connect(self.send)
cancelBtn.clicked.connect(self.cancel)
grid.addWidget(sendBtn, 7, 1)
grid.addWidget(cancelBtn, 7, 2)
which yields
Now obviously, I forgot to stretch the title and author line edits to the newly introduced column 2. Easy enough to fix but what really bothers me is the placement of the buttons.
WHY do they show up in the middle of the text edit? I can see how Qt chooses the column size, and why that would lead to the buttons' being of different size, but since the tutorial doesn't actually add buttons to the form, I have no idea how to fix that.
I could, of course, simply add boxes:
sendBtn = QPushButton("send")
cancelBtn = QPushButton("cancel")
sendBtn.clicked.connect(self.send)
cancelBtn.clicked.connect(self.cancel)
btns = QHBoxLayout()
btns.addStretch(1)
btns.addWidget(sendBtn)
btns.addWidget(cancelBtn)
l = QVBoxLayout()
l.addLayout(grid)
l.addLayout(btns)
self.setLayout(l)
With which the popup then actually starts looking closer to something acceptable:
But is there a way to fix this within the grid layout, instead?
You seem to have misunderstood the signature of addWidget. The second and third arguments specify the row and column that the widget is placed in, whilst the third and fourth specify the row-span and column-span.
In your example, the problems start here:
grid.addWidget(message, 3, 0)
grid.addWidget(messageEdit, 4, 0, 6, 0)
where you make the text-edit span six rows and zero columns - which I doubt is what you intended. Instead, you probably want this:
grid.addWidget(message, 3, 0, 1, 2)
grid.addWidget(messageEdit, 4, 0, 1, 2)
which will make the message label and text-edit span the two columns created by the title and author fields above.
Now when you add the buttons, they must have a layout of their own, since the top two rows are already determining the width of the two columns. If you added the buttons directly to the grid, they would be forced to have the same widths as the widgets in the top two rows (or vice versa). So the buttons should be added like this:
hbox = QHBoxLayout()
sendBtn = QPushButton("send")
cancelBtn = QPushButton("cancel")
sendBtn.clicked.connect(self.send)
cancelBtn.clicked.connect(self.cancel)
hbox.addStretch()
hbox.addWidget(sendBtn)
hbox.addWidget(cancelBtn)
grid.addLayout(hbox, 5, 0, 1, 2)