Python3/Tkinter text "displaylines" behavior issue - python

I am trying to count the actual lines used in a text box when it is pre-filled with data, and again when the form is submitted.
With the code below, it incorrectly shows the number of lines as chars (93) when it starts, but when you push the count button it shows the correct number of lines (4).. from the same code execution. What am I missing? (I'm new to posting on here, go easy.....)
import tkinter
text="adfa asdfkljds ;das asdjfkds fkldjasf dsf;dlsjfdkls jdkls fjd;lsfjd;ls fjd;lsafj ;dlsfj;asdlf"
def countit():
print(DE.count('1.0', 'end', 'displaylines')[0])
top = tkinter.Tk()
DE = tkinter.Text(top, height=5, width=30, wrap="word")
DE.pack()
DEButton = tkinter.Button(top, text="count", command=countit)
DEButton.pack() # WHEN BUTTON IS PRESSED, CORRECT NUMBER IS DISPLAYED
DE.insert("1.0", text)
countit() # FIRST RUN, INCORRECT NUMBER DISPLAYED
top.mainloop()

displaylines can't be calculated if the data isn't displayed, since the value depends on various factors such as the screen resolution, the actual font (which might be different than the requested font), the size of the widget once pack, place, or grid and done it's calculations, and so on. These can't be determined until the window is actually mapped to the screen.
To illustrate the point, add top.update() immediately before calling countit() and you'll see that the value is correctly printed the first time.
A better fix would be to not call countit until the window has been mapped to the screen. For example, you can add this line of code after creating the text widget:
DE.bind("<Map>", lambda event: countit())
The above will call countit() immediately after the widget has been mapped to the screen.

Related

change size of tkinter messagebox

In python, I am attempting the change the width of the tkinter messagebox window so that text can fit on one line.
import tkinter as tk
from tkinter import messagebox
root = tk.Tk()
messagebox.showinfo("info","this information goes beyond the width of the messagebox")
root.mainloop()
It's not possible to adjust the size of messagebox.
When to use the Message Widget
The widget can be used to display short text messages, using a single font. You can often use a plain Label instead. If you need to display text in multiple fonts, use a Text widget. -effbot
Also see:
Can I adjust the size of message box created by tkMessagebox?
#CharleyPathak is correct. You either need to put a newline in the middle of the text, because message boxes can display multiple lines, or create a custom dialog box.
Heres another method that gets the effect youre looking for but doesnt use messagebox. it looks a lot longer but it just offers much more in terms of customization.
def popupmsg():
popup = tk.Tk()
def leavemini():
popup.destroy()
popup.wm_title("Coming Soon")
popup.wm_attributes('-topmost', True) # keeps popup above everything until closed.
popup.wm_attributes("-fullscreen", True) # I chose to make mine fullscreen with transparent effects.
popup.configure(background='#4a4a4a') # this is outter background colour
popup.wm_attributes("-alpha", 0.95) # level of transparency
popup.config(bd=2, relief=FLAT) # tk style
# this next label (tk.button) is the text field holding your message. i put it in a tk.button so the sizing matched the "close" button
# also want to note that my button is very big due to it being used on a touch screen application.
label = tk.Button(popup, text="""PUT MESSAGE HERE""", background="#3e3e3e", font=headerfont,
width=30, height=11, relief=FLAT, state=DISABLED, disabledforeground="#3dcc8e")
label.pack(pady=18)
close_button = tk.Button(popup, text="Close", font=headerfont, command=leavemini, width=30, height=6,
background="#4a4a4a", relief=GROOVE, activebackground="#323232", foreground="#3dcc8e",
activeforeground="#0f8954")
close_button.pack()
I managed to have a proper size for my
"tkMessageBox.showinfo(title="Help", message = str(readme))" this way:
I wanted to show a help file (readme.txt).
def helpfile(filetype):
if filetype==1:
with open("readme.txt") as f:
readme = f.read()
tkMessageBox.showinfo(title="Help", message = str(readme))
I opened the file readme.txt and EDITED IT so that the length of all lines did not exeed about 65 chars. That worked well for me. I think it is important NOT TO HAVE LONG LINES which include CR/LF in between. So format the txt file properly.

How to update layers of tkinter widgets dynamically?

Tkinter Requirements
So I am relatively new to using tkinter and I am struggling with a very specific doubt here. I tried finding solutions to this but as much as I find it obvious, the solution to this doesn't seem to be easy to understand. So if you can see the image above, I am trying to create a GUI for a particular project which requires multi-layer (I am calling it 3D array based) widgets.
Let's say the variables used for this pointer system are i, j, and k.
I am creating individual layer widgets using for loop:
for n in range(i):
frame_x[i] = Frame(root).grid(row = 1, column = i)
entry_x[i] = Entry(frame_x[i]).grid(row = 2, column = i)
button_x[i] = Button(frame_x[i]).grid(row=3, column = i)
Please note this is not a functional code, I have tried to keep it to the point just to give an idea of the method I am using. (Let me know if you want a more detailed code block.)
Now coming to the problem. I am able to do the basic part of this. But the problem is that I want it to work dynamically.
Let's say if the user enters j = 4 first. 4 blocks will be created.
Later if he changes the value to j = 2 and the presses the button, ideally it should make the widgets at block j= 3 and 4 disappear. But I guess tkinter works on overlapping basis and doesn't change a grid element until something is specifically overlapped over it. How do I do that. I tried destroying the entire frame just after entering the for loop, but that doesn't work as for the first time no widget is created before destroying and python throws NameError saying I can't use a variable before assignment.
Anyways, please let me know how do I do this efficiently.
And also in general, if there is a better way to go about the whole thing. Please refer the image above and let me know if it doesn't make sense.
I am not very comfortable with classes in general. I prefer the inefficient way by only using functions to do everything I have to. So it would be great if you can share me a framework without using classes. But its okay if you use them. I know I should start working with classes at some point.
First off, I want to address this part of the question:
I guess tkinter works on overlapping basis and doesn't change a grid element until something is specifically overlapped over it.
I'm not entirely sure what you mean by that, but if it means what I think it means, it is a false statement. tkinter doesn't "work on an overlapping basis". If you destroy a widget, it is destroyed. It doesn't matter if it's overlapped or not.
Based on the tiny bit of code you posted, the main problem is that you aren't putting the entry and button in the frame. Because of that, they are not destroyed when you destroy the frame.
The reason you aren't putting the widgets into the frame is because of this line:
frame_x[i] = Frame(root).grid(row = 1, column = i)
In python, when you do x=y().z(), x has the value of z(). Thus, when you do frame_x[i] = Frame(...).grid(...), frame_x[i] has the value of .grid(...), and .grid(...) always returns None. Thus, frame_x[i] will be None.
When you next do entry_x[i] = Entry(frame_x[i]).grid(...), it's the same as doing entry_x[i] = Entry(None).grid(...). Because the master of the Entry is None, it becomes a child of the root window.
So, the first step is to separate the creation of the widget from the layout of the widget.
frame_x[i] = Frame(root)
frame_x[i].grid(row = 1, column = i)
Once you do that, the Entry and Button widgets will become a child of the frame, and you can remove widgets you don't want by destroying the frame (eg: frame_x[i].destroy()), since destroying a widget will also cause all children of the widget to be destroyed.
Once you have that in place, you can destroy unwanted widgets by simply calling .destroy() on the frame. For example, if you have previously created 10 groups and now need only 5, you can destroy the others and then remove them from the list like this:
# assume 'num' contains the number of frames that we want,
# and that it is smaller than the number of items in frames_x
for frame in frames_x[num:]:
frame.destroy()
frames_x = frames_x[:num]
Here is a complete working program to illustrate. Enter a number and click the button. It will create that many frame+entry+button combinations. Enter a new number that is larger or smaller and it will either add or remove widgets.
This would be easier if you used classes, but you specifically asked for a solution that doesn't use classes. In your real code you probably need to also save the entry widgets in an array so that you can reference them later, but this example is focuses on the creation of the widgets rather than writing your whole program for you.
import tkinter as tk
frames_x = [] def create_widgets():
global frames_x
num = int(num_widgets.get())
# if the number is less than the previous number of
# widgets, delete the widgets we no longer want
for frame in frames_x[num:]:
frame.destroy()
frames_x = frames_x[:num]
# if the number is greater than the previous number of
# widgets, create additional widgets
for i in range(len(frames_x), num):
# create new widget
frame = tk.Frame(root, bd=1, relief="raised")
entry = tk.Entry(frame)
button = tk.Button(frame, text="click me")
# pack entry and button in frame
button.pack(side="right")
entry.pack(side="left", fill="x", expand=True)
# grid the frame in the parent
frame.grid(row=i+1, column=0, columnspan=2)
# save the new frame in the array
frames_x.append(frame)
root = tk.Tk() num_widgets = tk.Entry(root) button = tk.Button(root, text="Create widgets", command=create_widgets)
button.grid(row=0, column=1) num_widgets.grid(row=0, column=0, sticky="ew")
root.mainloop()

Automatically resize text widget's height to fit all text

How can one automatically resize a text widget to fit a text widget's height
There will not be any \n om the text widget, instead the text will wrap (whole word) around and continue down. wrap=WORD
How can this done?
My idea of approaching the problem statement: I was wondering if it was possible to count every time the text was wrapped around in the text widget, and by that given value somehow calculate the height of the text widget? - Just a thought .. I have no clue whether it's possible.
**WHY THIS IS NOT A DUPLICATE **
This is not a duplicate, link to the question you claim it is a duplicate of if you think so. They all have implemented a solution in which it check a '\n' in each keystroke in the text widget. My text widget won't have any '\n' in it at all. But instead wrap the words around !
This is NOT the solution I am looking for, since it is looking for '\n' and changes the height accordingly to how many of them it finds. Since I won't be using any '\n' but instead wrap the words around (Text(frame, wrap=WORDS)) no '\n' will not appeare making that solution USELESS!"
That is why this code, from the question people claim this is a duplicate of, WONT fix this question, this is NOT a duplicate.
wont fix my problem since it looks for '\n':
import Tkinter
class TkExample(Tkinter.Frame):
def __init__(self, parent):
Tkinter.Frame.__init__(self, parent)
self.init_ui()
def init_ui(self):
self.pack()
text_box = Tkinter.Text(self)
text_box.pack()
text_box.bind("<Key>", self.update_size)
def update_size(self, event):
widget_width = 0
widget_height = float(event.widget.index(Tkinter.END))
for line in event.widget.get("1.0", Tkinter.END).split("\n"):
if len(line) > widget_width:
widget_width = len(line)+1
event.widget.config(width=widget_width, height=widget_height)
if __name__ == '__main__':
root = Tkinter.Tk()
TkExample(root)
root.mainloop()
edit example
This is the reason why I am not using message widgets, they doesn't rearrange the text to fill out the text widget.
The tkinter text widget isn't designed to grow or shrink to fit its contents like a label widget. If all you need is to display plain text with no need to interactively edit more text, a Label is probably a better choice than Text.
That being said, it's possible to get the number of displayed lines in a text widget, and with that information you can resize the widget.
Here's an example that shows how to cause it to resize when you insert text programatically. It won't handle resizing as you type, though it can be made to do it.
The trick is to know that internally the text widget has a count method which you can call to get the number of displayed lines. Unfortunately, this method isn't exposed at the tkinter layer and thus requires a bit of knowledge of how tkinter works internally.
class ExpandoText(tk.Text):
def insert(self, *args, **kwargs):
result = tk.Text.insert(self, *args, **kwargs)
self.reset_height()
return result
def reset_height(self):
height = self.tk.call((self._w, "count", "-update", "-displaylines", "1.0", "end"))
self.configure(height=height)
Here is an example of how to use it:
root = tk.Tk()
text = ExpandoText(root, width=20, wrap="word")
text.pack(fill="both", expand=True)
root.update_idletasks()
text.insert("1.0", "This is a line of text that will initially be wrapped.")
root.after(5000, text.insert, "end", "This is more text")
root.mainloop()
When you run the code, you should see a window that looks like this:
If you don't manually resize the window, and wait 5 seconds, the window will grow to show the added text:
Unfortunately you must call update_idletasks before the insertion so that tkinter knows how wide the window will actually be. This might have visual side effects depending on how the rest of your code works (read: you might see a flash when the UI first starts up).

Python Tkinter :removing widgets that were created using a for loop

I'm currently learning how to use the Tkinter library on python to create a GUI that takes in longitude and latitude points and outputing that into a file. Basically I'm trying to automate the process of having to copy the correct format of line of points to another file.
So I created a Entry and button field to see how many long/lat points are needed to generate a 'shape'. Using this integer input from user, I have a for loop to populate the GUI with multiple widgets asking for the long/lat points. I have that working properly, but now I am trying to have a clear button, which would allow the user to clear all these long/lat points and give them the ability to repopulate the field with the amount of points the other shape requires.
So far I have:
def clearGrid():
coordAmount = int(pointText.get())
latLabel.grid_forget()
longLabel.grid_forget()
.....(contains code that populates the GUI)
#creating clear site Button
clearButton = Button(main_gui, text="Clear Sites!",command=clearGrid)
clearButton.grid(row=lastRow+1, column=5, pady=10)
However, the problem that I am running into is that when the clear button is clicked, it only clears the latest instance of the widgets not all of them. So in a for loop that creates 5 instances/iteration of widgets, it will remove only the 5th instance/iteration of widgets.
I'm trying to have the clear button functionality be able to delete all 5 instances of these widgets.
So here is a shortened code of how I am populating the GUI with widgets
def generatePoints():
for x in range(0,3):
degLong_label = Label(main_gui, text="Degree:", height=2)
degLong_label.grid(row=y,column=6,sticky=E)
degLong = Entry(main_gui, width=4)
degLong.grid(row=y,column=7,sticky=W)
#minute
minLong_Label = Label(main_gui,text="Minutes:", height=2)
minLong_Label.grid(row=y,column=8,sticky=W)
minLong = Entry(main_gui,width=3)
minLong.grid(row=y,column=8,sticky=E)
#seconds
secLong_Label= Label(main_gui,text="Sec:",height=2)
secLong_Label.grid(row=y,column=9,sticky=W,padx=20)
secLong = Entry(main_gui,width=3)
secLong.grid(row=y,column=9,sticky=E,padx=20)
#direction
dirLong_Label = Label(main_gui,text="Direction:",padx=5,height=2)
dirLong_Label.grid(row=y,column=12,sticky=W)
dirLong = Entry(main_gui,width=3)
dirLong.grid(row=y,column=13)
You need to hold on to references to all those widgets, usually via a list. Try initializing a list (list_of_widgets) before your loop, then every time you create a widget, append it to that list. When you clear, you can iterate through that list of widgets and destroy each one. Once you're done clearing them, you can clear the list so you don't try to destroy a widget twice (Tkinter will error at that point).
def generatePoints():
list_of_widgets = [] # or take the list as a parameter
for x in range(3):
degLong_label = Label(...)
degLong_label.grid(...)
list_of_widgets.append(degLong_label)
degLong = Entry(...)
degLong.grid(...)
list_of_widgets.append(degLong)
# et al.
def clearGrid(list_of_widgets):
for widget in list_of_widgets:
widget.destroy()
Note that you probably want to actually destroy the widgets if you aren't planning on showing that specific widget again (initializing a new one doesn't count).

