TomSchimansky / CustomTkinter

A modern and customizable python UI-library based on Tkinter
MIT License
11.14k stars 1.05k forks source link

CTkScrollableFrame incompatible with tkinterweb.HtmlFrame #2502

Open AssimDelteil opened 1 month ago

AssimDelteil commented 1 month ago

Hello, I have an issue on my project where I use both a CTkScrollableFrame and a HtmlFrame from tkinterweb. When I scroll on my CTkScrollableFrame, an error message is displayed but nothing breaks (the scroll is still happening and the program is not crashing). The CTkScrollableFrame just need to be instanciated for this to happen, no need to display it.

Exception in Tkinter callback
Traceback (most recent call last):
  File "C:\Users\<user>\AppData\Local\Programs\Python\Python311\Lib\tkinter\__init__.py", line 1948, in __call__
    return self.func(*args)
  File "C:\Users\<user>\AppData\Local\Programs\Python\Python311\Lib\site-packages\customtkinter\windows\widgets\ctk_scrollable_frame.py", line 248, in _mouse_wheel_all
    if self.check_if_master_is_canvas(event.widget):
  File "C:\Users\<user>\AppData\Local\Programs\Python\Python311\Lib\site-packages\customtkinter\windows\widgets\ctk_scrollable_frame.py", line 280, in check_if_master_is_canvas
    elif widget.master is not None:
AttributeError: 'str' object has no attribute 'master'

Here is a sample code to reproduce the issue:

import tkinter as tk
import customtkinter
from tkinterweb import HtmlFrame

root = tk.Tk()

htmlBodyFrame = HtmlFrame(root, messages_enabled = False)
htmlBodyFrame.load_html('\<p\>This is an HTML rendering frame\</p\>') 
htmlBodyFrame.pack() 

scrollableFrame = customtkinter.CTkScrollableFrame(root)

root.mainloop()

Package version:

customtkinter==5.2.2
tkinterweb==3.23.10

Could you please look into this ? Regards,

PS: first time opening a github issue, sorry if i something is missing.

HawKen147 commented 1 month ago

Hello,

I tried you code and i got the same error, I tried to use the class method, but i still had an issue, so i had to "create" the side bar to scroll through the htmlFrame. Here is the code :

import customtkinter
from tkinterweb import HtmlFrame
import tkinter as tk

class App(customtkinter.CTk):
    def __init__(self):
        super().__init__()

        # Create a canvas object and a vertical scrollbar for scrolling it
        self.canvas = tk.Canvas(self)
        self.vsb = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
        self.canvas.configure(yscrollcommand=self.vsb.set)

        # Pack scrollbar and canvas
        self.vsb.pack(side="right", fill="y")
        self.canvas.pack(side="left", fill="both", expand=True)

        # Create a frame inside the canvas which will contain the HtmlFrame
        self.scrollable_frame = tk.Frame(self.canvas)
        self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")

        # Bind the configure event to update the scrollregion of the canvas
        self.scrollable_frame.bind("<Configure>", self.on_frame_configure)

        # Add HtmlFrame into the scrollable frame
        self.html_body_frame = HtmlFrame(self.scrollable_frame, messages_enabled = False)
        self.html_body_frame.load_html('<p>This is an HTML rendering frame</p>')
        self.html_body_frame.pack(fill="both", expand=True)

        # Bind mouse wheel event for scrolling
        self.bind_all("<MouseWheel>", self.on_mouse_wheel)

    def on_frame_configure(self, event):
        # Update scrollregion of the canvas
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))

    def on_mouse_wheel(self, event):
        # Scroll vertically by 2 units
        self.canvas.yview_scroll(int(-1*(event.delta/120)), "units")

app = App()
app.mainloop()

If the widget needs to be scrollable too :

import customtkinter
from tkinterweb import HtmlFrame
import tkinter as tk

class MyFrame(customtkinter.CTkScrollableFrame):
    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)

        # Create a canvas object and a vertical scrollbar for scrolling it
        self.canvas = tk.Canvas(self)
        self.vsb = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
        self.canvas.configure(yscrollcommand=self.vsb.set)

        # Pack scrollbar and canvas
        self.vsb.pack(side="right", fill="y")
        self.canvas.pack(side="left", fill="both", expand=True)

        # Create a frame inside the canvas which will contain the HtmlFrame
        self.scrollable_frame = tk.Frame(self.canvas)
        self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")

        # Bind the configure event to update the scrollregion of the canvas
        self.scrollable_frame.bind("<Configure>", self.on_frame_configure)

        # Add HtmlFrame into the scrollable frame
        self.html_body_frame = HtmlFrame(self.scrollable_frame, messages_enabled = False)
        self.html_body_frame.load_html('<p>This is an HTML rendering frame</p>')
        self.html_body_frame.pack(fill="both", expand=True)

        # Bind mouse wheel event for scrolling
        self.bind_all("<MouseWheel>", self.on_mouse_wheel)

    def on_frame_configure(self, event):
        # Update scrollregion of the canvas
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))

    def on_mouse_wheel(self, event):
        # Scroll vertically by 2 units
        self.canvas.yview_scroll(int(-1*(event.delta/120)), "units")

