How to dynamically wrap label text in tkinter with .bind and <Configure> - python

I'm trying to create a page that outputs a large amount of data, and wraps the text dynamically depending on window size. I started by setting wraplength = self.master.winfo_width(), which sets the text wrapping to the current window size, but it does not change when the window does. I found this answer, which seemed like it would solve the problem, but when trying to recreate it myself, something went wrong. I suspect that I'm misunderstanding something with .bind or <Configure>, but I can't be sure. My base code is as follows:
from tkinter import *
class Wrap_example(Frame):
def __init__(self):
Frame.__init__(self)
self.place(relx=0.5, anchor='n')
#Initalize list and variable that populates it
self.data_list = []
self.data = 0
#Button and function for creating a bunch of numbers to fill the space
self.button = Button(self, text = "Go", command = self.go)
self.button.grid()
def go(self):
for self.data in range(1, 20000, 100):
self.data_list.append(self.data)
#Label that holds the data, text = list, wraplength = current window width
self.data = Label(self, text = self.data_list, wraplength = self.master.winfo_width(), font = 'arial 30')
self.data.grid()
#Ostensibly sets the label to dynamically change wraplength to match new window size when window size changes
self.data.bind('<Configure>', self.rewrap())
def rewrap(self):
self.data.config(wraplength = self.master.winfo_width())
frame01 = Wrap_example()
frame01.mainloop()
A few things of note: I tried using the lambda directly as shown in the linked answer, but it didn't work. If I remove the rewrap function and use self.data.bind('<Configure>', lambda e: self.data.config(wraplength=self.winfo_width()), it throws a generic Syntax error, always targeting the first character after that line, (the d in def if the function is left in, the f in frame01 if it's commented out). Leaving rewrap as-is doesn't throw an error, but it doesn't perform any other apparent function, either. Clicking 'Go' will always spawn data that wraps at the current window size, and never changes.

There are few issues:
frame Wrap_example does not fill all the horizontal space when window is resized
label self.data does not fill all the horizontal space inside frame Wrap_example when the frame is resized
self.rewrap() will be executed immediately when executing the line self.data.bind('<Configure>', self.rewrap())
To fix the above issues:
set relwidth=1 in self.place(...)
call self.columnconfigure(0, weight=1)
use self.data.bind('<Configure>', self.rewrap) (without () after rewrap) and add event argument in rewrap()
from tkinter import *
class Wrap_example(Frame):
def __init__(self):
Frame.__init__(self)
self.place(relx=0.5, anchor='n', relwidth=1) ### add relwidth=1
self.columnconfigure(0, weight=1) ### make column 0 use all available horizontal space
#Initalize list and variable that populates it
self.data_list = []
self.data = 0
#Button and function for creating a bunch of numbers to fill the space
self.button = Button(self, text = "Go", command = self.go)
self.button.grid()
def go(self):
for self.data in range(1, 20000, 100):
self.data_list.append(self.data)
#Label that holds the data, text = list, wraplength = current window width
self.data = Label(self, text = self.data_list, wraplength = self.master.winfo_width(), font = 'arial 30')
self.data.grid()
#Ostensibly sets the label to dynamically change wraplength to match new window size when window size changes
self.data.bind('<Configure>', self.rewrap) ### remove () after rewrap
def rewrap(self, event): ### add event argument
self.data.config(wraplength = self.master.winfo_width())
frame01 = Wrap_example()
frame01.mainloop()

Related

How can I place a widget at the very bottom of a tkinter window when positioning with .grid()?

