[Tkinter] Help create scrollbar in chatbot with tkinter on python - Printable Version +- Python Forum (https://python-forum.io) +-- Forum: Python Coding (https://python-forum.io/forum-7.html) +--- Forum: GUI (https://python-forum.io/forum-10.html) +--- Thread: [Tkinter] Help create scrollbar in chatbot with tkinter on python (/thread-40480.html) |
Help create scrollbar in chatbot with tkinter on python - fenec10 - Aug-04-2023 Hi everyone, I'm trying to create a chatbot, the chatbot is ok but I'm struggling with the scrollbar, when I run my code everything is ok except the scrollbar, it is there, the scrollbar moves, but the chat doesn't move and it's impossible to see the scrolling chat. (I'm a beginner in coding). Could you please help me with how to make this scrollbar move the chat please ? :) Here is the code I'm using : from tkinter import * import datetime from tkinter import Scrollbar root = tk.Tk() root.geometry("600x400") # Set the initial size of the window text_widget = Text(root) scrollbar = Scrollbar(root) scroll.pack(side=RIGHT) text_widget.configure(yscrollcommand=scrollbar.set) scrollbar.config(command=text_widget.yview) scrollbar.grid(row=0, column=1, sticky='ns') text_widget.pack(side=RIGHT, fill=BOTH, expand=True) text_widget.bind("<MouseWheel>", lambda event: text_widget.yview_scroll(-1 * int((event.delta / 120)), "units")) # Custom widget for speech bubble class SpeechBubble(Frame): def __init__(self, master, message, is_client=True): super().__init__(master) self.is_client = is_client self.message = message self.create_widgets() def create_widgets(self): if self.is_client: bg_color = "#DCF8C6" # Client's message bubble color text_color = "black" align = "right" # Align client's bubble to the right padx = (50, 10) # Add some horizontal padding to the client's bubble pady = (5, 0) # Add some vertical padding to the client's bubble else: bg_color = "#F8F8F8" # king's message bubble color text_color = "black" align = "left" # Align king's bubble to the left padx = (10, 50) # Add some horizontal padding to king's bubble pady = (0, 5) # Add some vertical padding to king's bubble bubble_frame = Frame(self, bg=bg_color, padx=10, pady=5, borderwidth=2, relief="solid") bubble_frame.pack(side=align, fill="x", padx=padx, pady=pady) # Use side=align to align the bubble to the left or right bubble_label = Label(bubble_frame, text=self.message, wraplength=300, bg=bg_color, fg=text_color, justify="left", font=("Arial", 12)) bubble_label.pack() # Define who speaks def envoie(): message = e.get() message_with_prefix = "Me: " + message if txt.index("end-1c") != "1.0": # Check if there is content in the text widget (excluding the trailing newline) txt.insert(END, "\n") # Insert a newline to separate messages message_frame = Frame(txt) message_frame.pack(anchor="e" if txt.index("end-1c") == "1.0" else "w") # Align the message frame to the right if it's the first message, otherwise align to the left speech_bubble = SpeechBubble(message_frame, message_with_prefix, is_client=True) speech_bubble.pack(side="right") # Align the client's bubble to the right e.delete(0, END) text_widget.yview() if 'Hello' in message: response = "Hello" else: response = "I'm sorry, I don't understand that." response_frame = Frame(txt) response_frame.pack(anchor="w") # Align the response frame to the left response_bubble = SpeechBubble(response_frame, "king: " + response, is_client=False) response_bubble.pack(side="left") # Align king's bubble to the left # Scroll to the bottom of the text widget to show the latest message txt.see(END) text_widget.insert(END, message_with_prefix) # Define where the text goes: txt = Text(root, font=("Arial", 12), wrap="word", padx=10, pady=10) txt.grid(row=0, column=0, sticky="nsew") # Use sticky="nsew" to make the widget expand in all directions e = Entry(root, width=60) e.grid(row=2, column=0, padx=10, pady=10) # Bind the "Enter" key to the function envoie() e.bind("<Return>", lambda event: envoie()) # Define the "enter" button: envoyer = Button(root, text="Enter", command=envoie) envoyer.grid(row=2, column=1, padx=10, pady=10) # Make the rows and columns of the root grid expand to fill the available space root.grid_rowconfigure(0, weight=1) root.grid_columnconfigure(0, weight=1) root.title("king") root.mainloop() RE: Help create scrollbar in chatbot with tkinter on python - deanhystad - Aug-04-2023 The code you posted does not run. There are other errors. You don't create anything named scroll. You cannot use pack and grid in the same widget.
RE: Help create scrollbar in chatbot with tkinter on python - fenec10 - Aug-04-2023 Thanks a lot for your answer, here is the code without the scrollbar codes that I added, and it runs perfectly :) Do you know how I could add this scrollbar to the chatbox please ? :) The code without the scrollbar : from tkinter import * import datetime from tkinter import Scrollbar root = Tk() root.geometry("600x400") # Set the initial size of the window # Custom widget for speech bubble class SpeechBubble(Frame): def __init__(self, master, message, is_client=True): super().__init__(master) self.is_client = is_client self.message = message self.create_widgets() def create_widgets(self): if self.is_client: bg_color = "#DCF8C6" # Client's message bubble color text_color = "black" align = "right" # Align client's bubble to the right padx = (50, 10) # Add some horizontal padding to the client's bubble pady = (5, 0) # Add some vertical padding to the client's bubble else: bg_color = "#F8F8F8" # king's message bubble color text_color = "black" align = "left" # Align king's bubble to the left padx = (10, 50) # Add some horizontal padding to king's bubble pady = (0, 5) # Add some vertical padding to king's bubble bubble_frame = Frame(self, bg=bg_color, padx=10, pady=5, borderwidth=2, relief="solid") bubble_frame.pack(side=align, fill="x", padx=padx, pady=pady) # Use side=align to align the bubble to the left or right bubble_label = Label(bubble_frame, text=self.message, wraplength=300, bg=bg_color, fg=text_color, justify="left", font=("Arial", 12)) bubble_label.pack() # Define who speaks "client is Me" def envoie(): message = e.get() message_with_prefix = "Me: " + message if txt.index("end-1c") != "1.0": # Check if there is content in the text widget (excluding the trailing newline) txt.insert(END, "\n") # Insert a newline to separate messages message_frame = Frame(txt) message_frame.pack(anchor="e" if txt.index("end-1c") == "1.0" else "w") # Align the message frame to the right if it's the first message, otherwise align to the left speech_bubble = SpeechBubble(message_frame, message_with_prefix, is_client=True) speech_bubble.pack(side="right") # Align the client's bubble to the right e.delete(0, END) if 'Hello' in message: response = "Hello, how can I help you?" else: response = "I'm sorry, I don't understand that." response_frame = Frame(txt) response_frame.pack(anchor="w") # Align the response frame to the left response_bubble = SpeechBubble(response_frame, "king: " + response, is_client=False) response_bubble.pack(side="left") # Align king's bubble to the left # Scroll to the bottom of the text widget to show the latest message txt.see(END) # Define where the text goes: txt = Text(root, font=("Arial", 12), wrap="word", padx=10, pady=10) txt.grid(row=0, column=0, sticky="nsew") # Use sticky="nsew" to make the widget expand in all directions scrollbar = Scrollbar(root, orient=VERTICAL, command=txt.yview) scrollbar.grid(row=0, column=1, sticky=N+S) txt.config(yscrollcommand=scrollbar.set) e = Entry(root, width=60) e.grid(row=1, column=0, padx=10, pady=10) # Bind the "Enter" key to the function envoie() e.bind("<Return>", lambda event: envoie()) # Define the "enter" button: envoyer = Button(root, text="Enter", command=envoie) envoyer.grid(row=1, column=1, padx=10, pady=10) # Make the rows and columns of the root grid expand to fill the available space root.grid_rowconfigure(0, weight=1) root.grid_columnconfigure(0, weight=1) root.title("king") root.mainloop() RE: Help create scrollbar in chatbot with tkinter on python - deanhystad - Aug-05-2023 Quote:it runs perfectlyOh please stop making me laugh! I can't take it! Your main problem is that you cannot put speech bubbles in a Text widget. The text widget only knows how to scroll it's text. If you attach a scrollbar to a text widget it scrolls the text. If you added bubbles to the text widget the bubbles do not scroll. What you need to do is connect your scrollbar to a canvas. When your scroll a canvas it scrolls all the objects that have been added to the canvas. You can create a window on the canvas and add your speech bubbles to that window. When you scroll the canvas, the windows scrolls and that makes the speech bubbles scroll. This is a common approach for making scrollable windows in tkinter. You should have no trouble finding many examples. For something like this you could even skip making the window on the canvas and draw the speech bubbles directly on the canvas. This would give you more freedom on how the bubbles appear (rounded corners for example), but you would have to take over the layout management. The example below demonstrates the window in a scrolling canvas technique. import tkinter as tk class SpeechBubble(tk.Frame): """Label that uses color and alignment to indicate speaker in a conversation.""" def __init__(self, parent, text, is_client=True): super().__init__(parent, bg=parent["bg"]) bg = "lightblue" if is_client else "lightgreen" label = tk.Label( self, text=text, wraplength=300, bg=bg, font=(None, 14), padx=5, pady=5 ) label.pack(side=tk.LEFT if is_client else tk.RIGHT) self.pack(expand=True, fill=tk.X, padx=5, pady=5) class BubbleView(tk.Frame): """Scrolling frame that displays SpeechBubbles.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) bg = self["bg"] self.canvas = tk.Canvas(self, bg=bg) self.frame = tk.Frame(self, bg=bg) self.canvas.create_window(0, 0, window=self.frame, anchor=tk.NW) self.scrollbar = tk.Scrollbar(self, orient=tk.VERTICAL, command=self.canvas.yview) self.canvas.config(yscrollcommand=self.scrollbar.set) self.canvas.grid(row=0, column=0, sticky="news") self.scrollbar.grid(row=0, column=1, sticky="news") self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) # Add invisible frame to force self.frame to be wide as canvas. self.prybar = tk.Frame(self.frame, width=200, height=0, bg=bg) self.prybar.pack() def add_bubble(self, text, is_client=True): """Add speech bubble to conversation.""" bubble = SpeechBubble(self.frame, text, is_client) bubble.pack(expand=True, fill=tk.X) self.update() self._configure() self.canvas.yview_moveto(1) def _configure(self, event=None): """Uodate frame size and scroll region.""" wide = self.canvas.winfo_width()-5 self.prybar.config(width=wide) self.canvas.configure( scrollregion=(0, 0, wide, self.frame.winfo_height()) ) class Chat(tk.Tk): """ Demonstrate BubbleView.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.view = BubbleView(self, bg="black") self.entry = tk.Entry(self, font=(None, 16)) self.entry.bind("<Return>", self.talk) self.view.grid(row=0, column=0, sticky="news") self.entry.grid(row=1, column=0, sticky="news") self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) self.is_client = True def talk(self, event=None): """Add text to bubble view. Toggle between sources.""" self.view.add_bubble(self.entry.get(), self.is_client) self.is_client = not self.is_client self.entry.delete(0, tk.END) Chat().mainloop()There is some ugly in the code that is tied to the canvas not knowing how to do layout management (pack or grid). I have to force the bubble view frame to fill the width of the canvas because I cannot ask the frame to expand to fill its parent. That is what the prybar frame and _configure() method are for. When you resize the window, the _configure() method resizes the prybar to the width of the canvas, forcing the frame to the same size. One thing I could not figure out is how to move the text bubbles when the window is resized. I want frame's geometry manager to position the bubbles, but I don't know of a way to tell frame to update the layout. Unpacking and re-packing the last speech bubble might do it. RE: Help create scrollbar in chatbot with tkinter on python - deanhystad - Aug-07-2023 For fun I made a message box with speech balloons using tkinter canvas polygons. import tkinter as tk from math import sin, cos, radians import textwrap class RoundedRectangle(): """Make a canvas rectangle with rounded corners.""" def __init__( self, canvas, geometry, fill='', outline="black", border_width=1, anchor=tk.NW, radius=5): """Initialize RoundedRectangle Additional Args: camvas: The canvas on which I am drawn position: (x, y) position of bubble. fill: Fill color for bubble. Default is no fill. outline: Color of border around bubble. Default is black. border_width: Width of border around bubble. Default is 0. radius: Radius of rounded corner. In pixels. anchor: Where (x, y) is. NW (North West), NE, NC, CE, CW, SE, SC, SW, CENTER (default) """ self.canvas = canvas self.geometry = geometry self.radius = radius self.border_width = border_width self.outline = outline self.fill = fill self.anchor = anchor self.points = [] self.id = None def delete(self): """Remove polygon from canvas.""" if self.id: self.canvas.delete(self.id) self.id = None def create(self): """Create the canvas polygon.""" # Delete existing polygon if self.id: self.delete() # Get NW corner of polygon. x, y, w, h = self.geometry if self.anchor is None or self.anchor == tk.CENTER: x = x - w / 2 y = y - h / 2 else: horz, vert = self.anchor.lower() anchor = self.anchor.lower() if anchor[1] == "c": x = x - w / 2 elif anchor[1] == "e": x = x - w if anchor[0] == "c": y = y - h / 2 elif anchor[0] == "s": y = y - h # Create the polygon by creating points for the rounded corners.. def angle(start): """Helper function to get radian angles for corners.""" for deg in range(start, start+91, 5): yield radians(deg) r = self.radius self.points = ( [((x+r*(1+cos(a)), y+r*(1+sin(a)))) for a in angle(180)] + [((x+w-r*(1-cos(a)), y+r*(1+sin(a)))) for a in angle(270)] + [((x+w-r*(1-cos(a)), y+h-r*(1-sin(a)))) for a in angle(0)] + [((x+r*(1+cos(a)), y+h-r*(1-sin(a)))) for a in angle(90)] ) # Setting border width = 0 leaves a border, so set outline color to ''. outline = self.outline if self.border_width else '' self.id = self.canvas.create_polygon( self.points, outline=outline, width=self.border_width, fill=self.fill ) return self def bottom(self): """Return y value of bottom of balloon.""" x, y, wide, high = self.geometry return y + high class SpeechBubble(RoundedRectangle): """Text inside a rounded rectangle.""" def __init__( self, canvas, position, text, fill="", outline="black", border_width=0, font=None, word_wrap=40, radius=10, anchor=tk.NW): """Initialize speech bubble. Additional Args: text: Text to display in bubble. font: Font used to draw text. word_wrap: Number of columns per row. Default is None, no word wrap. """ x, y = position super().__init__( canvas, (x, y, 0, 0), fill=fill, outline=outline, border_width=border_width, anchor=anchor, radius=radius) self.text = text self.font = font self.word_wrap = word_wrap self.text_id = None def create(self): """Create the Speech bubble on the canvas.""" # Get balloon size from text text = self.text if self.word_wrap: text = "\n".join(textwrap.wrap(text, self.word_wrap)) id = self.canvas.create_text(-1000, -1000, text=text, font=self.font) x1, y1, x2, y2 = self.canvas.bbox(id) self.canvas.delete(id) x, y, wide, high = self.geometry self.geometry = (x, y, x2 - x1 + 20, y2 - y1 + 20) # Create the balloon. super().create() # Create the text. Center in the balloon. x1, y1, x2, y2 = self.canvas.bbox(self.id) pos = ((x1 + x2) / 2, (y1 + y2) / 2) self.text_id = self.canvas.create_text( pos, text=text, anchor=tk.CENTER, font=self.font, fill=self.outline ) return self def delete(self): """Remove polygon and text from canvas.""" if self.text_id: self.canvas.delete(self.text_id) self.text_id = None super().delete() def __del__(self): """Object delete callback""" self.delete() class ClientBubble(SpeechBubble): """Chat bubble for client side of conversation.""" def __init__(self, *args, anchor=tk.NW, fill="lightgreen", **kwargs): super().__init__(*args, anchor=anchor, fill=fill, **kwargs) class BotBubble(SpeechBubble): """Chat bubble for chatbot side of conversation.""" def __init__(self, *args, anchor=tk.NE, fill="lightblue", **kwargs): super().__init__(*args, anchor=anchor, fill=fill, **kwargs) class BubbleView(tk.Frame): """Scrolling frame that displays SpeechBubbles.""" font = (None, 12) # Default font def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.bubbles = [] bg = self["bg"] self.canvas = tk.Canvas(self, bg=bg) self.scrollbar = tk.Scrollbar(self, orient=tk.VERTICAL, command=self.canvas.yview) self.canvas.config(yscrollcommand=self.scrollbar.set) self.canvas.grid(row=0, column=0, sticky="news") self.scrollbar.grid(row=0, column=1, sticky="news") self.columnconfigure(0, weight=1) self.rowconfigure(0, weight=1) def add_bubble(self, text, is_client): """Add speech bubble to conversation.""" if is_client: bubble = ClientBubble( self.canvas, (5, self.bottom()+5), text, font=self.font ) else: bubble = BotBubble( self.canvas, (self.width()-5, self.bottom()+5), text, font=self.font ) bubble.create() self.bubbles.append(bubble) # Update scroll region to contain latest bubble. Display bubble. high = bubble.bottom() + 5 self.canvas.configure( scrollregion=(0, 0, self.width(), high) ) self.canvas.yview_moveto(1) def bottom(self): """Return y value of bottom button.""" if self.bubbles: return self.bubbles[-1].bottom() return 0 def width(self): """Return width of canvas.""" return self.canvas.winfo_width() def clear(self): self.bubbles = [] class Chat(tk.Tk): """ Demonstrate BubbleView.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.view = BubbleView(self, bg="black") self.entry = tk.Entry(self, font=(None, 12)) self.entry.bind("<Return>", self.talk) self.view.grid(row=0, column=0, sticky="news") self.entry.grid(row=1, column=0, sticky="news") self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) self.is_client = True def talk(self, event=None): """Add text to bubble view. Toggle between client and bot. Typing "clear" deletes the conversation. """ text = self.entry.get() if text == "clear": self.view.clear() else: self.view.add_bubble(text, self.is_client) self.is_client = not self.is_client self.entry.delete(0, tk.END) Chat().mainloop() |