Fitting text into a rectangle (width x by height y) with tkinter - python

I'm trying to make a program which will fit text into a rectangle (x by y) depending on the text, the font and the font size
Here is the code
def fit_text(screen, width, height, text, font):
measure_frame = Frame(screen) # frame
measure_frame.pack()
measure_frame.pack_forget()
measure = Label(measure_frame, font = font) # make a blank label
measure.grid(row = 0, column = 0) # put it in the frame
##########################################################
# make a certain number of lines
##########################################################
words = text.split(" ")
lines = []
num = 0
previous = 0
while num <= len(words):
measure.config(text = " ".join(words[previous:num])) # change text
line_width = measure.winfo_width() # get the width
print(line_width)
if line_width >= width: # if the line is now too long
lines.append(" ".join(words[previous:num - 1])) # add the last vsion which wasn't too long
previous = num - 1 # previous is now different
num = num + 1 # next word
lines.append(" ".join(words[previous:])) # add the rest of it
return "\n".join(lines)
from tkinter import *
window = Tk()
screen = Canvas(window)
screen.pack()
text = fit_text(screen, 200, 80, "i want to fit this text into a rectangle which is 200 pixels by 80 pixels", ("Purisa", 12))
screen.create_rectangle(100, 100, 300, 180)
screen.create_text(105, 105, text = text, font = ("Purisa", 12), anchor = "nw")
The problem with this is no matter what text is in the label the result from measure.winfo_width() is always 1. Here is where I found this from but it doesn't seem to work for me

The problem with your code is that you're using the width of a widget, but the width will be 1 until the widget is actually laid out on the screen and made visible, since the actual width depends on a number of factors that aren't present until that happens.
You don't need to put the text in a widget in order to measure it. You can pass a string to font.measure() and it will return the amount of space required to render that string in the given font.
For python 3.x you can import the Font class like this:
from tkinter.font import Font
For python 2.x you import it from the tkFont module:
from tkFont import Font
You can then create an instance of Font so that you can get information about that font:
font = Font(family="Purisa", size=18)
length = font.measure("Hello, world")
print "result:", length
You can also get the height of a line in a given font with the font.metrics() method, giving it the argument "linespace":
height = font.metrics("linespace")

The widget will not have a width until it is packed. You need to put the label into the frame, then pack it, then forget it.

I've actually stumbled across a way of doing this through trial and error
By using measure.update_idletasks() it calculates the width properly and it works! Bryan Oakley definitely has a more efficient way of doing it though but I think this method will be useful in other situations
P.S. I wouldn't mind some votes to get a nice, shiny, bronze, self-learner badge ;)

Related

Change Aspect ratio of tkinter canvas text object

I have placed some text onto a tkinter canvas.
How would I go about changing the aspect ratio of this text.
In my case I have a single letter on a canvas created with canvas.create_text.
I would like to change the height of this letter while retaining its width.
Visually it would look like the letter is falling forward or rotation about the horizon.
I also would like to change the width of the letter without changing its height.
Is this case it would visually look like it is rotating.
canvas.scale changes the coordinate system where the letter is drawn but not the scale of the letter it self.
Here is a short example. I want the height of the letter to follow the height of the lines on either side. But the width of the letter should remain the same.
import tkinter as tk
from tkinter import font
class FlipDigit():
def __init__(self,master):
self.master = master
self.digit = tk.Canvas(master,width= 150,height = 150,bg='black')
self.font = font.Font(family ='Calibri',size = 150)
self.letter = self.digit.create_text(75,75,text = 'A',fill = 'white', font = self.font)
self.digit.create_line(5,25,5,125,width=10,fill = '#808080')
self.digit.create_line(145,25,145,125,width = 10,fill = '#808080')
self.digit.pack()
self.count = 0
self.master.after(50,self.Bump)
def Bump(self):
yscale = .8
if self.count > 10:
yscale = 1.25
if self.count >20:
self.count = -1
self.count += 1
self.digit.scale('all',75,75,1,yscale)
self.master.after(150,self.Bump)
if __name__ == '__main__':
root = tk.Tk()
Digit = FlipDigit(root)
root.mainloop()
I don't think there is any way to do that in tkinter. You cannot change the height of a font independently from the width.

