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.
Related
I have a QLineEdit widget to enter text (a simple search pattern) that can include spaces.
You can't see the space characters, this is especially obvious for trailing spaces. I know you can set option ShowTabsAndSpaces by calling method setDefaultTextOption on the document of a QTextEdit widget, but is there a way to set this option (or something similar) on a QLineEdit widget?
Method setDefaultTextOption is not available for QLineEdit widgets, so I tried:
item = QLineEdit(value)
option = QTextOption()
option.setFlags(QTextOption.ShowTabsAndSpaces)
item.initStyleOption(option)
But that gives me an exception:
<class 'TypeError'>:
'PySide2.QtWidgets.QLineEdit.initStyleOption' called with wrong argument types:
PySide2.QtWidgets.QLineEdit.initStyleOption(PySide2.QtGui.QTextOption)
Supported signatures:
PySide2.QtWidgets.QLineEdit.initStyleOption(PySide2.QtWidgets.QStyleOptionFrame)
How do I set option ShowTabsAndSpaces on a QLineEdit widget or is there another way to make space characters visible in a QLineEdit widget. I would be happy with only space characters that are visible.
Used the suggestion of #ekhumoro to port https://stackoverflow.com/a/56298439/4459346
This resulted in:
#!/usr/bin/env python3
import sys
import time
from PySide2.QtWidgets import (QLineEdit, QPushButton, QApplication,
QVBoxLayout, QDialog, QMessageBox, QPlainTextEdit, QGridLayout)
from PySide2.QtCore import (Qt, QMimeData)
from PySide2.QtGui import (QTextOption, QFontMetrics)
# source: https://stackoverflow.com/a/56298439/4459346
class SpacesLineEdit(QPlainTextEdit):
def __init__(self, text=None):
if text:
super().__init__(text)
else:
super().__init__()
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setLineWrapMode(QPlainTextEdit.NoWrap)
self.setTabChangesFocus(True)
option = QTextOption()
option.setFlags(QTextOption.ShowTabsAndSpaces)
self.document().setDefaultTextOption(option)
# limit height to one line
self.setHeight(1)
# Stealing the sizeHint from a plain QLineEdit will do for now :-P
self._sizeHint = QLineEdit().sizeHint()
def minimumSizeHint(self):
return self._sizeHint
def sizeHint(self):
return self._sizeHint
def keyPressEvent(self, event):
if event.key() == Qt.Key_Return or event.key() == Qt.Key_Enter:
event.ignore()
return
super().keyPressEvent(event)
def insertFromMimeData(self, source):
text = source.text()
text = text.replace(str('\r\n'), str(' '))
text = text.replace(str('\n'), str(' '))
text = text.replace(str('\r'), str(' '))
processedSource = QMimeData()
processedSource.setText(text)
super().insertFromMimeData(processedSource)
def setText(self, text):
self.setPlainText(text)
def text(self):
return self.toPlainText()
def setHeight1(self):
# see
doc = self.document()
b = doc.begin()
layout = doc.documentLayout()
h = layout.blockBoundingRect(b).height()
self.setFixedHeight(h + 2 * self.frameWidth() + 1)
def setHeight(self, nRows):
# source: https://stackoverflow.com/a/46997337/4459346
pdoc = self.document()
fm = QFontMetrics(pdoc.defaultFont())
lineSpacing = fm.lineSpacing()
print(lineSpacing) # todo:test
margins = self.contentsMargins()
nHeight = lineSpacing * nRows + \
(pdoc.documentMargin() + self.frameWidth()) * 2 + \
margins.top() + margins.bottom()
self.setFixedHeight(nHeight)
class Form(QDialog):
def __init__(self, parent=None):
super(Form, self).__init__(parent)
demoText = " some text with spaces "
self.lineedit0 = QLineEdit(demoText)
self.lineedit1 = SpacesLineEdit(demoText)
self.lineedit2 = QLineEdit(demoText)
self.lineedit3 = QLineEdit(demoText)
layout = QGridLayout()
layout.addWidget(self.lineedit0, 0, 0)
layout.addWidget(self.lineedit1, 1, 0)
layout.addWidget(self.lineedit2, 0, 1)
layout.addWidget(self.lineedit3, 1, 1)
self.setLayout(layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
form = Form()
print('starting app...')
form.show()
sys.exit(app.exec_())
The spaces are now shown, just as I wanted.
The only thing I had to add was a method to set the height. See method setHeight, which I copied from https://stackoverflow.com/a/46997337/4459346.
The original method tries to calculate the exact line height, but seems it doesn't get the height exactly right:
The one on the bottom-left is the QPlainTextEdit, the other three are just qlineEdit widgets.
I can fix the incorrect height by subtracting a few pixels, but I rather not do that without knowing what I'm doing.
Anyone, any suggestions to improve the code in method setHeight?
Here is the sample code:
from PyQt5.QtWidgets import QApplication, QTableWidget, QTableWidgetItem, \
QMainWindow
from PyQt5.QtCore import QSize
import sys
DATA = {
f'col{i}': [f'{i * j}' for j in range(1, 10)] for i in range(1, 10)
}
class Table(QTableWidget):
def __init__(self, d):
m = len(d[next(iter(d))])
n = len(DATA)
super().__init__(m, n)
hor_headers = []
for n, (key, values) in enumerate(DATA.items()):
hor_headers.append(key)
for m, item in enumerate(values):
qtitem = QTableWidgetItem(item)
self.setItem(m, n, qtitem)
self.setHorizontalHeaderLabels(hor_headers)
# the sizeHint works fine if I disable this line
self.setVerticalHeaderLabels(f'row{i}' for i in range(1, m + 2))
self.resizeColumnsToContents()
self.resizeRowsToContents()
# improves the situation but still the window is smaller than the table
def sizeHint(self):
hh = self.horizontalHeader()
vh = self.verticalHeader()
fw = self.frameWidth() * 2
return QSize(
hh.length() + vh.width() + fw,
vh.length() + hh.height() + fw)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle('<TITLE>')
table = Table(DATA)
self.setCentralWidget(table)
# did not work
# self.setFixedSize(self.layout().sizeHint())
def main(args):
app = QApplication(args)
main_win = MainWindow()
main_win.show()
raise SystemExit(app.exec_())
if __name__ == "__main__":
main(sys.argv)
Here is the result:
The 9th row and the 9th column are not shown and there are scroll bars.
If I comment out the self.setVerticalHeaderLabels(f'row{i}' for i in range(1, m + 2)) line then it will work:
How can I perfectly fit the main window to the table widget while having vertical header labels?
As you can see in code comments, I have tried the solutions suggested at python qt : automatically resizing main window to fit content but they are not are not working.
The problem is that when a complex widget like an item view is not yet "mapped", the actual size of its children (headers and scroll bars) is not yet updated. Only when the view is finally shown and possibly added to a layout, then it will resize itself again in order to properly resize its children using updateGeometries.
This means that, until that point, the size of each header is based on its default basic contents (the row number for a vertical header).
The solution is simple: don't use the header size, but their hints, which are computed using the actual text that is going to be displayed:
def sizeHint(self):
hh = self.horizontalHeader()
vh = self.verticalHeader()
fw = self.frameWidth() * 2
return QSize(
hh.length() + vh.sizeHint().width() + fw,
vh.length() + hh.sizeHint().height() + fw)
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_())
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.
I created a few qpushbuttons in a for loop. What I want to do is to access their values (the number which the for loop iterates) by a context menu.
I can create the buttons and with left click, I get their values printed. But when I open a context menu on right click, the triggered command self.value.triggered.connect(partial(self.get_value, number)) only returns the highest value.
What do I have to change in order to get the value by the get_value entry from the context menu?
import sys
from functools import partial
from PyQt5.QtWidgets import QMainWindow, QPushButton, QApplication, QAction, QMenu
from PyQt5.QtCore import Qt
class Main(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.buttons = []
self.value = []
self.context_menu = []
pos_value = [30, 50]
for number in range(0,4):
button_text = "Button " + str(number)
self.buttons.append(QPushButton(button_text, self))
self.buttons[number].move(*pos_value)
self.buttons[number].clicked.connect(self.buttonClicked)
pos_value[0] += 120
self.buttons[number].setContextMenuPolicy(Qt.CustomContextMenu)
self.buttons[number].customContextMenuRequested.connect(partial(self.open_context_menu, number))
self.context_menu.append(QMenu(self))
self.value.append(QAction('Get value ' + str(number), self))
self.context_menu[-1].addAction(self.value[-1])
self.value[-1].triggered.connect(partial(self.get_value, number))
self.setGeometry(300, 300, 600, 150)
self.show()
def buttonClicked(self):
sender = self.sender()
print(sender.text() + ' was pressed')
def get_value(self, value):
print('Value ' + str(value))
def open_context_menu(self, i, point):
self.context_menu[-1].exec_(self.buttons[i].mapToGlobal(point))
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Main()
sys.exit(app.exec_())
EDIT: I found an error in my program, because I have overwritten the variables self.value and self.context_menu. Now this has been fixed, but the error remains.
Any ideas how to fix this?
def open_context_menu(self, i, point):
self.context_menu[-1].exec_(self.buttons[i].mapToGlobal(point))
# ^
# Your bug here