MCVE
import Tkinter as tk
root = tk.Tk()
canvas = tk.Canvas(root, width=100, height=100)
canvas.grid(row=0, column=0)
def print_click(event):
print event.x, event.y
canvas.bind('<Button-1>', print_click)
root.mainloop()
Issue
Clicking the canvas on the very top left prints (0, 0).
Clicking the canvas on the very bottom right prints (100, 100). I expected (99, 99).
This means the canvas is actually 101 pixels wide and high, not 100.
In my real program, I am showing an array (as an image) on the canvas and need the precise click position. If that position does not exist in the underlying image (i.e. (100, 100) for an 100x100 array), the program will crash.
Questions
Am I doing something wrong creating the canvas? Why is it one wider and higher than expected?
Is the simple fix here to just subtract 1 from width and height whenever setting up a canvas that needs to have width width and height height?
There are other things that contribute to the overall width and height of a widget besides just the width and height attributes. For example, both borderwidth and highlightthickness contribute to the overall size of the widget. Since you aren't setting those to zero, you're relying on the defaults for your platform, and those defaults apparently aren't zero.
You need to explicitly set those attributes to zero:
canvas = tk.Canvas(root, width=100, height=100, highlightthickness=0, borderwidth=0)
Related
I'm trying to resize a frame in tkinter, but the width does not change and function winfo_width() returns 1. How can i fix it?
from tkinter import *
root = Tk()
root.geometry('400x300')
Frame = LabelFrame(root, text="Test", width = 200)
Frame.grid(row = 0, column = 0)
label = Label(Frame, text = '').grid(row = 0, column=0)
print(Frame.winfo_width()) #output is 1 instead of 200
root.mainloop()
The width is returning 1 because the window hasn't been drawn yet. The actual width depends on the window being drawn since the actual width depends on many factors which can't be known before the window is actually drawn.
If you call root.update() before calling Frame.winfo_width() to force the window to be drawn, you will see it displaying the actual value.
As for how to change the width, that question is too broad to answer. Normally it's not wise to directly set the width of a frame. Tkinter by default will automaticaly resize a frame to fit its children. So, one way to make the frame wider is to add more widgets.
The width can also depend on how it is added to the display - whether you're using pack or grid or place, and how you have configured them. So, another way to make the frame wider is to use non-default options that cause the frame to grow or shrink to fit the space given to it.
If you want to specify an explicit size and ignore tkinter's automatic resizing, you can do that by turning off geometry propagation and then setting the width and height parameters for the frame. Depending on whether you're using grid or pack, you can call grid_propagate or pack_propagate with a False argument to disable propagation (place doesn't support geometry propagation).
Note that turning off geometry propagation is usually the least desirable solution because it requires you to do a lot more work to create a responsive UI. The best way to design GUI with tkinter is to focus on the size of the inner widgets and let tkinter compute the most efficient size for frames and the window itself.
As the others have pointed out how to set a static size frame using grid_propagate() I will show you how to set up your frame to resize automatically.
You need to tell the row and column to expand that the frame is in. This is done with columnconfigure() and rowconfigure(). Then you need to tell the frame to stick to all sides with sticky='nsew'. Adding widgets to the frame is no different then any other container. Simply tell the widget to be in the frame.
One potention issue I see is you are overwriting Frame() on this line: Frame = LabelFrame(root, text="Test", width = 200). This is a good example why you should not use import *. Instead do import tkinter as tk and use the tk. prefix for anything that needs it.
Example:
import tkinter as tk
root = tk.Tk()
root.geometry('400x300')
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
frame = tk.LabelFrame(root, text='Test', width=200)
frame.grid(row=0, column=0, sticky='nsew')
label = tk.Label(frame, text='label').grid(row=0, column=0)
root.mainloop()
Results:
Update:
If you do want something static make sure you define both height and width. If you only define one or the other then you will not see the frame in the window.
For a testable example for a static frame size:
import tkinter as tk
root = tk.Tk()
root.geometry('400x300')
root.rowconfigure(0, weight=1)
root.columnconfigure(0, weight=1)
frame = tk.LabelFrame(root, text='Test', height=200, width=200)
frame.grid(row=0, column=0)
frame.grid_propagate(False)
label = tk.Label(frame, text='label').grid(row=0, column=0)
root.mainloop()
Results:
Your frame can propagate on the grid based on the widgets on it, and not have fixed dimensions.
The output of 1 is due there being nothing on the Frame other than an empty Label. (It would still show 1 if there was no Label)
To get the output as 200, set the grid_propagate flag to False (only after setting your height and widht parameters), as follows:
frame = Frame(..., width=200)
frame.grid(row=0, column=0)
frame.grid_propagate(False)
I want buttons/labels in the same column will share the same width with a specific/first button/label. The problem is winfo_width() seems not to return what I want. The return value of winfo_width() is multiple times the button.
I don't want to make width fixed by a number I select. Thus, I did not find a solution to my problem.
Here is part of my code:
button_1.update_idletasks()
print(button_1.winfo_width())
new_label = Label(frame_1, bg= "#8432C7", width = 30, height = 5)
new_label.grid(row = 2, column = 0)
Since I don't have 10 reputation to post images, here is the link for the generated interface:
If you could see the above image, you should find the lower label (width = 30) is larger than the upper button (width = 157 ?).
But, according to my attempts, 157 seems not to be the width of button_1. I feel confused about what exactly winfo_width() returns here. Thus, I want to know what winfo_width() returns (why winfo_width() return 157 which should be a smaller number than 30) and how to get the exact width of the button.
I am stuck here for an hour since I just started to learn Tkinter recently.
Thanks in advance for anyone who can give me suggestions.
Fun fact: you don't have to bother with this at all.
Just pass sticky when you grid your widgets:
import tkinter as tk
root = tk.Tk()
button_1 = tk.Button(root,text="button_1")
button_1.grid(row = 1, column = 0, sticky="ew")
new_label = tk.Label(root, bg= "#8432C7", height = 5)
new_label.grid(row = 2, column = 0, sticky="ew")
root.mainloop()
Then your columns will be auto-fit and scaled to the same size.
This is because the width used in the arguments to create a button are in different units than what tkinter uses.
From the Documentation of a Button in tkinter. Width is The width of the button. If the button displays text, the size is given in text units. If the button displays an image, the size is given in pixels (or screen units). If the size is omitted, or zero, it is calculated based on the button contents. (width/Width)
You will find that if you use tkinters .place() to set a size, the size that .winfo_width() returns will be the same.
For example:
button_1.update_idletasks()
print(button_1.winfo_width())
new_label = Label(frame_1, bg= "#8432C7")
new_label.place(x=40, y=0, width=157, height=20)
You will find that the new_label will now have the same width as the button
I apologize in advance if my question is a duplicate however I have not found an answer to this question.
I'm learning Tkinter and I'm struggling with understanding the relation between a label's font type, it's size and it's width and the length of the string in it.
Specifically, what my problem is:
I have created a widget: a 800x640 canvas on which I want to place other
widgets.
On this canvas I want to place a label with some text which has the following
attributes: font: Helvetica, font size: 20, text = "main application". I want
to place this label widget at the very most top left corner of the
widget(meaning at point 0,0 with respect to the canvas). I want the label to
be 200 in width meaning it's background to take almost 1/3 of the canvas's
size(after I manage to do this I plan to add 2 more labels as well). I guess
the height of the label is determined by the font size in this case 20. I
placed the label at coordinate y=20 and this coordinate seems to be ok.
I did some googling and found out that the width parameter of the label widget is not the actual width but something related to the font and size of the label's text: something like if I understood correctly: if the width is 6 than the label will be wide enough to contain 6 characters of, in my case verdana size 20. But I was not able to figure out what width and what x coordinate I should give my label so it starts at the x point of the canvas. Here is the code that I wrote:
from tkinter import *
from tkinter.ttk import *
from tkinter import messagebox
from tkinter import Menu
# Define the application class where we will implement our widgets
class Application(Frame):
def __init__(self, master):
super(Application, self).__init__(master)
# CANVAS COLOUR DEFAULTS TO THE COLOUR OF THE WORKING WINDOW
canvas = Canvas(master, width=800, height = 640, bg="gray") # IF YOU DO .PACK() HERE IT WILL RETURN NONE AND THEN YOU WILL HAVE PROBLEMS BECAUSE .PACK() RETURNS A 'NONE' TYPE OBJECT
canvas.place(relx=0.5, rely=0.5, anchor=CENTER)
# The 'menu' of the application. The selection labels
main_application_label = Label(master, text="main_application", font=("Helvetica", 20))
main_application_window = canvas.create_window(103,20, window=main_application_label)
main_application = Tk()
main_application.title("main_application")
app = Application(main_application)
app_width = 800
app_height = 640
screen_width = main_application.winfo_screenwidth()
screen_height = main_application.winfo_screenheight()
x_coord = (screen_width/2) - (app_width/2)
y_coord = (screen_height/2) - (app_height/2)
main_application.geometry("%dx%d+%d+%d" % (app_width, app_height, x_coord, y_coord))
main_application.mainloop()
I have managed to somehow get the label at around point 0,0(by giving more values till I got it right) but the actual width of the label is not 200 pixels(~1/3 of the canvas). Please help me understand what values to the width parameter I should give so that my label's background is 1/3 of the canvas's size and if possible explain the relation between character font and width parameter so I can do that for any widgets regardless of their text's length. Thank you for reading my post!
Edit: What I wanted to do was to place 3 widgets(labels in this case but it doesn't matter) on the canvas. I did not understand what the 'anchor' option does and that was confusing me because I was expecting the center of the widget to be placed at the given coordinates all times but as I was changing anchor the placement of the center of the widget was changing and that was confusing me. It's all clear now thanks to #Bryan Oakley. Thanks.
If you want the upper left corner of the text to be at (0,0), you don't have to adjust the coordinates based on the width. You can use the anchor option when creating the canvas object:
main_application_window = canvas.create_window(0, 0, anchor="nw",
window=main_application_label)
If you really need to compute the actual size of the string, you can create a Font object and then use the measure method to find the actual width of a string in the given font.
from tkinter.font import Font
font = Font(family="Helvetica", size=20)
string_width = font.measure("main_application")
string_height = font.metrics("linespace")
This gives you the size of the rendered string. If you're using a label widget you'll also need to take into account the amount of padding and borders that the widget uses.
When you create items on a canvas, you can specify the width and height. For example, this makes the widget 200 pixels wide:
main_application_window = canvas.create_window(0, 0, anchor="nw", width=200,
window=main_application_label, width=400)
This tutorial is using the size of the canvas as coordinates for the lines:
http://effbot.org/tkinterbook/canvas.htm
However, if we edit the code to give the canvas no padding, we can see that this is not working correctly. If you look closely the second create_line() is not lining up with the corners correctly:
from tkinter import *
master = Tk()
w = Canvas(master, width=200, height=100,bd=0,highlightthickness=0)
w.configure(bg="black")
w.pack()
w.create_line(0, 0, 200, 100, fill="red")
w.create_line(0, 100, 200, 0, fill="red")
master.mainloop()
Another example with a 3x3 canvas:
from tkinter import *
root = Tk()
root.configure(bg="blue")
canvas = Canvas(root, width=3, height=3, borderwidth=0, highlightthickness=0, bg="black")
canvas.pack()
canvas.create_line(0,0,3,3, fill="red")
canvas.create_line(0,3,3,0, fill="red")
root.mainloop()
This problem seems to only effect lines going from bottom-left to top-right, or top-right to bottom-left.
If we change the coordinates of the second create_line() to -1 and 3 it now works correctly:
from tkinter import *
root = Tk()
root.configure(bg="blue")
canvas = Canvas(root, width=3, height=3, borderwidth=0, highlightthickness=0, bg="black")
canvas.pack()
canvas.create_line(0,0,3,3, fill="red")
canvas.create_line(-1,3,3,-1, fill="red")
root.mainloop()
My questions are: Why does this only effect the second create_line()? Why does the coordinate 0 become -1, if 3 does not become 2? Is this the way it's supposed to work, or does tkinter just have an inherent problem with drawing positive slopes correctly? It seems to me that the latter is the case. If I want to make a program that draws many lines based on a given set of coordinates, I would seemingly have to calculate if every given segment is a positive or negative slope before creating it.
I have had to put the program I'm making on a complete hold for several days because of this. Can somebody please provide any insight to this issue? Is there something I am missing or not understanding?
You make mistake in second line
Python counts from 0 so left bottom corner is (0,2), not (0,3). So you have to start second line in point (0,2)
First line has points (0,0), (1,1), (2,2) and (3,3) which is outside of canvas. Similar second line should have (0,2), (1,1), (2,0) and (3,-1) which is outside of canvas. But you can't skip (3,3) and (3,-1) because create_line doesn't draw last point - last point doesn't belong to line (similar like x doesn't belong to range(x)). If you skip (3,3) and (3,-1) then create_line doesn't draw (2,2) and (2,0)
Correct lines
canvas.create_line(0,0,3,3, fill="red")
canvas.create_line(0,2,3,-1, fill="red")
in other words
first line (a,b,a+3,b+3) gives (0,0,0+3,0+3) = (0,0,3,3)
second line (a,b,a-3,b-3) gives (0,2,0-3,2-3) = (0,2,3,-1)
I have been toying around with putting a scrollbar on my data graph. I have it on there and its scrolling the data but its also scrolling the scale(data values) on the right hand side of the screen. I've have toyed around this morning with the idea of creating two separate windows, one for the data graph and one for the scale. It looks rather unusual compared to what you normally see but I do notice one thing in particular when I do this. With the scale on the data graph, one gui, the scrolling is very slow as long as the scale still remains on the screen. Once the scale moves off the screen the scrolling speed picks up to what I would normally expect. When I move the scale to a completely separate gui the scrolling speed is consistently fine all the time. How do I overcome this problem?
I'm not sure why the scale is having any kind of effect on the scrolling speed. It's nothing more than:
self.DrawArea.create_line((1298, 12), (1300, 12), fill = "white")
self.DrawArea.create_line((1290, 25), (1300, 25), fill = "white")
self.DrawArea.create_line((1298, 37), (1300, 37), fill = "white")
self.DrawArea.create_text((1320, 25), text = "5.0", fill = 'white')
self.DrawArea.create_text((1320, 50), text = "4.5", fill = 'white')
self.DrawArea.create_text((1320, 75), text = "4.0", fill = 'white')
going down the screen(yes 5 to -5 marked out every every .125...labelled once every .5).
It is feasible to have the scale and the graph data on the same gui and still keep the scrolling speed. I haven't changed the font, either size or type as I'm not sure how to since nothing is really indicated in the tkinter documentation.
Also is there a way that I can limit where the graph data gets displayed. With one gui, I have the graph setup for 1350x615(600 with the bottom 15 being the scrollbar). 1300 should be the display data with the other 50 being the scale. Right now I have the issue that the data gets graphed underneath the scale(scale is obviously put on last). Is there any way I can limit it so the data only gets shown 0-1300 while the scale gets display 1301-1350? I've been toying around with Frames as well this morning but I have had no luck thus far at resolving this issue.
Edited:
When I was trying to use the keyboard for scrolling I was using the .move() command but when I went to change to using the scrollbar I wasn't using the keyboard at all and just using the scrollbar. When I have both the graph and scale on the same gui as long as the scale is on the screen(hasn't been scrolled off yet) the graph moves very slowly across the screen. Once it's off the screen the pace picks up and moves as though I didn't have the scale on the screen at all. It's the same way when I test with two separate windows. The scale on the main graph slows the scrolling down.
Moving the scale to another gui still doesn't help the load speed or the zoom in/out speed for displaying the graph though.
If you want to scroll with the scrollbar and not have the scale scroll, you should probably use two separate windows. I'm not sure what you mean by "unusual compared to what you normally see". If you put the two canvases side-by-side with no space between them and with the same background color, the user would have no way of knowing you're using two canvas widgets at the same time.
The tkinter canvas can scroll several thousand items before it starts to get sluggish, so it's hard to say without seeing your actual code.
here's an example that plots 10,000 points, with the scale in a separate canvas:
import Tkinter as tk
from random import randrange
class Example(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
self.DrawArea = tk.Canvas(self, width=1000, height=600, background="black",
borderwidth=0, highlightthickness=0)
self.scale = tk.Canvas(self, width=30, height=600, background="black",
borderwidth=0, highlightthickness=0)
self.hsb = tk.Scrollbar(self, orient="horizontal", command=self.DrawArea.xview)
self.vsb = tk.Scrollbar(self, orient="vertical", command=self.DrawArea.yview)
self.DrawArea.configure(yscrollcommand=self.vsb.set, xscrollcommand=self.hsb.set)
self.DrawArea.grid(row=0, column=0, sticky="nsew")
self.scale.grid(row=0, column=1, sticky="nsew")
self.vsb.grid(row=0, column=2, sticky="ns")
self.hsb.grid(row=1, column=0, columnspan=2,sticky="ew")
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.draw_scale()
self.draw_points()
def draw_scale(self):
value = 5.0
for y in range(25, 600, 25):
self.scale.create_text((25, y), text=str(value), fill="white", anchor="ne")
value -= 0.5
def draw_points(self):
import math
for x in range(5,10000):
y = randrange(600)
color = "green" if (300 > y > 200) else "red"
self.DrawArea.create_rectangle(x-2,y-2,x+2,y+2, fill=color)
self.DrawArea.configure(scrollregion = self.DrawArea.bbox("all"))
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()