How can I shift select multiple items in TKinter listbox? - python

I am trying to select multiple items from a Listbox, it seems intuitive to press shift while selecting and have a block of items be selected, but there seems to be no build in functionality for this in Tkinter.
So I'm trying to implement it on my own, by registering the shift key and getting the latest selection. But I get stuck trying to figure out the latest selection in the Listbox. listbox.get(ACTIVE) seems to be one behind what I expect.
Here is what I have tried to do so far, I am aware that I need to do more when I know the newest selection but that will come later.
from Tkinter import *
class GUI():
def __init__(self,frame): # Some Init
self.listbox = Listbox(root, height=20, width=51, selectmode=MULTIPLE, exportselection=0, yscrollcommand=yscrollbar.set, xscrollcommand=xscrollbar.set)
# -- Some Grid setup here --
self.listbox.bind("<<ListboxSelect>>", self.selectionCallback)
frame.bind("<Shift_L>", self.shiftCallback)
frame.bind("<KeyRelease-Shift_L>", self.shiftCallback)
def selectionCallback(self,event):
print self.listbox.get(ACTIVE) # This is where im stuck
def shiftCallback(self,event):
if event.type is 2: #KeyPress
self.shift = True
elif event.type is 3: #KeyRelease
self.shift = False
if __name__ == "__main__":
root = Tk()
GUI(root)

The behavior you seem to want actually is available by default, use
Listbox(..., selectmode=EXTENDED, ...)
From effbot:
The listbox offers four different selection modes through the selectmode option. These are SINGLE (just a single choice), BROWSE (same, but the selection can be moved using the mouse), MULTIPLE (multiple item can be choosen, by clicking at them one at a time), or EXTENDED (multiple ranges of items can be chosen, using the Shift and Control keyboard modifiers). The default is BROWSE. Use MULTIPLE to get “checklist” behavior, and EXTENDED when the user would usually pick only one item, but sometimes would like to select one or more ranges of items.
As for listbox.get(ACTIVE), the item that is ACTIVE is the one that is underlined. You can see that this is only updated upon release of the mouse button. Because the <<ListboxSelect>> event is triggered on the mouse press, you get the previously selected item, because the ACTIVE is not updated yet.

Related

PyQt5 Radio Button Tab Stop Dysfunction

I have some programs that use dialog boxes with groups of radio buttons. The expected behavior when initially designating a button with setChecked(True) is that it will be assigned the tab stop for its group. That's how it works in the Windows API. But it appears that the tab stop always goes to the first button in the group (even though it's not checked) until you manually re-select the intended button with a mouse click or arrow key. You can use setFocus(True) on a designated button, but this only can be used for one group of buttons. This happens with Windows 10 and Ubuntu, using PyQt5 versions 5.12 and 5.15. I've tried the various radio button focus-related functions listed in the Qt documentation without success.
This question was asked 2 years ago (53645767/radio-button-tab-sequencing), without an answer that explains definitively either how to set the tab stops, or that it's not an option.
I adapted this script from a Web tutorial. For both groups, I initialize the second button in the group with setChecked(True), and use setFocus(True) on the button in the first group. Cycling with "Tab" shows the tab stop at the second button of the first group as intended, but it stays with the first (unchecked) button of the second group until you click the second button or use the arrow key to re-select it. Am I missing something here or it this an intentional "feature"?
def init_ui(self):
self.label = QLabel('What is your favorite color?')
self.rbtn1 = QRadioButton('blue')
self.rbtn2 = QRadioButton('red')
self.label2 = QLabel("")
self.label3 = QLabel('What is your favorite element?')
self.rbtn3 = QRadioButton('bolognium')
self.rbtn4 = QRadioButton('unobtainium')
self.label4 = QLabel("")
self.btngroup1 = QButtonGroup()
self.btngroup2 = QButtonGroup()
self.btngroup1.addButton(self.rbtn1)
self.btngroup1.addButton(self.rbtn2)
self.btngroup2.addButton(self.rbtn3)
self.btngroup2.addButton(self.rbtn4)
self.rbtn1.toggled.connect(self.onClickedColor)
self.rbtn2.toggled.connect(self.onClickedColor)
self.rbtn3.toggled.connect(self.onClickedElement)
self.rbtn4.toggled.connect(self.onClickedElement)
layout = QVBoxLayout()
layout.addWidget(self.label)
layout.addWidget(self.rbtn1)
layout.addWidget(self.rbtn2)
layout.addWidget(self.label2)
layout.addWidget(self.label3)
layout.addWidget(self.rbtn3)
layout.addWidget(self.rbtn4)
layout.addWidget(self.label4)
self.setGeometry(200, 200, 300, 300)
self.setLayout(layout)
self.rbtn2.setChecked(True)
self.rbtn2.setFocus(True)
self.rbtn4.setChecked(True)
self.show()
def onClickedColor(self):
radioBtn = self.sender()
if radioBtn.isChecked():
self.label2.setText("Your favorite color is " + radioBtn.text())
def onClickedElement(self):
radioBtn = self.sender()
if radioBtn.isChecked():
self.label4.setText("Your favorite element is " + radioBtn.text())
That seems like a bug (btw, the question you linked only shows a similar behavior, but considering it's about HTML I believe it's unrelated).
Unfortunately, "auto-focus" management is pretty complex and is based on various aspects: order of creation, parenthood, focus policies, position of widgets and system behavior.
A possible (and "hacky") solution could be to override focusNextPrevChild().
The trick is to get the current focused widget and cycle through its own nextFocusInChain and previousInFocusChain (by checking its focus policy and recursively until a valid policy is found), and then ensure that the widget is contained in the button group.
Note that the following is a very raw implementation, and you'll probably need further tests in order to get it correctly working and ensure that no bug/recursion issue happens.
def focusNextPrevChild(self, isNext):
if isNext:
func = 'nextInFocusChain'
reason = Qt.TabFocusReason
else:
func = 'previousInFocusChain'
reason = Qt.BacktabFocusReason
current = self.focusWidget()
other = getattr(current, func)()
while True:
while not other.focusPolicy():
other = getattr(other, func)()
if isinstance(other, QRadioButton):
for group in self.btngroup1, self.btngroup2:
if other in group.buttons():
checked = group.checkedButton()
if checked == current and not isNext:
continue
if checked:
checked.setFocus(reason)
else:
other.setFocus(reason)
return True
break
return super().focusNextPrevChild(isNext)

