CustomTkinter icon indicating copy to clipboard operation
CustomTkinter copied to clipboard

filedialog/messagebox and CTkToplevel

Open Geo9999 opened this issue 1 year ago • 4 comments

Hello, I'm new to programming and github so excuse any omission or issue with this Issue. Let me know if uploading part of the code would help, I wasn't sure whether I should.

I'm working on an app with a main window and one or more CTkToplevel windows that should be able to load files and make plots on separate windows (or on the main one). The Toplevel is it's own class called hkl_input_window and will be called from the main window. Problem is the tkinter messagebox, the filedialog windows and the toplevels containing the plots open over the root window and not the toplevel. When I tried to add my Toplevel window (called hkl_input_window) as a parent/master to them, I got the following error: " type object 'hkl_input_window' has no attribute 'tk' ". I added the full error here. Similar errors happened with the messagebox.showerror etc.

Exception in Tkinter callback Traceback (most recent call last): File "C:\Users\GeorgicsB\AppData\Local\Programs\Python\Python312\Lib\tkinter_init_.py", line 1968, in call return self.func(*args) ^^^^^^^^^^^^^^^^ File "C:\Users\GeorgicsB\AppData\Local\Programs\Python\Python312\Lib\site-packages\customtkinter\windows\widgets\ctk_button.py", line 554, in _clicked self.command() File "c:\Users\GeorgicsB\Desktop\New_folder\hkl_input_window.py", line 345, in self.button=CTkButton(self.frame2, text="Plot poles in a separate window", height=30, command=lambda: self.calculate_poles(plotting="separate")) #### Plot Pole Figures in a separate window #### ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "c:\Users\GeorgicsB\Desktop\New_folder\hkl_input_window.py", line 420, in calculate_poles self.separate_plot_window(data) File "c:\Users\GeorgicsB\Desktop\New_folder\hkl_input_window.py", line 427, in separate_plot_window plot_window = customtkinter.CTkToplevel(hkl_input_window) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "C:\Users\GeorgicsB\AppData\Local\Programs\Python\Python312\Lib\site-packages\customtkinter\windows\ctk_toplevel.py", line 36, in init super().init(*args, **pop_from_dict_by_set(kwargs, self.valid_tk_toplevel_arguments)) File "C:\Users\GeorgicsB\AppData\Local\Programs\Python\Python312\Lib\tkinter_init.py", line 2708, in init BaseWidget.init(self, master, 'toplevel', cnf, {}, extra) File "C:\Users\GeorgicsB\AppData\Local\Programs\Python\Python312\Lib\tkinter_init.py", line 2653, in init self.setup(master, cnf) File "C:\Users\GeorgicsB\AppData\Local\Programs\Python\Python312\Lib\tkinter_init.py", line 2622, in _setup self.tk = master.tk ^^^^^^^^^ AttributeError: type object 'hkl_input_window' has no attribute 'tk'

I tried the following:

  • Removing the .attributes("-topmost", 1) from the Toplevel window, which makes it appear under the root and is undesirable,
  • Using .attributes("-toolwindow", 1), which makes it appear under the root, but if clicked it is brought to the top and the rest appears over it. This one has other problems such as pushing the window to the back when the filedialog window opens and some very minor changes in how it looks
  • Using CTkMessagebox instead of the classic messagebox, which corrected the problems I had with the tkinter messagebox.
  • Adding .attributes("-topmost", 1) to the plotting window, which appeared in front of hkl_input_window only for a moment and then moved behind, on top of the root window.
  • Using the .lift() and .lower() on the toplevel and/or the root, which either did not do anything or did not fix the relative placement of the root, toplevel and rest of the windows Any time I tried to enter my Toplevel as a parent/master in the filedialog.askopenfilename or plotting windows I got the " type object 'hkl_input_window' has no attribute 'tk' "

Is it possible that the CTkToplevel widget isn't able to be used like this? What possibilities exist to combat this?

Sorry for the long message and thank you in advance for any help!!

Geo9999 avatar Nov 27 '24 15:11 Geo9999

@Geo9999 Could you please have Sample Reproducible Code along with its visual output?

Regards.

dipeshSam avatar Nov 27 '24 16:11 dipeshSam

I've written two similar mock versions of the code, no1 works but does not try to pass the secondary_window as a parent, hence the load/plots appear over the root and behind the toplevel, and no2 which produces the same error. I have attached the code in txt and some screenshots of the result, hope this gives some more information. Thanks for the help!

sample_for_github_1.txt sample_for_github_2.txt sample_1 1 sample_1 2 sample_2

Geo9999 avatar Nov 28 '24 12:11 Geo9999

@Geo9999 The bug lies in your code misspelling. It is recommended that you should use proper naming conventions. Also, instead of passing object reference as self, you were passing the class name itself by confusing with secondary_window name. And to bring the window up, we can wait 200 ms (milliseconds) and then can lift the window.

Here is your corrected code: sample_for_github_2(corrected)

import tkinter
from tkinter.messagebox import showerror, showinfo
from tkinter import filedialog as fd
import customtkinter
from customtkinter import CTkButton, CTkToplevel, CTkFrame
from CTkMessagebox import CTkMessagebox
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)


class secondary_window(customtkinter.CTkToplevel):
    def __init__(self, parent, **kwargs):
        super().__init__(parent, **kwargs)

        self.title("Toplevel Window")    #### Should open over the main
        self.geometry("300x300")
       
        self.data = [[1,2,3,4], [5,6,7,8]]

        self.button=CTkButton(self, text="load", command=lambda: self.load_cif(self))
        self.button.grid(row=0, column=0)
        self.button=CTkButton(self, text="plot", command=lambda: self.toplevel_plots(self.data))
        self.button.grid(row=1, column=0)
        
        self.after_idle(self.lift)

    def load_cif(self, parent):
        filetypes = (
                ('cif␣files', '*.cif'),
                ('All␣files', '*.*')
                )
                

        filename = fd.askopenfilename(
        title='Open a file',
        initialdir='/',
        filetypes=filetypes, parent=parent)
        
        

        if filename:
            CTkMessagebox(self, title='Selected File', message=filename)
        else:
            showinfo(title='Message',
                    message="No file was selected!",
                    parent = parent)


    def toplevel_plots(self, data):
        window=CTkToplevel(self)
        window.title("Plots in separate window")
        
        window.after(200, window.lift)
    
        frame = CTkFrame(window)
        frame.grid(row=0, column=0, sticky="nsew")
        canvas = self.create_canvas(frame, data)
        canvas.get_tk_widget().grid(row=0, column=0, sticky="nsew")
        print(data)


    def create_canvas(self, master, data):
        x, y = data
        fig = plt.figure()
        ax = fig.add_axes([0.05,0.05,0.9,0.9])
        canvas = FigureCanvasTkAgg(fig, master=master)
        ax.scatter(x, y)
        canvas.draw()
        return canvas
    


def secondary():
    window = secondary_window(parent=root)

root = customtkinter.CTk()    #### Should always stay open at the back, everything else opens from in
root.title("Main Window")
root.geometry("300x300")
button=CTkButton(root, command=secondary)
button.grid(row=0, column=0)
root.mainloop()

You could read some standard naming convention over here.

I also created a refined version of this code: new_refined_version.py

from customtkinter import (
    CTk,
    CTkButton,
    CTkFrame,
    CTkToplevel
)
import matplotlib.pyplot as plt
from tkinter import messagebox, filedialog
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

class PlottingWindow(CTkToplevel):
    def __init__(self, data: list, **kwargs):
        self._data = data
        super().__init__(**kwargs)

        self.wm_title("Plotting Window")
        
        # Setting it over the option window
        self.after(200, self.lift)
        
        self._frame = CTkFrame(self)
        self._frame.grid(row=0, column=0, sticky="nsew")

        self._create_canvas()
        
    def _create_canvas(self):
        x, y = self._data
        fig = plt.figure()
        ax = fig.add_axes([0.05, 0.05, 0.9, 0.9])
        canvas = FigureCanvasTkAgg(fig, master=self._frame)
        ax.scatter(x, y)
        canvas.draw()
        canvas.get_tk_widget().grid(row=0, column=0, sticky="nsew")

    
class OptionWindow(CTkToplevel):
    def __init__(self, master: CTk = None, **kwargs):
        super().__init__(master, **kwargs)
        
        self.wm_title("Option Window")
        self.geometry("400x250")
        
        self._plotting_loaded: bool = False
        self._default_data: list[list[int]] = [[1,2,3,4], [5,6,7,8]]
        
        # Setting it over the main window
        self.after(200, self.lift)
        
        # Adding options
        self._load_button = CTkButton(self, text="Load", command=self._on_load)
        self._load_button.place(relx=0.5, anchor="center", rely=0.4)

        self._plot_button = CTkButton(self, text="Plot", command=self._on_plot)
        self._plot_button.place(relx=0.5, anchor="center", rely=0.6)


    def _on_load(self):
        filetypes = (('cif␣files', '*.cif'), ('All␣files', '*.*'))
                
        filename = filedialog.askopenfilename(title="Open a file", 
                                              initialdir='/', 
                                              filetypes=filetypes, 
                                              parent=self)
        
        if filename:
            messagebox.showinfo("Message", f"Selected file is:\n\"{filename}\"", parent = self)
        else:
            messagebox.showinfo("Message", f"No file was selected!", parent = self)
        
        
    def _on_plot(self):
        if not self._plotting_loaded:
            self._plotting_loaded = True
            plotting_window = PlottingWindow(self._default_data)
            plotting_window.bind("<Destroy>", self._restore, True)
        
    def _restore(self, e=None):
        self._plotting_loaded = False
        
        
    
class MainWindow(CTk):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)

        self.wm_title("Main Window")
        self.geometry("400x250")
        
        self._options_loaded: bool = False
        
        self._option_button = CTkButton(self, text="Options", command=self._on_option)
        self._option_button.place(relx=0.5, anchor="center", rely=0.4)
        
        self._exit_button = CTkButton(self, text="Quit", command=self.quit)     # If it is not used, internal bug error may occur after using plot.
        self._exit_button.place(relx=0.5, anchor="center", rely=0.6)
                        
        
    def _on_option(self):
        if not self._options_loaded:
            self._options_loaded = True
            option_window = OptionWindow(self)
            option_window.bind("<Destroy>", self._restore, True)
        
    def _restore(self, e=None):
        self._options_loaded = False



if __name__ == "__main__":
    app = MainWindow()
    app.mainloop()

Refined output: Screenshot (280)

Hope, this information is helpful to you. Regards.

dipeshSam avatar Nov 28 '24 19:11 dipeshSam

Thank you so much for the help, I really appreciate how fast and detailed you were. I will implement the changes tomorrow and check again. Wishing you the best!

Geo9999 avatar Dec 01 '24 11:12 Geo9999