Akascape / CTkListbox

A simple listbox for customtkinter (extenstion/add-on)
MIT License
129 stars 14 forks source link

Updating items in the list #65

Open ohshitgorillas opened 1 month ago

ohshitgorillas commented 1 month ago

I am relatively new to Python, so reading the source code for this is hard for me. Can you please add an example of how to use update_listvar and listvariable to the documentation? I love the way that this frame looks, but I am trying to update the text content of a list item in place and running into errors when I try to delete and re-insert the item.

Basically, I am writing mass spectrometry data reduction software and I want to give the user a list of samples in the sequence, preceded by a ☐ when the data has not been reduced, and a ☑ when the data has been reduced. Also, if the user clicks on a sample in the list, the other frames should be updated to show that sample's data.

This is the function giving me trouble:

# highlights the current sample in the list
def highlight_current_sample(sample_listbox, filtered_data):
    global analysis_index
    # replace the sample name in the list with a checked box if it is reduced
    if filtered_data[analysis_index].reduced and sample_listbox.get(analysis_index)[0] == '☐':
        sample_listbox.delete(analysis_index)
        sample_listbox.insert(analysis_index, f'☑ {filtered_data[analysis_index].analysis_label}')
    sample_listbox.activate(analysis_index)
ohshitgorillas commented 1 month ago

I've made some progress here:

    analysis_list_var = tk.StringVar()
    analysis_list = []
    for analysis in filtered_data:
        if analysis.reduced:
            analysis_list.append(f'☑ {analysis.analysis_label}')
        else:
            analysis_list.append(f'☐ {analysis.analysis_label}')
    analysis_list_var.set(analysis_list)
    analysis_listbox = CTkListbox(right_frame, width=220)
    analysis_listbox.listvariable = analysis_list_var
    analysis_listbox.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)
    analysis_listbox.bind("<<ListboxSelect>>",
        lambda event: on_analysis_select(analysis_listbox, analysis_list_var, filtered_data, fit_type, figure, canvas, stats_frame)
    )

and

# highlights the current analysis in the list
def highlight_current_analysis(analysis_listbox, analysis_list_var, filtered_data):
    global analysis_index
    # check that the analysis is reduced and that the symbol is ☐
    if filtered_data[analysis_index].reduced and analysis_list_var.get()[analysis_index][0] == '☐':
        print('checking box')
        analysis_list_var.set(analysis_list_var.get()[:analysis_index] + f'☑ {analysis_list_var.get()[analysis_index+3:]}')
        analysis_listbox.update_listvar()
    analysis_listbox.activate(analysis_index)

# moves to the selected analysis when clicked in the list
def on_analysis_select(analysis_listbox, analysis_list_var, filtered_data, fit_type, figure, canvas, stats_frame):
    global analysis_index
    selected_item = analysis_listbox.curselection()
    analysis_index = int(analysis_listbox.get(selected_item))
    interactive_update(filtered_data[analysis_index], fit_type, figure, canvas, stats_frame)
    update_buttons(filtered_data)
    highlight_current_analysis(analysis_listbox, analysis_list_var, filtered_data)

This should work, however, I'm encountering an empty list instead of a populated one. I can also confirm that anaylsis_list_var isn't empty:

analysis_list_var: ('☑ CB2244 5min', '☑ LB1023', '☑ LB1024', '☑ Q3778_', '☑ Q3779_', '☑ DT913_', '☑ Q3780_', '☑ DT914_', '☑ Q3781_', '☑ DT915_', '☑ Q3782_', '☑ Q3783_', '☑ LB1025', '☑ LB1026', '☑ CB2245 5min')
ohshitgorillas commented 1 month ago

With a friend's help I figured out that I shouldn't be calling listboxvar or update_listbox, but just inserting and deleting items like normally.

So I've come back to this configuration:

    # insert items into the analysis listbox
    analysis_listbox = CTkListbox(right_frame, width=220)
    for i, analysis in enumerate(filtered_data):
        if analysis.reduced:
            analysis_listbox.insert(i, f'☑ {analysis.analysis_label}')
        else:
            analysis_listbox.insert(i, f'☐ {analysis.analysis_label}')
    analysis_listbox.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)
    analysis_listbox.bind("<<ListboxSelect>>",
        lambda event: on_analysis_select(analysis_listbox, filtered_data, fit_type, figure, canvas, stats_frame)
    )

with these helper functions:

