Related
I am using Python to parse entries from a log file, and display the entry contents using Tkinter and so far it's been excellent. The output is a grid of label widgets, but sometimes there are more rows than can be displayed on the screen. I'd like to add a scrollbar, which looks like it should be very easy, but I can't figure it out.
The documentation implies that only the List, Textbox, Canvas and Entry widgets support the scrollbar interface. None of these appear to be suitable for displaying a grid of widgets. It's possible to put arbitrary widgets in a Canvas widget, but you appear to have to use absolute co-ordinates, so I wouldn't be able to use the grid layout manager?
I've tried putting the widget grid into a Frame, but that doesn't seem to support the scrollbar interface, so this doesn't work:
mainframe = Frame(root, yscrollcommand=scrollbar.set)
Can anyone suggest a way round this limitation? I'd hate to have to rewrite in PyQt and increase my executable image size by so much, just to add a scrollbar!
Overview
You can only associate scrollbars with a few widgets, and the root widget and Frame aren't part of that group of widgets.
There are at least a couple of ways to do this. If you need a simple vertical or horizontal group of widgets, you can use a text widget and the window_create method to add widgets. This method is simple, but doesn't allow for a complex layout of the widgets.
A more common general-purpose solution is to create a canvas widget and associate the scrollbars with that widget. Then, into that canvas embed the frame that contains your label widgets. Determine the width/height of the frame and feed that into the canvas scrollregion option so that the scrollregion exactly matches the size of the frame.
Why put the widgets in a frame rather than directly in the canvas? A scrollbar attached to a canvas can only scroll items created with one of the create_ methods. You cannot scroll items added to a canvas with pack, place, or grid. By using a frame, you can use those methods inside the frame, and then call create_window once for the frame.
Drawing the text items directly on the canvas isn't very hard, so you might want to reconsider that approach if the frame-embedded-in-a-canvas solution seems too complex. Since you're creating a grid, the coordinates of each text item is going to be very easy to compute, especially if each row is the same height (which it probably is if you're using a single font).
For drawing directly on the canvas, just figure out the line height of the font you're using (and there are commands for that). Then, each y coordinate is row*(lineheight+spacing). The x coordinate will be a fixed number based on the widest item in each column. If you give everything a tag for the column it is in, you can adjust the x coordinate and width of all items in a column with a single command.
Object-oriented solution
Here's an example of the frame-embedded-in-canvas solution, using an object-oriented approach:
import tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.canvas = tk.Canvas(self, borderwidth=0, background="#ffffff")
self.frame = tk.Frame(self.canvas, background="#ffffff")
self.vsb = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.vsb.set)
self.vsb.pack(side="right", fill="y")
self.canvas.pack(side="left", fill="both", expand=True)
self.canvas.create_window((4,4), window=self.frame, anchor="nw",
tags="self.frame")
self.frame.bind("<Configure>", self.onFrameConfigure)
self.populate()
def populate(self):
'''Put in some fake data'''
for row in range(100):
tk.Label(self.frame, text="%s" % row, width=3, borderwidth="1",
relief="solid").grid(row=row, column=0)
t="this is the second column for row %s" %row
tk.Label(self.frame, text=t).grid(row=row, column=1)
def onFrameConfigure(self, event):
'''Reset the scroll region to encompass the inner frame'''
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
if __name__ == "__main__":
root=tk.Tk()
example = Example(root)
example.pack(side="top", fill="both", expand=True)
root.mainloop()
Procedural solution
Here is a solution that doesn't use a class:
import tkinter as tk
def populate(frame):
'''Put in some fake data'''
for row in range(100):
tk.Label(frame, text="%s" % row, width=3, borderwidth="1",
relief="solid").grid(row=row, column=0)
t="this is the second column for row %s" %row
tk.Label(frame, text=t).grid(row=row, column=1)
def onFrameConfigure(canvas):
'''Reset the scroll region to encompass the inner frame'''
canvas.configure(scrollregion=canvas.bbox("all"))
root = tk.Tk()
canvas = tk.Canvas(root, borderwidth=0, background="#ffffff")
frame = tk.Frame(canvas, background="#ffffff")
vsb = tk.Scrollbar(root, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
canvas.pack(side="left", fill="both", expand=True)
canvas.create_window((4,4), window=frame, anchor="nw")
frame.bind("<Configure>", lambda event, canvas=canvas: onFrameConfigure(canvas))
populate(frame)
root.mainloop()
Make it scrollable
Use this handy class to make the frame containing your widgets scrollable. Follow these steps:
create the frame
display it (pack, grid, etc)
make it scrollable
add widgets inside it
call the update() method
import tkinter as tk
from tkinter import ttk
class Scrollable(tk.Frame):
"""
Make a frame scrollable with scrollbar on the right.
After adding or removing widgets to the scrollable frame,
call the update() method to refresh the scrollable area.
"""
def __init__(self, frame, width=16):
scrollbar = tk.Scrollbar(frame, width=width)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, expand=False)
self.canvas = tk.Canvas(frame, yscrollcommand=scrollbar.set)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.config(command=self.canvas.yview)
self.canvas.bind('<Configure>', self.__fill_canvas)
# base class initialization
tk.Frame.__init__(self, frame)
# assign this obj (the inner frame) to the windows item of the canvas
self.windows_item = self.canvas.create_window(0,0, window=self, anchor=tk.NW)
def __fill_canvas(self, event):
"Enlarge the windows item to the canvas width"
canvas_width = event.width
self.canvas.itemconfig(self.windows_item, width = canvas_width)
def update(self):
"Update the canvas and the scrollregion"
self.update_idletasks()
self.canvas.config(scrollregion=self.canvas.bbox(self.windows_item))
Usage example
root = tk.Tk()
header = ttk.Frame(root)
body = ttk.Frame(root)
footer = ttk.Frame(root)
header.pack()
body.pack()
footer.pack()
ttk.Label(header, text="The header").pack()
ttk.Label(footer, text="The Footer").pack()
scrollable_body = Scrollable(body, width=32)
for i in range(30):
ttk.Button(scrollable_body, text="I'm a button in the scrollable frame").grid()
scrollable_body.update()
root.mainloop()
Extends class tk.Frame to support a scrollable Frame
This class is independent from the widgets to be scrolled and can be used to replace a standard tk.Frame.
import tkinter as tk
class ScrollbarFrame(tk.Frame):
"""
Extends class tk.Frame to support a scrollable Frame
This class is independent from the widgets to be scrolled and
can be used to replace a standard tk.Frame
"""
def __init__(self, parent, **kwargs):
tk.Frame.__init__(self, parent, **kwargs)
# The Scrollbar, layout to the right
vsb = tk.Scrollbar(self, orient="vertical")
vsb.pack(side="right", fill="y")
# The Canvas which supports the Scrollbar Interface, layout to the left
self.canvas = tk.Canvas(self, borderwidth=0, background="#ffffff")
self.canvas.pack(side="left", fill="both", expand=True)
# Bind the Scrollbar to the self.canvas Scrollbar Interface
self.canvas.configure(yscrollcommand=vsb.set)
vsb.configure(command=self.canvas.yview)
# The Frame to be scrolled, layout into the canvas
# All widgets to be scrolled have to use this Frame as parent
self.scrolled_frame = tk.Frame(self.canvas, background=self.canvas.cget('bg'))
self.canvas.create_window((4, 4), window=self.scrolled_frame, anchor="nw")
# Configures the scrollregion of the Canvas dynamically
self.scrolled_frame.bind("<Configure>", self.on_configure)
def on_configure(self, event):
"""Set the scroll region to encompass the scrolled frame"""
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
Usage:
class App(tk.Tk):
def __init__(self):
super().__init__()
sbf = ScrollbarFrame(self)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
sbf.grid(row=0, column=0, sticky='nsew')
# sbf.pack(side="top", fill="both", expand=True)
# Some data, layout into the sbf.scrolled_frame
frame = sbf.scrolled_frame
for row in range(50):
text = "%s" % row
tk.Label(frame, text=text,
width=3, borderwidth="1", relief="solid") \
.grid(row=row, column=0)
text = "this is the second column for row %s" % row
tk.Label(frame, text=text,
background=sbf.scrolled_frame.cget('bg')) \
.grid(row=row, column=1)
if __name__ == "__main__":
App().mainloop()
I'm creating a simple user dialog window with a basic text on top and a tree view with one column below, that gives the user a couple of choices. A button at the bottom is used to confirm the selection.
Now I can't get the Message widget, which I use to display the instructions, to fill the Frame I've created for it. Meanwhile, the Treeview widget fills the Frame as I want it to.
Many proposed solutions on other StackOverflow questions state, that putting my_message.pack(fill=tk.X, expand=True) should work. It doesn't in my case.. In a different scenario it is recommended to put my_frame.columnconfigure(0, weight=1), which doesn't help either.
Here is the code:
import tkinter as tk
from tkinter import ttk
class MessageBox(object):
""" Adjusted code from StackOverflow #10057662. """
def __init__(self, msg, option_list):
root = self.root = tk.Tk()
root.geometry("400x400")
root.title('Message')
self.msg = str(msg)
frm_1 = tk.Frame(root)
frm_1.pack(expand=True, fill=tk.X, ipadx=2, ipady=2)
message = tk.Message(frm_1, text=self.msg)
message.pack(expand=True, fill=tk.X) # <------------------------------------ This doesn't show the desired effect!
frm_1.columnconfigure(0, weight=1)
self.tree_view = ttk.Treeview(frm_1)
self.tree_view.heading("#0", text="Filename", anchor=tk.CENTER)
for idx, option in enumerate(option_list):
self.tree_view.insert("", idx+1, text=option)
self.tree_view.pack(fill=tk.X, padx=2, pady=2)
choice_msg = "Long Test string to show, that my frame is unfortunately not correctly filled from side to side, as I would want it to."
choices = ["Test 1", "Test 2", "Test 3"]
test = MessageBox(choice_msg, choices)
test.root.mainloop()
I'm slowly going nuts, because I know that there is probably something very basic overruling the correct positioning of the widget, but I've been trying different StackOverflow solutions and browsing documentation for hours now with no luck.
Try to set a width of message in the tk.Message constructor, something like this:
message = tk.Message(frm_1, text=self.msg, width=400-10) # 400 - is your window width
message.pack() # In that case you can delete <expand=True, fill=tk.X>
The problem you are facing is a feature: The Message widget tries to lay out the text in one of two ways:
according to an aspect (width-to-height ratio in percent)
according to a maximum width (lines will be broken if longer)
Both of these goals seem not to cooperate well with the automatic resizing a Message widget experiences from a grid or pack layout manager. What can be done is to bind a handler to the resize event of the widget to adjusts the width option dynamically. Also, there are better options to use with the pack layout manager than shown in OP.
I derived an AutoMessage widget to get the event handler out of the way:
import tkinter as tk
from tkinter import ttk
class AutoMessage(tk.Message):
"""Message that adapts its width option to its actual window width"""
def __init__(self, parent, *args, **options):
tk.Message.__init__(self, parent, *args, **options)
# The value 4 was found by experiment, it prevents text to be
# displayed outside of the widget (exceeding the right border)
self.padx = 4 + 2 * options.get("padx", 0)
self.bind("<Configure>", self.resize_handler)
def resize_handler(self, event):
self.configure(width=event.width - self.padx)
class MessageBox(object):
"""Adjusted code from StackOverflow #10057662."""
def __init__(self, msg, option_list):
root = self.root = tk.Tk()
root.geometry("400x400")
root.title("Message")
self.msg = str(msg)
self.frm_1 = tk.Frame(root)
self.frm_1.pack(side=tk.TOP, fill=tk.X, padx=2, pady=2)
self.message = AutoMessage(self.frm_1, text=self.msg, anchor=tk.W)
self.message.pack(side=tk.TOP, fill=tk.X)
self.frm_1.columnconfigure(0, weight=1)
self.tree_view = ttk.Treeview(self.frm_1)
self.tree_view.heading("#0", text="Filename", anchor=tk.CENTER)
for idx, option in enumerate(option_list):
self.tree_view.insert("", idx + 1, text=option)
self.tree_view.pack(fill=tk.X, padx=2, pady=2)
choice_msg = "Long Test string to show, that my frame is unfortunately not correctly filled from side to side, as I would want it to."
choices = ["Test 1", "Test 2", "Test 3"]
test = MessageBox(choice_msg, choices)
test.root.mainloop()
Currently resisting the temptation to throw my laptop out of the window and smash it with a bat at this point.
Currently, I'm trying to create a simple GUI for what used to be a nice simple text based RPG game. But trying to work with a GUI makes me want to die.
I just want to have a scaleable way to swap between frames in the game. (Currently there exists the main menu and the Work in progress character creation screen because I can't even manage to get even just that to work.)
I've tried most things that I can find on this website and on discord servers and I seem to just get a new error every time.
I just want to know how to swap between these since trying anything that I can find online just creates more errors.
There are more "screens" to come since it's a game so a scaleable solution would be perfect thanks.
import tkinter
from tkinter import *
from tkinter import ttk
from PIL import ImageTk, Image
root = Tk()
content = ttk.Frame(root)
root.geometry("600x600")
class CharacterCreate(tkinter.Frame):
def __init__(self, parent):
tkinter.Frame.__init__(self)
self.parent = parent
backgroundchar = ImageTk.PhotoImage(Image.open("plont2.png"))
backgroundlabelchar = tkinter.Label(content, image = backgroundchar)
backgroundlabelchar.image = backgroundchar
backgroundlabelchar.grid(row=1,column=1)
Charname = tkinter.Label(content, text = "Enter your character name here:").grid(row=0)
e1 = tkinter.Entry(content)
e1.grid(row=0, column=1)
e1.lift()
CharBtn1 = Button(content, text="Return to main menu", width = 15, height = 1)
CharBtn1.grid(row=2, column=2)
CharBtn1.lift()
class MainMenu(tkinter.Frame):
def __init__(self, parent):
tkinter.Frame.__init__(self)
self.parent = parent
background = ImageTk.PhotoImage(Image.open("bred.png"))
content.grid(column=1, row=1)
Btn1 = Button(content, text="Play", width=5, height=1, command = CharacterCreate.lift(1))
Btn2 = Button(content, text="Quit", width=5, height=1, command = root.quit)
backgroundlabel = tkinter.Label(content, image=background)
backgroundlabel.image = background
backgroundlabel.grid(row=1, column=1)
Btn1.grid(row=1, column=1, padx=(50), pady=(50))
Btn1.columnconfigure(1, weight=1)
Btn1.rowconfigure(1, weight=1)
Btn1.lift()
Btn2.grid(row=1, column=2, padx=(50), pady=(50))
Btn2.columnconfigure(2, weight=1)
Btn2.rowconfigure(1, weight=1)
Btn2.lift()
MainMenu(1)
root.mainloop()
You have five major problems:
you are calling a function immediately (command=CharacterCreate.lift(1)) rather than at the time the button is clicked (command=CharacterCreate.lift),
you are passing an invalid argument to lift - you are passing 1, but the argument to lift must be another widget,
you are calling lift on a class rather than an instance of a class.
you never create an instance of CharacterCreate
your classes inherit from Frame but you never use the classes as frames -- they each place their widgets directly in container
Switching between pages usually involves one of two techniques: create all the frames at startup and then lift the active frame above the others, or destroy the current frame and recreate the active frame. You seem to be attempting to do the latter, so this answer will show you how to do that.
Since fixing your program is going to require many changes, I am instead going to show you a template that you can use to start over.
Let's start with an import, and then the definition of your pages. To keep the example short, each class will have a single label so that you can distinguish between them (note: importing tkinter "as tk" is done simply to make the code a bit easier to read and type):
import tkinter as tk
class CharacterCreate(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
label = tk.Label(self, text="I am CharacterCreate")
label.pack(padx=20, pady=20)
class MainMenu(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
label = tk.Label(self, text="I am MainMenu")
label.pack(padx=20, pady=20)
Your original code created a container, so we'll do that next. We need to create the root window, too:
root = tk.Tk()
container = tk.Frame(root)
container.pack(fill="both", expand=True)
Now we need to create an instance of each page, giving them the container as the parent. As a rule of thumb, the code that creates a widget should be the code that calls pack, place, or grid on the widget, so we have to do that too. We need to make sure that grid is configured to give all weight to row 0 column 0.
main = MainMenu(container)
cc = CharacterCreate(container)
main.grid(row=0, column=0, sticky="nsew")
cc.grid(row=0, column=0, sticky="nsew")
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
We need a way to lift one of the classes above the other. That's best handled by a function. To make the code easier to understand we'll save the pages in a dictionary so we can reference them by name. This name will be the argument to the function.
pages = {"cc": cc, "main": main}
def switch(name):
page = pages[name]
page.lift()
Finally, we need to start with the main menu on top, and we need to start the event loop:
switch('main')
root.mainloop()
With that, you have a program that runs and displays the main menu. To finish the example lets add a button to the menu to switch to the create page, and create a button in the create page to switch back to the menu.
First, inside the __init__ of MainMenu add the following after the code that creates the label. Notice that because we need to pass an argument to switch, we use lambda:
button = tk.Button(self, text="Go to creater", command=lambda: switch('cc'))
button.pack()
And next, inside the __init__ of CharacterCreate add the following after the code that creates the label:
button = tk.Button(self, text="Go to main menu", command=lambda: switch('main'))
button.pack()
With that, you now have the basic structure to create as many pages as you want, and easily switch to them by name.
I try do add a scrolling bar in my frame, with no success. I have read posts about this subject on stackoverflow and tried many suggestions, but they don't work for me.
I have tried this.
import tkinter as tk
class Interface(tk.Frame):
def __init__(self, root, **kwargs):
tk.Frame.__init__(self, root, width=768, height=576, **kwargs)
#self.pack(fill=tk.BOTH, expand=True)
self.canvas = tk.Canvas(root, borderwidth=0, background="#ffffff")
self.frame = tk.Frame(self.canvas, background="#ffffff")
self.vsb = tk.Scrollbar(root, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.vsb.set)
self.vsb.pack(side="right", fill="y")
self.canvas.pack(side="left", fill="both", expand=True)
self.canvas.create_window((4,4), window=self.frame, anchor="nw",
tags="self.frame")
self.frame.bind("<Configure>", self.onFrameConfigure)
self.populate()
def command():
global parameters
temp=[entry.get() for entry in self.entries]
parameters=temp
self.bouton_Executer = tk.Button(self.frame, text="Exécuter le programme", fg="red", command=command)
self.bouton_Executer.place(x=400 ,y= 840)
def populate(self):
self.all_entries= []
label=tk.Label(self.frame, text="a").place(x = 20, y = 60)
entry=tk.Entry(self.frame)
entry.place(x = 60, y = 60)
self.all_entries.append(entry)
label=tk.Label(self.frame, text="b").place(x = 20, y = 80)
entry=tk.Entry(self.frame)
entry.place(x = 60, y = 80)
self.all_entries.append(entry)
def onFrameConfigure(self, event):
#'''Reset the scroll region to encompass the inner frame'''
self.canvas.configure(scrollregion=self.canvas.bbox("all")
root = tk.Tk()
interface = Interface(root)
interface.mainloop()
interface.destroy()
I want to have a window with the two widgets a and b, and a scrolling bar (because in the entire code, I have many widgets and all the widgets don't appear on the window).
By creating the object 'interface', no widgets appear in the window and I see the scrolling bar but I can't scroll the window.
When you use pack or grid, the default behavior is for the containing widget to grow or shrink to fit all of its children. place does not have this behavior, so when you use place to put widgets in a frame, the frame will retain whatever its requested size is. You didn't give the frame a size, so it will default to 1x1 pixels.
It is rarely a good idea to use place. Not only for this reason, but also because you have to do all of the work of making sure the layout fits the window, is responsive, and works on machines that may have a different resolution and different fonts.
If you switch to using grid or pack, your frame will automatically grow to fit all of the widgets contained inside.
I use the same format of frame but it doesn't show in the interface, hope someone could tell me the solution, thanks.
class Interface(Frame):
def __init__(self,parent=None):
Frame.__init__(self,parent)
self.master.title("measurement")
self.grid()
# fix the size and parameters of widget
self.master.geometry("700x400+100+50")
self.master.Frame1 = Frame(self,relief=GROOVE,bg='white')
self.master.Frame1.grid(column=1,row=9)
self.can =Canvas(self, bg="ivory", width =200, height =150)
self.master.canvas = Canvas(self.master, width=150, height=120, background='snow')
ligne1=self.master.canvas.create_line(75, 0, 75, 120)
if __name__ == "__main__":
window = Tk()
window.resizable(False, False)
Interface(window).mainloop()
I can't figure out why you have 2 Canvas's, but the problem is that you aren't placing them on their respective parents. I cut out a lot of the code that seemed unnecessary and restructured your code to make it more logical:
class Interface(Frame):
def __init__(self, parent):
self.parent = parent
super().__init__(self.parent)
self.Frame1 = Frame(self, relief=GROOVE)
self.Frame1.grid()
self.canvas = Canvas(self.Frame1, bg="ivory", width=200, height=150)
self.canvas.grid()
self.canvas.create_line(75, 0, 75, 120)
if __name__ == "__main__":
root = Tk()
# Tk configurations are not relevant to
# the Interface and should be done out here
root.title('Measurement')
root.geometry('700x400+100+50')
root.resizable(False, False)
Interface(root).pack()
root.mainloop()
i think I don't really understand your problem, you don't see your frame because you don't have any widget in it, that's all
import tkinter as tk
class Interface(tk.Frame):
def __init__(self,parent=None):
tk.Frame.__init__(self,parent)
self.master.title("measurement")
self.grid(row=0, column=0)
# fix the size and parameters of widget
self.master.geometry("700x400+100+50")
self.master.Frame1 = tk.Frame(self,relief='groove',bg='white')
self.master.Frame1.grid(column=1,row=9)
labelExemple =tk.Label(self.master.Frame1, text="Exemple")
labelExemple.grid(row=0,column=0)
self.can = tk.Canvas(self, bg="ivory", width =200, height =150)
self.master.canvas = tk.Canvas(self.master, width=150, height=120, background='snow')
self.ligne1=self.master.canvas.create_line(75, 0, 75, 120)
if __name__ == "__main__":
window = tk.Tk()
window.resizable(False, False)
Interface(window).mainloop()
PS : use import tkinter as tk instead of from tkinter import *
There are several problems with those few lines of code, almost all having to do with the way you're using grid:
you aren't using the sticky option, so widgets won't expand to fill the space they are given
you aren't setting the weight for any rows or columns, so tkinter doesn't know how to allocate unused space
you aren't using grid or pack to put the canvases inside of frames, so the frames stay their default size of 1x1
The biggest problem is that you're trying to solve all of those problems at once. Layout problems are usually pretty simple to solve as long as you're only trying to solve one problem at a time.
Start by removing all of the widgets from Interface. Then, give that frame a distinctive background color and then try to make it fill the window (assuming that's ultimately what you want it to do). Also, remove the root.resizable(False, False). It's rarely something a user would want (they like to be able to control their windows), plus it makes your job of debugging layout problems harder.
Once you get your instance of Interface to appear, add a single widget and make sure it appears too. Then add the next, and the next, adding one widget at a time and observing how it behaves.