Need help Dynamically resizing my text to fit in the tkinter canvas

I was working on a small python script lately when I came across this problem. I tried to create a canvas in a strip shape and write text into it expecting that the text would auto adjust itself to the boundaries of canvas(similar to how a text box works in word processing software). But the text is apparently going out of boundaries.
ScreenShot
Code
from tkinter import *
top = Tk()
top.geometry("130x370")
c = Canvas(top,bg = "pink",height = "370")
c.create_text(30,30,fill="darkblue",font="Times 20 italic bold",text="Hey There!")
c.pack()
top.mainloop()
Firstly, the .create_text() method of the Canvas has a width option which sets the maximum width of the text beyond which it is wrapped. To get a dynamical effect when resizing the window, this width option can be changed in a function bound to the <Configure> event (the resize() function in the example below).
Secondly, to check that the text fits vertically in the canvas, I use the .bbox(item_id) method of the Canvas to get the coordinates of the bounding box of the text. Then, I decrement the fontsize as long as the bottom of the text is lower than the bottom of the canvas.
Here is the example:
import tkinter as tk
top = tk.Tk()
top.geometry("130x370")
def resize(event):
font = "Times %i italic bold"
fontsize = 20
x0 = c.bbox(text_id)[0] # x-coordinate of the left side of the text
c.itemconfigure(text_id, width=c.winfo_width() - x0, font=font % fontsize)
# shrink to fit
height = c.winfo_height() # canvas height
y1 = c.bbox(text_id)[3] # y-coordinate of the bottom of the text
while y1 > height and fontsize > 1:
fontsize -= 1
c.itemconfigure(text_id, font=font % fontsize)
y1 = c.bbox(text_id)[3]
c = tk.Canvas(top, bg="pink", height="370")
text_id = c.create_text(30, 30, anchor="nw", fill="darkblue", font="Times 20 italic bold", text="Hey There!")
c.pack(fill="both", expand=True)
c.bind("<Configure>", resize)
top.mainloop()
Also note that I set the anchor of the text to north west in .create_text() so that (30, 30) are the coordinates of the top-left corner of the text and not of the center to ensure that the start of the text is visible.

Tkinter.text - How to calculate the height of a dynamic string?