Trying to check multiple qt radio buttons with python

I need to check multiple radio buttons from a qt ui with python.
Up to now we are using something similar to:
if main.ui.radioButton_1.isChecked():
responses["q1"] = "1"
elif main.ui.radioButton_2.isChecked():
responses["q1"] = "2"
elif main.ui.radioButton_3.isChecked():
responses["q1"] = "3"
if main.ui.radioButton_4.isChecked():
responses["q2"] = "1"
elif main.ui.radioButton_5.isChecked():
responses["q2"] = "2"
elif main.ui.radioButton_6.isChecked():
responses["q2"] = "3"
...
Since there are very many buttons and many different categories (q1, q2, ...) I was thinking of optimizing it a bit. So this is what I hoped would work (adopted from How to get the checked radiobutton from a groupbox in pyqt):
for i, button in enumerate(["main.ui.radioButton_" + str(1) for i in range(1, 8)]):
if button.isChecked():
responses["q1"] = str(i - 1)
I get why this doesn't work but writing it I hoped it would.
So I tried to iterate through the buttons using something similar to (Is there a way to loop through and execute all of the functions in a Python class?):
for idx, name, val in enumerate(main.ui.__dict__.iteritems()):
and then use some modulo 3 and such to assign the results. But that doesn't work either. Not sure if it's because i used __ dict __ or something else. The error I got was:
TypeError: 'QLabel' object is not iterable
Now some people could say that implicit is better that explicit and also because of readability the if elif chain is good the way it is but there are 400+ lines of that. Also after reading this post, Most efficient way of making an if-elif-elif-else statement when the else is done the most?, I thought there must be a better and more efficient way of doing this (see examples 3.py and 4.py of the of the accepted answer). Because I need to check the Boolean value of main.ui.radioButton_1.isChecked() and then assign thevalue according to the Buttons group (q1, q2,...), I haven't managed to implement the solution using dictionaries as described in the post.
Am I stuck with the if elif chain or is there a way to not only reduce the LOC but also make the code more efficient (faster)?
It looks like you have used Qt Designer to create your ui, so I would suggest putting each set of radio buttons in a QButtonGroup. This will give you a simple, ready-made API for getting the checked button in a group without having to query each button individually.
In Qt Designer, buttons can be added to a button-group by selecting them, and then choosing Assign to button group > New button group from the context menu. The button IDs (which you will need to use later) are assigned in the order the buttons are selected. So use Ctrl+Click to select each button of a group in the correct order. The IDs start at 1 for each group and just increase by one for each button that is added to that group.
When a new button-group is added, it will appear in the Object Inspector. This will allow you to select it and give it a more meaningful name.
Once you've created all the groups, you can get the checked button of a group like this:
responses["q1"] = str(main.ui.groupQ1.checkedId())
responses["q2"] = str(main.ui.groupQ2.checkedId())
# etc...
This could be simplified even further to process all the groups in a loop:
for index in range(1, 10):
key = 'q%d' % index
group = 'groupQ%d' % index
responses[key] = str(getattr(main.ui, group).checkedId())
Another way to do it is using signals. If you had lots of radio button in an application, I suspect this kind of approach would be noticeably faster. For example:
import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class MoodExample(QGroupBox):
def __init__(self):
super(MoodExample, self).__init__()
# Create an array of radio buttons
moods = [QRadioButton("Happy"), QRadioButton("Sad"), QRadioButton("Angry")]
# Set a radio button to be checked by default
moods[0].setChecked(True)
# Radio buttons usually are in a vertical layout
button_layout = QVBoxLayout()
# Create a button group for radio buttons
self.mood_button_group = QButtonGroup()
for i in xrange(len(moods)):
# Add each radio button to the button layout
button_layout.addWidget(moods[i])
# Add each radio button to the button group & give it an ID of i
self.mood_button_group.addButton(moods[i], i)
# Connect each radio button to a method to run when it's clicked
self.connect(moods[i], SIGNAL("clicked()"), self.radio_button_clicked)
# Set the layout of the group box to the button layout
self.setLayout(button_layout)
#Print out the ID & text of the checked radio button
def radio_button_clicked(self):
print(self.mood_button_group.checkedId())
print(self.mood_button_group.checkedButton().text())
app = QApplication(sys.argv)
mood_example = MoodExample()
mood_example.show()
sys.exit(app.exec_())
I found more information at:
http://codeprogress.com/python/libraries/pyqt/showPyQTExample.php?index=387&key=QButtonGroupClick
http://www.pythonschool.net/pyqt/radio-button-widget/

