I have been looking for a treeView widget option where I can select only a single row at a time by clicking it and then de-select it by clicking on it again. I found how to make it so that you can select only one row at a time, but I was unable to find an option that allowed u to click the selected row to de-select it.
Does anyone know how this can be done? Any help would be appreciated.
You can set a custom binding to deselect the item if it's currently selected. If your binding returns the string break, it will stop event propagation and thus prevent the default behavior for double-click.
...
self.tree = ttk.Treeview(...)
self.tree.bind("<1>", self.on_click)
...
def on_click(self, event):
selection = self.tree.selection()
item = self.tree.select_row(event.y)
if item in selection:
self.tree.selection_remove(item)
return "break"
Unfortunately, the only built-in pattern to toggle the selection state of a treeview item is by selecting a different item but you can do a hack of your own.
Related
I'm trying to create a listbox in which pressing 'Enter key' AKA <Return> will
pop up a menu in front of the row selected, like this
I tried using this example but it seems that menu pops up only where the mouse is located
Is what I'm asking even possible to do? If so, how can I get the x,y locations of any row in the listbox?
sadly, I couldn't even provide a pseudo code to explain further because I have no idea what it would even look like, so please pardon my lack of clarity
The curselection method of the listbox will return a list of selected items. The bbox method of the listbox will give the coordinates of an item relative to the listbox itself. You can use winfo_rootx and winfo_rooty to get the absolute coordinates of the listbox on the screen. You can use all of this information to place the menu near the selected item.
For example:
def show_popup(event):
selection = event.widget.curselection()
if selection:
item = selection[0]
rootx = event.widget.winfo_rootx()
rooty = event.widget.winfo_rooty()
itemx, itemy, itemwidth, itemheight = event.widget.bbox(item)
popup_menu.tk_popup(rootx+event.widget.winfo_width()-10, rooty+itemy+10)
When you have a QListView and you click on an item in view, the default behaviour is to cancel selection of (deselect) any existing selected item(s), and then set the item clicked to selected and also to "current item/index".
How might I change things so that clicking like this has no effect on selection, but does set the clicked item to "current item/index"?
NB experimentation shows that the view's selectionChanged slot is called before the view's clicked signal fires. One workaround could therefore be to record the deselected items (available in the selectionChanged slot) and apply selection again to these items on detecting that an (instant) click has been fired. But this would be clunky: is a more elegant way available?
Thanks to musicamante's comment I was able to devise what I wanted:
def mousePressEvent(self, event):
if self.selectionModel() and event.modifiers() == QtCore.Qt.NoModifier:
pos = event.pos()
index = self.indexAt(pos)
if index.isValid():
self.selectionModel().setCurrentIndex(index, QtCore.QItemSelectionModel.Current)
return
super().mousePressEvent(event)
... allowing Ctrl-click, Shift-click, etc. to behave as normal, but tweaking the normal behaviour for unmodified click.
I want to be able to select an item in a Tkinter Listbox using either the left-click or right-click. Is there any way to either bind "<button-3>" to some sort of function that selects an item OR invoke a left-click from the right-click when hovering over the Listbox?
Okay, I figured it out.
First, use the bind command:
self.listBox.bind("<Button-3>", self.rightClick)
Then use selection_clear and selection_set with the function nearest to get the index where the cursor is at, then activate it:
def rightClick(self,event):
self.listBox.selection_clear(0,tk.END)
self.listBox.selection_set(self.listBox.nearest(event.y))
self.listBox.activate(self.listBox.nearest(event.y))
When you want to select an item in a Treeview, you usually use the double-click:
def print_element(event):
print(my_treeview.selection()[0])
my_treeview.bind("<Double-1>", print_element)
Today I tried to do the same but using a single click instead:
my_treeview.bind("<Button-1>", print_element)
But it wouldn't work. The output was just an empty tuple.
I started to search online for an explanation... why is it not working?
EDIT:
My goal was actually to do something every time a treeview item was selected.
I proposed a solution myself using the identify() function of Tkinter
Another user proposed to use the Tkinter callback <ButtonRelease-1> which is much more appropriate
Finally, a third user focused his answer on using the Tkinter callback <<TreeviewSelect>>, which is for sure the best option
The reason it doesn't work the way you expect is because your custom single-click binding happens before the default behavior. So, when your single-click is processed, that happens before an item is selected. The second time you click, your function will print the previously selected item.
If you want to have a function called when an item is selected, you should bind to <<TreeviewSelect>>, which will fire immediately after the user selects an item with a single click or via the keyboard.
The default behavior of a treeview supports selecting multiple items at once, so the following code will print out the text of all of the selected items as a list, even if only a single item is selected. You can, of course, modify this to only print out the first selected item if you so desire.
def print_element(event):
tree = event.widget
selection = [tree.item(item)["text"] for item in tree.selection()]
print("selected items:", selection)
tree.bind("<<TreeviewSelect>>", print_element)
It is because the selection is not set yet when the <Button-1> (it is the same as <ButtonPress-1>, i.e. when the mouse button 1 is pressed and not released) event callback is called.
You should bind on <ButtonRelease-1> or <<TreeviewSelect>> instead as the selection is set when the event callback is being executed.
Why it doesn't work
When you click an item in a treeview, that item is still not in a SELECTED status in the moment the callback is activated. You are changing the status in that very moment.
Using a double-click, the first click change the status, and at the second click you are activating your callback, so the status has already been changed.
How can it work
Kudos to this website
In short,
def print_element(event):
print(my_treeview.identify('item', e.x, e.y))
my_treeview.bind("<Button-1>", print_element)
This time, print_element() will check the coordinates of the mouse, and will discover the selected item check what is under the mouse. Nice and clean!
[I'm using PyQt4, but I think this Qt4 issue is not Python specific.]
I have a QTableWidget. In each row, the first column holds a button. When clicked, the row is removed.
To remove the row, I use removeRow(int row) method, which takes as argument the index of the row. When connecting the signal, I can't know the index of the row because it might change in the meantime (for instance if the first row is removed, all row indexes are changed).
The accepted answer here suggests to pass the callback an instance of a QTableWidgetItem in the line, then get the row number from this item at deletion time.
This would be nice, except none of the elements of the row is a QTableWidgetItem. The elements are the button itself and a few ComboBoxes.
I can't figure out a way around this.
Can I somehow fit one of my elements into a QTableWidgetItem? Should I add a QTableWidgetItem in some sort of hidden column?
Our current implementation uses indexAt(QtGui.qApp.focusWidget()) (see other answer to question mentioned above), which looks like a sorry workaround to me.
If I replace the button with a checkable QTableWidgetItem like this
rm_item = QtGui.QTableWidgetItem()
rm_item.setFlags(QtCore.Qt.ItemIsUserCheckable |
QtCore.Qt.ItemIsEnabled)
I have a QTableWidgetItem I can use to get back to the row index. But I don't know how to catch a "checked" or "clicked" event from it like I do with the button. All I found is the itemClicked signal of QTableWidget, but then I'd have to filter all the other widgets out.
There has to be something obvious I'm missing.
Edit
From what I read here, I could add both a QTableWidgetItem with setItem and a Button widget with setCellWidget to the same cell. This doesn't seem so natural to me, but apparently it works (can't test right now).
I guess I'll do that. Add the Button, plus a dummy QTableWidgetItem on the same cell to pass as a reference to the row.
Is this how it is meant to be?
Edit 2
Or maybe QTableWidget is not the proper Widget and I should be using a Layout, as suggested here.
It seems that using a layout rather than a table is possibly the most "correct" answer, but that may come with it's own difficulties, as seen in my answer to this question:
How to delete widgets from gridLayout
If you want to continue using a table, a somewhat cleaner solution than adding dummy items would be to use a persistent model index:
button = QtGui.QPushButton(text, self.table)
self.table.setCellWidget(row, column, button)
index = QtCore.QPersistentModelIndex(
self.table.model().index(row, column))
button.clicked.connect(
lambda *args, index=index: self.handleButton(index))
def handleButton(self, index):
print('button clicked:', index.row())
if index.isValid():
self.table.removeRow(index.row())
If I understand your question correctly:
def set_button(self, row, col):
# create a push button
btn = QtGui.QPushButton('Remove row')
# connect to action
btn.clicked.connect(self.remove_row)
# set in cell
self.setCellWidget(row, col, btn)
def remove_row(self):
# find what is clicked
clicked = QtGui.qApp.focusWidget()
# position
idx = self.indexAt(clicked.pos())
# remove this row
self.removeRow(idx.row())