# highlights the current analysis in the list
def highlight_current_analysis(analysis_listbox, filtered_data):
    global analysis_index
    # check that the analysis is reduced and that the symbol is ☐ before changing to ☑
    if filtered_data[analysis_index].reduced and analysis_listbox.get(analysis_index)[0] == '☐':
        analysis_listbox.delete(analysis_index)
        analysis_listbox.insert(analysis_index, f'☑ {filtered_data[analysis_index].analysis_label}')
    analysis_listbox.activate(analysis_index)

# moves to the selected analysis when clicked in the list
def on_analysis_select(analysis_listbox, filtered_data, fit_type, figure, canvas, stats_frame):
    global analysis_index
    analysis_index = analysis_listbox.curselection()
    print(analysis_index)
    interactive_update(filtered_data[analysis_index], fit_type, figure, canvas, stats_frame)
    update_buttons(filtered_data)
    analysis_listbox.unbind("<<ListboxSelect>>")
    highlight_current_analysis(analysis_listbox, filtered_data)
    analysis_listbox.bind("<<ListboxSelect>>",
        lambda event: on_analysis_select(analysis_listbox, filtered_data, fit_type, figure, canvas, stats_frame)
    )

however, the behavior is extremely erratic with the analysis clicked sometimes being deleted and never re-added, sometimes another sample's name is deleted and re-added, etc accompanied by the following errors:

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Users\atomg\AppData\Local\Programs\Python\Python311\Lib\tkinter\__init__.py", line 1948, in __call__
    return self.func(*args)
           ^^^^^^^^^^^^^^^^
  File "C:\Users\atomg\AppData\Local\Programs\Python\Python311\Lib\tkinter\__init__.py", line 861, in callit
    func(*args)
  File "C:\Users\atomg\AppData\Local\Programs\Python\Python311\Lib\site-packages\CTkListbox\ctk_listbox.py", line 125, in <lambda>
    self.after(100, lambda: selected_button.configure(hover=self.hover))
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\atomg\AppData\Local\Programs\Python\Python311\Lib\site-packages\customtkinter\windows\widgets\ctk_button.py", line 442, in configure
    super().configure(require_redraw=require_redraw, **kwargs)
  File "C:\Users\atomg\AppData\Local\Programs\Python\Python311\Lib\site-packages\customtkinter\windows\widgets\core_widget_classes\ctk_base_class.py", line 130, in configure
    super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_frame_attributes))  # configure tkinter.Frame
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\atomg\AppData\Local\Programs\Python\Python311\Lib\tkinter\__init__.py", line 1702, in configure
    return self._configure('configure', cnf, kw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\atomg\AppData\Local\Programs\Python\Python311\Lib\tkinter\__init__.py", line 1689, in _configure
    return self._getconfigure(_flatten((self._w, cmd)))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\atomg\AppData\Local\Programs\Python\Python311\Lib\tkinter\__init__.py", line 1673, in _getconfigure
    for x in self.tk.splitlist(self.tk.call(*args)):
                               ^^^^^^^^^^^^^^^^^^^
_tkinter.TclError: invalid command name ".!ctkframe.!ctkframe2.!ctkframe2.!ctkframe.!canvas.!ctklistbox.!ctkbutton5"
ohshitgorillas commented 1 month ago

Sorry for spamming. I got it working, except for one error when I click on the item in the list:

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Users\atomg\AppData\Local\Programs\Python\Python311\Lib\tkinter\__init__.py", line 1948, in __call__
    return self.func(*args)
           ^^^^^^^^^^^^^^^^
  File "C:\Users\atomg\AppData\Local\Programs\Python\Python311\Lib\tkinter\__init__.py", line 861, in callit
    func(*args)
  File "C:\Users\atomg\AppData\Local\Programs\Python\Python311\Lib\site-packages\CTkListbox\ctk_listbox.py", line 125, in <lambda>
    self.after(100, lambda: selected_button.configure(hover=self.hover))
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\atomg\AppData\Local\Programs\Python\Python311\Lib\site-packages\customtkinter\windows\widgets\ctk_button.py", line 442, in configure
    super().configure(require_redraw=require_redraw, **kwargs)
  File "C:\Users\atomg\AppData\Local\Programs\Python\Python311\Lib\site-packages\customtkinter\windows\widgets\core_widget_classes\ctk_base_class.py", line 130, in configure
    super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_frame_attributes))  # configure tkinter.Frame
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\atomg\AppData\Local\Programs\Python\Python311\Lib\tkinter\__init__.py", line 1702, in configure
    return self._configure('configure', cnf, kw)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\atomg\AppData\Local\Programs\Python\Python311\Lib\tkinter\__init__.py", line 1689, in _configure
    return self._getconfigure(_flatten((self._w, cmd)))
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\atomg\AppData\Local\Programs\Python\Python311\Lib\tkinter\__init__.py", line 1673, in _getconfigure
    for x in self.tk.splitlist(self.tk.call(*args)):
                               ^^^^^^^^^^^^^^^^^^^

