Python Forum
[Tkinter] Help create scrollbar in chatbot with tkinter on python
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[Tkinter] Help create scrollbar in chatbot with tkinter on python
#1
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()
deanhystad write Aug-04-2023, 01:56 PM:
Please post all code, output and errors (it it's entirety) between their respective tags. Refer to BBCode help topic on how to post. Use the "Preview Post" button to make sure the code is presented as you expect before hitting the "Post Reply/Thread" button.
Reply
#2
The code you posted does not run.
Error:
File "....", line 4, in <module> root = tk.Tk() ^^ NameError: name 'tk' is not defined. Did you mean: 'Tk'?
There are other errors. You don't create anything named scroll. You cannot use pack and grid in the same widget.
fenec10 likes this post
Reply
#3
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()
Reply
#4
Quote:it runs perfectly
Oh 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.
Reply
#5
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()
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  [Tkinter] Scrollbar apffal 7 3,156 Oct-11-2021, 08:26 PM
Last Post: deanhystad
Question [Tkinter] How to configure scrollbar dimension? water 6 3,468 Jan-03-2021, 06:16 PM
Last Post: deanhystad
  [PyQt] scrollbar in tab issac_n 1 3,617 Aug-04-2020, 01:33 PM
Last Post: deanhystad
  [Tkinter] Scrollbar in tkinter PatrickNoir 2 3,330 Jul-26-2020, 06:02 PM
Last Post: deanhystad
  Tkinter: Create an homepage look like PeroPuri 8 5,935 Jun-26-2020, 12:57 AM
Last Post: menator01
  Create image on a Toplevel using tkinter ViktorWong 3 7,868 Jun-13-2020, 03:21 PM
Last Post: deanhystad
  [Tkinter] Help with Scrollbar JJota 6 3,679 Mar-10-2020, 05:25 AM
Last Post: Larz60+
  [Tkinter] Scrollbar doesn't work on Canvas in Tkinter DeanAseraf1 3 9,393 Sep-19-2019, 03:26 PM
Last Post: joe_momma
  [Tkinter] Same Scrollbar for two text area smabubakkar 3 2,878 Jun-19-2019, 05:26 PM
Last Post: Denni
  Scrollbar rturus 5 17,658 Jun-06-2019, 01:04 PM
Last Post: heiner55

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020