Auto truncation of a Tkinter label

I have a label sitting on a frame which is updated periodically to show the status of the application. Periodically, the name of the item being processed will not fit into the window, and with the way I currently have the label configured the window expands to accomidate the label.
Ideally, I'd like way to smartly truncate the text on the label (and then expand if someone expands the window). Is there an easy way to accomplish this?
Practically speaking, how can I just stop the window to expanding based on changes to text in the label?
Edit:
This is an aproximation of the code I'm working on that is not exhibiting the desired behavior (there is a link at the bottom to the actual code file):
r = tk.Tk()
statusFrame = tk.Frame(r, relief=tk.SUNKEN, borderwidth=2)
statusFrame.pack(anchor=tk.SW, fill=tk.X, side=tk.BOTTOM)
statusVar = tk.StringVar()
statusVar.set("String")
tk.Label(statusFrame, textvariable=statusVar).pack(side=tk.LEFT)
statusVar.set("this is a long text, window size should remain the same")
Actual code available here.
The answer depends very much on the way you currently have configured the widget.
For example, I can get the desired functionality as such:
>>> import Tkinter as tk
>>> r=tk.Tk()
>>> r.title('hello')
''
>>> l= tk.Label(r, name='lbl', width=20, text='reduce the window width')
>>> l.pack(fill=tk.BOTH) # or tk.X, depends; check interactive resizing now
>>> l['text']= "This is a long text, window size should remain the same"
Tell us what you do in your code for a more precise (appropriate for your code) answer.

Categories