Naming a widget to auto-destroy-replace it - python-3.x

I'm making a picture gallery that exists independently in several places within the app. When any of the galleries close, the current picture displayed in it is posted to a central graphics edit area. So the pictures were gridding on top of each other and I was planning to add code to delete the one that was already there when I stumbled on some unexpected behavior that seems to make this unnecessary. When I used the name option to give the container a name, instead of gridding a new container and picture on top of the existing one, apparently the old one was destroyed and replaced automatically.
My question is, can I rely on this behavior, or is it some kind of fluke? I can't find any documentation on it. From Fredrick Lundh, one of Tkinter's authors, "If you really need to specify the name of a widget, you can use the name option when you create the widget. One (and most likely the only) reason for this is if you need to interface with code written in Tcl." So I'm worried that I'll be relying on a fluke but so far it's a good fluke because I don't have to figure out how to keep widgets from gridding on top of each other if tkinter is going to auto-destroy the old one for me. It's not that hard but this name option thing is easier.
The example shows this behavior in action. If you remove the name option from the functions that create the widgets, the widgets grid on top of each other. But with the name option used, the widgets replace each other.
import tkinter as tk
def create():
f1 = tk.Frame(root, bg='red', name='exists') #
f1.grid(column=0, row=0)
l1 = tk.Label(f1, bg='yellow', text='This is a very long sentence.')
l1.grid(column=0, row=0)
count_labels()
def replace():
f1 = tk.Frame(root, bg='green', name='exists') #
f1.grid(column=0, row=0)
l1 = tk.Label(f1, bg='tan', text='short sentence')
l1.grid(column=0, row=0)
count_labels()
def count_labels():
root.update_idletasks()
for child in root.winfo_children():
x = child
print(len(x.winfo_children()))
root = tk.Tk()
b1 = tk.Button(root, text='create', command=create)
b1.grid(column=0, row=1)
b2 = tk.Button(root, text='replace', command=replace)
b2.grid(column=1, row=1)
root.mainloop()

Related

How to enter text into two text widgets by just entring into same widget

I want a method from which I can insert text into two widgets by entering text into single text widget. Simply in programming is I want to bind all functions and events of text widget to another text widget.
i had tried
txt=Text(root,height=300,width=300)
txt.pack()
text=Text(root,height=300,width=300)
text.pack()
def func(event):
text.delete("1.0","end")
text.insert(INSERT,txt.get("1.0","end"))
txt.bind(func,<Any-KeyPress>)
but it is not a good option because it is taking time and it shows some delay, and some long delay when the text goes long.
If you want the contents of the two text widgets to be identical, the text widget has a little-used feature known as peer widgets. In effect, you can have multiple text widgets that share the same underlying data structure.
The canonical tcl/tk documentation describes peers like this:
The text widget has a separate store of all its data concerning each line's textual contents, marks, tags, images and windows, and the undo stack.
While this data store cannot be accessed directly (i.e. without a text widget as an intermediary), multiple text widgets can be created, each of which present different views on the same underlying data. Such text widgets are known as peer text widgets.
Unfortunately, tkinter's support of text widget peering is not complete. However, it's possible to create a new widget class that makes use of the peering function.
The following defines a new widget, TextPeer. It takes another text widget as its master and creates a peer:
import tkinter as tk
class TextPeer(tk.Text):
"""A peer of an existing text widget"""
count = 0
def __init__(self, master, cnf={}, **kw):
TextPeer.count += 1
parent = master.master
peerName = "peer-{}".format(TextPeer.count)
if str(parent) == ".":
peerPath = ".{}".format(peerName)
else:
peerPath = "{}.{}".format(parent, peerName)
# Create the peer
master.tk.call(master, 'peer', 'create', peerPath, *self._options(cnf, kw))
# Create the tkinter widget based on the peer
# We can't call tk.Text.__init__ because it will try to
# create a new text widget. Instead, we want to use
# the peer widget that has already been created.
tk.BaseWidget._setup(self, parent, {'name': peerName})
You use this similar to how you use a Text widget. You can configure the peer just like a regular text widget, but the data will be shared (ie: you can have different sizes, colors, etc for each peer)
Here's an example that creates three peers. Notice how typing in any one of the widgets will update the others immediately. Although these widgets share the same data, each can have their own cursor location and selected text.
import tkinter as tk
root = tk.Tk()
text1 = tk.Text(root, width=40, height=4, font=("Helvetica", 20))
text2 = TextPeer(text1, width=40, height=4, background="pink", font=("Helvetica", 16))
text3 = TextPeer(text1, width=40, height=8, background="yellow", font=("Fixed", 12))
text1.pack(side="top", fill="both", expand=True)
text2.pack(side="top", fill="both", expand=True)
text3.pack(side="top", fill="both", expand=True)
text2.insert("end", (
"Type in one, and the change will "
"appear in the other."
))
root.mainloop()
The quickest way to update text in a 2nd box that I have found is to use replace() and get(). That said after testing your example I am not really seeing a noticeable delay.
We can use the Modified event to manage our updates and after each modification we can tell text1 that Modified is False so we get an update on every change.
Let me know if this was what you were looking for.
Try this:
import tkinter as tk
def update_text2(_=None):
text2.replace('1.0', 'end', text1.get('1.0', 'end'))
text1.edit_modified(False)
root = tk.Tk()
text1 = tk.Text(root)
text2 = tk.Text(root)
text1.pack()
text2.pack()
text1.bind('<<Modified>>', update_text2)
root.mainloop()

