How do I auto-update button text in Tkinter? - python

I just started working with Tkinter today and can't seem to figure out how to auto-update the text of a button (like binding).
The buttons are dynamically created:
#creates frames visualization
for frame_num in range(OSBehavior.NUM_FRAMES):
f_label = Label(frames_frame, text='Frame ' + str(frame_num))
f_label.grid(row=frame_num, column=0, padx=5)
f_button = Button(frames_frame, width=30, textvariable=msim.frames[frame_num].page.name)
f_button.grid(row=frame_num, column=1, padx=5, pady=3)
When msim.frames[frame_num].page.name (type is string) changes, I'd like the button text to reflect that. It indeed changes as I have a text-based version running at the same time. However, the button text doesn't change.
If possible, I'm avoiding setting the text of the buttons manually in the backend code.
If someone could point me in the right direction, that would be awesome. Thanks!

msim.frames[frame_num].page.name needs to be an instance of a StringVar, it won't work with ordinary variables.

Related

Customizing Button Commands in Python Tkinter

I've recently started learning how to use Tkinter. I'm working on the front-end for a small project to design an Enigma Machine, and a part of it involves creating a keyboard-like layout of buttons to simulate the plugboard. When clicking one button, I want it to go to a method I've created to handle all button presses and tell the method which letter the button corresponds to, and act accordingly. The main task right now is to make it so that pressing one button and then pressing another button after that draws a line between both buttons.
What I've added below is a scuffed version of the actual program to cut out repeated code like the button declarations.
from tkinter import *
from tkinter import ttk
from PIL import ImageTk, Image
class Steckerbrett:
root = Tk()
frm = ttk.Frame(root, padding=10)
frm.grid(row=0, column=0)
ttk.Label(frm, text="Enigma Machine").grid(column=0, row=0)
ttk.Label(frm, text="Steckerbrett").grid(column=0, row=1)
ttk.Label(frm, text="").grid(column=0, row=2)
# plugboard keys row 1
frm1 = ttk.Frame(root, padding=10)
frm1.grid(row=1, column=0)
ttk.Button(frm1, text="Q", command=connection).grid(column=0, row=0)
ttk.Button(frm1, text="W", command=connection).grid(column=1, row=0)
# i have declarations like this for all 26 letters
root.mainloop()
def connection(self, letter):
ttk.Label(self.frm, text=letter).grid(column=0, row=2)
sb = Steckerbrett()
The issue here is that since 'command' doesn't allow entering a function with arguments, I can't pass the letter to the function, and I can not make a custom method for every single button, because I would need to process first button presses and second button presses differently to make the connections. Is there a way to use a single function in the command but customize it to each button? Alternatively, if there isn't, are there any other solutions I can try? I am willing to try using other graphics packages as well.

Disable mouse press on Tkinter Text Widget

