Select from Listbox populated by selecting from another Listbox - python

I have two Listboxes. Clicking on an item of the first Listbox inserts information in the second one.
When I click on one of the inserted items, I get an error.
The listboxes are defined as:
list_1 = Listbox(root,selectmode=SINGLE)
list_2 = Listbox(root,selectmode=SINGLE)
To get the selected item:
list_1.bind('<<ListboxSelect>>',CurSelect)
which refers to:
def CurSelect(evt):
list_2.delete(0,END)
selected = list_1.get(list_1.curselection())
for i in range(2):
list_2.insert(END,i)
Clicking on one item of list_1 inserts items in list_2.
If I select an item of List_2 this appears:
Exception in Tkinter callback
Traceback (most recent call last):
File "C:\Users\XXX\Anaconda3\lib\tkinter\__init__.py", line 1699, in __call
__
return self.func(*args)
File "unbenannt.py", line 28, in CurSelect
selected = liste.get(liste.curselection())
File "C:\Users\XXX\Anaconda3\lib\tkinter\__init__.py", line 2792, in get
return self.tk.call(self._w, 'get', first)
_tkinter.TclError: bad listbox index "": must be active, anchor, end, #x,y, or a
number
I had this kind of problem when selecting in the first Listbox, but solved it with << ListboxSelect>>.
Previously clicking items in the second Listbox worked, although I didn't change anything since then.
Full Code Example:
from tkinter import *
class Code():
def __init__(self):
Code.Window(self)
def Window(self):
root = Tk()
scrollbar = Scrollbar(root)
scrollbar.grid(row=4,rowspan=3,column=1,sticky=N+S+W)
liste = Listbox(root,selectmode=SINGLE,width=12,yscrollcommand=scrollbar.set)
liste.grid(row=4,rowspan=3,column=0)
for j in range(2):
liste.insert(END,j+5)
scrollbar.config(command=liste.yview)
scrollbar_2 = Scrollbar(root)
scrollbar_2.grid(row=4,rowspan=3,column=3,sticky=N+S+W)
eintrag = Listbox(root,selectmode=SINGLE,yscrollcommand=scrollbar_2.set)
eintrag.grid(row=4,rowspan=3,column=2,sticky=W)
def CurSelect(evt):
eintrag.delete(0,END)
selected = liste.get(liste.curselection())
for i in range(2):
eintrag.insert(END,str(i)+str(selected))
liste.bind('<<ListboxSelect>>',CurSelect)
root.mainloop()
Code()
This example doesn't make anything useful, but the problem appears anyway.

The problem is that by default only one listbox can have a selection at a time. When you select something in the second listbox, the selection is removed from the first listbox. When that happens, your binding fires but your bound function assumes that liste.curselection() returns a non-empty string.
The simplest solution is to allow both listboxes to have a selection at the same time. You do that by setting the exportselection attribute to False:
liste = Listbox(..., exportselection=False)
...
eintrag = Listbox(..., exportselection=False)

The error raises because when the right-hand-side list box, eintrag, gets focus, any item selected in the left-hand-side list box,liste, gets deselected and that invokes the event callback for '<<ListboxSelect>>' in which it is assumed that liste.curselection() is never empty, which is untrue in this particular case and thus liste.get(liste.curselection()) throws the error as it tries to get an item that is in the index of "".
Nesting entire event handler with an if resolves the issue:
def CurSelect(evt):
if liste.curselection(): # To call below statements only when liste has a selected item
eintrag.delete(0,END)
selected = liste.get(liste.curselection())
for i in range(2):
eintrag.insert(END,str(i)+str(selected))

Related

multiple lists with the same callback function lead to IndexError