Is there a way I can make buttons in Tkinter with a for loop, while also giving each one a different command?

I'm making a revision system for school, and I want it to be able to use a modular amount of subjects just in case a subject is added to the system, so I need a way to make a number of buttons with different subject names, and be able to differentiate between those buttons using tkinter. So for example, if they were to click the Mathematics button, it would take them to another bit of code specially suited for mathematics(Although, it can't be solely for Mathematics, since then I would need definitions for subjects that haven't even been added yet)
First I simply tried setting the command to "print(subjectnames[subcount-1])", thinking it would print the name of the button, but that just printed both names out straight away without even pressing a button. Then I tried changing the variable name subject to the name of the button, which I didn't expect to work, I was just stumped and desperate
Here I started setting up the definition
def chooseQuiz():
clearWindow()
subjectnames=[]
button=[]
This was probably unimportant, just labels for the title and spacing
Label(mainWindow, text="Which quizzes would you like to take?", bg='purple3', font=('constantia',25,"bold")).grid(row=0, column=0, padx=100, pady=0)
Label(mainWindow, bg='purple3').grid(row=1, column=0, padx=0, pady=15)
Here I extract data from an SQL table to get all subject names from all topics, again probably unimportant but here is where most of the variables are made
c.execute("SELECT Subject_name FROM topics")
for row in c.fetchall():
if row[0] in subjectnames:
pass
elif row[0] not in subjectnames:
subjectnames.append(row[0])
else:
messagebox.showerror("Error", "subjectnames are not appending")
chooseQuiz()
This is the main part of this question, where I tried to form a fluid number of buttons all with different commands, but to no avail
for subcount in range(len(subjectnames)):
button.append(Button(mainWindow, text=str(subjectnames[subcount-1]), bg='grey', fg='black', font=('cambria',15), width=25, command=(subject==subjectnames[subcount-1])))
button[-1].grid(row=subcount+2,column=0, padx=0, pady=15)
I expected the subject variable to be the same as the button I pressed, but it remained at 0(original value). I think this is due to wrong use of the command function in tkinter on my part. The buttons still showed up fine(only 2 subjects currently, Mathematics and Physics, and both showed up fine).
Yes, it is possible.
The following example creates a window with a reset button; upon clicking reset, a frame is populated with buttons corresponding to a random number of buttons chosen randomly from possible subjects. Each button has a command that calls a display function that redirects the call to the proper topic, which, in turn prints the name of its topic to the console, for simplicity of the example. You could easily create functions/classes corresponding to each topic, to encapsulate more sophisticated behavior.
Adding subjects, is as simple as adding a key-value pair in SUBJECTS
Pressing reset again, removes the current button and replaces them with a new set chosen randomly.
import random
import tkinter as tk
from _tkinter import TclError
SUBJECTS = {'Maths': lambda:print('Maths'),
'Physics': lambda:print('Physics'),
'Chemistry': lambda:print('Chemistry'),
'Biology': lambda:print('Biology'),
'Astronomy': lambda:print('Astronomy'),
'Petrology': lambda:print('Petrology'),}
topics = []
def topic_not_implemented():
print('this topic does not exist')
def get_topics():
"""randomly creates a list of topics for this example
"""
global topics
topics = []
for _ in range(random.randrange(1, len(SUBJECTS))):
topics.append(random.choice(list(SUBJECTS.keys())))
return topics
def reset_topics():
global topics_frame
try:
for widget in topics_frame.winfo_children():
widget.destroy()
topics_frame.forget()
topics_frame.destroy()
except UnboundLocalError:
print('error')
finally:
topics_frame = tk.Frame(root)
topics_frame.pack()
for topic in get_topics():
tk.Button(topics_frame, text=topic, command=lambda topic=topic: display(topic)).pack()
def display(topic):
"""redirects the call to the proper topic
"""
SUBJECTS.get(topic, topic_not_implemented)()
root = tk.Tk()
reset = tk.Button(root, text='reset', command=reset_topics)
reset.pack()
topics_frame = tk.Frame(root)
topics_frame.pack()
root.mainloop()

