Created
May 17, 2021 03:50
-
-
Save apple1417/7eab94b0207eae816de643220c744017 to your computer and use it in GitHub Desktop.
tkinter marquee
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import tkinter as tk | |
from threading import Lock | |
from tkinter import font | |
from typing import ClassVar, Optional | |
from util import Animatable, set_callback_property | |
class Marquee(tk.Canvas, Animatable): | |
_text_id: Optional[int] | |
_draw_lock: Lock | |
_should_animate: bool | |
_initalized: bool | |
text: str = set_callback_property("_text", "_update_text") | |
text_font: font.Font = set_callback_property("_text_font", "_update_text") | |
text_fill: str = set_callback_property("_text_fill", "_update_text") | |
speed: float | |
L_MARGIN: ClassVar[int] = 2 | |
def __init__( | |
self, | |
text: str, | |
text_font: font.Font, | |
text_fill: str, | |
speed: float, | |
) -> None: | |
self._initalized = False | |
super().__init__(bd=-2) # Supress the default border | |
self._text_id = None | |
self._draw_lock = Lock() | |
self.text = text | |
self.text_font = text_font | |
self.text_fill = text_fill | |
self.speed = speed | |
self._should_animate = False | |
self._initalized = True | |
self._update_text() | |
self.bind("<Configure>", self._handle_resize) | |
def _update_text(self, _old=None, _new=None) -> None: | |
if not self._initalized: | |
return | |
with self._draw_lock: | |
if self._text_id is not None: | |
self.delete(self._text_id) | |
self._text_id = self.create_text( | |
# Placeholder coords - we need to create the text first to get it's boundaries and check | |
# if to scroll or not | |
-1e6, -1e6, | |
fill=self.text_fill, | |
text=self.text, | |
font=self.text_font, | |
anchor=tk.W, | |
) | |
w = self.winfo_width() | |
y = (self.winfo_height() / 2) - 1 | |
x_min, _, x_max, _ = self.bbox(self._text_id) | |
if x_max - x_min < w: | |
self._should_animate = False | |
self.coords(self._text_id, self.L_MARGIN, y) | |
else: | |
self._should_animate = True | |
self.coords(self._text_id, w, y) | |
def _handle_resize(self, event: tk.Event) -> None: | |
with self._draw_lock: | |
x_min, _, x_max, _ = self.bbox(self._text_id) | |
w = self.winfo_width() | |
x: float | |
if x_max - x_min < w: | |
self._should_animate = False | |
x = self.L_MARGIN | |
elif x_min > w: | |
self._should_animate = True | |
x = w | |
else: | |
self._should_animate = True | |
x = x_min | |
y = (event.height / 2) - 1 | |
self.coords(self._text_id, x, y) | |
def tick(self, delta: float) -> None: | |
with self._draw_lock: | |
if not self._should_animate: | |
return | |
_, _, x_max, _ = self.bbox(self._text_id) | |
if x_max < self.L_MARGIN: | |
w = self.winfo_width() | |
h = self.winfo_height() | |
self.coords(self._text_id, w, (h / 2) - 1) | |
else: | |
self.move(self._text_id, -self.speed * delta, 0) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import tkinter as tk | |
from datetime import datetime, timezone | |
from tkinter import font | |
from typing import ClassVar, Optional | |
from marquee import Marquee | |
from util import Animatable | |
LONG_TEXT: str = "aaaaaaaaaa bbbbbbbbbb cccccccccc dddddddddd eeeeeeeeee ffffffffff gggggggggg" | |
SHORT_TEXT: str = "aaaaaaaaaa" | |
# TODO: config file for all these | |
DEFAULT_W: ClassVar[int] = 500 | |
DEFAULT_H: ClassVar[int] = 27 | |
DEFAULT_FONT_FAMILY: ClassVar[str] = "Open Sans" | |
DEFAULT_FONT_SIZE: ClassVar[int] = 15 | |
DEFAULT_FONT_FILL: ClassVar[str] = "black" # "white" | |
DEFAULT_FPS: ClassVar[int] = 144 | |
DEFAULT_SPEED: ClassVar[int] = 64 | |
class App(tk.Tk): | |
label: Marquee | |
animated_elements: list[Animatable] | |
_last_animate: datetime | |
_after_id: Optional[str] | |
fps: int | |
def __init__(self, fps: int) -> None: | |
super().__init__() | |
self.fps = fps | |
self.minsize(10, 10) | |
self.geometry(f"{DEFAULT_W}x{DEFAULT_H}") | |
self.label = Marquee( | |
LONG_TEXT, | |
font.Font( | |
family=DEFAULT_FONT_FAMILY, | |
size=DEFAULT_FONT_SIZE, | |
), | |
DEFAULT_FONT_FILL, | |
DEFAULT_SPEED | |
) | |
self.label.pack(fill="both", expand=True) | |
self.animated_elements = [self.label] | |
self._last_animate = datetime.now(timezone.utc) | |
self._after_id = None | |
self.start_animation() | |
# temp | |
self.bind("r", self.test) | |
self.bind("t", self.test2) | |
def start_animation(self) -> None: | |
if self._after_id is None: | |
self._after_id = self.after(round(1000 / DEFAULT_FPS), self.animate) | |
def stop_animation(self) -> None: | |
if self._after_id is not None: | |
self.after_cancel(self._after_id) | |
self._after_id = None | |
def animate(self) -> None: | |
# TODO: adjust this based on the previous delta | |
# 144 fps actually runs at 142.9 because it always rounds the same | |
self._after_id = self.after(round(1000 / self.fps), self.animate) | |
now = datetime.now(timezone.utc) | |
delta = (now - self._last_animate).total_seconds() | |
for element in self.animated_elements: | |
element.tick(delta) | |
self._last_animate = now | |
def test(self, _) -> None: | |
self.label.text = LONG_TEXT | |
def test2(self, _) -> None: | |
self.label.text = SHORT_TEXT | |
App(DEFAULT_FPS).mainloop() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from abc import ABC, abstractmethod | |
from typing import TypeVar | |
_T = TypeVar("_T") | |
def set_callback_property(name: str, callback_name: str) -> property: | |
def getter(self) -> _T: | |
return getattr(self, name) | |
def setter(self, new_value: _T) -> None: | |
setattr(self, name, new_value) | |
getattr(self, callback_name)() | |
return property(getter, setter) | |
class Animatable(ABC): | |
@abstractmethod | |
def tick(self, delta: float) -> None: | |
""" | |
Argument called to animate each frame. | |
Args: | |
delta: The amount of ms since the last time this was called. | |
""" | |
raise NotImplementedError |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment