In the end, the problem I'm trying to solve is that of someone editing a field in a QTableWidget and then clicking "OK" before hitting the enter key or changing focus out of the table cell.
Default behavior seems to be to ignore this cell, as it hasn't "committed".
Here's a quick example:
#!/usr/bin/env python
import sys
import pprint
from PyQt4 import QtCore,QtGui
class Dialog(QtGui.QDialog):
def __init__(self,parent=None):
super(Dialog,self).__init__(parent)
self.table = QtGui.QTableWidget(5,2)
button_box = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok|QtGui.QDialogButtonBox.Cancel)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
layout = QtGui.QVBoxLayout()
layout.addWidget(self.table)
layout.addWidget(button_box)
self.setLayout(layout)
def accept(self):
ret = {}
for i in range(self.table.rowCount()):
k = self.table.item(i,0)
v = self.table.item(i,1)
if not k:
continue
if k.text().isEmpty():
continue
if not v:
v = QtGui.QTableWidgetItem("")
ret[str(k.text())] = str(v.text())
pprint.pprint(ret)
def main():
app = QtGui.QApplication(sys.argv)
main = Dialog()
main.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
In this example, if I enter a in the first cell then b in the second cell; then click "OK" without first hitting the enter key or changing focus, I will see printed:
{'a': ''}
When I want to see:
{'a': 'b'}
An idea I had was to treat the cell like a QLineEdit and use textChanged to see when the user was typing, and then, behind the scenes, setItem of the cell with each key stroke -- the idea being that the data in the cell is always up to date. I attempted this by using QStyledItemDelegate (below) so that it edits like a QLineEdit (which has a textChanged signal). This works to some degree, as I can print out the changes from the delegate itself, but I can't seem to get the textChanged signal anywhere it's useful (in other words, the dialog doesn't see this, therefore it can't setItem in the table).
class LineEditDelegate(QtGui.QStyledItemDelegate):
textChanged = QtCore.pyqtSignal(str)
def createEditor(self, parent, option, index):
editor = QtGui.QLineEdit(parent)
editor.textChanged.connect(self.textChanged)
return editor
But that's not doing the trick.
I also tried emitting a commitData signal when the QLineEdit's textChanged fires, but that also has not helped.
Is there a way to get cell contents while the cell is still being edited?
Related
I have a QListWidget which is populated by QLabel via .setItemWidget() and a drag and drop mode InternalMove, when I move an item inside the list its label disappears.
How can I solve this issue?
A minimal example to reproduce
from PyQt5.QtWidgets import (
QApplication, QLabel, QStyle,
QListWidget, QListWidgetItem
)
from PyQt5.QtCore import QSize
import sys
if __name__ == '__main__':
app = QApplication(sys.argv)
list = QListWidget()
list.setFixedHeight(400)
list.setDragDropMode(QListWidget.DragDropMode.InternalMove)
for _ in range(8):
item = QListWidgetItem()
item.setSizeHint(QSize(40, 40))
list.addItem(item)
label = QLabel()
label.setPixmap(list.style().standardIcon(
QStyle.StandardPixmap.SP_ArrowUp).pixmap(QSize(40,40)))
list.setItemWidget(item, label)
list.show()
sys.exit(app.exec())
edit
After reading the documentation for the .setItemWidget() which states:
This function should only be used to display static content in the place of a list widget item. If you want to display custom dynamic content or implement a custom editor widget, use QListView and subclass QStyledItemDelegate instead.
I wonder if this is related to the issue and what does "static content" mean in this context, is QLabel considered "dynamic content"?
edit #2
The problem is inside a dropEvent() a dropMimeData() is called which in turn creates a complete new item? (rowsInserted is called), which isn't supposed to happen for self items I guess, because a widget set in the dragged item isn't serialized and stored inside mimedata so the widget is decoupled, The dropMimeData() is usually called when you drag and drop items from a different list.
So I guess an ugly way to solve this is to store a manually serialized widget inside a QListWidget.mimeData() as a custom mimetype via QMimeData.setData() and recreate the widget after a drop inside QListWidget.dropMimeData().
for example:
from PyQt5.QtWidgets import (
QApplication, QLabel, QStyle,
QListWidget, QListWidgetItem
)
from PyQt5.QtCore import QSize, QMimeData, QBuffer, QIODevice
from PyQt5.QtGui import QPixmap
import pickle
import sys
class ListWidget(QListWidget):
def mimeData(self, items:list[QListWidgetItem]) -> QMimeData:
mimedata = QListWidget.mimeData(self, items)
# e.g. serialize pixmap
custommime = []
for item in items:
label:QLabel = self.itemWidget(item)
buff = QBuffer()
buff.open(QIODevice.OpenModeFlag.WriteOnly)
label.pixmap().save(buff, 'PNG')
buff.close()
custommime.append(buff.data())
mimedata.setData('application/custommime', pickle.dumps(custommime))
#
return mimedata
def dropMimeData(self, index:int, mimedata:QMimeData, action) -> bool:
result = QListWidget.dropMimeData(self, index, mimedata, action)
# e.g. recreate pixmap
if mimedata.hasFormat('application/custommime'):
for i, data in enumerate(
pickle.loads(mimedata.data('application/custommime')),
start=index):
pixmap = QPixmap()
pixmap.loadFromData(data, 'PNG')
label = QLabel()
label.setPixmap(pixmap)
self.setItemWidget(self.item(i), label)
#
return result
if __name__ == '__main__':
app = QApplication(sys.argv)
list = ListWidget()
list.setFixedHeight(400)
list.setDragDropMode(QListWidget.DragDropMode.InternalMove)
list.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection)
for i in range(8):
item = QListWidgetItem()
item.setSizeHint(QSize(40, 40))
list.addItem(item)
label = QLabel()
label.setPixmap(list.style().standardIcon(
QStyle.StandardPixmap.SP_DialogOkButton + i).pixmap(QSize(40,40)))
list.setItemWidget(item, label)
list.show()
sys.exit(app.exec())
This is caused by a Qt bug which only affects fairly recent versions. I can consistently reproduce it when using Qt-5.15.6 and Qt-6.4.0 - but not e.g. Qt-5.12.1. The issue seems to be closely related to QTBUG-100128.
UPDATE:
Unfortunately, after some further experimentation today, it seems the suggested work-around given below isn't an effective solution. I have found it's also possible to make item-widgets disappear by drag and drop onto non-empty areas.
After testing some other versions of Qt5, I can confirm that the bug is completely absent in 5.12.x, 5.13.x, 5.14.x, 5.15.0 and 5.15.1. This agrees with the existing Qt bug report above which identified Qt-5.15.2 as the version where the bug was introduced.
Contrary to what is suggested in the question, there's no reason whatsoever why a label should not be used as an item-widget. The term "static content", just means "not updated by user-defined custom drawing".
UPDATE 2:
This bug seems to be a regression from QTBUG-87057, which made quite a large number of internal changes to how list-view rows are moved during drag and drop. The complexity of those change probably means a simple work-around that undoes its negative side-effects probably isn't possible. The changes affect all Qt5 versions greater than 5.15.1 and Qt6 versions greater than 6.0.
AFAICS, this only affects dragging and dropping the current last item in the view onto a blank area. Other items and multiple selections aren't affected. This suggests the following work-around:
class ListWidget(QListWidget):
def dropEvent(self, event):
if (self.currentRow() < self.count() - 1 or
self.itemAt(event.pos()) is not None):
super().dropEvent(event)
list = ListWidget()
...
or using an event-filter:
class Monitor(QObject):
def eventFilter(self, source, event):
if event.type() == QEvent.Drop:
view = source.parent()
if (view.currentRow() == view.count() - 1 and
view.itemAt(event.pos()) is None):
return True
return super().eventFilter(source, event)
monitor = Monitor()
list = QListWidget()
list.viewport().installEventFilter(monitor)
...
from tkinter import *
from tkinter.ttk import Combobox
v1=[]
root = Tk()
root.geometry('500x500')
frame1=Frame(root,bg='#80c1ff',bd=5)
frame1.place(relx=0.5,rely=0.1,relwidth=0.75,relheight=0.1,anchor='n')
lower_frame=Frame(root,bg='#80c1ff',bd=10)
lower_frame.place(relx=0.5,rely=0.25,relwidth=0.75,relheight=0.6,anchor='n')
v=[]
def maincombo():
Types=["MA","MM","MI","SYS","IN"]
combo1=Combobox(frame1,values=Types)
combo1.place(relx=0.05,rely=0.25)
combo2=Combobox(frame1,values=v)
combo2.bind('<<ComboboxSelected>>', combofill)
combo2.place(relx=0.45,rely=0.25)
def combofill():
if combo1.get()=="MA":
v=[1,2,3,45]
combo2=Combobox(frame1,values=v)
combo2.place(relx=0.45,rely=0.25)
if combo1.get()=="MM":
v=[5,6,7,8,9]
combo2=Combobox(frame1,values=v)
combo2.place(relx=0.45,rely=0.25)
maincombo()
root.mainloop()
I want to populate the one combobox based on selection of other combobox I,e types.But failed to do so with simple functions.
Looking at you code, most of what you need is already there. The changes I have made are as follows:
Bound to combo1 rather than combo2 (as combo1 is the one you want to monitor)
Set combo1 and combo2 as global variables (so they can be used in the combofill method)
Set the combofill method to accept the event arg (it would raise a TypeError otherwise)
Use the .config method on combo2 rather than creating a new one each time
Set combo2 to be empty when neither "MA" or "MM" are selected
Here is my implementation of that:
from tkinter import *
from tkinter.ttk import Combobox
v1=[]
root = Tk()
root.geometry('500x500')
frame1=Frame(root,bg='#80c1ff',bd=5)
frame1.place(relx=0.5,rely=0.1,relwidth=0.75,relheight=0.1,anchor='n')
lower_frame=Frame(root,bg='#80c1ff',bd=10)
lower_frame.place(relx=0.5,rely=0.25,relwidth=0.75,relheight=0.6,anchor='n')
v=[]
def maincombo():
global combo1, combo2
Types=["MA","MM","MI","SYS","IN"]
combo1=Combobox(frame1,values=Types)
combo1.place(relx=0.05,rely=0.25)
combo1.bind('<<ComboboxSelected>>', combofill)
combo2=Combobox(frame1,values=v)
combo2.place(relx=0.45,rely=0.25)
def combofill(event):
if combo1.get()=="MA":
v=[1,2,3,45]
elif combo1.get()=="MM":
v=[5,6,7,8,9]
else:
v=[]
combo2.config(values=v)
maincombo()
root.mainloop()
A couple other ideas for potential future consideration:
I would recommend using the grid manager rather than the place manager as it will stop widgets overlapping, etc. (on my system, combo2 slightly covers combo1)
Use a dictionary rather than if ... v=... elif ... v= ... and then use the get method so you can give the default argument. For example:
v={"MA": [1,2,3,45],
"MM": [5,6,7,8,9]}. \
get(combo1.get(), [])
EDIT:
Responding to the question in the comments, the following is my implementation of how to make a "toggle combobox" using comma-separated values as requested.
As the combobox has already overwritten the value of the text area when our <<ComboboxSelected>> binding is called, I had to add a text variable trace so we could keep track of the previous value of the text area (and therefore append the new value, etc.). I am pretty sure that explanation is completely inadequate so: if in doubt, look at the code!
from tkinter import *
from tkinter.ttk import Combobox
root = Tk()
def log_last():
global last, cur
last = cur
cur = tx.get()
def append_tx(event):
if last:
v = last.split(",")
else:
v = []
v = list(filter(None, v))
if cur in v:
v.remove(cur)
else:
v.append(cur)
tx.set(",".join(v))
combo.selection_clear()
combo.icursor("end")
last, cur = "", ""
tx = StringVar()
combo = Combobox(root, textvariable=tx, values=list(range(10)))
combo.pack()
combo.bind("<<ComboboxSelected>>", append_tx)
tx.trace("w", lambda a, b, c: log_last())
root.mainloop()
import tkinter
window = tkinter.Tk()
def abc(event):
ans=0
numberss=['7','8','9']
omenu2['menu'].delete(0, 'end')
for number in numberss:
omenu2['menu'].add_command(label=numberss[ans], command=efg)
ans=ans+1
def efg(event=None):
print('yee')
numbers = ['1','2', '3']
number=['4','5','6']
var = tkinter.StringVar(window)
var1 = tkinter.StringVar(window)
omenu = tkinter.OptionMenu(window, var, *numbers, command = abc)
omenu.grid(row=1)
omenu2 = tkinter.OptionMenu(window, var1, *number, command = efg)
omenu2.grid(row=2)
after you have entered the first option menu, it will update the second one. when you enter data into the second one, it runs the command, but doesn't show you what you entered. i do not want to include a button, and i know that the command works and not on the second
i found some code that changed the options of the second menu, however when i ran this, the command wouldn't work as it was changed to tkinter.setit (i would also like to know what is does. i do not currently understand it)
omenu2['menu'].add_command(label=numberss[ans], command=tkinter._setit(var1, number))
this has been taken from a larger piece of code, and has thrown the same error
You should set your StringVar(var1) new value.
def abc(event):
numberss=['7','8','9']
omenu2['menu'].delete(0, 'end')
for number in numberss:
omenu2['menu'].add_command(label=number, command=lambda val=number: efg(val))
def efg(val, event=None):
print('yee')
var1.set(val)
You are using for loop so you don't need ans(at least not in this code) since it iterates over items themselves.
I've worked through the excellent Matplotlib GUI tutorial found at: http://blog.rcnelson.com/building-a-matplotlib-gui-with-qt-designer-part-1/. This program uses a QListWidget to select plots to show. Everything works correctly but I have one additional need. Once a item in the list is selected you can select the next or previous item with the arrow keys. The next or previous item is highlighted. What I want is a means to trigger the same event that is triggered by clicking. The click event is handled by the following code:
self.mplfigs.itemClicked.connect(self.changefig)
I've tried the following and neither works:
self.mplfigs.itemEntered.connect(self.changefig)
self.mplfigs.currentRowChanged.connect(self.changefig)
Much Google searching hasn't helped so any hints are very welcome.
You probably need to use itemSelectionChanged signal, in your case self.mplfigs.itemSelectionChanged.connect(self.changefig) should trigger the function, I don't have the full code but that should work and please take look here
Adding a minimal working example:
from PyQt4.QtGui import *
from PyQt4.QtCore import *
import sys
class myListWidget(QListWidget):
def Clicked(self,item=None):
if not item:
item = self.currentItem()
QMessageBox.information(self, "ListWidget", "You clicked: "+item.text())
def main():
app = QApplication(sys.argv)
listWidget = myListWidget()
#Resize width and height
listWidget.resize(300,120)
listWidget.addItem("Item 1");
listWidget.addItem("Item 2");
listWidget.addItem("Item 3");
listWidget.addItem("Item 4");
listWidget.setWindowTitle('PyQT QListwidget Demo')
listWidget.itemSelectionChanged.connect(listWidget.Clicked)
listWidget.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Original code is take from here
#Achayan got me very close. Here is what solved the problem. Using the following line is as was suggested:
self.mplfigs.itemSelectionChanged.connect(self.changefig)
I needed to change changefig from:
def changefig(self, item):
text = item.text()
self.rmmpl()
self.addmpl(self.fig_dict[text])
To:
def changefig(self, item=None):
if not item:
item = self.mplfigs.currentItem()
text = item.text()
self.rmmpl()
self.addmpl(self.fig_dict[text])
Unlike itemClicked, itemSelectionChanged doesn't emit the item so the extra if statement was needed to get the particular item necessary within changefig.
However, the following line of code seems to work without modifying changefig.
self.mplfigs.currentItemChanged.connect(self.changefig)
Evidently currentItemChanged emits the item like itemClicked does.
I have several QComboBoxes in my PyQt4/Python3 GUI and they are filled with some entries from a database during the initialisation. Initial CurrentIndex is set to 0. There is also a tick box which changes the language of the items in my combo boxes. To preserve current user selection I backup index of the current item and setCurrentIndex to this number after I fill in ComboBox with translated items. All those actions emit currentIndexChanged signal.
Based on the items selected in QComboBoxes some plot is displayed. The idea is to redraw the plot online - as soon as the user changes any of ComboBox current item. And here I have a problem since if I redraw the plot every time signal currentIndexChanged is emited, I redraw it also several times during initialization and if the translation tick box selection was changed.
What is the best way to separate these cases? In principle I need to separate programmical current Index Change from the user, and update the plot only in the later case (during GUI initialisation I can programically call update plot function once). Should I write/rewrite any signal? If so, I never did that before and would welcome any hint or a good example. Use another signal? Or maybe there is a way to temporary block all signals?
There are a few different things you can try.
Firstly, you can make sure you do all your initialization before you connect up the signals.
Secondly, you could use the activated signal, which is only sent whenever the user selects an item. (But note that, unlike currentIndexChanged, this signal is sent even if the index hasn't changed).
Thirdly, you could use blockSignals to temporarily stop any signals being sent while the current index is being changed programmatically.
Here's a script that demonstrates these possibilities:
from PyQt4 import QtGui, QtCore
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
layout = QtGui.QVBoxLayout(self)
self.combo = QtGui.QComboBox()
self.combo.setEditable(True)
self.combo.addItems('One Two Three Four Five'.split())
self.buttonOne = QtGui.QPushButton('Change (Default)', self)
self.buttonOne.clicked.connect(self.handleButtonOne)
self.buttonTwo = QtGui.QPushButton('Change (Blocked)', self)
self.buttonTwo.clicked.connect(self.handleButtonTwo)
layout.addWidget(self.combo)
layout.addWidget(self.buttonOne)
layout.addWidget(self.buttonTwo)
self.changeIndex()
self.combo.activated['QString'].connect(self.handleActivated)
self.combo.currentIndexChanged['QString'].connect(self.handleChanged)
self.changeIndex()
def handleButtonOne(self):
self.changeIndex()
def handleButtonTwo(self):
self.combo.blockSignals(True)
self.changeIndex()
self.combo.blockSignals(False)
def changeIndex(self):
index = self.combo.currentIndex()
if index < self.combo.count() - 1:
self.combo.setCurrentIndex(index + 1)
else:
self.combo.setCurrentIndex(0)
def handleActivated(self, text):
print('handleActivated: %s' % text)
def handleChanged(self, text):
print('handleChanged: %s' % text)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())