I am aware that you cannot use different types of geometry managers within the same Tkinter window, such as .grid() and .pack(). I have a window that has been laid out using .grid() and I am now trying to add a status bar that would be snapped to the bottom of the window. The only method I have found online for this is to use .pack(side = BOTTOM), which will not work since the rest of the window uses .grid().
Is there a way that I can select the bottom of the window to place widgets from when using .grid()?
from tkinter import *
from tkinter.ttk import *
import tkinter as tk
class sample(Frame):
def __init__(self,master=None):
Frame.__init__(self, master)
self.status = StringVar()
self.status.set("Initializing")
statusbar = Label(root,textvariable = self.status,relief = SUNKEN, anchor = W)
statusbar.pack(side = BOTTOM, fill = X)
self.parent1 = Frame()
self.parent1.pack(side = TOP)
self.createwidgets()
def createwidgets(self):
Label(self.parent1,text = "Grid 1,1").grid(row = 1, column = 1)
Label(self.parent1,text = "Grid 1,2").grid(row = 1, column = 2)
Label(self.parent1,text = "Grid 2,1").grid(row = 2, column = 1)
Label(self.parent1,text = "Grid 2,2").grid(row = 2, column = 2)
if __name__ == '__main__':
root = Tk()
app = sample(master=root)
app.mainloop()
So using labels since I was kinda lazy to do other stuff, you can do frames to ensure that each section of your window can be packed/grid as required. Frames will be a useful tool for you to use when trying to arrange your widgets. Note that using a class can make things a little easier when deciding your parents. So imagine each frame is a parent and their children can be packed as required. So I would recommend drawing out your desired GUI and see how you will arrange them. Also if you want to add another frame within a frame simply do:
self.level2 = Frame(self.parent1)
You can check out additional settings in the docs
http://effbot.org/tkinterbook/frame.htm
PS: I am using a class hence the self, if you don't want to use classes then its okay to just change it to be without a class. Classes make it nicer to read though
Just give it a row argument that is larger than any other row. Then, give a weight to at least one of the rows before it.
Even better is to use frames to organize your code. Pack the scrollbar on the bottom and a frame above it. Then, use grid for everything inside the frame.
Example:
# layout of the root window
main = tk.Frame(root)
statusbar = tk.Label(root, text="this is the statusbar", anchor="w")
statusbar.pack(side="bottom", fill="x")
main.pack(side="top", fill="both", expand=True)
# layout of the main window
for row in range(1, 10):
label = tk.Label(main, text=f"R{row}")
label.grid(row=row, sticky="nsew")
main.grid_rowconfigure(row, weight=1)
...

Handling visual overflow in a tkinter frame?

