I have a handful of QPushButtons which play different .wav files either on mouse click or when a keyboard shortcut (single letter key) is pressed.
I would like to know if there is a way to recognise when the buttons are clicked in a particular sequence, and then play a different sound? The closest I have been able to get so far was using setShortcut to assign a sound to play when a particular key sequence is pressed, but this only works using keys which are not assigned as pushbutton shortcuts.
I am new to Python (and PySide) so I'm not sure whether this is even possible.
What you need is an event that is fired if and only if a sequence of other events has fired in between in the correct order. I don't know of any inbuilt framework in Qt that does it except for pressing sequences of keys. So you must build this for yourself. It's not that difficult, you can for example listen to each button then calling a method with a certain number (the position in the sequence of events you are interested in) then checking that you are having positions in this sequence in strictly increasing order (otherwise reseting) and if you arrive at a certain length firing your own event.
Example:
from functools import partial
from PySide import QtGui
class MyEvent():
def __init__(self):
self.last_level = 0
self.top_level = 3
def update(self, level):
if level == self.last_level + 1:
self.last_level += 1
if level == self.top_level:
print('beep')
self.last_level = 0
else:
if level == 1:
self.last_level = level
else:
self.last_level = 0
app = QtGui.QApplication([])
e = MyEvent()
w = QtGui.QWidget()
l = QtGui.QVBoxLayout(w)
b1 = QtGui.QPushButton('Button 1')
b1.clicked.connect(partial(e.update, 1))
l.addWidget(b1)
b2 = QtGui.QPushButton('Button 2')
b2.clicked.connect(partial(e.update, 3))
l.addWidget(b2)
b3 = QtGui.QPushButton('Button 3')
b3.clicked.connect(partial(e.update, 2))
l.addWidget(b3)
w.show()
app.exec_()
This prints "beep" if buttons 1, 3, 2 are pressed in this order.
Put all the buttons in a button-group, so that activating a button sends an identifier that can be recorded by a central signal handler. The identifiers can then be added together to form a sequence that is looked up in a dictionary of sound files.
Here's a simple demo:
from PySide import QtCore, QtGui
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
layout = QtGui.QGridLayout(self)
self.buttonGroup = QtGui.QButtonGroup(self)
for column in range(3):
identifier = column + 1
button = QtGui.QPushButton('&%d' % identifier, self)
self.buttonGroup.addButton(button, identifier)
layout.addWidget(button, 0, column)
self.edit = QtGui.QLineEdit(self)
self.edit.setReadOnly(True)
layout.addWidget(self.edit, 1, 0, 1, 3)
self.buttonGroup.buttonClicked[int].connect(self.handleButtons)
self._sounds = {
'123': 'moo.wav', '132': 'bark.wav',
'213': 'meow.wav', '231': 'baa.wav',
'312': 'oink.wav', '321': 'quack.wav',
}
self._sequence = ''
def handleButtons(self, identifier):
self._sequence += str(identifier)
if len(self._sequence) == 3:
self.edit.setText(self._sounds.get(self._sequence, ''))
self._sequence = ''
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 300, 300, 100)
window.show()
sys.exit(app.exec_())
Related
In QMdiArea, we can select (point) to the activate subwindow. In my application, I want to select multiple subwindows (maybe using the "Ctrl" button) and set them as active windows (>=2 subwindows) and create a list pointer for them. I am trying to get pointers for more than one subwindow at the same time. Yes, activeSubWindow() gives only one window. But I wonder if I can use somthing like the "Ctrl" button in keyboard to select two subwindows and print the pointers to these subwindows. The idea is to get the widgets inside each subwindow (e.g TextEditor) at the same time to do afterward tasks, e.g., comparison
from PyQt5.QtWidgets import QApplication, QMainWindow, QMdiArea, QAction, QMdiSubWindow, QTextEdit
import sys
class MDIWindow(QMainWindow):
count = 0
def __init__(self):
super().__init__()
self.mdi = QMdiArea()
self.setCentralWidget(self.mdi)
bar = self.menuBar()
file = bar.addMenu("File")
file.addAction("New")
file.addAction("cascade")
file.addAction("Tiled")
file.addAction("selected_subwindows")
file.triggered[QAction].connect(self.WindowTrig)
self.setWindowTitle("MDI Application")
def WindowTrig(self, p):
if p.text() == "New":
MDIWindow.count = MDIWindow.count + 1
sub = QMdiSubWindow()
sub.setWidget(QTextEdit())
sub.setWindowTitle("Sub Window" + str(MDIWindow.count))
self.mdi.addSubWindow(sub)
sub.show()
if p.text() == "cascade":
self.mdi.cascadeSubWindows()
if p.text() == "Tiled":
self.mdi.tileSubWindows()
if p.text()=="selected_subwindows":
"""I want to select multiple subwindows and and set as activate
windows with the "Ctrl" button and return a points fot all active windows"""
print("active windows: ", self.mdi.activeSubWindow())
app = QApplication(sys.argv)
mdi =MDIWindow()
mdi.show()
app.exec_()
Just like with normal window handling, it's not possible to have multiple active sub windows even in an MDI area.
In order to achieve a "multiple selection" system, you need to track the activation state of the sub windows, which can be tricky.
Subwindows can be activated in different ways:
by clicking on their title bar (including any of its buttons);
by clicking on its contained widget;
by programmatically activating it with setActiveSubWindow() (which is similar to selecting a normal window from the task bar);
While Qt provides the aboutToActivate signal, it's not always reliable: it is always emitted even when the top level window gets focus, so there's no direct way to know the reason of the activation.
The same also goes for the windowStateChanged signal (which is emitted after the state has changed).
For your situation, the best approach is mainly based on the mousePressEvent implementation of the subwindow, but also considering the window state changes, because you need to keep track of the current active windows whenever the activation is changed in any other way (by clicking on the widget or by using setActiveSubWindow().
Since mouse events are handled after the window activation is changed, the proper solution is to create a signal for which the emission will be delayed (scheduled), in order to know if the activation was actually achieved by a mouse button press on the subwindow (not on the child widget) and finally check if the Ctrl key was pressed in the meantime.
Please note that the following code is very basic, and you might need to do some adjustments. For instance, it doesn't consider activations for minimized windows (unlike normal windows, a subwindow could be active even if it's minimized), nor considers activations when clicking on any of the window buttons.
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
class SubWindow(QMdiSubWindow):
activated = pyqtSignal(object, bool)
ctrlPressed = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setAttribute(Qt.WA_DeleteOnClose)
self.windowStateChanged.connect(self.delayActivated)
self.activatedTimer = QTimer(
singleShot=True, interval=1, timeout=self.emitActivated)
def delayActivated(self, oldState, newState):
# Activation could also be triggered for a previously inactive top
# level window, but the Ctrl key might still be handled by the child
# widget, so we should always assume that the key was not pressed; if
# the activation is done through a mouse press event on the subwindow
# then the variable will be properly set there.
# Also, if the window becomes inactive due to programmatic calls but
# *after* a mouse press event, the variable has to be reset anyway.
self.ctrlPressed = False
if newState & Qt.WindowActive:
self.activatedTimer.start()
elif not newState and self.activatedTimer.isActive():
self.activatedTimer.stop()
def emitActivated(self):
self.activated.emit(self, self.ctrlPressed)
self.ctrlPressed = False
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.ctrlPressed = event.modifiers() & Qt.ControlModifier
self.activatedTimer.start()
super().mousePressEvent(event)
class MDIWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("MDI Application")
self.activeWindows = []
activeContainer = QWidget()
activeLayout = QVBoxLayout(activeContainer)
activeLayout.setContentsMargins(0, 0, 0, 0)
self.activeList = QListWidget()
# Note: the following "monkey patch" is only for educational purposes
# and done in order to keep the code short, you should *not* normally
# do this unless you really know what you're doing.
self.activeList.sizeHint = lambda: QSize(150, 256)
activeLayout.addWidget(self.activeList)
self.compareBtn = QPushButton('Compare', enabled=False)
activeLayout.addWidget(self.compareBtn)
self.activeDock = QDockWidget('Selected windows')
self.activeDock.setWidget(activeContainer)
self.addDockWidget(Qt.LeftDockWidgetArea, self.activeDock)
self.activeDock.setFeatures(self.activeDock.NoDockWidgetFeatures)
self.mdi = QMdiArea()
self.setCentralWidget(self.mdi)
bar = self.menuBar()
fileMenu = bar.addMenu("File")
self.newAction = fileMenu.addAction("New")
self.cascadeAction = fileMenu.addAction("Cascade")
self.tileAction = fileMenu.addAction("Tiled")
self.compareAction = fileMenu.addAction("Compare subwindows")
fileMenu.triggered.connect(self.menuTrigger)
self.compareBtn.clicked.connect(self.compare)
def menuTrigger(self, action):
if action == self.newAction:
windowList = self.mdi.subWindowList()
if windowList:
count = windowList[-1].index + 1
else:
count = 1
sub = SubWindow()
sub.index = count
sub.setWidget(QTextEdit())
sub.setWindowTitle("Sub Window " + str(count))
self.mdi.addSubWindow(sub)
sub.show()
sub.activated.connect(self.windowActivated)
elif action == self.cascadeAction:
self.mdi.cascadeSubWindows()
elif action == self.tileAction:
self.mdi.tileSubWindows()
elif action == self.compareAction:
self.compare()
def windowActivated(self, win, ctrlPressed):
if not ctrlPressed:
self.activeWindows.clear()
if win in self.activeWindows:
self.activeWindows.remove(win)
self.activeWindows.append(win)
self.activeList.clear()
self.activeList.addItems([w.windowTitle() for w in self.activeWindows])
valid = len(self.activeWindows) >= 2
self.compareBtn.setEnabled(valid)
self.compareAction.setEnabled(valid)
def compare(self):
editors = [w.widget() for w in self.activeWindows]
if len(editors) < 2:
return
it = iter(editors)
oldEditor = next(it)
while True:
try:
editor = next(it)
except:
msg = 'Documents are equal!'
break
if oldEditor.toPlainText() != editor.toPlainText():
msg = 'Documents do not match!'
break
oldEditor = editor
QMessageBox.information(self, 'Comparison result', msg, QMessageBox.Ok)
app = QApplication(sys.argv)
mdi = MDIWindow()
mdi.show()
app.exec_()
Note that I had to make some further changes to your code:
action checking should never be done by string comparison: the style or localization could potentially add mnemonics or text variations to action texts, and you'll never get your action triggered: create proper instance attributes and verify the action by object comparison instead.
the count must be an instance attribute, not a class one: if, for any reason, you have to create multiple instances of the main window, you'll get an inconsistent count; you should also consider the currently existing windows;
you should not specify signal overloads if there are no overloads at all (which is the case of QMenu.triggered) nor create local variables if they are being used only once (and their names are not that long, like self.menuBar());
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 have a column of auto-generated buttons which, if there are too many of, can squash UI elements in the window. Therefore, I want to automatically convert the single column of buttons - nominally inside of a QVBoxLayout referred to as self.main_layout - into a multi-column affair by:
Removing the buttons from self.main_layout
Adding them to alternating new columns represented by QVBoxLayouts
Changing self.main_layout to a QHBoxLayout
Adding the new columns to this layout
My attempt simply results in the buttons staying in a single column but now don't even resize to fill the QSplitter frame they occupy:
app = QApplication(sys.argv)
window = TestCase()
app.exec_()
class TestCase(QMainWindow):
def __init__(self):
super().__init__()
test = QWidget()
self.layout = QVBoxLayout()
test.setLayout(self.layout)
for i in range(10):
temp_btn = QPushButton(str(i))
temp_btn.pressed.connect(self.multi_col)
self.layout.addWidget(temp_btn)
self.setCentralWidget(test)
#pyqtSlot()
def multi_col(self):
cols = [QVBoxLayout(), QVBoxLayout()]
while self.layout.count():
child = self.layout.takeAt(0)
if child.widget():
self.layout.removeItem(child)
cols[0].addItem(child)
cols[1], cols[0] = cols[0], cols[1]
self.layout = QHBoxLayout()
self.layout.addLayout(cols[0])
self.layout.addLayout(cols[1])
Any glaringly obvious thing I'm doing wrong here?
Replacing a layout of a QWidget is not so simple with assigning another object to the variable that stored the reference of the other layout. In a few lines of code you are doing:
self.layout = Foo()
widget.setLayout(self.layout)
self.layout = Bar()
An object is not the same as a variable, the object itself is the entity that performs the actions but the variable is only a place where the reference of the object is stored. For example, objects could be people and variables our names, so if they change our name it does not imply that they change us as a person.
The solution is to remove the QLayout using sip.delete and then set the new layout:
import sys
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import (
QApplication,
QHBoxLayout,
QMainWindow,
QPushButton,
QVBoxLayout,
QWidget,
)
import sip
class TestCase(QMainWindow):
def __init__(self):
super().__init__()
test = QWidget()
self.setCentralWidget(test)
layout = QVBoxLayout(test)
for i in range(10):
temp_btn = QPushButton(str(i))
temp_btn.pressed.connect(self.multi_col)
layout.addWidget(temp_btn)
#pyqtSlot()
def multi_col(self):
cols = [QVBoxLayout(), QVBoxLayout()]
old_layout = self.centralWidget().layout()
while old_layout.count():
child = old_layout.takeAt(0)
widget = child.widget()
if widget is not None:
old_layout.removeItem(child)
cols[0].addWidget(widget)
cols[1], cols[0] = cols[0], cols[1]
sip.delete(old_layout)
lay = QHBoxLayout(self.centralWidget())
lay.addLayout(cols[0])
lay.addLayout(cols[1])
def main():
app = QApplication(sys.argv)
window = TestCase()
window.show()
app.exec_()
if __name__ == "__main__":
main()
I'd like to propose an alternative solution, which is to use a QGridLayout and just change the column of the widgets instead of setting a new layout everytime. The "trick" is that addWidget() always adds the widget at the specified position, even if it was already part of the layout, so you don't need to remove layout items.
Obviously, the drawback of this approach is that if the widgets have different heights, every row depends on the minimum required height of all widgets in that row, but since the OP was about using buttons, that shouldn't be the case.
This has the major benefit that the switch can be done automatically with one function, possibly by setting a maximum column number to provide further implementation.
In the following example the multi_col function actually increases the column count until the maximum number is reached, then it resets to one column again.
class TestCase(QMainWindow):
def __init__(self):
super().__init__()
test = QWidget()
self.layout = QGridLayout()
test.setLayout(self.layout)
for i in range(10):
temp_btn = QPushButton(str(i))
temp_btn.clicked.connect(self.multi_col)
self.layout.addWidget(temp_btn)
self.setCentralWidget(test)
self.multiColumns = 3
self.columns = 1
def multi_col(self):
maxCol = 0
widgets = []
for i in range(self.layout.count()):
item = self.layout.itemAt(i)
if item.widget():
widgets.append(item.widget())
row, col, rSpan, cSpan = self.layout.getItemPosition(i)
maxCol = max(col, maxCol)
if maxCol < self.multiColumns - 1:
self.columns += 1
else:
self.columns = 1
row = col = 0
for widget in widgets:
self.layout.addWidget(widget, row, col)
col += 1
if col > self.columns - 1:
col = 0
row += 1
Note: I changed to the clicked signal, as it's usually preferred against pressed due to the standard convention of buttons (which "accept" a click only if the mouse button is released inside it), and in your case it also creates two issues:
visually, the UI creates confusion, with the pressed button becoming unpressed in a different position (since the mouse button is released outside its actual and, at that point, different geometry);
conceptually, because if the user moves the mouse again inside the previously pressed button before releasing the mouse, it will trigger pressed once again;
My list widget placed inside a window prints out values (0 - 1000) when button1 is pressed. But some of these values are 0's and signify an error. The user is expected to update these values with any other positive integer. Once the value is confirmed by pressing a button2, the list will be updated or an error is shown that it needs to be further updated.
The following is my code.
import Ui_ImageSort # designed using Qt-Designer
from PyQt5 import QtCore
from PyQt5.QtWidgets import QMainWindow, QListWidgetItem
class ImageSort(Ui_ImageSort.Ui_MainWindow, QMainWindow):
def __init__(self):
super(ImageSort, self).__init__()
self.setupUi(self)
self.pushButton_1.clicked.connect(list_view)
self.pushButton_2.clicked.connect(print_scales)
def list_view(self):
self.listWidget.clear()
for i in range(len(self.scale_txt)):
item = QListWidgetItem(str(self.scale_txt[i]))
self.listWidget.addItem(str(i + 1) + ") " + item.text())
def print_scales(self):
self.listWidget.clear()
error = 0
for i in range(len(self.scale_txt)):
if (self.scale_txt[i]==0):
error = 1
if (error ==0):
self.listWidget.addItem("\nThe scale values are confirmed ")
for i in range(len(self.scale_txt)):
print(self.scale_txt[i])
print("\n")
else:
self.listWidget.addItem("\nThe scale values have an error")
self.listWidget.addItem("Press button 1 again to edit")
I would like to edit only the digits the the listwidget prints out. The output will look something like this:
1) 100 microns
2) 200 microns
3) 0 microns
4) 100 microns
The value 0 needs to be corrected. So it should be marked red. And the user can change without affecting the remaining line ("microns"). For example: "0 microns" is invalid, marked in red and should be editable, but after the user replaces it with some other digits, presses button2, then it becomes valid and is, therefore, turned black and is no longer editable.
One possible solution is to use a delegate, and the data is passed through the roles:
from PyQt5 import QtCore, QtGui, QtWidgets
MaskRole = QtCore.Qt.UserRole + 1000
class ListDelegate(QtWidgets.QStyledItemDelegate):
def createEditor(self, parent, option, index):
mask = index.data(MaskRole)
if mask is not None:
editor = QtWidgets.QLineEdit(parent)
editor.setInputMask(mask)
return editor
def setModelData(self, editor, model, index):
if editor.hasAcceptableInput():
text = editor.text()
model.setData(index, text, QtCore.Qt.DisplayRole)
re = QtCore.QRegularExpression(r"(\d+)\) (\d+) microns")
match = re.match(index.data())
color = QtGui.QColor("red")
if match.hasMatch():
val = match.captured(match.lastCapturedIndex())
if val != "0":
color = QtGui.QColor("black")
model.setData(index, color, QtCore.Qt.ForegroundRole)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QListWidget()
delegate = ListDelegate(w)
w.setItemDelegate(delegate)
scales = [100, 200, 0, 500, 0, 300, 0]
for i, scale in enumerate(scales):
item = QtWidgets.QListWidgetItem()
if scale == 0:
item.setData(MaskRole, "{}) 900 micro\\ns".format(i))
item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
item.setForeground(QtCore.Qt.red)
item.setText("{}) {} microns".format(i, scale))
w.addItem(item)
w.show()
sys.exit(app.exec_())
In the example below I am trying to use the selection's of two separate comboboxes as two arguments of a function. If 3 and 4 are selected an output of 12 should be produced and so on. How do I write this to send the first combo selection as the first argument and the second combo selection as the second argument?
At the moment, both of the connections return a 'multiply() takes exactly 2 arguments (1 given)" error because the two comboboxes are not simultaneously but separately connected to the function.
from PyQt4 import QtCore, QtGui
class Ui_MainWindow(QtGui.QMainWindow):
def setupUi(self):
window = QtGui.QMainWindow(self)
window.table = QtGui.QTableWidget()
window.table.setRowCount(2)
window.table.setColumnCount(1)
window.setCentralWidget(window.table)
def multiply(x, y):
return x * y
combo_x = QtGui.QComboBox()
combo_y = QtGui.QComboBox()
for i in range(1, 10):
combo_x.addItem(str(i))
combo_y.addItem(str(i))
combo_x.activated[int].connect(multiply)
combo_y.activated[int].connect(multiply)
window.table.setCellWidget(0, 0, combo_x)
window.table.setCellWidget(1, 0, combo_y)
desired = []
for x in range(1, 10):
for y in range(1, 10):
desired.append(multiply(x, y))
window.show()
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
ui = Ui_MainWindow()
ui.setupUi()
sys.exit(app.exec_())
The problem that you face is that you don't get the values to multiply both from the same event.
The solution is to use a function which gets called on every change and which agregates the values to process from the two (or more) items in the table.
So instead of "sending" the values, the function you call "pulls" them from that table. To this end the table needs of course be visible from outside the function, which can be done by making it a class attribute.
from PyQt4 import QtGui
class Ui_MainWindow(QtGui.QMainWindow):
def setupUi(self):
self.table = QtGui.QTableWidget()
self.table.setRowCount(3)
self.table.setColumnCount(1)
self.setCentralWidget(self.table)
combo_x = QtGui.QComboBox()
combo_y = QtGui.QComboBox()
for i in range(1, 10):
combo_x.addItem(str(i))
combo_y.addItem(str(i ** 2))
combo_x.activated.connect(self.update)
combo_y.activated.connect(self.update)
self.table.setCellWidget(0, 0, combo_x)
self.table.setCellWidget(1, 0, combo_y)
self.table.setCellWidget(2,0,QtGui.QLabel(""))
self.show()
def multiply(self,x,y):
return x*y
def update(self):
x = self.table.cellWidget(0, 0).currentText() #row, col
y = self.table.cellWidget(1, 0).currentText()
result = self.multiply(int(x), int(y))
self.table.cellWidget(2, 0).setText(str(result))
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
ui = Ui_MainWindow()
ui.setupUi()
sys.exit(app.exec_())
So, first of all, I would recommend not defining functions inside setupUI and make the widgets that you want to make use children/attributes of the QMainWindow. Then you could access them in your multiply method. For your answer in particular, I would do the following:
class Ui_MainWindow(QtGui.QMainWindow):
def setupUi(self):
# No Changes made here
window = QtGui.QMainWindow(self)
window.table = QtGui.QTableWidget()
window.table.setRowCount(2)
window.table.setColumnCount(1)
window.setCentralWidget(window.table)
# Make attribute of MainWindow
self.combo_x = QtGui.QComboBox()
self.combo_y = QtGui.QComboBox()
for i in range(1, 10):
self.combo_x.addItem(str(i))
self.combo_y.addItem(str(i))
self.combo_x.activated[int].connect(self.multiply)
self.combo_y.activated[int].connect(self.multiply)
window.table.setCellWidget(0, 0, self.combo_x)
window.table.setCellWidget(1, 0, self.combo_y)
window.show()
def multiply(self):
# Grab Index of comboboxes
x = int(self.combo_x.currentIndex())+1
y = int(self.combo_y.currentIndex())+1
# Multiply
print x * y
Hope this is what you are looking for.