Search code examples
python-3.xtkinter

Tkinter Canvas - Changing text inside of Class


all! I've been banging my head against a wall for days trying to figure this out. I pulled the Marquee class from How to make marquee on tkinter in label? (from the first answer) and am trying to dynamically update the marquee with new messages as needed. Anytime I try to access inside of that class, however, it tells me that 'NoneType' object has no attribute 'att name here'. I understand this has to do with init returning None. Changing it to new throws more errors, such as '_tkinter.tkapp' object has no attribute '_setup'. Any ideas or other help would be great.

import tkinter as tk 
from tkinter import *

#Pulled from: https://stackoverflow.com/questions/47224061/how-to-make-marquee-on-tkinter-in-label
class Marquee(tk.Canvas):
    def __init__(self, parent, text, margin=2, borderwidth=1, relief='flat', fps=30):
        super().__init__(parent, borderwidth=borderwidth, relief=relief)

        self.fps = fps
        
        # start by drawing the text off screen, then asking the canvas
        # how much space we need. Use that to compute the initial size
        # of the canvas. 
        text = self.create_text(0, -1000, text=text, anchor="w", tags=("text",))
        (x0, y0, x1, y1) = self.bbox("text")
        width = (x1 - x0) + (2*margin) + (2*borderwidth)
        height = (y1 - y0) + (2*margin) + (2*borderwidth)
        self.configure(width=325, height=height)

        # start the animation
        self.animate()

    def animate(self):
        (x0, y0, x1, y1) = self.bbox("text")
        if x1 < 0 or y0 < 0:
            # everything is off the screen; reset the X
            # to be just past the right margin
            x0 = self.winfo_width()
            y0 = int(self.winfo_height()/2)
            self.coords("text", x0, y0)
        else:
            self.move("text", -1, 0)

        # do again in a few milliseconds
        self.after_id = self.after(int(1000/self.fps), self.animate) 

#added this to try and update the text
def update_Marquee(self, marqueeText):

    if marqueeText == "" or None:
         marqueeText = marqueeTextDefault
    marquee.itemconfig("text", marqueeText)

#Info on Tkinter: https://tkdocs.com/shipman/index-2.html
root = tk.Tk()
root.title("Testing, Testing, Testing")
root.maxsize(width=340,height=393)

marquee = Marquee(root, text=testing, , borderwidth=1, relief="sunken").grid(column=0,row=0,padx=5,pady=1,columnspan=3,sticky=W)

I've tried: -Adding a function to the Marquee class to either overwrite the existing marquee or to update the text, both fail with the NoneType message -accessing marquee.itemconfig in multiple places, all failing


Solution

  • I've addressed some syntax errors and other issues, and moved the definition of update_marquee into the Marquee class where it belongs. I've also added marquee_default_text as a class attribute since it was undefined.

    Now when you instantiate Marquee, the value passed in as text will be treated as the default text, and you can call update_marquee to change that text as needed.

    As an example here, I'm using after to change the text to something else after 5 seconds.

    I've also made some minor adjustments to the names of things to adhere to PEP8 - I'd recommend familiarizing yourself with it! Understanding the Python style guide will help you to write better, more readable Python.

    import tkinter as tk
    
    #Pulled from: https://stackoverflow.com/questions/47224061/how-to-make-marquee-on-tkinter-in-label
    class Marquee(tk.Canvas):
        def __init__(
            self, parent, text, margin=2, borderwidth=1, relief='flat', fps=30
        ) -> None:
            super().__init__(parent, borderwidth=borderwidth, relief=relief)
            self.fps = fps
            self.marquee_text_default = text
    
            # start by drawing the text off screen, then asking the canvas
            # how much space we need. Use that to compute the initial size
            # of the canvas.
            text = self.create_text(
                0, -1000, text=text, anchor="w", tags=("text",)
            )
            (x0, y0, x1, y1) = self.bbox("text")
            width = (x1 - x0) + (2 * margin) + (2 * borderwidth)
            height = (y1 - y0) + (2 *  margin) + (2 * borderwidth)
            self.configure(width=325, height=height)
    
            # start the animation
            self.animate()
    
        def animate(self) -> None:
            (x0, y0, x1, y1) = self.bbox("text")
            if x1 < 0 or y0 < 0:
                # everything is off the screen; reset the X
                # to be just past the right margin
                x0 = self.winfo_width()
                y0 = int(self.winfo_height()/2)
                self.coords("text", x0, y0)
            else:
                self.move("text", -1, 0)
    
            # do again in a few milliseconds
            self.after_id = self.after(int(1000/self.fps), self.animate)
    
        def update_marquee(self, marquee_text=None) -> None:
            if not marquee_text:
                # NOTE: marquee_text_default was undefined.
                # I've added it as a class attr.
                marquee_text = self.marquee_text_default
            self.itemconfig("text", text=marquee_text)
    
    
    root = tk.Tk()
    root.title("Testing, Testing, Testing")
    root.maxsize(width=340,height=393)
    
    marquee = Marquee(root, text='testing', borderwidth=1, relief="sunken")
    # NOTE: always call grid (or pack or place) on a separate line!
    # otherwise your object will evaluate to None because that's what grid returns
    marquee.grid(column=0, row=0, padx=5, pady=1, columnspan=3, sticky='w')
    # change the marquee text after 5 seconds
    root.after(5000, marquee.update_marquee, 'Hello there!')
    root.mainloop()