I'm writing a class that adds a scrolling frame. It detects when the frame's contents exceed its height, and then configures the scrollbar. The problem is that when I scroll down, the items in the frame scroll outside of the top of the frame and appear above it.
I've tested it out using plain labels, and it worked fine, but I'm using a class object that has some nested frames, and the child objects are what show up above the scrolling frame.
This is the gist of the code that's giving me problems (please note that the layout doesn't match the full project. I used this as my reference for the ScrollFrame() class.)
Just running this, pressing the button, and scrolling down will show you what's wrong with it.
import tkinter as tk
import tkinter.simpledialog as sd
class ScrollFrame(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
### setting up the objects used ###
self.canvas = tk.Canvas(master)
self.frame = tk.Frame(self.canvas)
self.scrollbar = tk.Scrollbar(master, orient = 'vertical',
command = self.canvas.yview)
### scrollbar moves with current canvas scrollamount ###
self.canvas.configure(yscrollcommand = self.scrollbar.set)
self.scrollbar.pack(side = 'right', fill = 'y')
self.canvas.pack(side = 'left', fill = 'both', expand = True)
### creating frame to pack widgets onto ###
self.canvas.create_window((4, 4), window = self.frame,
anchor = 'nw', tags = 'self.frame')
### setting scrollbar height on load ###
self.frame.bind('<Configure>', self.frameConfig)
### scroll when a user's mouse wheel is used inside the canvas ###
def scrollCanvas(event):
self.canvas.yview_scroll(-1*(event.delta//120), 'units')
self.canvas.bind('<MouseWheel>', scrollCanvas)
### set the scrollregion of the canvas ###
def frameConfig(self, event):
self.canvas.configure(scrollregion = self.canvas.bbox('all'))
class OptionSet(tk.Frame):
def __init__(self, master, **kwargs):
super().__init__()
self.all = tk.Frame(master)
self.all.configure(bd = 1, relief = 'solid')
# independent label
self.text = '' if not kwargs['text'] else kwargs['text']
self.label = tk.Label(text = self.text)
# list of all buttons
self.buttons = tk.Frame()
buttons = [] if not kwargs['buttons'] else kwargs['buttons']
self.button_list = []
if buttons:
for button in buttons:
self.button_list.append(
tk.Button(self.buttons, text = button)
)
self.style()
def style(self, default = 1, **kwargs):
if default:
self.label.pack(in_ = self.all, side = 'left')
for button in self.button_list:
button.pack(side = 'left')
self.buttons.pack(in_ = self.all, side = 'right')
root = tk.Tk()
list_items = []
current = {
'month': 'August'
# ...
}
def btn_fcn(num):
for i in list_items:
i.grid_forget()
'''
# get event as input
event = sd.askstring('Event Input',
f"What is happening on {current['month']} {num}?")
# insert new list_item
list_items.append(OptionSet(event_list.frame, text = event,
buttons = ['Edit', 'Delete']))
print(event)
'''
for i in range(10):
list_items.append(OptionSet(event_list.frame, text = 'test',
buttons = ['Edit', 'Delete']))
for i in list_items:
i.all.grid(sticky = 'we')
tk.Button(root, text = 'Add', command = lambda: btn_fcn(22)).pack()
event_list = ScrollFrame(root)
event_list.pack()
root.mainloop()
I want the buttons and labels to cut off outside of the ScrollFrame. I don't know whether they're overflowing from the frame or the canvas, but they should cut off normally if all goes according to plan.
Thanks.
The problem is that you're being very sloppy with where you create your widgets. For example, you aren't putting the canvas and scrollbar in the ScrollFrame, you're putting it in master. Every widget defined in ScrolLFrame needs to be in self or a child of self.
The same is true with OptionSet - you're putting the inner frame (self.all) a d the other widgets in master rather than inside the OptionSet itself.
My recommendation is to temporarily remove all of the OptionSet code (or just don't use it) and instead, just add labels to the scroll frame. Focus on getting that working without the complexity of a custom class being added to a custom class. Once you are able to scroll just labels, the you can add back in the OptionSet code.

Pack a text widget on the side doesn't work

I'm kind of new to tkinter. I tried to create a Text widget on the left side at 0,0, but it appears in the middle, like a default pack().
Here is my code:
from Tkinter import *
# the ui of the main window
class Ui(object):
# the init of the client object
def __init__(self):
self.root = Tk()
self.mid_height = self.root.winfo_screenheight() / 2
self.mid_width = self.root.winfo_screenwidth() / 2
self.root.title("Journey-opening")
self.root.geometry("600x600+{}+{}".format(self.mid_width - 300, self.mid_height - 300))
self.root.resizable(width=0, height=0)
self.cyan = "#0990CB"
self.root["background"] = self.cyan
self.frame = Frame(self.root)
self.frame.pack()
self.chat_box = Text(self.frame, height=30, width=50)
self.chat_box.pack(side=LEFT)
def open(self):
self.root.mainloop()
wins = Ui()
wins.open()
I also tried with grid method but it did not change anything, and also created another widget because maybe it needs 2 widgets at least.
I guess its something with my frame but I follow a tutorial and everything seems fine.
"Pack a text widget on the side doesn't work"
That is incorrect the line self.chat_box.pack(side=LEFT) does pack the Text widget to side. It's just that it is done inside self.frame which allocates exactly as much space needed for the widgets it encapsulates(in this case that is only the text widget) by default. So in a way, the Text widget is packed, not just to left, but to all sides.
In order to have self.chat_box on the upper left corner, you should let frame to occupy more space than needed, in this case, it can simply occupy all space in the x-axis inside its parent(self.root). In order to do that, replace:
self.frame.pack()
with:
self.frame.pack(fill='x') # which is the same as self.frame.pack(fill=X)

Event when entry's xview changes

I am creating a simple program using Tkinter. I want a function to be called every time xview property of entry changes. But there doesn't seem to be an event like this, at least not one that I can find.
The <Configure> event fires only on resize, which I already handled, but it doesn't fire when actual value I'm tracking changes in a different way, such as the user dragging his mouse to see the end of the entry.
Here is the code:
import Tkinter as Tk
import tkFileDialog
root = Tk.Tk()
class RepositoryFolderFrame(Tk.Frame):
def __init__(self, root):
Tk.Frame.__init__(self, root)
self.build_gui()
self.set_entry_text("Searching...")
#root.after(0, self.find_repo)
self.prev_entry_index = len(self.entry.get())
root.bind("<Configure>", self.on_entry_resize)
#self.entry.bind(???, self.on_entry_change)
#self.entry.bind("<Configure>", self.on_entry_change)
def on_entry_resize(self, event):
cur_entry_index = self.entry.xview()[1]
if cur_entry_index != self.prev_entry_index:
self.entry.xview(self.prev_entry_index)
def on_entry_change(self, event):
# This should be called when xview changes...
cur_entry_index = self.entry.xview()[1]
self.prev_entry_index = cur_entry_index
def set_entry_text(self, text):
self.entry_text.set(text)
self.entry.xview("end")
def build_gui(self):
label = Tk.Label(self, text = "Repository folder:")
label.pack(side = Tk.LEFT)
self.label = label
entry_text = Tk.StringVar()
self.entry_text = entry_text
entry = Tk.Entry(self, width = 50, textvariable = entry_text)
entry.configure(state = 'readonly')
entry.pack(side = Tk.LEFT, fill = Tk.X, expand = 1)
self.entry = entry
button = Tk.Button(self, text = "Browse...")
button.pack(side = Tk.LEFT)
self.button = button
repo_frame = RepositoryFolderFrame(root)
repo_frame.pack(fill = Tk.X, expand = 1)
root.mainloop()
There is no mechanism for getting notified when the xview changes. There are ways to do it by modifying the underlying tcl code, but it's much more difficult than it's worth.
A simple solution is to write a function that polls the xview every few hundred milliseconds. It can keep track of the most recent xview, compare it to the current, and if it has changed it can fire off a custom event (eg: <<XviewChanged>>) which you can bind to.
It would look something like this:
class RepositoryFolderFrame(Tk.Frame):
def __init__(self, root):
...
self.entry.bind("<<XviewChanged>>", self.on_entry_change)
# keep a cache of previous xviews. A dictionary is
# used in case you want to do this for more than
self._xview = {}
self.watch_xview(self.entry)
def watch_xview(self, widget):
xview = widget.xview()
prev_xview = self._xview.get(widget, "")
self._xview[widget] = xview
if xview != prev_xview:
widget.event_generate("<<XviewChanged>>")
widget.after(100, self.watch_xview, widget)
You'll need to modify that for the edge case that the entry widget is destroyed, though you can handle that with a simple try around the code. This should be suitably performant, though you might need to verify that if you have literally hundreds of entry widgets.

Tkinter frame alignment (grid layout)

In Python/Tkinter, I am trying to get a value (integer) from a frame, do a calculation on it and return it as an output. I was able to make it work when using only a master frame and putting everything in that one frame. Now I want to separate it and another operation into their own frames.
My program converts board feet to Lineal feet (frame A) and
Lineal feet to board feet (frame B).
I get errors like: NameError: global name 'b_entry' is not defined. I have a feeling I might be doing things like referencing frames (self, master, a_frame, etc.) improperly.
Here's a simple example:
import Tkinter as tk
class App:
def __init__(self, master):
self.master = master
master.grid()
b_frame = tk.Frame(master).grid(row=0)
b_name = tk.Label(b_frame, text='Blah').grid(row=1, columnspan=4)
b_label = tk.Label(b_frame, text='Label').grid(row=2, column=0)
b_entry = tk.Entry(b_frame).grid(row=2, column=1)
b_output = tk.Label(b_frame, text='Output')
b_output.grid(row=3, columnspan=2)
b_button = tk.IntVar()
tk.Button(b_frame, text='calc', command=self.convert).grid(row=4, columnspan=2)
def convert(self):
a = int(b_entry.get())
result = int(a * a)
b_output.configure(text=result)
root = tk.Tk()
App(root)
root.mainloop()
I see two issues.
You should not assign a widget to a variable and pack/grid it on the same line. This causes the variable to have a value of None, because that's what pack and grid return. This will screw up your widget hierarchy because when b_frame is None, tk.Label(b_frame, ... will make the label a child of the Tk window instead of the Frame.
If you want a variable to be visible in all of a class' methods, you should assign it as an attribute of self.
So you should change lines like
b_entry = tk.Entry(b_frame).grid(row = 2, column = 1)
To
self.b_entry = tk.Entry(b_frame)
self.b_entry.grid(row = 2, column = 1)
Then you will be able to properly reference the entry in convert.
a = int(self.b_entry.get())

Categories

Resources