class App(customtkinter.CTk):
    def __init__(self):
        super().__init__()

        self.my_frame = MyFrame(master=self, width=300, height=200)
        self.my_frame.grid(row=0, column=0, padx=20, pady=20)

app = App()
app.mainloop()
AssimDelteil commented 1 month ago

Hello,

Thanks for your help ! Unfortunately, this did not solved my issue as i did not give the proper context. I want a html rendering frame beside a scrollable frame (with a grid), not a html frame inside of a scrollable frame. And i want them both to be scrollable (depending on which widget my cursor is hovering)

When I try to add this to your code i get:

import tkinter as tk
import customtkinter
from tkinterweb import HtmlFrame
from tkinter import ttk

class MyFrame(customtkinter.CTkScrollableFrame):
    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)

        # # Create a canvas object and a vertical scrollbar for scrolling it
        self.canvas = tk.Canvas(self)
        self.vsb = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
        self.canvas.configure(yscrollcommand=self.vsb.set)

        # Pack scrollbar and canvas
        self.vsb.pack(side="right", fill="y")
        self.canvas.pack(side="left", fill="both", expand=True)

        # Create a frame inside the canvas which will contain the HtmlFrame
        self.scrollable_frame = tk.Frame(self.canvas)
        self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")

        # Bind the configure event to update the scrollregion of the canvas
        self.scrollable_frame.bind("<Configure>", self.on_frame_configure)

        # Bind mouse wheel event for scrolling
        self.bind_all("<MouseWheel>", self.on_mouse_wheel)

        for i in range(100):
            label = ttk.Label(self.canvas, text="Hello world %i!"%i)
            label.pack()

    def on_frame_configure(self, event):
        # Update scrollregion of the canvas
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))

    def on_mouse_wheel(self, event):
        # Scroll vertically by 2 units
        self.canvas.yview_scroll(int(-1*(event.delta/120)), "units")

root = tk.Tk()

htmlBodyFrame = HtmlFrame(root, messages_enabled = False)
htmlBodyFrame.load_html('<p>This is an HTML rendering frame</p>') 
htmlBodyFrame.grid(row=0, column=0) 

scrollableFrame = MyFrame(root)
scrollableFrame.grid(row=0, column=1) 

root.mainloop()

Where no error is displayed upon scrolling but it does not scroll the scrollable frame.

Here is the more flushed out sample for what i want:

import tkinter as tk
import customtkinter
from tkinterweb import HtmlFrame
from tkinter import ttk

html=""
for i in range(100):
    html += "<p>This is an HTML rendering frame %i</p>"%i

class ScrollableFrame(customtkinter.CTkScrollableFrame):
    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)

        for i in range(100):
            label = ttk.Label(self, text="Hello world %i!"%i)
            label.pack()

class App(customtkinter.CTk):
    def __init__(self):
        super().__init__()

        self.scrollableFrame = ScrollableFrame(self)
        self.scrollableFrame.grid(row=0, column=1)

        htmlBodyFrame = HtmlFrame(self, messages_enabled = False)
        htmlBodyFrame.load_html(html) 
        htmlBodyFrame.grid(row=0, column=0) 

app = App()
app.mainloop()

Sorry for the confusion. Regards,

HawKen147 commented 1 month ago

Hello,

I tried your code and it looks like the mouse wheel event is the problem, when i used the sidebar i didn't have any error diplayed.

So i bind the mousewheel event :

import tkinter as tk
import customtkinter
from tkinterweb import HtmlFrame
from tkinter import ttk

html = ""
for i in range(100):
    html += "<p>This is an HTML rendering frame %i</p>" % i

class ScrollableFrame(customtkinter.CTkScrollableFrame):
    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)

        for i in range(100):
            label = ttk.Label(self, text="Hello world %i!" % i)
            label.pack()

        self.bind_all("<MouseWheel>", self._on_mouse_wheel)

    def _on_mouse_wheel(self, event):
        # Get the scrollbar and scroll based on the event delta (is aplied to the widget only, if you comment and pass, the widget is not scrolable anymore whith the mouse wheel)
        self._parent_canvas.yview_scroll(int(-1 * (event.delta / 2)), "units")

class App(customtkinter.CTk):
    def __init__(self):
        super().__init__()

        self.scrollableFrame = ScrollableFrame(self)
        self.scrollableFrame.grid(row=0, column=1)

        htmlBodyFrame = HtmlFrame(self, messages_enabled=False)
        htmlBodyFrame.load_html(html)
        htmlBodyFrame.grid(row=0, column=0)

app = App()
app.mainloop()

The last thing i uncounter is that both of the frame and widget are now scrolable but are linked. If you scroll on the master frame, the widget will scroll too. But it doesn't work the other way arround, the widget to not scroll the master frame.

regards,