Hiding/displaying a canvas

How do you hide a canvas so it only shows when you want it displayed?
self.canvas.config(state='hidden')
just gives the error saying you can only use 'disabled' or 'normal'
In the comments you say you are using pack. In that case, you can make it hidden by using pack_forget.
import tkinter as tk
def show():
canvas.pack()
def hide():
canvas.pack_forget()
root = tk.Tk()
root.geometry("400x400")
show_button = tk.Button(root, text="show", command=show)
hide_button = tk.Button(root, text="hide", command=hide)
canvas = tk.Canvas(root, background="pink")
show_button.pack(side="top")
hide_button.pack(side="top")
canvas.pack(side="top")
root.mainloop()
However, it's usually better to use grid in such a case. pack_forget() doesn't remember where the widget was, so the next time you call pack the widget may end up in a different place. To see an example, move canvas.pack(side="top") up two lines, before show_button.pack(side="top")
grid, on the other hand, has a grid_remove method which will remember all of the settings, so that a subsequent call to grid() with no options will put the widget back in the exact same spot.

How to make a frame (like a data entry form or a status bar) always appear on top of other tkinter frames in Python 3?

So, I have a small Python 3 project that should have a window composed of 3 main areas:
Tool bar (top);
Table/treeview with database data (center) - this table should be able to grow downwards, with scrollbars, but I still do not know how to make them work with treeview;
Status bar (bottom).
I am also trying to add a quick data entry form that should apear on user demand right above the status bar. I think I have been able to create the main structure for this window, using pack(). The data entry form also shows up and goes away when the user clicks the proper buttons. But there are a couple of things that I don't know how to solve. The first one is that when the user resizes the window, making it smaller, the status bar and the data entry form both disappear under the table/treeview. How can I make them be always on top?
Here is my code:
#!/usr/local/bin/python3
# encoding: utf-8
from tkinter import *
from tkinter import ttk
entryform_visible = 0
class mainWindow:
def __init__(self, master):
self.master = master
master.title("AppTittle")
master.minsize(width=700, height=200)
def show_entryform():
global entryform_visible
if entryform_visible == 1:
hide_entryform()
return
else:
entryform_visible = 1
# Form for data entry (bottom of main window)
status_txt.set("Introducing new data...")
self.bottomframe.pack(side=BOTTOM, fill=X)
def hide_entryform():
global entryform_visible
self.bottomframe.pack_forget()
entryform_visible = 0
status_txt.set("Introduction of data was cancelled.")
def add_register():
hide_entryform()
status_txt.set("New register added!")
# Tool bar (buttons)
self.topframe = ttk.Frame(root, padding="3 3 12 12")
self.topframe.pack(side=TOP, fill=X)
btn_add = ttk.Button(self.topframe, text="Button1").pack(side=LEFT)
btn_add = ttk.Button(self.topframe, text="+", width=2, command=show_entryform).pack(side=RIGHT)
btn_add = ttk.Button(self.topframe, text="Button2").pack(side=RIGHT)
# Data table (center area of window)
self.mainframe = ttk.Frame(root)
self.mainframe.pack(side=TOP, fill=X)
tree = ttk.Treeview(self.mainframe, height=25, selectmode='extended')
tree['columns'] = ('id', 'name', 'descr')
tree.pack(side=TOP, fill=BOTH)
tree.column('#0', anchor=W, minwidth=0, stretch=0, width=0)
tree.column('id', anchor=W, minwidth=30, stretch=0, width=40)
tree.column('name', minwidth=30, stretch=1, width=30)
tree.column('descr', minwidth=100, stretch=1, width=200)
tree.heading('id', text="ID")
tree.heading('name', text="Name")
tree.heading('descr', text="Description")
# Data entry form
self.bottomframe = ttk.Frame(root, padding="3 3 12 12")
ttk.Label(self.bottomframe, text="Field 1").pack(side=LEFT)
text_input_obj = ttk.Entry(self.bottomframe, width=13)
text_input_obj.pack(side=LEFT)
text_input_obj.focus_set()
btn_add = ttk.Button(self.bottomframe, text="Add", command=add_register).pack(side=RIGHT)
# We are going to pack() this later (when the user clicks the '+' button)
# Status bar
self.statusframe = ttk.Frame(root, padding="2 2 2 2")
self.statusframe.pack(side=BOTTOM, fill=X)
status_txt = StringVar()
self.statusbar = ttk.Label(self.statusframe, textvariable=status_txt).pack(side=LEFT)
root = Tk()
appwindow = mainWindow(root)
root.mainloop()
The first one is that when the user resizes the window, making it
smaller, the status bar and the data entry form both disappear under
the table/treeview. How can I make them be always on top?
This has to do with how the packer works. It will start chopping off widgets when the space available is less than the space needed. It does this in the reverse order of how widgets were packed.
In this case, because the bottom frame was packed last, when you shrink the window to be too small to fit everything, pack first shrinks any windows that might have expanded to larger than their default size, and then starts chopping off widgets starting with the last widget that was packed. In this case it is self.statusframe that gets chopped since it was the last to be packed.
There are two ways to solve this. One is to simply reverse the order that you pack everything into the root window. The other is to initially make the tree widget very small, and then let it grow to fill the space. Since the natural size of the treeview is small, tkinter will shrink it before it starts removing widgets from view. If you do this second technique, you can use wm_geometry to make the window large when it starts out (otherwise the tree won't have any space to grow into)
My advice is to always group your pack and grid statements. It makes changes like this very easy, and it make visualizing your code much easier. For example, using the first technique I would move the packing of the main areas to the bottom of the function:
# layout the main GUI
self.topframe.pack(side=TOP, fill=X)
self.statusframe.pack(side=BOTTOM, fill=X)
self.mainframe.pack(side=TOP, fill=BOTH, expand=True)
With this, when space gets tight pack will start to chop off the tree. But since the tree is big, there's plenty to chop before it starts to disappear.
When you do this, you'll expose another problem in your layout, and that is that you don't have the treeview expanding to fill extra space in mainframe. You need to pack it like this:
tree.pack(side=TOP, fill=BOTH, expand=True)
This brings up another problem. Namely, your popup bottomframe may pop up hidden. That is because pack isn't designed to work well in this sort of situation. Again, that is because pack will start to chop off widgets in the reverse order that they were added. Since you're adding it last, it's the first widget to get chopped off. If the user resizes the window to be smaller, packing it at the bottom may cause it to never appear.
A better solution in this specific case is to not use pack for this one widget. Instead, since you want it to "pop up", you can use place. place is perfect for popping up widgets on top of other widgets. The only other thing you need to do is make sure that this frame is higher in the stacking order than the statusbar, which you can do with lift:
def show_entryform():
...
self.bottomframe.place(relx=.5, rely=1, anchor="s", relwidth=1.0)
self.bottomframe.lift()
And, of course, use place_forget instead of pack_forget:
def hide_entryform():
...
self.bottomframe.place_forget()
...

StringVar.get() returns a blank value

I've asked this before - but I think I phrased it incorrectly.
What I essentially want is to have a user fill out a tkinter entry box, and have the value saved - somewhere.
I the want to click on a button, and have the user's entered text to manipulate.
I want essentially what is below - although that doesn't work at all - as I can't enter a StringVar there, apparently. I'm really at my wits end here :(
How can I rewrite this, so it will work?
class Driver():
firstname = StringVar()
def __init__(self):
root = Tk()
root.wm_title("Driver")
firstname_label = ttk.Label(root, text="First Name *").grid(row=0, column=0)
firstname_field = ttk.Entry(root, textvariable=self.firstname).grid(row=0, column=1)
ttk.Button(root, text="Send", command=self.submit).grid()
root.mainloop()
def submit(self):
print(self.firstname.get())
I've having lots, and lots of trouble with this. It seems to printing blank values and references to the variable - rather than the value inside it
You cannot use StringVar in this way -- you can't create a StringVar until after you've created the root window. Because you are creating the root window inside the constructor the code will throw an error.
The solution is to move the creation of the StringVar inside your constructor:
class Driver():
def __init__(self):
root = Tk()
root.wm_title("Driver")
self.firstname = StringVar()
firstname_label = ttk.Label(root, text="First Name *").grid(row=0, column=0)
Note that the way you've written the code, firstname_label and firstname_field will always be None, because that is what grid returns. It's always best to separate widget creation from layout.
Also, you don't really need the StringVar under most circumstances (assuming you correctly store a reference to the widget). Just omit it, and when you want the value of the entry widget you can just get it straight from the entry widget:
...
self.firstname_field = Entry(...)
...
print(self.firstname_field.get())
The use of a StringVar is only necessary if you want to share the value between widgets, or you want to put a trace on the variable.

Resources