I'm using tkinter for GUI and I create two lists:
# widgets
create_list(win, 20, 20, ["Test", "Apfel", "Birne"])
create_list(win, 220, 20, ["Alpha", "Beta", "Gamma", "Delta"])
where create_list is a function:
def create_list(win, xx, yy, items=\[\]):
lb = Listbox(win)
i = 1
for item in items:
lb.insert(i, item)
i += 1
lb.bind('<<ListboxSelect>>', on_select)
lb.place(x=xx, y=yy)
Nothing special so far.
I have one on_select function as callback for selection changes for both lists.
def on_select(event):
w = event.widget
index = int(w.curselection()[0])
value = w.get(index)
print(f'You selected item {index}: {value}')`
it works as expected, prints me the selected item.
However if I click an item from the second list (and wise versa) I get the error:
Exception in Tkinter callback Traceback (most recent call last):
File "/usr/lib/python3.8/tkinter/init.py", line 1892, in call
return self.func(*args) File
"/home/userx/projects/python/modules/ws_list.py", line 8, in on_select
index = int(w.curselection()[0]) IndexError: tuple index out of range
If I then select another item in the same list, the issue is gone.
How am I solving that? Do I need a different callback function for each list?
According to this answer, the <<ListboxSelect>> event will be fired if selection is removed from a listbox, which is what happens when you switch between lists. This will then result in an empty curselection which is what causes your error. To avoid this, check if the selection is empty before using it.
def on_select(event):
w = event.widget
selection = w.curselection()
if selection:
index = int(selection[0])
value = w.get(index)
print(f'You selected item {index}: {value}')
When you change from listbox1 to listbox2, the on_select is triggered on both.
So either you account for the posibility that w.curselection() is empty, and hence:
def on_select(event):
w = event.widget
if w.curselection():
index = int(w.curselection()[0])
value = w.get(index)
print(f'You selected item {index}: {value} in listbox: {w}')
or use exportselection=False when creating the LB to allow selecting from them individually.
You can also give listboxes a name (name=xxx) and you'll be able to differentiate which listbox the on_select is called from.

Get state of checkbox cellWidget in QTableWidget PyQT

I work on python plugin for QGIS. In this plugin I have created a QTableWidget with 3 columns. These columns are QCheckbox, QTableWidgetItem and QComboBox. I would like to retrieve the values contained in these 3 columns. For the moment I managed to get the values of QComboBox and QTableWidgetItem but I can't seem to get the value of the QCheckBox.
liste = ['Carte 1','Carte 2','Carte 3','Carte 4','Carte 5','Carte 6']
combo_box_options = ["A4 Paysage","A4 Portrait", "A3 Paysage","A3 Portrait"]
self.dlg_format = Dialog_format()
self.dlg_format.tableWidget.setRowCount(len(liste))
for index in range(len(liste)):
item = QTableWidgetItem(liste[index])
self.dlg_format.tableWidget.setItem(index, 1, item)
self.dlg_format.tableWidget.setColumnWidth(0, 20)
self.dlg_format.tableWidget.setColumnWidth(1, 350)
combo = QComboBox()
for t in combo_box_options:
combo.addItem(t)
self.dlg_format.tableWidget.setCellWidget(index, 2, combo)
widget = QWidget()
checkbox = QCheckBox()
checkbox.setCheckState(Qt.Checked)
playout = QHBoxLayout(widget)
playout.addWidget(checkbox)
playout.setAlignment(Qt.AlignCenter)
playout.setContentsMargins(0,0,0,0)
widget.setLayout(playout)
self.dlg_format.tableWidget.setCellWidget(index, 0, widget)
self.dlg_format.show()
result = self.dlg_format.exec_()
if result:
for index in range(len(liste)):
text = self.dlg_format.tableWidget.item(index, 1).text()
format = self.dlg_format.tableWidget.cellWidget(index, 2).currentText()
check = self.dlg_format.tableWidget.cellWidget(index, 0).checkState() #Does not work
The QWidget is what is set as cell widget, not the checkbox, and that widget obviously has no checkState attribute.
There are various possibilities for this scenario.
Make the checkbox an attribute of the widget:
widget = QWidget()
widget.checkbox = QCheckBox()
playout.addWidget(widget.checkbox)
# ...
check = self.dlg_format.tableWidget.cellWidget(index, 0).checkbox.checkState()
Make the checkbox's checkState function a reference of the widget (note: no parentheses!) so that you can access it with the existing cellWidget(index, 0).checkState():
checkbox = QCheckBox()
widget.checkState = checkbox.checkState
Since all happens within the same scope (the function), you can totally ignore the cellWidget and use a list of tuples that contains the widgets:
widgets = []
for index in range(len(liste)):
# ...
widgets.append((item, combo, checkbox))
# ...
if result:
for item, combo, checkbox in widgets:
text = item.text()
format = combo.currentText()
check = checkbox.checkState()
Note that:
checkState()
returns a Qt.CheckState enum, which results in 2 (Qt.Checked) for a checked box; if you need a boolean, use isChecked() instead;
you can use enumerate instead of range, since you are iterating through the list items anyway: for index, text in enumerate(liste):;
if you don't need to add item data and the contents of the combo are always the same, just use combo.addItems(combo_box_options);
setting the column width for every cycle is pointless, just do it once outside the for loop;
if you use QHBoxLayout(widget) there's no need for widget.setLayout(playout), as the widget argument on a layout already sets that layout on the widget;
instance attribute are created in order to make them persistent (it ensures that they are not garbage collected and allows future access); from your code it seems unlikely that you're going to use that dialog instance after that function returns, so making it a member of the instance (self.dlg_format) is unrequired and keeps resources unnecessarily occupied: the dialog would be kept in memory even after it's closed, and would be then deleted and overwritten as soon as it's created again; just make it a local variable (dlg_format = Dialog_format());

How to insert a scrollbar to a menu?

I want to add another widget, in this case a scale widget, to a menu widget in tkinter.
Right now the only solutions I see are creating a new command and open a new window with the scale widget or creating the scale widget elsewhere. Both don't seem too appealing to me.
Any ideas how to archive this are welcome :)
You cant add a scrollbar to it, but I have coded something similar to this. Its a hacky way and maybe its hard to understand but I can try to explain.
Note as Bryan mentioned in the linked Thread, this seems to be a a Windows only solution.
import tkinter as tk
def my_first_function():
print('first')
def my_second_function():
print('second')
def check_for_scroll(event):
check = root.call(event.widget, "index","active")
if check == 0: #index of button up
scroll_up()
root.after(100,lambda e=event:check_for_scroll(e)) # check again after 100ms
if check == file_menu.index('end'):
scroll_down()
root.after(100,lambda e=event:check_for_scroll(e))
def scroll_up():
index_of_first_command=1
index_of_last_command=1
label_of_last_command = file_menu.entrycget(index_of_first_command, 'label')
try:
for i, k in enumerate(dict_of_commands):
if k == label_of_last_command:
previous_command_label = list(dict_of_commands)[i-1]
previous_command = list(dict_of_commands.values())[i-1]
if i != 0: #avoid to get the last as first
file_menu.delete(index_of_first_command) #first before pull down button
file_menu.insert_command(index_of_first_command,
label=previous_command_label,
command=previous_command)
except Exception as e:
print(e)
def scroll_down():
index_of_first_command=1
index_of_last_command=1
label_of_last_command = file_menu.entrycget(index_of_last_command, 'label')
try:
for i, k in enumerate(dict_of_commands):
if k == label_of_last_command:
next_command_label = list(dict_of_commands)[i+1]
next_command = list(dict_of_commands.values())[i+1]
file_menu.delete(index_of_first_command) #first after pull up button
file_menu.insert_command(index_of_last_command,
label=next_command_label,
command=next_command)
except:
pass
space = ' '
dict_of_commands = {'first' : my_first_function,
'second': my_second_function}
root = tk.Tk()
menubar = tk.Menu(root)
root.config(menu=menubar)
file_menu = tk.Menu(menubar,tearoff=0)
menubar.add_cascade(label='File', menu=file_menu)
file_menu.bind('<<MenuSelect>>', check_for_scroll)
file_menu.add_command(label=space+u'\u25B2'+space, font=["Arial", 8,'bold'])
file_menu.add_command(label='first', command=my_first_function)
file_menu.add_command(label=space+u'\u25BC'+space, font=["Arial", 8,'bold'])
root.mainloop()
So this code creates your window and a menubar on it as usal:
root = tk.Tk()
menubar = tk.Menu(root)
root.config(menu=menubar)
file_menu = tk.Menu(menubar,tearoff=0)
menubar.add_cascade(label='File', menu=file_menu)
file_menu.add_command(label=space+u'\u25B2'+space, font=["Arial", 8,'bold'])
file_menu.add_command(label='first', command=my_first_function)
file_menu.add_command(label=space+u'\u25BC'+space, font=["Arial", 8,'bold'])
root.mainloop()
Important for you, is this line here:
file_menu.bind('<<MenuSelect>>', check_for_scroll)
This line binds the event MenuSelect and it happens/triggers if your cursor hovers over a command of your menu. To this event I have bound a function called check_for_scroll and it looks like this:
def check_for_scroll(event):
check = root.call(event.widget, "index","active")
if check == 0: #index of button up
scroll_up()
root.after(100,lambda e=event:check_for_scroll(e)) # check again after 100ms
if check == file_menu.index('end'):
scroll_down()
root.after(100,lambda e=event:check_for_scroll(e))
The line below checks for the index of the command that has triggered the event. With this we check if its button of our interest like the first or last, with the arrows.
check = root.call(event.widget, "index","active")
if its the first for example this code here is executed:
if check == 0: #index of button up
scroll_up()
root.after(100,lambda e=event:check_for_scroll(e)) # check again after 100ms
it calls/triggers the function scroll_up and uses then the after method of tkinter to retrigger itself, like a loop. The scroll_up function is build like the scroll_down just in the opposite direction. Lets have a closer look:
def scroll_up():
index_of_first_command=1
index_of_last_command=1
label_of_last_command = file_menu.entrycget(index_of_first_command, 'label')
try:
for i, k in enumerate(dict_of_commands):
if k == label_of_last_command:
previous_command_label = list(dict_of_commands)[i-1]
previous_command = list(dict_of_commands.values())[i-1]
if i != 0: #avoid to get the last as first
file_menu.delete(index_of_first_command) #first before pull down button
file_menu.insert_command(index_of_first_command,
label=previous_command_label,
command=previous_command)
except Exception as e:
print(e)
In this function we need to know the first and the last position of commands, because we want to delete one and insert another on that position/index. To achieve this I had created a dictionary that contains the label and the the function of the command item of tkinter like below. (This could be created dynamically, but lets keep it for another question)
dict_of_commands = {'first' : my_first_function,
'second': my_second_function}
So we iterate over this enumerated/indexed dictionary in our function and check if the k/key/label is our item of interest. If true, we get the previous_command by listing the dictionary keys and get the extract the key before by this line:
next_command_label = list(dict_of_commands)[i+1]
similar to the value of the dictionary with this line:
next_command = list(dict_of_commands.values())[i+1]
After all we can delete one and insert one where we like to with this:
file_menu.delete(index_of_first_command) #first after pull up button
file_menu.insert_command(index_of_last_command,
label=next_command_label,
command=next_command)
I know that this code can improved by a lot but it seems hard enough to understand as it is. So dont judge me please.
I hope this solves your question, even if it isnt in the way you wanted to. But this code avoids you from hardly code a own menubar.
If there are questions left on my answer, let me know.

How Do I assign a function to a button in Python/TKinter?

I am fairly new to TKinter and have been trying to convert my normal Python code into a GUI (TKinter Code)! I have been working on this code and so far, I have got the basic layout made but I am having problems coding buttons and using entries. You will most likely find a lot of errors in my code so be warned! :D
I have got an entry at the top of the window and I want the user to input a number into the entry and then I want to use the text inputted in the entry in some code (btn1()). I also want the user to press a button and then the button to run some code with labels underneath where the buttons are showing the results of the code (the labels in the btn1() function).
First, I want the user to input a number into the entry. Then, I want the user to click on a button which is underneath the entry. Finally, I want the results of the code behind the button, to be show underneath the buttons (in labels!).
Here is my code:
from tkinter import *
class window_design:
def __init__(self):
root=Tk()
root.title("Bag Weight")
root.geometry("500x700")
root.wm_iconbitmap('favicon.ico')
image=PhotoImage(file="Weight Program.png")
imagelabel=Label(root,image=image)
imagelabel.pack()
weightentrylabel=Label(root,text="Enter Weight!")
weightentrylabel.pack()
self.string=StringVar()
weightentry=Entry(root,textvariable=self.string)
weightentry.pack()
menutext=Label(root,text="What coin are you using?")
menutext.pack(side=LEFT)
values=['1p','2p','5p','10p','20p','50p','£1','£2','Exit']
def btn1(self,btn1code):
p1=3.56
p1should=356
if (self.string.get()) > p1should:
weightdif=(self.string.get())-p1should
coins=weightdif/p1
labeldif=Label(text=weightdif)
labelcoins=Label(text=coins)
elif (self.string.get()) < p1should:
weightdif=p1should-(self.string.get())
coins=weightdif/p1
labeldif=Label(text=weightdif)
labelcoins=Label(text=coins)
button1=Button(root,text="1p",command=btn1)
button1.pack(side=LEFT)
root.mainloop()
window_design()
I am currently getting this error:
Exception in Tkinter callback
Traceback (most recent call last):
File "C:\Users\cjay2\AppData\Local\Programs\Python\Python35-32\lib\tkinter\__init__.py", line 1549, in __call__
return self.func(*args)
TypeError: btn1() missing 2 required positional arguments: 'self' and 'btn1code'
You should use self.btn1 ( btn1 is a class method) in button1=Button(root,text="1p",command=btn1).
btn1() is called with one argument and it needs two arguments, set a default value to btn1code or removeit (if you do not use it).
When you call get() method on StringVar() it will return a string so you need to convert before comparing with integers.
To show result in label use a self.result = StringVar() then call self.result.set(a_string).
Check the following code:
from tkinter import *
class window_design:
def __init__(self):
root=Tk()
root.title("Bag Weight")
#root.geometry("500x700")
root.wm_iconbitmap('favicon.ico')
image=PhotoImage(file="Weight Program.png")
imagelabel=Label(root,image=image)
imagelabel.pack()
weightentrylabel=Label(root,text="Enter Weight!")
weightentrylabel.pack()
self.string=StringVar()
weightentry=Entry(root,textvariable=self.string)
weightentry.pack()
menutext=Label(root,text="What coin are you using?")
#menutext.pack(side=LEFT)
menutext.pack()
values=['1p','2p','5p','10p','20p','50p','£1','£2','Exit']
button1=Button(root,text="1p",command=self.btn1)
#button1.pack(side=LEFT)
button1.pack()
#--------------------------------------------------
self.result=StringVar()
resultlabel=Label(root, textvariable = self.result)
resultlabel.pack()
#--------------------------------------------------
root.mainloop()
#-------------------------------------
def btn1(self):
p1=3.56
p1should=356
if not self.string.get(): return
value = int(self.string.get())
if value > p1should:
weightdif = value - p1should
coins=weightdif/p1
elif value < p1should:
weightdif=p1should - value
coins=weightdif/p1
self.result.set(coins)
#-----------------------------------
window_design()
You defined btn1() as needing an argument besides self, but Tkinter is calling it without one. It looks like you are not even using btn1code, so you can change your function definition to def btn1(self):
you can declare btn1code as member variable,and remove it from your function definition, or if you want call function with arguments from the Button; use lambda function like this :
button1=Button(root,text="1p",command=lambda: btn1(btn1code))

capturing tkinter checkbox input

I am running a script with tkinter that captures user input and then opens a second and possibly a third window based on the input. The issue I am having is capturing user input from the third and final window. Each window is divided up into it's own python class on execution.
Here is the code that calls the third window, which executes properly:
test_assign = TestAssign(mylist)
Here is the third window code:
class TestAssign:
def __init__(self, mylist):
self.mylist = mylist
self.selected_values = []
self.master = Tk()
for i in range(len(mylist)):
setattr(self, 'item'+mylist[i], IntVar())
ch = Checkbutton(master, text='item'+mylist[i], variable=getattr(self, 'item'+mylist[i])
ch.pack()
b = Button(master, text='Next', command=self.get_selected_values)
b.pack()
mainloop()
def get_selected_values(self):
for i in range(len(self.mylist)):
if getattr(self, 'item'+self.mylist[i]) == 1:
self.selected_values.append(self.mylist[i])
self.master.destroy()
Control then returns to the call point (at least I believe it does). Where I attempt to print the selected values:
test_assign = TestAssign(mylist)
while not test_assign.selected_values:
pass
print test_assign.selected_values
Everytime execution gets to the print statement it prints an empty list whether there are boxes checked or not. If I call dir(test_assign) for testing purposes, the checkbox attrs are there. Not sure why I am not able to capture it like this.
Can anyone see the flaw in my code?
Two things:
1)
ch = Checkbutton(master, text='item'+mylist[i], variable=getattr(self, 'item'+mylist[i])
and
b = Button(master, text='Next', command=self.get_selected_values)
I think master should be self.master (but honestly, that almost certainly just a copy/pasting error.)
2) The important one:
if getattr(self, 'item'+self.mylist[i]) == 1:
should be
if getattr(self, 'item'+self.mylist[i]).get() == 1:
(you need to call get on your IntVars to read the value.)

Categories

Resources