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.
Related
I have a similar problem to Using lambda expression to connect slots in pyqt. In my case, I want to send two different pieces of information. Which button is being clicked (the port number) and the sensor associated with that port number (stored in a QComboBox).
This is what I want to achieve and this works fine:
self.portNumbers[0].clicked.connect(lambda: self.plot(1, self.sensorNames[0].currentText()))
self.portNumbers[1].clicked.connect(lambda: self.plot(2, self.sensorNames[1].currentText()))
self.portNumbers[2].clicked.connect(lambda: self.plot(3, self.sensorNames[2].currentText()))
...
But when I put this in a loop as this:
for i, (portNumber, sensorName) in enumerate(zip(self.portNumbers, self.sensorNames)):
portNumber.clicked.connect(lambda _, x=i + 1, y=sensorName.currentText(): self.plot(x, y))
I get the correct port numbers but the change in the Combo box is not reflected.
Minimum reproducible code:
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox
portNumbers = [None] * 8
sensorNames = [None] * 8
SENSORS = ["Temperature", "Pressure", "Height"]
class MyWidget(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
self.connectButtonsToGraph()
def init_ui(self):
vbox = QVBoxLayout()
h1box = QHBoxLayout()
h2box = QHBoxLayout()
for i, (portNumber, sensorName) in enumerate(zip(portNumbers, sensorNames)):
# Set portNumber as pushButton and list sensorNames in ComboBox
portNumber = QPushButton()
sensorName = QComboBox()
h1box.addWidget(portNumber)
h2box.addWidget(sensorName)
# Give identifier, text info to individual ports and modify the list
portNumberName = "port_" + str(i + 1)
portNumber.setObjectName(portNumberName)
portNumberText = "Port " + str(i + 1)
portNumber.setText(portNumberText)
portNumbers[i] = portNumber
# Add the textual information in PushButton and add modify the list
sensorNameStringName = "portSensorName_" + str(i + 1)
sensorName.setObjectName(sensorNameStringName)
for counter, s in enumerate(SENSORS):
sensorName.addItem("")
sensorName.setItemText(counter, s)
sensorNames[i] = sensorName
vbox.addLayout(h1box)
vbox.addLayout(h2box)
self.setLayout(vbox)
self.show()
def connectButtonsToGraph(self):
for i, (portNumber, sensorName) in enumerate(zip(portNumbers, sensorNames)):
portNumber.clicked.connect(lambda _, x=i + 1, y=sensorName.currentText(): self.plot(x, y))
def plot(self, portNumber, sensorName):
print(portNumber, sensorName)
def run():
app = QApplication([])
mw = MyWidget()
app.exec_()
if __name__ == '__main__':
run()
Thank you, #musicamante, for the explanation. I am just adding it in the answer to mark it as solved if someone has similar issues.
As they mentioned, the value of the keyword argument of lambda is evaluated when it's created. So, creating a reference to sensorName.currentText() used the value of the current text at that time. But creating a reference to the ComboBox itself allowed for getting the value of the text in run-time.
I am working with Python 3.6 and pyqt 4.11. I have two QTreeViews stacked in a Widget, both of them display some batch jobs so each step can expand to show all functions. I want to be able to double click on one line of the tree view and generate a pop up dialogue where I can edit the parameters of the function I double clicked on.
If I connect the double click signal without capturing the position:
self.connect(self.QTreeView, QtCore.SIGNAL('mouseDoubleClickEvent()'),print('OK'))
it works and OK is printed.
However as soon as I try to catch the cursor position nothing happens anymore. I have tried both to connect the whole widget and the treeView to a simple test function. It doesn't work at all, not even OK gets printed.
self.connect(self.QTreeView, QtCore.SIGNAL('mouseDoubleClickEvent(const QPoint &)'),self.showDlg)
def showDlg (self, point):
print ('OK')
treeidx=self.treeview.indexAt(point)
print (treeidx)
A ContextMenu is triggered by right click on the whole Widget and it works
self.QTreeWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.connect(self.QTreeWidget, QtCore.SIGNAL('customContextMenuRequested(const QPoint &)'), self.customMyContextMenu)
But double clicking on the same Widget gives no result
self.connect(self.QTreeWidget, QtCore.SIGNAL('mouseDoubleClickEvent(const QPoint &)'),self.showDlg)
I would like to use the pointer position to know in what leaf of the tree view the changes have to happen, I thought
treeview.indexAt(point)
would be the way to do that, but since my simple function doesn't get called at all, there must be some other problem I don't see.
I find it strange that in your first code is printed "OK", what returns to me is an error because connect also expects a callable, but print('OK') returns None that is not a callable. Besides, mouseDoubleClickEvent is not a signal but an event which reaffirms my strangeness.
Instead you have to use the doubleClicked signal that returns the QModelIndex associated with the item, and to get the position you have to use the QCursor::pos() next to mapFromGlobal() of the viewport() of QTreeView. You must also use the new connection syntax.
from PyQt4 import QtCore, QtGui
def create_model(parent):
model = QtGui.QStandardItemModel(parent)
for i in range(3):
parent_item = QtGui.QStandardItem("Family {}".format(i))
for j in range(3):
child1 = QtGui.QStandardItem("Child {}".format(i * 3 + j))
child2 = QtGui.QStandardItem("row: {}, col: {}".format(i, j + 1))
child3 = QtGui.QStandardItem("row: {}, col: {}".format(i, j + 2))
parent_item.appendRow([child1, child2, child3])
model.appendRow(parent_item)
return model
class Widget(QtGui.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self._tree_view = QtGui.QTreeView()
self._tree_view.setModel(create_model(self))
self._tree_view.expandAll()
lay = QtGui.QVBoxLayout(self)
lay.addWidget(self._tree_view)
self._tree_view.doubleClicked.connect(self.on_doubleClicked)
#QtCore.pyqtSlot("QModelIndex")
def on_doubleClicked(self, ix):
print(ix.data())
gp = QtGui.QCursor.pos()
lp = self._tree_view.viewport().mapFromGlobal(gp)
ix_ = self._tree_view.indexAt(lp)
if ix_.isValid():
print(ix_.data())
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
PySide Version:
from PySide import QtCore, QtGui
def create_model(parent):
model = QtGui.QStandardItemModel(parent)
for i in range(3):
parent_item = QtGui.QStandardItem("Family {}".format(i))
for j in range(3):
child1 = QtGui.QStandardItem("Child {}".format(i * 3 + j))
child2 = QtGui.QStandardItem("row: {}, col: {}".format(i, j + 1))
child3 = QtGui.QStandardItem("row: {}, col: {}".format(i, j + 2))
parent_item.appendRow([child1, child2, child3])
model.appendRow(parent_item)
return model
class Widget(QtGui.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
self._tree_view = QtGui.QTreeView()
self._tree_view.setModel(create_model(self))
self._tree_view.expandAll()
lay = QtGui.QVBoxLayout(self)
lay.addWidget(self._tree_view)
self._tree_view.doubleClicked.connect(self.on_doubleClicked)
#QtCore.Slot("QModelIndex")
def on_doubleClicked(self, ix):
print(ix.data())
gp = QtGui.QCursor.pos()
lp = self._tree_view.viewport().mapFromGlobal(gp)
ix_ = self._tree_view.indexAt(lp)
if ix_.isValid():
print(ix_.data())
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
I have a created a GUI in PyQt5. I have 1 current widget, and others to come soon, that I want to put on my GUI. However, I can't find a way to postion them however I want. I am using things like grid layouts, but they only allow "locked" places i.e. (0,1) (1,0) (1,1), I don't think they allow you to just use things like move() to position the elements.
I have already tried, as mentioned, QGridLayout, QHBoxLayout and QVBoxLayout. I have also tried groups.
import sys
from PyQt5 import QtWidgets
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class Settings():
SCENE_SIZE_X = 1200
SCENE_SIZE_Y = 900
GRID_COUNT_X = 4
GRID_COUNT_Y = 5
GRID_BOX_WIDTH = 100
GRID_BOX_HEIGHT = 100
class Grid(QtWidgets.QGraphicsScene):
def __init__(self):
super().__init__()
self.lines = []
self.scrolled = 0
self.initUI()
def initUI(self):
self.draw_grid(Settings.GRID_COUNT_X, Settings.GRID_COUNT_Y, Settings.GRID_BOX_WIDTH, Settings.GRID_BOX_HEIGHT)
self.set_opacity(1.0)
def draw_grid(self, x_count, y_count, x_size, y_size):
width = x_count * x_size
height = y_count * y_size
self.setSceneRect(0, 0, width, height)
self.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex)
pen = QPen(QColor(0,0,0), 1, Qt.SolidLine)
for x in range(0,x_count+1):
xc = x * x_size
self.lines.append(self.addLine(xc,0,xc,height,pen))
self.addText
for y in range(0,y_count+1):
yc = y * y_size
self.lines.append(self.addLine(0,yc,width,yc,pen))
def set_opacity(self,opacity):
for line in self.lines:
line.setOpacity(opacity)
def delete_grid(self):
for line in self.lines:
self.removeItem(line)
del self.lines[:]
def wheelEvent(self,event):
self.scrolled += event.delta()/90
if(self.scrolled > -30.0):
self.delete_grid()
self.draw_grid(Settings.GRID_COUNT_X, Settings.GRID_COUNT_Y, Settings.GRID_BOX_WIDTH + self.scrolled, Settings.GRID_BOX_HEIGHT + self.scrolled)
else:
self.scrolled = -30.0
class App(QtWidgets.QDialog):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 100, Settings.SCENE_SIZE_X, Settings.SCENE_SIZE_Y)
grid = QtWidgets.QGraphicsView(Grid())
layout = QGridLayout(self)
layout.addWidget(grid, 0, 1)
layout.addWidget(QtWidgets.QPushButton("hello!"), 0, 0)
layout.setRowStretch(2, 1)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
app.setStyle("Fusion")
ex = App()
ex.show()
sys.exit(app.exec_())
I am getting the elements to show up, just not where I want them to be located as I am using layouts.
As I mentioned, I did try groups and they sort of worked. I got everything correct but it looked messy as there were widgets inside of groups, inside of groups:
What is the best way to allow me to position them to, maybe, single pixel precision, i.e. using move()?
Thanks,
Dream
EDIT: the code and the image don't line up becuase it was an old iteration of the code
I am trying to use a for loop to assign methods to the valueChanged signals of QScrollbars to make my code cleaner. However, this does not seem to work correctly. What am I doing wrong?
import sys
from PyQt5.QtWidgets import QScrollBar, QDialog, QVBoxLayout, QApplication
class MainWindow(QDialog):
def __init__(self):
super().__init__()
self.layout = QVBoxLayout(self)
self.scrollbar1 = QScrollBar(self)
self.scrollbar2 = QScrollBar(self)
self.scrollbars = [self.scrollbar1, self.scrollbar2]
self.names = ['scrollbar 1', 'scrollbar 2']
self.layout.addWidget(self.scrollbar1)
self.layout.addWidget(self.scrollbar2)
for scrollbar, name in zip(self.scrollbars, self.names):
print(scrollbar, name)
scrollbar.valueChanged.connect(lambda: self.test(name))
# self.scrollbar1.valueChanged.connect(
# lambda: self.test(self.names[0])
# )
# self.scrollbar2.valueChanged.connect(
# lambda: self.test(self.names[1])
# )
self.show()
def test(self, scrollbar):
print(scrollbar)
if __name__ == '__main__':
app = QApplication(sys.argv)
GUI = MainWindow()
sys.exit(app.exec_())
Assigning the methods "manually" works as expected, i.e. different names are passed on. When using the for loop however, for both scrollbars the same name is printed on a value change.
EDIT: Here is my snap_slider method
old:
def snap_slider(scrollbar):
x = np.modf(scrollbar.value() / scrollbar.singleStep())
if x[0] < 0.5:
scrollbar.setSliderPosition(
int(x[1] * scrollbar.singleStep()))
else:
scrollbar.setSliderPosition(
int((x[1]+1) * scrollbar.singleStep()))
new:
def snap_slider(self):
x = np.modf(self.sender().value() / self.sender().singleStep())
if x[0] < 0.5:
self.sender().setSliderPosition(
int(x[1] * self.sender().singleStep()))
else:
self.sender().setSliderPosition(
int((x[1] + 1) * self.sender().singleStep()))
A few things here, since you are trying to make your code cleaner:
You should prefer the use of the sender() method to a lambda function
It is a good practice to isolate the UI setup in a separate function that you could call elsewhere
Same thing about the show() method: avoid to put it in the __init__ method, since you may need to instanciate a MainWindow without showing it (eg: for testing purposes)
Which would give something like:
import sys
from PyQt5.QtWidgets import QScrollBar, QDialog, QVBoxLayout, QApplication
class MainWindow(QDialog):
def __init__(self):
super().__init__()
self.createWidgets()
def createWidgets(self):
self.layout = QVBoxLayout(self)
self.scrollbar1 = QScrollBar(self)
self.scrollbar2 = QScrollBar(self)
for widget in [self.scrollbar1, self.scrollbar2]:
widget.valueChanged.connect(self.test)
self.layout.addWidget(widget)
def test(self, event):
print(self.sender())
if __name__ == '__main__':
app = QApplication(sys.argv)
GUI = MainWindow()
GUI.show()
sys.exit(app.exec_())
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_())