As for the above code, the solution was to insert but NOT delete:

# highlights the current analysis in the list
def highlight_current_analysis(analysis_listbox, analysis_index, filtered_data):
    # check that the analysis is reduced and that the symbol is ☐ before changing to ☑
    if filtered_data[analysis_index].reduced and analysis_listbox.get(analysis_index)[0] == '☐':
        analysis_listbox.insert(analysis_index, f'☑ {filtered_data[analysis_index].analysis_label}')
    analysis_listbox.activate(analysis_index)

# moves to the selected analysis when clicked in the list
def on_analysis_select(analysis_listbox, filtered_data, fit_type, figure, canvas, stats_frame):
    global analysis_index
    analysis_index = analysis_listbox.curselection()
    interactive_update(filtered_data[analysis_index], fit_type, figure, canvas, stats_frame)
    update_buttons(filtered_data)
    analysis_listbox.unbind("<<ListboxSelect>>")
    highlight_current_analysis(analysis_listbox, analysis_index, filtered_data)
    analysis_listbox.bind("<<ListboxSelect>>",
        lambda event: on_analysis_select(analysis_listbox, filtered_data, fit_type, figure, canvas, stats_frame)
    )
ohshitgorillas commented 2 weeks ago

Any idea on the error I'm getting and how I can hide/resolve it?

Traceback (most recent call last):
  File "C:\Users\atomg\AppData\Local\Programs\Python\Python37\lib\tkinter\__init__.py", line 1705, in __call__
    return self.func(*args)
  File "C:\Users\atomg\AppData\Local\Programs\Python\Python37\lib\tkinter\__init__.py", line 749, in callit
    func(*args)
  File "C:\Users\atomg\OneDrive - Thermochron Systems LLC\heman_code\Pro\.venv\lib\site-packages\CTkListbox\ctk_listbox.py", line 125, in <lambda>
    self.after(100, lambda: selected_button.configure(hover=self.hover))
  File "C:\Users\atomg\OneDrive - Thermochron Systems LLC\heman_code\Pro\.venv\lib\site-packages\customtkinter\windows\widgets\ctk_button.py", line 442, in configure
    super().configure(require_redraw=require_redraw, **kwargs)
  File "C:\Users\atomg\OneDrive - Thermochron Systems LLC\heman_code\Pro\.venv\lib\site-packages\customtkinter\windows\widgets\core_widget_classes\ctk_base_class.py", line 130, in configure
    super().configure(**pop_from_dict_by_set(kwargs, self._valid_tk_frame_attributes))  # configure tkinter.Frame
  File "C:\Users\atomg\AppData\Local\Programs\Python\Python37\lib\tkinter\__init__.py", line 1485, in configure
    return self._configure('configure', cnf, kw)
  File "C:\Users\atomg\AppData\Local\Programs\Python\Python37\lib\tkinter\__init__.py", line 1473, in _configure
    return self._getconfigure(_flatten((self._w, cmd)))
  File "C:\Users\atomg\AppData\Local\Programs\Python\Python37\lib\tkinter\__init__.py", line 1457, in _getconfigure
    for x in self.tk.splitlist(self.tk.call(*args)):
_tkinter.TclError: invalid command name ".!ctkframe.!ctkframe2.!ctkframe2.!ctkframe.!canvas.!ctklistbox.!ctkbutton13"

The behavior is totally fine, everything works as expected, this is just a random error I'm trying to clear. It only occurs when I click on an analysis that I haven't touched yet, if I go back to an analysis that has already been seen/loaded then there's no error.

MaxGroiss commented 2 weeks ago

I kinda (for me) fixed the error problem by replacing the code line 125 in ctk_listbox.py by : self.after(100, lambda: selected_button.configure(hover=self.hover) if selected_button.winfo_exists() else None)

It checks if the selected_button object exists at the time the lamda function is called. (Thanks to GitHub Copilot)