Home > Blockchain >  How to create a TKinter scrollable frame with constant new labels being added
How to create a TKinter scrollable frame with constant new labels being added

Time:06-10

I'm trying to create a GUI for a sort of chat client. I'm fetching the chat with async and I am using multiprocessing to pipe the data to TKinter. That all works fine, but the problem I am currently having is when the new messages come in, they get added to the canvas and I can't scroll and the canvas is horizontally small so I would only be able to see the first two letters of the message anyways. I am like completely new to Python GUI so this is probably a really simple solution.

Here's the code that I have right now:

import tkinter as tk

class App(tk.Tk):

    def __init__(self, pipe, *args, **kw):
        super().__init__(*args, **kw)
        self.app_pipe, _ = pipe
        self.check_interval = 250
        global message_queue
        message_queue = []

        self.configure(bg='gray')
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)

        frame_main = tk.Frame(self, bg='gray')
        frame_main.grid(sticky='news')
        frame_main.columnconfigure(0, weight=1)

        label1 = tk.Label(frame_main, text='Channel')
        label1.grid(row=0, column=0, sticky='nw')

        frame_canvas = tk.Frame(frame_main, bg='red')
        frame_canvas.grid(row=1, column=0, sticky='nw')

        canvas = tk.Canvas(frame_canvas, bg='yellow')
        canvas.grid(row=0, column=0)

        vsb = tk.Scrollbar(canvas, orient='vertical', command=canvas.yview)
        vsb.grid(row=0, column=1, sticky='ns')
        canvas.configure(yscrollcommand=vsb.set)

        global names_frame
        names_frame = tk.Frame(canvas)
        canvas.create_window((0, 0), window=names_frame, anchor='nw')
        frame_canvas.bind('<Configure>', lambda e: canvas.configure(scrollregion=canvas.bbox('all')))

        self.bind('<<newMessage>>', self.add_message)

        self.after(self.check_interval, self.check)
    
    def check(self):
        while self.app_pipe.poll():
            msg = self.app_pipe.recv()

            if len(message_queue) < 200:
                message_queue.append(msg)
            elif len(message_queue) == 199:
                message_queue.pop(0)
                message_queue.append(msg)

            self.event_generate('<<newMessage>>', when='tail')

        self.after(self.check_interval, self.check)

    def add_message(self, *args):
        if message_queue[-1]['command'] == 'message':
            message = message_queue[-1]['data']
            print(message.author.name, len(message_queue))
            name = tk.Label(names_frame, text=message.author.name)
            name.grid(row=len(message_queue)-1, column=0)

This is what the GUI ends up looking like: enter image description here

CodePudding user response:

There are two issues:

  • You have created the scrollbar vsb as a child of canvas. It should be a child of frame_canvas instead

  • You need to update the scrollregion of canvas whenever names_frame is resized, not frame_canvas

class App(tk.Tk):

    def __init__(self, pipe, *args, **kw):
        super().__init__(*args, **kw)
        self.app_pipe, _ = pipe
        self.check_interval = 250
        global message_queue
        message_queue = []

        self.configure(bg='gray')
        self.columnconfigure(0, weight=1)
        self.rowconfigure(0, weight=1)

        frame_main = tk.Frame(self, bg='gray')
        frame_main.grid(sticky='news')
        frame_main.columnconfigure(0, weight=1)

        label1 = tk.Label(frame_main, text='Channel')
        label1.grid(row=0, column=0, sticky='nw')

        frame_canvas = tk.Frame(frame_main, bg='red')
        frame_canvas.grid(row=1, column=0, sticky='nw')

        canvas = tk.Canvas(frame_canvas, bg='yellow')
        canvas.grid(row=0, column=0)

        # should create 'vsb' as child of 'frame_canvas' instead
        vsb = tk.Scrollbar(frame_canvas, orient='vertical', command=canvas.yview)
        vsb.grid(row=0, column=1, sticky='ns')
        canvas.configure(yscrollcommand=vsb.set)

        global names_frame
        names_frame = tk.Frame(canvas)
        canvas.create_window((0, 0), window=names_frame, anchor='nw')
        # should bind '<Configure>' on 'names_frame' instead
        names_frame.bind('<Configure>', lambda e: canvas.configure(scrollregion=canvas.bbox('all')))

        self.bind('<<newMessage>>', self.add_message)

        self.after(self.check_interval, self.check)
...

Note that it is better to make message_queue and names_frame instance variables instead of global variables.

  • Related