Background
I have created a tk.Text widget within a frame and used grid to place successfully within my tool. I use methods to run read commands and display the results within the tk.Text widget. I only want the tk.Text to display the results nothing else and this works as expected.
resultsbox = tk.Text(centerframe)
resultsbox.config(state='disabled', bd='10', height=38, width=92, bg='black', fg='green', font='fixedsys')
resultsbox.grid(row=0, column=1, sticky="e")
I use the following commands on each method to enable my write results and then disable the user from typing into the tk.Text widget:
Example
resultsbox.config(state='normal')
resultsbox.insert(tk.INSERT, RESULT1, 'standard', "\n\n")
resultsbox.config(state='disabled')
This again works as expected.
Issue
The problem i have is if a user clicks within the tk.Text widget even though it is disabled the next result that the tool writes to the widget will place it at the point where the user last clicked causing a very unreadable result.
Example
This Screenshot shows if a user has clicked within the widget and then the next result is written.
Summary
Is it possible to stop the user pressing within the tk.Text widget?
You don't have to disable your mouse keys. It is very simple. You just replace tk.INSERT with tk.END. This tells where to insert. Either at the marked position or at the end. Check out my example. I created 2 buttons, one with tk.INSERT and one with tk.END.
import tkinter as tk
def insert_END():
RESULT1 = '---OUTPUT---'
resultsbox.config(state='normal')
resultsbox.insert(tk.END, RESULT1, 'standard', "\n\n")
resultsbox.config(state='disabled')
def insert_INSERT():
RESULT1 = '---OUTPUT---'
resultsbox.config(state='normal')
resultsbox.insert(tk.INSERT, RESULT1, 'standard', "\n\n")
resultsbox.config(state='disabled')
root = tk.Tk()
root.geometry("1000x1000")
frame1 = tk.Frame(root).grid(row=1, column=1)
resultsbox = tk.Text(frame1)
resultsbox.config(state='disabled', bd='10', height=38, width=92, bg='black', fg='green', font='fixedsys')
resultsbox.grid(row=0, rowspan=2, column=1, sticky="e")
btn1 = tk.Button(master=frame1, text='END', command=insert_END)
btn1.grid(row=0, column=0, padx=30)
btn2 = tk.Button(master=frame1, text='INSERT', command=insert_INSERT)
btn2.grid(row=1, column=0, padx=30)
root.mainloop()
The state 'disampled' is an editing property and means that the text cannot be modified. I do not know if that is linked with the cursor position issue you describe (as I get it).
If I understand it correctly the issue is that even when the textbox is disabled and you click in it, the position of the click is remembered and the next time you enable the textbox again it inserts the new text at the last position. If this is the issue, I would try to place the cursor at the end of the actual text (e.g. Text.insert(tk.END,...) before the disable command. When you change the status to 'enable' you can then add the new text to the end of the previous string. Alternatively I would try each time I enable the Text widget to check where the cursor is and move it to the end of the previous string.

Python tkinter - how to send StringVar() content to Scrolled Text box using grid() and using procedural code

Aspects of this question have most certainly been asked before, but not altogether.
[using Python 3.7.1]
Most of it works fine, but the display of scrolling textboxes is just not working... they expand southwards, and the scrollbar sliders never appear.
Never mind that the boxes and labels are too spaced apart.
Whilst it would probably be better to start again, with classes and use pack(), for the sake of understanding better, I would like to see if there's a way to make this work with grid() in a procedural way.
I can't quite get it over the line...
rtmiss = StringVar()
...
# COMMENT: Scrolled Text boxes
scrolledtext1_box=ScrolledText(framemain,width=30,height=10)
scrolledtext1_box.grid(sticky='n',row=5,column=0,rowspan=1,columnspan=1)
ttk.Label(scrolledtext1_box, anchor='w', width=20, text='placeholder', style='TLabel').grid(column=1, row=0, padx=0, pady=0)
tdiff_label = ttk.Entry(scrolledtext1_box, width=20, textvariable=tdiff)
scrolledtext1_box.insert(1.0, chars="anything")
tdiff_label.grid(column=0, row=5, columnspan=1, padx=1, pady=1)
no idea why this is so difficult. It's not the same as just inserting some free text into a textbox and adding a scrollbar.
tdiff_labelframe = ttk.LabelFrame(framemain, text='Title')
tdiff_labelframe.grid(column=0, row=5, padx=8, pady=4)
scrol_w = 30
scrol_h = 3
scrolledtext1_box = scrolledtext.ScrolledText(tdiff_labelframe, width=scrol_w, height=scrol_h, wrap=tk.WORD)
scrolledtext1_box.grid(sticky='nw',row=5,column=0,rowspan=1,columnspan=1)
tdiff_labelframe = ttk.Entry(framemain, width=20, textvariable=tdiff)
tdiff_entry = ttk.Entry(scrolledtext1_box, width=12, textvariable=tdiff)
tdiff_entry.grid(column=0, row=5)
I looked inside the Class Entry(Widget, XView) stuff...
def insert(self, index, string):
"""Insert STRING at INDEX."""
You can insert a string, as most examples will show you, as "my string", and I am able to get the contents of a StringVar into the text box, but only as a contiguous string, with no scrolling.
In the form in the codesnippet above...
tk.Tk()= myframe(root)
my_stringvar = stringvar()
my_labelframe = ttk.LabelFrame(my_frame, text='My Text')
my_labelframe = Entry(my_frame, textvariable = my_stringvar)
The scrolled stuff seems to be on a different "layer". The two blocks of code seem to be unconnected: apart from the scrolledtext function variable referring to the labelframe, which just sticks it onto that layer.
my_scrollheight = 20
my_scrollwidth = 10
my_scrolledtext = scrolledtext.ScrolledText(my_labelframe, width=my_scrollwidth, height=my_scrollheight, wrap=tk.WORD)
my_scrolledtext.grid(row=0,column=0,sticky='news')
So is the StringVar just being inserted into the Frame, above the LabelFrame layer, and thus not being "seen" by the scrolledtext part?
I have a partial answer to this, that I happened upon myself, but it creates a new problem. Before I could print the filepath, but not get the file contents to print and scroll; now it's the other way round.
I started off with this code:
Button errors in Tkinter - Selecting files, adding their paths
...trimmed it down to bare bones and one of each thing; then added my own junk, and eventually I accidentally made it work.
The solution to getting the text into a scrolled text widget, as in the question "how to send StringVar() content to Scrolled Text box using grid() and using procedural code?" is this:
def forkscroll():
mylabelframe = tk.LabelFrame(myframe, text='My Long Text:').pack()
scroll_w = 30
scroll_h = 10
myscrolledtext = scrolledtext.ScrolledText(mylabelframe, width=scroll_w, height=scroll_h, wrap=tk.WORD)
myscrolledtext.vbar.config(width=20)
myscrolledtext.grid(sticky='news', row=6, column=0, rowspan=1, columnspan=1, padx=5, pady=5)
# Open a dialogue; select and load a text file; read the text file directly into the scrolledtext widget (box)
fork = open(filedialog.askopenfilename(), 'r') # it's the same as "infile" above; but it's not a string
myscrolledtext.insert(tk.END, fork.read()) ### This reads the content of "fork" into the scrolledtext widget
...mainly those bottom two lines.
I then go on to put the file path of the selected text file into an Entry widget, but I can't get it to work... the best I could do was to keep the browsefunc and open the file twice... once for the path, and once for the contents... which is obviously a rubbish bodge.
forkit = StringVar()
forkit.set(fork.get())
mynormaltext = Text(mylabelframe, width = 10, height = 1, wrap=tk.WORD)
mynormaltext.grid(sticky='news', row=5, column=0, rowspan=1, columnspan=1, padx=5, pady=5)
mynormaltext.insert(tk.END, forkit.get())
# Even though it does the same thing in one line as it does in three, the outcome is not the same for scrolledtext
# forkname = filedialog.askopenfilename()
# forkfile = open(forkname, 'r')
# fork = forkfile.read()
# above... "fork" is a textIO, but Text can read it...
The best I can do, is get the content of the text file to fill the scrolledtext widget, and to print "<_IO.TextWrapper", as in the attached image. That's a bit better than just printing "PYVAR4" or something.
I have tried converting the "TextIO" or "PathLike" file into a string to inset it; I've tried doing set()/get(), and read() and lambda and tried putting things in different functions, and inputting and returning things, all sorts to try and get the filepath out again, but not really getting anywhere.
At least I've become more familiar with what some things do, and how, but I think I've hit a wall with the total solution, which is:
present 2 labels with 2 buttons each with a field: one as a text widget field; and scrolledtext widget field.
to click a button to browse to a text file, and read it into memory, and print its path into the text widget field
to click the other button, and print the contents of the same file into the scrolledtext field, and have it present within the frame, with a nice long scrollbar.

tkinter - understand pack and grid - simple layout. (always confused...)

I'm trying to configure a text box and below to the text box 3 buttons in a row centered
I don't want to expand them to fill all the area. just be at the center to stay in their original size.
I was trying to do it with pack or grid. but I'm really get confused. I also trying to to put the text box and the buttons on different frame so maybe it will separate the widgets and let me configure it without messing up things (because everything is relative to the other..) ... but I came with nothing that looks good.
I also want to learn how to use the grid in the correct way if I have all kinds of widgets and buttons one below the other without "columnspan" or adjust the text length inside the buttons as well to match the widgets above them...
In this example. How I can center the buttons? I have to use side=tkinter.LEFT in order to put them one after one in a row. but the problem that they also stick to the left...
import tkinter
window = tkinter.Tk()
frame1 = tkinter.Frame(window).pack()
textbox1 = tkinter.Text(frame1, width=70, height=15).pack(side=tkinter.TOP)
button1 = tkinter.Button(frame1, text="button1").pack(side=tkinter.LEFT)
button2 = tkinter.Button(frame1, text="button2").pack(side=tkinter.LEFT)
button3 = tkinter.Button(frame1, text="button3").pack(side=tkinter.LEFT)
window.mainloop()
in this example if I set another frame to do separation between the widgets ...
It's not get to the center either....
import tkinter
window = tkinter.Tk()
frame1 = tkinter.Frame(window).pack(side=tkinter.TOP)
textbox1 = tkinter.Text(frame1, width=70, height=15).pack(side=tkinter.TOP)
frame2 = tkinter.Frame(window).pack(side=tkinter.TOP)
button1 = tkinter.Button(frame2, text="button1").pack(side=tkinter.LEFT)
button2 = tkinter.Button(frame2, text="button2").pack(side=tkinter.LEFT)
button3 = tkinter.Button(frame2, text="button3").pack(side=tkinter.LEFT)
window.mainloop()
And in this example. with grid, if I'm using different frames the button just jump on the text box and messed up everything....
import tkinter
window = tkinter.Tk()
frame0 = tkinter.Frame(window).grid(row=0, column=0)
frame1 = tkinter.Frame(window).grid(row=1, column=0)
textbox = tkinter.Text(frame0, width=70, height=15).grid(row=0, column=0)
button1 = tkinter.Button(frame1, text="button1").grid(row=0, column=0)
button2 = tkinter.Button(frame1, text="button2").grid(row=0, column=1)
button3 = tkinter.Button(frame1, text="button3").grid(row=0, column=2)
window.mainloop()
Can someone explain to me please in which way it's better to use and how to understand it better...?
it's always confusing me...
thanks in advance,
eliran
I was trying to do it with pack or grid. but I'm really get confused.
I also trying to to put the text box and the buttons on different
frame so maybe it will separate the widgets and let me configure it
without messing up things (because everything is relative to the
other..) ... but I came with nothing that looks good.
Your second example is fairly close to working, but it has a fatal flaw. If you add some debugging statements you'll see that frame1 and frame2 are None. Thus, any widgets with those as a parent actually end up in the root window.
This is because foo().bar() always returns the result of .bar(). In tkinter, .grid(...) always returns None, so Frame(...).grid(...) will always return None.
The best practice is to always separate widget creation from widget layout. For example:
frame1 = tkinter.Frame(window)
frame2 = tkinter.Frame(window)
frame1.pack(side="top")
frame2.pack(side="top")
With that, frame1 and frame2 are properly set to the frames. And when that happens, the rest of the code in your second example works as you expect and the buttons are centered.
And in this example. with grid, if I'm using different frames the button just jump on the text box and messed up everything....
That happens for the same reason as mentioned above: you think you're using separate frames, but everything is going in the root window. Because they are all in the root window, and you put the text widget and a button in the same row and column, they overlap.
I also want to learn how to use the grid in the correct way if I have
all kinds of widgets and buttons one below the other without
"columnspan" or adjust the text length inside the buttons as well to
match the widgets above them...
grid is not the right choice in this specific case, since you aren't actually creating a grid. You can use it, but it requires more code than using pack. grid is the right choice if you're creating an actual grid. In this case you aren't.
Using grid in this case requires a little creativity. While it's not the only solution, I would recommend that you divide the bottom frame into five columns - an empty column on the left and right, and three columns in the middle for the buttons. The empty columns can be used to take up all extra space, forcing the middle columns to all be centered.
A best practice for using grid is that every window that uses grid to manage its children needs at least one row and one column with a non-zero weight. That lets tkinter know where to allocate any extra space, such as when the user resizes the window.
Here's a complete solution using grid:
import tkinter
window = tkinter.Tk()
frame0 = tkinter.Frame(window)
frame1 = tkinter.Frame(window)
window.grid_rowconfigure(0, weight=1)
window.grid_columnconfigure(0, weight=1)
frame0.grid(row=0, column=0, sticky="nsew")
frame1.grid(row=1, column=0, sticky="nsew")
frame0.grid_rowconfigure(0, weight=1)
frame0.grid_columnconfigure(0, weight=1)
textbox = tkinter.Text(frame0, width=70, height=15)
textbox.grid(row=0, column=0, sticky="nsew")
button1 = tkinter.Button(frame1, text="button1")
button2 = tkinter.Button(frame1, text="button2")
button3 = tkinter.Button(frame1, text="button3")
frame1.grid_rowconfigure(0, weight=1)
frame1.grid_columnconfigure((0,4), weight=1)
button1.grid(row=0, column=1)
button2.grid(row=0, column=2)
button3.grid(row=0, column=3)
window.mainloop()
Can someone explain to me please in which way it's better to use and how to understand it better...?
In summary, your instinct to use separate frames is the right place to start. You should divide your UI into logical groups, and use separate frames for each group. Then, you are free to pick either grid or pack for each group separately. However, you need to be diligent with grid to make sure that the sticky option is used correctly, and that you've set weights for all of the right columns.
And finally, you have to start with the proper practice of separating widget creation from widget layout.
I have had these kinds of problems before. Even though the .pack() and .grid() systems are excellent, when things are getting hectic you can use the .place() system. .place() allows you to exactly pin-point your tkinter and ttk widgets using x-y axis coordinates.
The coordinates (0,0) are not at the center but at the topmost left corner of your tkinter window.
Eg:
some_widget_name = Button(root, text="Click me!"....)
some_widget_name.place(x=100, y=50)
This will make your widget move right 100 pixels and move down 50 pixels from the topmost left corner.
However, sometimes when you really want to make the location of the widgets precise, you may have to do some trial-and-error to make it visually pleasing.

Tkinter text insert: " 'Nonetype' object has no attribute 'insert'

Relatively new to coding in general, and have been trying to create a simple chat client for Python. Trying to work the GUI, and have encountered the problem shown below.
I have a functioning GUI and use the "example.get()" function to retrieve the string text from an entry box. The program then prints the text to the command prompt (Just to prove it's been retrieved) and then should place it in a text box, however it gives me a "Nonetype" error. Code is below. Does anyone have any idea how to fix this?
Thanks
from tkinter import *
#Create GUI
root=Tk()
root.title("Chat test")
root.geometry("450x450+300+300")
#Declare variables
msg=StringVar()
#Get and post text to chat log
def postaction():
msg1=msg.get()
print(msg1)
chatlog.insert(INSERT,msg1+'\n')
root.mainloop()
#Build GUI components
chatlog=Text(root, height=10, state=DISABLED).pack(side=TOP, fill=X)
entry=Entry(root, textvariable=msg).pack(side=BOTTOM, fill=X)
button=Button(root, command=postaction, text="Button").pack()
The .pack method of a widget always returns None. So, you need to place the calls to .pack on their own line:
chatlog=Text(root, height=10, state=DISABLED)
chatlog.pack(side=TOP, fill=X)
entry=Entry(root, textvariable=msg)
entry.pack(side=BOTTOM, fill=X)
button=Button(root, command=postaction, text="Button")
button.pack()

Categories