Having arrow keys send clicks when using QListWidget - pyqt

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.

Related

ttk Treeview, get item's bbox after scrolling into view

I am working on an editable tkinter.ttk.Treeview subclass. For editing I need to place the edit widget on top of a choosen "cell" (list row/column). To get the proper coordinates, there is the Treeview.bbox() method.
If the row to be edited is not in view (collapsed or scrolled away), I cannot get its bbox obviously. Per the docs, the see() method is meant to bring an item into view in such a case.
Example Code:
from tkinter import Tk, Button
from tkinter.ttk import Treeview
root = Tk()
tv = Treeview(root)
tv.pack()
iids = [tv.insert("", "end", text=f"item {n}") for n in range(20)]
# can only get bbox once everything is on screen.
n = [0]
def show_bbox():
n[0] += 1
iid = iids[n[0]]
b = tv.bbox(iid)
if not b:
# If not visible, scroll into view and try again
tv.see(iid)
# ... but this still doesn't return a valid bbox!?
b = tv.bbox(iid)
print(f"bbox of item {n}", b)
btn = Button(root, text="bbox", command=show_bbox)
btn.pack(side="bottom")
root.mainloop()
(start, then click the button until you reach an invisible item)
The second tv.bbox() call ought to return a valid bbox, but still returns empty string. Apparently see doesnt work immediately, but enqeues the viewport change into the event queue somehow. So my code cannot just proceed synchronously as it seems.
How to solve this? Can see() be made to work immediately? If not, is there another workaround?
The problem is that even after calling see, the item isn't visible (and thus, doesn't have a bounding box) until it is literally drawn on the screen.
A simple solution is to call tv.update_idletasks() immediately after calling tv.see(), which should cause the display to refresh.
Another solution is to use tv.after to schedule the display of the box (or the overlaying of an entry widget) to happen after mainloop has a chance to refresh the window.
def print_bbox(iid):
bbox = tv.bbox(iid)
print(f"bbox of item {iid}", bbox)
def show_bbox():
n[0] += 1
iid = iids[n[0]]
tv.see(iid)
tv.after_idle(print_bbox, iid)

Tkinter display different frames

UPDATE : The problem was solved by removing the window.mainloop() in my second function.
I'm trying to make a game in Python 3.7 using tkinter.
The game begins with a menu (button-widgets in a frame). Clicking in the 'Play' button should open another menu using a different frame. This second menu should contain a 'back' button to return to the first menu.
Each menu is defined in a function. So to go from the main menu to the play menu I call the function playMenu(window) in the function used as command by the 'Play' button.
It looks like this :
def clickButtonPlay():
menuFrame.grid_remove()
playMenu(window)
menuFrame.grid()
In the play menu, the function used as 'back button' command put an end to the function by destroying its frame and using return.
So the program should get back to the clickButtonPlay() function and show the frame of the main menu back, but instead I get a tkinter error :
_tkinter.TclError: can't invoke "grid" command: application has been destroyed
But my frame menuFrame hasn't been destroyed, just un-grid!
Can anyone help me understand what's wrong with the code or find an easier way to do the same thing?
Thank you very much!
Here's a sample of how my program works:
mainMenu file :
import tkinter as tk
from PlayMenu import playMenu
window = tk.Tk()
window.grid()
def menu(window):
def clickButtonPlay():
menuFrame.grid_remove()
playMenu(window)
menuFrame.grid()
menuFrame = tk.Frame(window)
menuFrame.grid()
background = tk.Label(menuFrame, image= backgroundImage)
background.grid()
playButton = tk.Button(menuFrame, image= playButtonImage[0], command= clickButtonPlay)
playButton.place(relx= 0.5, rely= 0.15)
window.mainloop()
menu(window)
playMenu file :
class MyError(Exception):
pass
def _playMenu(window):
def clickButtonBack():
playMenuFrame.destroy()
raise MyError
playMenuFrame = tk.Frame(window)
playMenuFrame.grid()
background = tk.Label(playMenuFrame, image= backgroundImage)
background.grid()
backButton = tk.Button(playMenuFrame, image= backButtonImage[0], command= clickButtonBack)
backButton.place(relx=0.375, rely=0.8)
window.mainloop()
def playMenu(window):
try:
return _playMenu(window)
except MyError:
return
The problem (or at least a problem) is that you're calling mainloop more than once. Each time you call it, a new infinite loop is created. The new loop won't exit until the main window is destroyed. Once that happens, the previous loop will likely throw errors since the widgets it's managing no longer exist.

tkinker optionmenu not showing chosen result

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.

PyQt: How to get QTableWidgetItem contents while item is being edited?

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?

Separate user interaction from programmical change: PyQt, QComboBox

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_())

Resources