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()
Related
I have a very long automation that uses a very long WHILE loop. I would like to be able to have the text in a textbox update as the process is progressing. However, based on the research I have done, this does not seem to be possible and all of the results "dump" at once when the WHILE loop completes. This is useless for me.
I would love it if the textbox could update as the SHELL updates as that follows along synchronously with the actual process.
I have made a simple TEST file to see if I can get it to work. Here is the code for the TEST file:
from tkinter import *
import time
import tkinter as tk
import tkinter.font as tkFont
root=Tk()
myFont = tkFont.Font(family = 'Helvetica', size = 18, weight = 'bold')
text_cell_bg="cyan" #TEXT CELL BACKGROUND COLOR
text_cell_fg="black" #TEXT CELL TEXT COLOR
text_cell_height=2 #TEXT CELL HEIGHT
text_cell_width=30 #TEXT CELL WIDTH
button_bg="blue" #BUTTON CELL BACKGROUND COLOR
button_fg="white" #BUTTON CELL TEXT COLOR
button_height=2 #BUTTON CELL HEIGTH
button_width=10 #BUTTON CELL WIDTH
textbox=Text(root)
textbox.insert(END, 'Default Text\n\n')
def count_print ():
count = 0
letter = "A"
while count < 5:
print("Count = ",count,". Letter = ",letter,".")
textbox_value = "Count = {}. Letter = {}.\n".format(count,letter)
textbox.insert(1.0, textbox_value)
count += 1
time.sleep(1)
textbox.pack()
button1=tk.Button(root, text='output', command=count_print, font = myFont,
height=button_height,
width=button_width,
bg = button_bg,
fg = button_fg)
button1.pack()
root.mainloop()
You can call your function using Tkinter from inside the function itself.
Tkinter has a special function for this.
from tkinter import *
import time
import tkinter as tk
import tkinter.font as tk_font
root = Tk()
myFont = tk_font.Font(family='Helvetica', size=18, weight='bold')
text_cell_bg = "cyan" # TEXT CELL BACKGROUND COLOR
text_cell_fg = "black" # TEXT CELL TEXT COLOR
text_cell_height = 2 # TEXT CELL HEIGHT
text_cell_width = 30 # TEXT CELL WIDTH
button_bg = "blue" # BUTTON CELL BACKGROUND COLOR
button_fg = "white" # BUTTON CELL TEXT COLOR
button_height = 2 # BUTTON CELL HEIGHT
button_width = 10 # BUTTON CELL WIDTH
textbox = Text(root)
textbox.insert(END, 'Default Text\n\n')
def count_print(n_times=0): # tell your function how many times you have called it
(on the first call this will be 0)
n = n_times + 1
letter = "A"
if n <= 5:
print("Count = ", n, ". Letter = ", letter, ".")# you can change the n var on these lines to n_times if you want 0-4 rather than printing 1-5.
textbox_value = "Count = {}. Letter = {}.\n".format(n, letter)
textbox.insert(1.0, textbox_value)
root.after(1000, count_print, n) # this number replaces and time.sleep it's set to 1 second right now it also call the count_print function passing n as a variable
textbox.pack()
button1 = tk.Button(root, text='output', command=count_print, font=myFont,
height=button_height,
width=button_width,
bg=button_bg,
fg=button_fg)
button1.pack()
root.mainloop()
I hope this helps. also, I don't use Tkinter for my projects and am definitely not very good at using it either so credit for me knowing about root. after goes to this answer,
Make Tkinter force a "refresh" a text widget in mid-command?
So, I'm working on a timer using Python and Tkinter's GUI. To do so, I intend to convert the minutes into seconds (as seen in my code) and use Python's time.sleep command to count down (not yet implemented).
from tkinter import *
import time
countdown = Tk()
countdown.geometry('500x300')
minute = Text(countdown, height = 0.01, width = 5)
minute.place(x=100, y=100)
minlabel = Label(text = "Input Minutes", font = ("MS Sans Serif", 10), bg = 'Light Blue')
minlabel.place(x=85, y = 80)
def go(event):
minseconds = int(minute.get("1.0", END))*60
print(int(minseconds))
countdown.bind('<Return>', go)
countdown.mainloop()
However, when I convert it to minutes, it works the first time (I.E, when I input 3, 180 returns), but any time after that I get this:
ValueError: invalid literal for int() with base 10: '3\n3\n\n'
Any idea what could be causing this? And why it works the first time but then stops working? Thanks :)
The problem is every time Enter is pressed a newline character is entered into the Text widget named minute and they mess up the conversion to int.
The simplest way I can think of to avoid that would be to just clear the widget in the event handler function:
def go(event):
minseconds = int(minute.get("1.0", END))*60
print(int(minseconds))
minute.delete('1.0', END) # <---- ADDED.
so actually the error is quite simple. You created a Text widget, which is just adding lines when you press Enter (corresponding to the \n characters). Just make your text widget bigger (instead of height = 0.01 maybe 2) and you will see it.
To do it more elegantly, you might try Entry() instead of Text(). See the example.
import time
import tkinter as tk
def go(event):
minseconds = int(min_entry.get())
seconds = minseconds*60
print(int(seconds))
countdown = tk.Tk()
countdown.geometry('500x300')
minlabel = tk.Label(text = "Input Minutes", font = ("MS Sans Serif", 10), bg = 'Light Blue')
minlabel.place(x=85, y = 80)
min_entry = tk.Entry()
min_entry.place(x=100, y=100)
countdown.bind('<Return>', go)
countdown.mainloop()
Furthermore try to put your functions always on top of your code, as they have to be defined before you call them. This might be causing trouble as you code gets bigger.
Hope you get a better understanding from tkinter by this :)
Check this update function. Import the module at top
import messagebox as msg
def go(event):
try:
minseconds = int(minute.get("1.0", END))*60
print(int(minseconds))
minute.delete("1.0", END)
except Exception:
msg.showerror("Error","invalid input provided.")
minute.delete("1.0", END)
But I would suggest to go with Entry With a StringVar()
from tkinter import *
from tkinter import messagebox as msg
countdown = Tk()
countdown.geometry('500x300')
minute=StringVar()
minut = Entry(countdown,textvariable=minute, width = 5)
minut.place(x=100, y=100)
minlabel = Label(text = "Input Minutes", font = ("MS Sans Serif", 10), bg = 'Light Blue')
minlabel.place(x=85, y = 80)
def go(event):
try:
minseconds = int(minute.get())*60
print(int(minseconds))
minute.set("")
except ValueError:
msg.showerror("Error","Invalid input provided.")
countdown.bind('<Return>', go)
countdown.mainloop()
I am trying to create a harmless prank joke on my friends, and I want the background of a python tkinter window(not canvas) to change to a random colour every second, then, after ten rounds, it will destroy itself. The problem is that when root.config(background=random_colour)is called, it will not change it's background colour. The entire code is below:
from tkinter import *
import pyglet
import time
import random
root = Tk()
text = Label( padx = 1000, pady = 999, text = 'VIRUS!' )
text.pack()
text.config(font=('Courier', 44))
root.attributes("-fullscreen", True)
root.update()
I'm cutting this bit out because it's just a list of all the named colours in python(It's called COLOURS).
for x in range(0, 9):
colours_length = len(COLOURS)
number = random.randint(0, colours_length)
random_colour = COLOURS[number]
root.config(background=random_colour)
time.sleep(1)
root.update()
root.destroy()
I've took acw1668's advice from the comments section, and it works now. Turns out that the label was covering the entire root window, and that was why it wasn't working.
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.
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 ;)