I have a Text widget that holds a custom string that contains \n chars (multiple lines).
The widget is placed within a vertical panedwindow which I want to adjust the panedwindow's sash to display the whole string in the Text widget.
The string is dynamic by nature (which means, it is being updated by other methods in my application).
As the Text widget is configured with wrap='word', how can I calculate the string height in pixels to adjust the sash accordingly?
I tried to use text.dlineInfo('end -1c')[1] + text.dlineinfo('end -1c')[3] (for line's y coordinate + height) after the string was loaded to the widget. The problem is that if the last line is not visible, then dlineinfo returns none.
I also tried to use Font.measure routine, but this doesn't include wrap aspects of the Text widget.
Here is a Minimal, Complete, and Verifiable example:
import tkinter
from tkinter import scrolledtext
class GUI():
def __init__(self, master):
self.master = master
self.body_frame = tkinter.PanedWindow(self.master, orient='vertical', sashwidth=4)
self.body_frame.pack(expand=1, fill='both')
self.canvas_frame = tkinter.Frame(self.body_frame)
self.description_frame = tkinter.Frame(self.body_frame)
self.body_frame.add(self.canvas_frame, sticky='nsew')
self.body_frame.add(self.description_frame, sticky='nsew')
tkinter.Button(self.canvas_frame, text='Update Text', command = lambda : self.update_text("""
A very long string with new lines
A very long string with new lines
A very long string with new lines
A very long string with new lines
A very long string with new lines
A very long string with new lines
""")).pack(fill='x')
self.field_description = scrolledtext.ScrolledText(self.description_frame, width=20, wrap='word')
self.field_description.pack(expand=1, fill='both')
self.master.update()
self.body_frame.sash_place(0,0,self.body_frame.winfo_height() - 50) # force sash to be lower
def update_text(self, description):
self.field_description.delete('1.0', 'end')
self.field_description.insert('1.0', description)
height = self.body_frame.winfo_height()
lastline_index = self.field_description.index('end - 1c')
text_height = self.field_description.dlineinfo(lastline_index)[1] + \
self.field_description.dlineinfo(lastline_index)[3]
self.body_frame.sash_place(0, 0, height - text_height)
root = tkinter.Tk()
my_gui = GUI(root)
root.mainloop()
I don't know of any built-in method that returns the total number of lines (including wrapped lines) in a tkinter Text widget.
However, you can manually calculate this number by comparing the lengths of the unbroken strings in the Text widget to the Text widget's exact width (minus padding). This is what the LineCounter class below does:
# python 2.x
# from tkFont import Font
# python 3.x
from tkinter.font import Font
class LineCounter():
def __init__(self):
"""" This class can count the total number of lines (including wrapped
lines) in a tkinter Text() widget """
def count_total_nb_lines(self, textWidget):
# Get Text widget content and split it by unbroken lines
textLines = textWidget.get("1.0", "end-1c").split("\n")
# Get Text widget wrapping style
wrap = text.cget("wrap")
if wrap == "none":
return len(textLines)
else:
# Get Text widget font
font = Font(root, font=textWidget.cget("font"))
totalLines_count = 0
maxLineWidth_px = textWidget.winfo_width() - 2*text.cget("padx") - 1
for line in textLines:
totalLines_count += self.count_nb_wrapped_lines_in_string(line,
maxLineWidth_px, font, wrap)
return totalLines_count
def count_nb_wrapped_lines_in_string(self, string, maxLineWidth_px, font, wrap):
wrappedLines_count = 1
thereAreCharsLeftForWrapping = font.measure(string) >= maxLineWidth_px
while thereAreCharsLeftForWrapping:
wrappedLines_count += 1
if wrap == "char":
string = self.remove_wrapped_chars_from_string(string,
maxLineWidth_px, font)
else:
string = self.remove_wrapped_words_from_string(string,
maxLineWidth_px, font)
thereAreCharsLeftForWrapping = font.measure(string) >= maxLineWidth_px
return wrappedLines_count
def remove_wrapped_chars_from_string(self, string, maxLineWidth_px, font):
avgCharWidth_px = font.measure(string)/float(len(string))
nCharsToWrap = int(0.9*maxLineWidth_px/float(avgCharWidth_px))
wrapLine_isFull = font.measure(string[:nCharsToWrap]) >= maxLineWidth_px
while not wrapLine_isFull:
nCharsToWrap += 1
wrapLine_isFull = font.measure(string[:nCharsToWrap]) >= maxLineWidth_px
return string[nCharsToWrap-1:]
def remove_wrapped_words_from_string(self, string, maxLineWidth_px, font):
words = string.split(" ")
nWordsToWrap = 0
wrapLine_isFull = font.measure(" ".join(words[:nWordsToWrap])) >= maxLineWidth_px
while not wrapLine_isFull:
nWordsToWrap += 1
wrapLine_isFull = font.measure(" ".join(words[:nWordsToWrap])) >= maxLineWidth_px
if nWordsToWrap == 1:
# If there is only 1 word to wrap, this word is longer than the Text
# widget width. Therefore, wrapping switches to character mode
return self.remove_wrapped_chars_from_string(string, maxLineWidth_px, font)
else:
return " ".join(words[nWordsToWrap-1:])
Example of use:
import tkinter as tk
root = tk.Tk()
text = tk.Text(root, wrap='word')
text.insert("1.0", "The total number of lines in this Text widget is " +
"determined accurately, even when the text is wrapped...")
lineCounter = LineCounter()
label = tk.Label(root, text="0 lines", foreground="red")
def show_nb_of_lines(evt):
nbLines = lineCounter.count_total_nb_lines(text)
if nbLines < 2:
label.config(text="{} line".format(nbLines))
else:
label.config(text="{} lines".format(nbLines))
label.pack(side="bottom")
text.pack(side="bottom", fill="both", expand=True)
text.bind("<Configure>", show_nb_of_lines)
text.bind("<KeyRelease>", show_nb_of_lines)
root.mainloop()
In your specific case, the height of the wrapped text in your ScrolledText can be determined in update_text() as follows:
from tkinter.font import Font
lineCounter = LineCounter()
...
class GUI():
...
def update_text(self, description):
...
nbLines = lineCounter.count_total_nb_lines(self.field_description)
font = Font(font=self.field_description.cget("font"))
lineHeight = font.metrics("linespace")
text_height = nbLines * lineHeight
...
You know the number of lines in your Text. And you can tell when a line is off the scrolled region when dlineinfo returns None. So go through each line and "see" it, to make sure it's visible before you run the dlineinfo() call on it. Then sum them all up, and that's the minimum new height you need for the lines to all appear at the current width. From the height of a line's bbox and the height of the biggest font in the line, you can determine if the line is wrapped, and if so, how many times, if you care about that. The trick is to then use paneconfig() to modify the height of the paned window. Even if the child window would resize automatically normally, the paned window will not. It must be told to resize through the paneconfig() call.
If you "see" each line before measuring, you'll get all the measurements. And "seeing" each line shouldn't be a big deal since you intend to show them all at the end anyway.

how to resize an image to fit the label size? (python)

My aim is to create a random country generator, and the flag of the country that is picked will appear. However, if the image file is bigger than the predetermined size of the label, then only part of the image is displayed. Is there a way of resizing the image to fit the label? (All other questions like this which I have seen have been answered, mentioning the PIL or Image modules. I tested them both, and they both came up with this error:
Traceback (most recent call last):
File "C:\python\country.py", line 6, in
import PIL
ImportError: No module named 'PIL'
This is my code, if it helps:
import tkinter
from tkinter import *
import random
flags = ['England','Wales','Scotland','Northern Ireland','Republic of Ireland']
def newcountry():
country = random.choice(flags)
flagLabel.config(text=country)
if country == "England":
flagpicture.config(image=England)
elif country == "Wales":
flagpicture.config(image=Wales)
elif country == "Scotland":
flagpicture.config(image=Scotland)
elif country == "Northern Ireland":
flagpicture.config(image=NorthernIreland)
else:
flagpicture.config(image=Ireland)
root = tkinter.Tk()
root.title("Country Generator")
England = tkinter.PhotoImage(file="england.gif")
Wales = tkinter.PhotoImage(file="wales.gif")
Scotland = tkinter.PhotoImage(file="scotland.gif")
NorthernIreland = tkinter.PhotoImage(file="northern ireland.gif")
Ireland = tkinter.PhotoImage(file="republic of ireland.gif")
blackscreen = tkinter.PhotoImage(file="black screen.gif")
flagLabel = tkinter.Label(root, text="",font=('Helvetica',40))
flagLabel.pack()
flagpicture = tkinter.Label(root,image=blackscreen,height=150,width=150)
flagpicture.pack()
newflagButton = tkinter.Button(text="Next Country",command=newcountry)
newflagButton.pack()
The code works perfectly fine apart from only showing part of the image. Is there a way to resize the images within the code itself?(I am using Python 3.5.1)
If you don't have PIL installed, first you need to install
pip install pillow
Once installed you can now import from PIL:
from PIL import Image, ImageTk
Tk's PhotoImage can only display .gif's, whereas PIL's ImageTk will let us display various image formats in tkinter and PIL's Image class has a resize method we can use to resize the image.
I trimmed your code down some.
You can resize the image and then just configure the label, the label will expand to be the size of the image. If you gave the label a specific height and width, lets say height=1 and width=1 and you resized the image to be 500x500 and then configured the widget. It would still display a 1x1 label since you've set these attributes explicitly.
In the below code, modifiying the dict, it is not okay to modify a dict while iterating over it. dict.items() returns a copy of the dict.
There's various ways to do this, I just though a dict was convenient here.
Link to an image that's over the height / width limit - kitty.gif
from tkinter import *
import random
from PIL import Image, ImageTk
WIDTH, HEIGHT = 150, 150
flags = {
'England': 'england.gif',
'Wales': 'wales.gif',
'Kitty': 'kitty.gif'
}
def batch_resize():
for k, v in flags.items():
v = Image.open(v).resize((WIDTH, HEIGHT), Image.ANTIALIAS)
flags[k] = ImageTk.PhotoImage(v)
def newcountry():
country = random.choice(list(flags.keys()))
image = flags[country]
flagLabel['text'] = country
flagpicture.config(image=image)
if __name__ == '__main__':
root = Tk()
root.configure(bg='black')
batch_resize()
flagLabel = Label(root, text="", bg='black', fg='cyan', font=('Helvetica',40))
flagLabel.pack()
flagpicture = Label(root)
flagpicture.pack()
newflagButton = Button(root, text="Next Country", command=newcountry)
newflagButton.pack()
root.mainloop()
Instead of randomly selecting a country to display its flag, we loop through the flags dictionary that is key-sorted. Unlike the random choice which will inevitably repeat the flags, this scheme runs through the countries in alphabetical order. Meanwhile, we resize all the images to a fixed pixel size based on the width and height of the root window multiplied by a scale factor. Below is the code:
import tkinter as tk
from PIL import Image, ImageTk
class Flags:
def __init__(self, flags):
self.flags = flags
self.keyList = sorted(self.flags.keys()) # sorted(flags)
self.length = len(self.keyList)
self.index = 0
def resize(self, xy, scale):
xy = [int(x * y) for (x, y) in zip(xy, scale)]
for k, v in self.flags.items():
v = Image.open(r'C:/Users/user/Downloads/' + v)
v = v.resize((xy[0], xy[1]), Image.ANTIALIAS)
self.flags[k] = ImageTk.PhotoImage(v)
def newCountry(self, lbl_flag, lbl_pic):
country = self.keyList[self.index]
lbl_flag["text"] = country
img = self.flags[country]
lbl_pic.config(image = img)
self.index = (self.index + 1) % self.length # loop around the flags dictionary
def rootSize(root):
# Find the size of the root window
root.update_idletasks()
width = int(root.winfo_width() * 1.5) # 200 * m
height = int(root.winfo_height() * 1.0) # 200 * m
return width, height
def centerWsize(root, wh):
root.title("Grids layout manager")
width, height = wh
# Find the (x,y) to position it in the center of the screen
x = int((root.winfo_screenwidth() / 2) - width/2)
y = int((root.winfo_screenheight() / 2) - height/2)
root.geometry("{}x{}+{}+{}".format(width, height, x, y))
if __name__ == "__main__":
flags = {
"Republic of China": "taiwan_flag.png",
"United States of America": "america_flag.gif",
"America": "america_flag.png",
}
root = tk.Tk()
wh = rootSize(root)
centerWsize(root, wh)
frame = tk.Frame(root, borderwidth=5, relief=tk.GROOVE)
frame.grid(column=0, row=0, rowspan=3)
flag = Flags(flags)
zoom = (0.7, 0.6) # Resizing all the flags to a fixed size of wh * zoom
flag.resize(wh, zoom)
lbl_flag = tk.Label(frame, text = "Country name here", bg = 'white', fg = 'magenta', font = ('Helvetica', 12), width = 30)
lbl_flag.grid(column = 0, row = 0)
pic_flag = tk.Label(frame, text = "Country flag will display here")
pic_flag.grid(column = 0, row = 1)
btn_flag = tk.Button(frame, text = "Click for next Country Flag",
bg = "white", fg = "green", command = lambda : flag.newCountry(lbl_flag, pic_flag))
btn_flag.grid(column = 0, row = 2)
root.mainloop()
You can use the PIL(pillow module to resize the image)
But in order to resize the images to exactly to the widget size, You can do the following. (Assuming that you are familiar with basic tkinter syntax structure)
your_widget.update() #We are calling the update method in-order to update the
#widget size after it's creartion, Other wise, it will just print '1' and '1'
#rather than the pixel size.
height=your_widget.winfo_height()
width=your_widget.winfo_width()
print(height,weight)
Now you can use the height and width information to create a new image that you can size it perfectly to your widget.
But if you have your image is already created, you can use the PIL module to resize the image to your size
first open your image with
flag_temp = Image.open("file location")
next resize the image to your size
flag_new = flag_temp.resize((10,10))# example size
make your final image to add in your widget
flag_final = ImageTk.PhotoImage(flag_new)
now you can use the 'flag final' varible in your widget.
IF YOUR APP HAS TO BE RESIZED AT ANY POINT, you can use the height and width varible created in the first code para, to dynamically resize the image
But you should make sure that the function is called regularly to update it.
You should also pass in the height and width variable in the place of (10,10) something like this
flag_new = flag_temp.resize((height,widht)
Hopefully this was helpful, I think the answer is bit long for your question, If you have any problems pls comment below.

How to find out the required window size?

I have a window containing a label w whose text can be any of a given variety (in this case "Hello World! " once, twice and thrice):
from Tkinter import *
import time
root = Tk()
text = "Hello, World! "
w = Label(root)
for i in range(1, 4):
w["text"] = text*i
w.update_idletasks()
w.grid()
w.update_idletasks()
time.sleep(1)
root.mainloop()
I would like to set the size of the window to a fixed width. This width should be the one which is required for the longest text that w can get. How can I do this most easily? Do I have to cycle through all possible texts, read the respective window width and set the final width to the maximum of these values? If this is the best way, how can I do this without the window appearing on the screen?
How can you know the size of the largest text if you don't cycle through them ? The only possibility would be knowing the size of each one earlier, but then you would have solved the problem already. Therefore all you have to do is set the label and check the required width for the window, then you check whether this width is larger than the current maximum and update it if needed. If the window happens to show during this process, you can call withdraw, do everything you need, and then call deiconify.
import time
import random
import Tkinter
root = Tkinter.Tk()
root.withdraw()
text = "Hello, World! "
w = Tkinter.Label()
w.grid()
maxwidth = 0
for _ in range(10):
i = random.randint(1, 5)
w["text"] = text*i
print i
w.update_idletasks()
maxwidth = max(maxwidth, root.winfo_reqwidth())
root.wm_minsize(maxwidth, root.winfo_reqheight())
root.wm_maxsize(maxwidth, int(1e6))
root.deiconify()
root.mainloop()
You can set a fixed width for the label, and wrap the text using the textwrap module. textwrap.wrap(text, width) will split text into lines no longer than width characters. It also has options for breaking long words or hyphens, but see the documentation for that.
from Tkinter import *
import time
import textwrap
root = Tk()
text = "Hello, World! "
w = Label(root, width=35)
w.grid()
for i in range(1, 4):
w.update()
s = text*i
s = "\n".join(textwrap.wrap(s, 21))
w["text"] = s
time.sleep(1)
root.mainloop()

Categories

Resources