Don't set a Gtk.TreeView's selection when focusing?

The following code displays a window with a button and tree view. A handle for the 'clicked' signal is attached to the button and focuses the tree view. When the window is initially displayed, the tree selection has no selected items, but when the tree view receives focus, the first item is automatically selected. Is there a way to keep a selection from being made when the tree view receives focus?
Before click, button has focus and tree selection has no selected items. After click, tree view has focus, but an item has been selected.
The issue that arises from this is that I have an interface that keeps some things in sync by attaching to the 'changed' signal on the tree selection of the tree view. When the window is displayed, depending on where the tree views are in the interface, they may receive focus by default. That causes a 'changed' signal, and unexpected synchronization happens. It's possible to call set_can_focus(False) for all the tree views, but that:
only prevents keyboard cycling focus, not programmatic focus, and the selection still turns on with programmatic focus; and
seems to disable the ability to deselect a selection (e.g., by control-clicking on a row).
Similarly I can use grab_default to ensure that something else gets focus first when the window is displayed, but it doesn't keep a stray focus event from making an unexpected selection.
Based on a posted answer that says that says that selection mode SINGLE "requires at least one item to be selected", and that that explains why an element is selected on focus, I looked more into the selection mode constants. Of these, SINGLE and BROWSE seem most relevant. The pygtk documentation, GTK Selection Mode Constants, only says that:
gtk.SELECTION_SINGLE A single selection allowed by clicking.
gtk.SELECTION_BROWSE A single selection allowed by browsing with the pointer.
The GTK+3 documentation, enum GtkSelectionMode, goes into a bit more detail:
GTK_SELECTION_SINGLE Zero or one element may be selected.
GTK_SELECTION_BROWSE Exactly one element is selected. In some
circumstances, such as initially or during a search operation, it’s
possible for no element to be selected with GTK_SELECTION_BROWSE. What
is really enforced is that the user can’t deselect a currently
selected element except by selecting another element.
I don't see anything here to suggest that at least one element must be selected when the selection mode is SINGLE.
Here's code to reproduce the window and serve as an example.
from gi.repository import Gtk
# A ListStore with some words
list_store = Gtk.ListStore(str)
for selection in "Can a machine think?".split():
list_store.append([selection])
# A TreeView with a single column
tree_view = Gtk.TreeView(model=list_store)
cell_renderer = Gtk.CellRendererText()
tree_view_column = Gtk.TreeViewColumn(cell_renderer=cell_renderer,text=0,title='Words')
tree_view.append_column(tree_view_column)
# A button to focus the list
focus = Gtk.Button(label='Focus List')
focus.connect('clicked',lambda *_: tree_view.grab_focus())
# A Box to hold everything, and a Window for the Box.
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
box.add(focus) # button on top gets initial focus
box.add(tree_view) # tree_view below doesn't, and has no selected items
window = Gtk.Window()
window.add(box)
window.show_all()
Gtk.main()
Looking at the source in root/gtk/gtktreeview.c for tree_view.grab_focus(), we can see that gtk_tree_view_focus_to_cursor always gets called, and selects the first element. You can work around this, in some cases, though.
This is a nasty hack.
It overrides the grab_focus method, stores the selection before calling grab_focus, and clears the selection afterwards if there was no selection before.
def tree_view_grab_focus():
selection = tree_view.get_selection()
_, selected = selection.get_selected()
Gtk.TreeView.grab_focus(tree_view)
if selected is None:
selection.unselect_all()
tree_view.grab_focus = tree_view_grab_focus
Unfortunately it only applies when calling grab_focus from Python, other callers (such as GTK's keyboard navigation) don't.

Tkinter listbox change highlighted item programmatically

I have a listbox in Tkinter and I would like to change the item selected programatically when the user presses a key button. I have the keyPressed method but how do I change the selection in the Listbox in my key pressed method?
Because listboxes allow for single vs. continuous vs. distinct selection, and also allow for an active element, this question is ambiguous. The docs explain all the different things you can do.
The selection_set method adds an item to the current selection. This may or may not unselect other items, depending on your selection mode.
If you want to guarantee that you always get just that one item selected no matter what, you can clear the selection with selection_clear(0, END), then selection_set that one item.
If you want to also make the selected item active, also call activate on the item after setting it.
To understand about different selection modes, and how active and selected interact, read the docs.
If you need ListboxSelect event to be also triggered, use below code:
# create
self.lst = tk.Listbox(container)
# place
self.lst.pack()
# set event handler
self.lst_emails.bind('<<ListboxSelect>>', self.on_lst_select)
# select first item
self.lst.selection_set(0)
# trigger event manually
self.on_lst_select()
# event handler
def on_lst_select(self, e = None):
# Note here that Tkinter passes an event object to handler
if len(self.lst.curselection()) == 0:
return
index = int(self.lst.curselection()[0])
value = self.lst.get(index)
print (f'new item selected: {(index, value)}')

Make Tkinter.Listbox selection persist

I have a program where I need to take a selection from Tkinter.Listbox and an entry field and do something with that data. However, if I highlight any text within the entry field (i.e., to delete previous entry), the Listbox selection is cleared. How can I overcome it so that the Listbox selection persists?
import Tkinter as tk
master = tk.Tk()
listbox = tk.Listbox(master)
listbox.grid(row=0, column=0)
items = ['a', 'b', 'c']
for item in items:
listbox.insert(tk.END, item)
efield = tk.Entry(master)
efield.grid(row=1, column=0)
tk.mainloop()
Steps to reproduce:
Type something in the entry field.
Select something in the listbox.
Highlight whatever you entered in the entry field => selection in the listbox gets cleared.
This related question with similar issue How to select at the same time from two Listbox? suggests to use exportselection=0, which doesn't seem to work for me. In such case selection = listbox.selection_get() throws an error while the right line is still highlighted.
I know this is an old question, but it was the first google search result when I came across the same problem. I was seeing odd behavior using 2 list boxes when using selection_get() and also the selection persistence issue.
selection_get() is a universal widget method in Tkinter, and was returning selections that were last made in other widgets, making for some really strange behavior. Instead, use the ListBox method curselection() which returns the selected indices as a tuple, then you can use the ListBox's get(index) method to get the value if you need.
To solve the persistence issue, set exportselection=0 when instantiating the ListBox instance.
list_box = tk.Listbox(master, exportselection=False)
...
selected_indices = list_box.curselection()
first_selected_value = list_box.get(selected_indices[0])
As for now, I wasn't able to cleanly overcome the problem. One way around is to create a variable which will store the selected list value on click:
selected = None
def onselect(e):
global selected
selected = listbox.selection_get()
listbox.bind('<<ListboxSelect>>', onselect)
This doesn't keep the highlight, but the selection is now stored in a variable and can be used further.

Categories

Resources