So I've been using the canvas widget in tkinter to create a frame full of labels which has a scrollbar. All is working good except that the frame only expands to the size of the labels placed in it - I want the frame to expand to the size of the parent canvas.
This can easily be done if I use pack(expand = True) (which I have commented out in the code below) for the frame in the canvas but then then the scrollbar doesn't work.
Here's the appropriate bit of code:
...
self.canvas = Canvas(frame, bg = 'pink')
self.canvas.pack(side = RIGHT, fill = BOTH, expand = True)
self.mailbox_frame = Frame(self.canvas, bg = 'purple')
self.canvas.create_window((0,0),window=self.mailbox_frame, anchor = NW)
#self.mailbox_frame.pack(side = LEFT, fill = BOTH, expand = True)
mail_scroll = Scrollbar(self.canvas, orient = "vertical",
command = self.canvas.yview)
mail_scroll.pack(side = RIGHT, fill = Y)
self.canvas.config(yscrollcommand = mail_scroll.set)
self.mailbox_frame.bind("<Configure>", self.OnFrameConfigure)
def OnFrameConfigure(self, event):
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
I've also provided an image with colored frames so you can see what I'm getting at. The pink area is the canvas that needs filling by the mailbox_frame (You can see the scrollbar on the right):
Just for future reference in case anyone else needs to know:
frame = Frame(self.bottom_frame)
frame.pack(side = LEFT, fill = BOTH, expand = True, padx = 10, pady = 10)
self.canvas = Canvas(frame, bg = 'pink')
self.canvas.pack(side = RIGHT, fill = BOTH, expand = True)
self.mailbox_frame = Frame(self.canvas, bg = 'purple')
self.canvas_frame = self.canvas.create_window((0,0),
window=self.mailbox_frame, anchor = NW)
#self.mailbox_frame.pack(side = LEFT, fill = BOTH, expand = True)
mail_scroll = Scrollbar(self.canvas, orient = "vertical",
command = self.canvas.yview)
mail_scroll.pack(side = RIGHT, fill = Y)
self.canvas.config(yscrollcommand = mail_scroll.set)
self.mailbox_frame.bind("<Configure>", self.OnFrameConfigure)
self.canvas.bind('<Configure>', self.FrameWidth)
def FrameWidth(self, event):
canvas_width = event.width
self.canvas.itemconfig(self.canvas_frame, width = canvas_width)
def OnFrameConfigure(self, event):
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
Set a binding on the canvas <Configure> event, which fires whenever the canvas changes size. From the event object you can get the canvas width and height, and use that to resize the frame.
Just an updated answer which covers both horizontal and vertical scrollbars without breaking them.
def FrameWidth(self, event):
if event.width > self.mailbox_frame.winfo_width():
self.canvas.itemconfig(self.canvas_frame, width=event.width-4)
if event.height > self.mailbox_frame.winfo_height():
self.canvas.itemconfig(self.canvas_frame, height=event.height-4)
Only sets the frame height and width if they are less than the canvas width. Respects both Horizontal and Vertical scrollbars.
Related
I am coding a game in Python with tkinter. The basic functionality is that an image is displayed but it's covered with a grid of boxes that can be destroyed. I've written a linear version of the code that is as simplified as I can get it:
from tkinter import *
from PIL import ImageTk, Image
class Box:
def __init__(self, parent, row, column, width, height):
self.row = row
self.column = column
self.canvas = Canvas(parent, bg='black', width=width, height=height)
self.canvas.grid(row=row, column=column, sticky=NSEW)
self.canvas.bind('<Button-1>', self.destroy)
def destroy(self, event):
print('Destroyed box at', self.row, self.column)
self.canvas.destroy()
root = Tk()
root.title('Image Reveal')
root.state('zoomed')
image_raw = None
# Create a frame for the image
image_frame = Frame(root, highlightbackground="blue", highlightthickness=5)
image_frame.pack(expand=True, anchor=CENTER, fill=BOTH, padx=0, pady=0)
# Load image and put it in a label
image_raw = Image.open('image.png')
image = ImageTk.PhotoImage(image_raw)
image_label = Label(image_frame, image=image, borderwidth=5, relief='ridge')
image_label.pack(anchor=CENTER, expand=True)
# Calculate the size of the boxes
image_label.update()
width = image.width()
height = image.height()
column_amount = 10
row_amount = 10
box_width = width / column_amount
box_height = height / row_amount
# Create boxes that fill the grid
for row in range(row_amount):
for column in range(column_amount):
Box(image_label, row, column, box_width, box_height)
root.mainloop()
You can click on the box to destroy it and it also logs the coordinates of the box destroyed.
The desired behavior: You can click on the boxes to destroy them, gradually revealing the image. The other boxes should stay in the same place (the same cells of the grid), so the grid shape doesn't change.
The unexpected behavior: Destroying one box makes the rest of the boxes disappear somehow. Yet you can still click them (it still logs the destruction message). Overall this is the closest I've got to my desired behavior but I can't figure out why this is happening or how to approach this functionality better.
WORKING CODE:
Lukas Krahbichler helped achieve most of the desired behavior. Here is the updated code.
Destroying the boxes however led to the grid resizing when a full row or a column of boxes gets destroyed. In order to fix that, I instead hide the box by lowering it behind the image. At first that didn't work, but then I found this -
Turns out to lower the WHOLE canvas, there is no direct method for that. The final code:
from tkinter import *
from PIL import ImageTk, Image
class Box:
def __init__(self, root, image_frame, image_label, row, column):
self.root = root
self.row = row
self.column = column
self.image_label = image_label
self.image_frame = image_frame
self.visible = True
self.canvas = Canvas(image_frame, bg='black')
self.canvas.grid(row=row, column=column, sticky=NSEW, padx=5, pady=5)
self.canvas.bind('<Button-1>', self.hide)
def hide(self, event):
# Lower the box under the image to hide it
self.canvas.tk.call('lower', self.canvas._w, None)
self.visible = False
print('Hid box at', self.row, self.column)
root = Tk()
root.title('Image Reveal')
root.state('zoomed')
image_raw = None
column_amount = 10
row_amount = 10
boxes = []
# Create a frame for the image
image_frame = Frame(root, highlightbackground="blue", highlightthickness=0)
image_frame.pack(anchor=CENTER, fill=None, padx=0, pady=0)
# Load image and put it in a label
image_raw = Image.open('image.png')
image = ImageTk.PhotoImage(image_raw)
width = image.width()
height = image.height()
# Create a label for the image
image_label = Label(image_frame, image=image, borderwidth=0, relief='ridge')
# Configure the image frame so it doesn't resize <----- UPDATED
image_frame.config(width=width, height=height)
image_frame.grid_propagate(False)
image_label.grid_propagate(False)
# Place the image label in the frame
image_label.grid(rowspan=row_amount, columnspan=column_amount, sticky="NSEW")
# Configure the grid weights <----- UPDATED
for row in range(10):
image_frame.rowconfigure(row, weight=1)
for column in range(10):
image_frame.columnconfigure(column, weight=1)
# Calculate the size of the boxes
image_label.update()
# Create boxes that fill the grid
for row in range(row_amount):
for column in range(column_amount):
box = Box(root, image_frame, image_label, row, column)
boxes.append(box)
root.mainloop()
In tkinter you are not really supposed to place other widgets (for a example a Canvas) in a Label. If you grid everything in the "image_frame" and give the "image_label" a column and a rowspan it should work.
from tkinter import *
from PIL import ImageTk, Image
class Box:
def __init__(self, parent, row, column, width, height):
self.row = row
self.column = column
self.canvas = Canvas(parent, bg='black', width=width, height=height)
self.canvas.grid(row=row, column=column, sticky=NSEW)
self.canvas.bind('<Button-1>', self.destroy)
def destroy(self, event):
print('Destroyed box at', self.row, self.column)
self.canvas.destroy()
root = Tk()
root.title('Image Reveal')
root.state('zoomed')
image_raw = None
# Create a frame for the image
image_frame = Frame(root, highlightbackground="blue", highlightthickness=5)
image_frame.pack(expand=True, anchor=CENTER, fill=BOTH, padx=0, pady=0)
# Define row and column amount
column_amount = 10
row_amount = 10
# Load image and put it in a label
image_raw = Image.open('image.png')
image = ImageTk.PhotoImage(image_raw)
image_label = Label(image_frame, image=image, borderwidth=5, relief='ridge')
image_label.grid(rowspan=row_amount, columnspan=column_amount, sticky="NSEW")
# Calculate the size of the boxes
image_label.update()
width = image.width()
height = image.height()
box_width = width / column_amount
box_height = height / row_amount
# Create boxes that fill the grid
for row in range(row_amount):
for column in range(column_amount):
Box(image_frame, row, column, box_width, box_height)
root.mainloop()
I am trying to showcase tables of monday and tuesday however the canvas rectangles seem to keep a very large gap between them.Is it possible to not have any gaps at all or have a very minimal gap?
class SmokingPlan:
def __init__(self, master):
self.master = master
self.master.title('Kill Smoking')
self.master.geometry("1200x1200")
#Monday
canvas_monday = Canvas(self.master)
canvas_monday.grid(row = 0, column = 3)
canvas_monday.create_rectangle(10,10, 150, 200)
canvas_monday.create_text(60,25, text = "Monday")
#Tuesday
canvas_tuesday = Canvas(self.master)
canvas_tuesday.grid(row=0, column = 5)
canvas_tuesday.create_rectangle(10,10, 150, 200)
canvas_tuesday.create_text(60,25, text = "Tuesday")
You are drawing two Canvas widgets. The Canvas widget has a default size. By changing the color of your canvas you will see why you have a gap in between.
import tkinter as tk
class SmokingPlan:
def __init__(self, master):
self.master = master
self.master.title('Kill Smoking')
self.master.geometry("1200x1200")
#Monday
canvas_monday = tk.Canvas(self.master, bg="blue")
canvas_monday.grid(row = 0, column = 0)
canvas_monday.create_rectangle(10,10, 150, 200)
canvas_monday.create_text(60,25, text = "Monday")
#Tuesday
canvas_tuesday = tk.Canvas(self.master, bg="yellow")
canvas_tuesday.grid(row=0, column = 1)
canvas_tuesday.create_rectangle(10,10, 150, 200)
canvas_tuesday.create_text(60,25, text = "Tuesday")
if __name__ == '__main__':
root = tk.Tk()
app = SmokingPlan(root)
root.mainloop()
Now you could set smaller canvas size or even better put both rectangles in the same canvas and change the coords.
Set Canvas size example:
canvas_monday = tk.Canvas(self.master, bg="blue", width=200)
will shrink your monday canvas as follow:
Im trying to get a tkinter gui that lets the user sign in im really new to tkinter. I made a frame and when i use grid to put another frame widget inside of the frame it does it based off of the root and not the inner_frame thats what I think is happening. In the code I made a grey box to demonstrate and I dont understand why it is below the blue frame and not inside the yellow frame under the "sign in" text. Thanks for the help.
from tkinter import *
root = Tk()
root.title("sighn in test")
#colors
background = "#273E47"
accent = "#d8973c"
red = "#bb4430"
white = "#edf2f4"
#this creates and places the background frame
main_buttons_frame = Frame(root, height = 500, width = 400, bg = background).grid(row = 0, column = 0)
#this creates and places the inner frame
inner_frame = Frame(main_buttons_frame, height = 450, width = 300, bg = accent).grid(row = 0, column = 0)
#this creates and places the "sighn in text"
top_text = Label(inner_frame, text = "sign in", font = ("helvitica", 30, "bold"), bg = accent, fg =
background).grid(row = 0, column = 0)
#this is a test to demonstrate
test_frame = Frame(inner_frame, bg = "grey", height = 100, width = 100).grid(row = 1, column = 0)
root.mainloop()
You have very common mistake of beginners
inner_frame = Frame(...).grid...()
It assigns None to inner_frame because grid()/pack()/place() gives None.
So later Frame(inner_frame, ..) means Frame(None, ..) and it adds to root
You have to do it in two steps
inner_frame = Frame(...)
inner_frame.grid(...)
And now you have Frame assigned to inner_frame
EDIT:
With correctly assigned widgets I get
and now gray box is inside yellow frame but image shows different problem - grid()/pack() automatically calculate position and size and external frame automatically change size to fit to child's size.
Using .grid_propagate(False) you can stop it
But it shows other problem - cells don't use full size of parent so yellow frame is moved to left top corner, not centered :) Empty cells have width = 0 and heigh = 0 so moving all to next row and next column will not change it - it will be still in left top corner :)
You need to assign weight to column and/or row which decide how to use free space. All columns/rows has weight=0 and when you set weight=1 then this row and/or column will use all free space - (this would need better explanation) - and then element inside cell will be centered.
main_buttons_frame.grid_columnconfigure(0, weight=1)
main_buttons_frame.grid_rowconfigure(0, weight=1)
import tkinter as tk # PEP8: `import *` is not preferred
# --- colors ---
background = "#273E47"
accent = "#d8973c"
red = "#bb4430"
white = "#edf2f4"
# --- main ---
root = tk.Tk()
root.title("sighn in test")
main_buttons_frame = tk.Frame(root, height=500, width=400, bg=background) # PEP8: without spaces around `=` inside `( )`
main_buttons_frame.grid(row=0, column=0)
#main_buttons_frame = None
main_buttons_frame.grid_propagate(False)
main_buttons_frame.grid_columnconfigure(0, weight=1)
main_buttons_frame.grid_rowconfigure(0, weight=1)
inner_frame = tk.Frame(main_buttons_frame, height=450, width=300, bg=accent)
inner_frame.grid(row=0, column=0)
#inner_frame = None
inner_frame.grid_propagate(False)
inner_frame.grid_columnconfigure(0, weight=1)
inner_frame.grid_rowconfigure(0, weight=1)
top_text = tk.Label(inner_frame, text="sign in", font=("helvitica", 30, "bold"), bg=accent, fg=background)
top_text.grid(row=0, column=0,)
#top_text = None
#top_text.grid_propagate(False)
#top_text.grid_columnconfigure(0, weight=1)
#top_text.grid_rowconfigure(0, weight=1)
test_frame = tk.Frame(inner_frame, bg="grey", height=100, width=100)
test_frame.grid(row=1, column=0)
#test_frame = None
#test_frame.grid_propagate(False)
#test_frame.grid_columnconfigure(1, weight=1)
#test_frame.grid_rowconfigure(0, weight=1)
root.mainloop()
BTW: PEP 8 -- Style Guide for Python Code
self.root = tkinter.Tk()
self.frame = Frame(self.root, width = 1300, height = 1300, bg = "yellow")
self.frame.grid(row = 0 , column = 0, sticky = 'nw')
self.frame.grid_rowconfigure(0, weight=1)
self.frame.grid_columnconfigure(0, weight=1)
self.frame.grid_propagate(True)
self.canvas_main = Canvas(self.frame,
height = 300,
width = 300,
scrollregion=(500,500,0,500)
)
self.canvas_main.grid(row = 0, column = 0, padx= 10, pady = 10, sticky = "news")
self.scrollbar = Scrollbar(self.root, orient = VERTICAL, command = self.canvas_main.yview)
self.scrollbar.grid(row = 0, column = 1, sticky = 'ns')
self.canvas_main.configure(yscrollcommand=self.scrollbar.set)
In above code block, I have entered Scrollbar element for enabling vertical scrollbar however, it does not reflect. Can anyone find out what I am doing wrong in this block?
You've set the scrollable region (scrollregion) to be zero pixels tall, so tkinter thinks there's nothing to be scrolled in the vertical direction.
Often you will do your drawing first and then set the scrollable region to encompass everything you've drawn. You do this by getting the bounding box -- the smallest rectangle that includes all of the canvas objects -- and then using that as the value for scrollregion.
Example:
canvas = tk.Canvas(...)
<draw a bunch of objects>
canvas.configure(scrollregion=canvas.bbox("all"))
The other method is to define ahead of time how big the virtual drawing area is. For example, you've set your canvas to have a width and height of 300. If you want the actual drawing area to be twice the visible size then you could set the scrollregion to an area twice that big.
This example sets the scrollable region to begin at 0,0 (the upper left corner), and extend to 600,600 (the bottom right corner):
canvas = tk.Canvas(..., width=300, height=300)
canvas.configure(scrollregion=(0,0,600,600)
Having issues packing two frames to get an expected outcome as defined below:
Frame 1 to have a width of 150 and scale on Y value, color of blue.
Frame 2 to scale on both X and Y, color of red.
So that when the window is resized the frame 1 keeps its x position and the frame 2 will scale.
As seen in picture below:
Expected Outcome
So this was the code used:
import tkinter as tk
frame1 = tk.Frame(bg = 'blue', width= 150, height = 150)
frame2 = tk.Frame(bg = 'red')
frame1.pack(fill = tk.Y, expand = 1, anchor = tk.W, side = tk.LEFT)
frame2.pack_propagate(True)
frame2.pack(fill = tk.BOTH, expand = 1, anchor = tk.E, side = tk.RIGHT)
Though it produces this: Actual Outcome
A requirement for this is I still need to use pack, but stuck on how to get the outcome required.
Your only problem is that frame1 needs expand to be set to False or 0.
By setting it to 1, you're asking Tkinter to expand the area given to the left frame. Since you only have it fill in the "y" direction, the frame on the left doesn't fill up that extra area even though it has been allocated, which is why you see the blank space between the left and the right side.
fairly simple, if you want them to stack side by side you need to keep stacking them from the same side, and the first frame doesn't need expand=1
try the following:
import tkinter as tk
root = tk.Tk()
frame1 = tk.Frame(bg = 'blue', width= 150, height = 150)
frame2 = tk.Frame(bg = 'red')
frame1.pack(fill = tk.Y, anchor = tk.W, side = tk.LEFT)
frame2.pack_propagate(True)
frame2.pack(fill = tk.BOTH, expand = 1, anchor = tk.E, side = tk.LEFT)
root.mainloop()