Skip to content

Instantly share code, notes, and snippets.

@thegamecracks
Last active May 12, 2024 12:35
Show Gist options
  • Save thegamecracks/5595dad631ec50bdc021f945054e86fb to your computer and use it in GitHub Desktop.
Save thegamecracks/5595dad631ec50bdc021f945054e86fb to your computer and use it in GitHub Desktop.
A tkinter label with automatic text wrapping
import sys
from tkinter import Event, Tk
from tkinter.ttk import Frame, Label
if sys.platform == "win32":
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(2)
LOREM_IPSUM = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce malesuada ipsum enim, feugiat venenatis sapien tempus et. Morbi auctor egestas metus vel faucibus. Nullam in nunc nisi. Maecenas eget sagittis ante. Sed ut turpis turpis. Morbi suscipit massa ac efficitur iaculis. Suspendisse risus nisi, tempor non pulvinar sed, vehicula a sapien. Pellentesque iaculis ligula sed sapien faucibus, eget mollis magna volutpat. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Morbi porttitor luctus tellus, nec tincidunt lacus. Nunc dui eros, laoreet sed pretium id, porttitor eget nunc."
class WrapLabel(Label):
"""A label that automatically wraps its text when resized.
When using this label, the geometry manager must be configured
to resize the widget in some way, otherwise the width will
default to the text's max length.
This can mean using ``.pack(fill="both", expand=True)``, or
``.place(relwidth=1, relheight=1)``, or by configuring grid
weights appropriately.
``minwidth=`` can be specified to prevent wrapping under
a certain width which can significantly improve performance
for longer lines of text.
As a limitation of the current implementation, this class can only
match its bounding box when used with the grid geometry manager.
When using other geometry managers like pack or place, WrapLabel will
always wrap to the window's entire width, regardless of the actual
bounding box allocated for them. As a result, the text may appear clipped.
If you use those geometry managers, it is recommended to grid / pack
each WrapLabel inside a frame so it can calculate the wrap length from
that frame's width. For example::
# Instead of packing the label directly:
label = WrapLabel(root, text="Some long piece of text")
label.pack(fill="both", expand=True, padx=200, pady=200)
# Pack a container frame and have the label expand inside it:
frame = Frame(root)
frame.pack(fill="both", expand=True, padx=200, pady=200)
label = WrapLabel(frame, text="Some long piece of text")
label.pack(fill="both", expand=True)
In effect, the container frame serves as a way to compute the true
bounding box allocated by the geometry manager, helping the label
wrap to the correct width.
"""
def __init__(self, *args, minwidth: int = 1, **kwargs):
super().__init__(*args, **kwargs)
self.minwidth = minwidth
self.bind("<Configure>", self.__on_configure)
def __on_configure(self, event: Event):
width = max(self.minwidth, self.__get_width())
if width != 1: # Prevent wrapping on initial configuration
self.configure(wraplength=width)
def __get_width(self) -> int:
if self.winfo_manager() == "grid":
# Wrap to the bounding box reserved for the label
# instead of the container's full width.
# Not doing this might lead to clipped text.
options = self.grid_info()
bbox = self.master.grid_bbox(options["column"], options["row"])
if bbox is None:
return 1
width = bbox[2]
if isinstance(options["padx"], int):
padx = options["padx"] * 2
else:
padx = sum(options["padx"])
return width - padx
else:
return self.master.winfo_width()
class MyFrame(Frame):
def __init__(self, parent):
super().__init__(parent)
# In this example, if both grid weights are removed, the label won't wrap.
self.grid_columnconfigure(1, weight=1)
for i in range(5):
self.grid_rowconfigure(i, weight=1)
col1 = Label(self, text=f"This is row {i+1}!")
col1.grid(row=i, column=0)
col2 = WrapLabel(self, text=LOREM_IPSUM, minwidth=600)
col2.grid(row=i, column=1)
app = Tk()
app.geometry("640x480")
app.grid_columnconfigure(0, weight=1)
app.grid_rowconfigure(1, weight=1)
label = WrapLabel(app, text=LOREM_IPSUM, minwidth=600)
label.grid(padx=10, pady=10, sticky="ew")
frame = MyFrame(app)
frame.grid(padx=10, pady=(0, 10), sticky="nesw")
app.mainloop()
@thegamecracks
Copy link
Author

wrap

@Mews
Copy link

Mews commented May 11, 2024

goated

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment