Python Tkinter: adding a scrollbar to frame within a canvas - python

I am quite new to Tkinter, but, nevertheless, I was asked to "create" a simple form where user could provide info about the status of their work (this is sort of a side project to my usual work).
Since I need to have quite a big number of text widget (where users are required to provide comments about status of documentation, or open issues and so far), I would like to have something "scrollable" (along the y-axis).
I browsed around looking for solutions and after some trial and error I found something that works quite fine.
Basically I create a canvas, and inside a canvas a have a scrollbar and a frame. Within the frame I have all the widgets that I need.
This is a snipet of the code (with just some of the actual widgets, in particular the text ones):
from Tkinter import *
## My frame for form
class simpleform_ap(Tk):
# constructor
def __init__(self,parent):
Tk.__init__(self,parent)
self.parent = parent
self.initialize()
#
def initialize(self):
#
self.grid_columnconfigure(0,weight=1)
self.grid_rowconfigure(0,weight=1)
#
self.canvas=Canvas(self.parent)
self.canvas.grid(row=0,column=0,sticky='nsew')
#
self.yscrollbar = Scrollbar(self,orient=VERTICAL)
self.yscrollbar.grid(column =4, sticky="ns")
#
self.yscrollbar.config(command=self.canvas.yview)
self.yscrollbar.pack(size=RIGTH,expand=FALSE)
#
self.canvas.config(yscrollcommand=self.yscrollbar.set)
self.canvas.pack(side=LEFT,expand=TRUE,fill=BOTH)
#
self.frame1 = Frame(self.canvas)
self.canvas.create_window(0,0,window=self.frame1,anchor='nw')
# Various Widget
# Block Part
# Label
self.labelVariableIP = StringVar() # Label variable
labelIP=Label(self.frame1,textvariable=self.labelVariableIP,
anchor="w",
fg="Black")
labelIP.grid(column=0,row=0,columnspan=1,sticky='EW')
self.labelVariableIP.set(u"IP: ")
# Entry: Single line of text!!!!
self.entryVariableIP =StringVar() # variable for entry field
self.entryIP =Entry(self.frame1,
textvariable=self.entryVariableIP,bg="White")
self.entryIP.grid(column = 1, row= 0, sticky='EW')
self.entryVariableIP.set(u"IP")
# Update Button or Enter
button1=Button(self.frame1, text=u"Update",
command=self.OnButtonClickIP)
button1.grid(column=2, row=0)
self.entryIP.bind("<Return>", self.OnPressEnterIP)
#...
# Other widget here
#
# Some Text
# Label
self.labelVariableText = StringVar() # Label variable
labelText=Label(self.frame1,textvariable=
self.labelVariableText,
anchor="nw",
fg="Black")
labelText.grid(column=0,row=curr_row,columnspan=1,sticky='EW')
self.labelVariableTexta.set(u"Insert some texts: ")
# Text
textState = TRUE
self.TextVar=StringVar()
self.mytext=Text(self.frame1,state=textState,
height = text_height, width = 10,
fg="black",bg="white")
#
self.mytext.grid(column=1, row=curr_row+4, columnspan=2, sticky='EW')
self.mytext.insert('1.0',"Insert your text")
#
# other text widget here
#
self.update()
self.geometry(self.geometry() )
self.frame1.update_idletasks()
self.canvas.config(scrollregion=(0,0,
self.frame1.winfo_width(),
self.frame1.winfo_height()))
#
def release_block(argv):
# Create Form
form = simpleform_ap(None)
form.title('Release Information')
#
form.mainloop()
#
if __name__ == "__main__":
release_block(sys.argv)
As I mentioned before, this scripts quite does the work, even if, it has a couple of small issue that are not "fundamental" but a little annoying.
When I launch it I got this (sorry for the bad screen-capture):
enter image description here
As it can be seen, it only shows up the first "column" of the grid, while I would like to have all them (in my case they should be 4)
To see all of the fields, I have to resize manually (with the mouse) the window.
What I would like to have is something like this (all 4 columns are there):
enter image description here
Moreover, the scrollbar does not extend all over the form, but it is just on the low, right corner of the windows.
While the latter issue (scrollbar) I can leave with it, the first one is a little more important, since I would like to have the final user to have a "picture" of what they should do without needing to resize the windows.
Does any have any idea on how I should proceed with this?
What am I missing?
Thanks in advance for your help

In the __init__ method of your class, you do not appear to have set the size of your main window. You should do that, or it will just set the window to a default size, which will only show whatever it can, and in your case, only 1 column. Therefore, in the __init__ method, try putting self.geometry(str(your_width) + "x" + str(your_height)) where your_width and your_height are whatever integers you choose that allow you to see what you need to in the window.
As for your scrollbar issue, all I had to do was change the way your scrollbar was added to the canvas to a .pack() and added the attributes fill = 'y' and side = RIGHT to it, like so:
self.yscrollbar.pack(side = 'right', fill = 'y')
Also, you don't need:
self.yscrollbar.config(command=self.canvas.yview)
self.yscrollbar.pack(size=RIGHT,expand=FALSE)
Just add the command option to the creation of the scrollbar, like so:
self.scrollbar = Scrollbar(self,orient=VERTICAL,command=self.canvas.yview)
In all, the following changes should make your code work as expected:
Add:
def __init__(self,parent):
Tk.__init__(self,parent)
self.parent = parent
self.initialize()
# Resize the window from the default size to make your widgets fit. Experiment to see what is best for you.
your_width = # An integer of your choosing
your_height = # An integer of your choosing
self.geometry(str(your_width) + "x" + str(your_height))
Add and Edit:
# Add `command=self.canvas.yview`
self.yscrollbar = Scrollbar(self,orient=VERTICAL,command=self.canvas.yview)
# Use `.pack` instead of `.grid`
self.yscrollbar.pack(side = 'right', fill = 'y')
Remove:
self.yscrollbar.config(command=self.canvas.yview)
self.yscrollbar.pack(size=RIGHT,expand=FALSE)

Related

How can I bring a Canvas to front?

I overlapped 5 Tk.Canvas objects and each will have different images. I want to bring each canvas to front of every other canvases to draw pictures in the most-front canvas.
class window_tk():
def __init__(self,main):
self.main=main
self.canvas_org = tk.Canvas(self.main, bg='white')
self.canvas_layer1 = tk.Canvas(self.main, bg='red')
self.canvas_layer2 = tk.Canvas(self.main, bg='green')
self.canvas_layer3 = tk.Canvas(self.main, bg='blue')
self.canvas_layer4 = tk.Canvas(self.main, bg='black')
self.btn_load = tk.Button(self.main,text = "Load Image",command = self.load_ct)
self.btn_layer1 = tk.Button(self.main,text = "Draw in L1",command = self.bring_1)
self.btn_layer2 = tk.Button(self.main,text = "Draw in L2",command = self.bring_2)
self.btn_layer3 = tk.Button(self.main,text = "Draw in L3",command = self.bring_3)
self.btn_layer4 = tk.Button(self.main,text = "Draw in L4",command = self.bring_4)
def bring_1(self):
self.canvas_layer1.place(x=50,y=00)
def bring_2(self):
self.canvas_layer2.place(x=50, y=00)
def bring_3(self):
self.canvas_layer3.place(x=50, y=00)
def bring_4(self):
self.canvas_layer4.place(x=50, y=00)
I thought the canvas.place() function will bring the canvas front but it was not. Which function can I use ? Or should I unpack all other canvases ?
Since Canvas has override the .tkraise() function, you need to call TCL command directly:
self.canvas.tk.call('raise', self.canvas._w)
Please see the answer given by acw1668. The lift function doesn't work for Canvas objects. His answer is correct.
All tkinter objects, Canvas included, support the following method:
w.lift(aboveThis=None)
If the argument is None, the window containing w is moved to the top of the window stacking order. To move the window just above some Toplevel window w, pass w as an argument.
This gives you full control over which widget sits on top.
https://anzeljg.github.io/rin2/book2/2405/docs/tkinter/universal.html
Now that I re-read that, I see that its language is slightly incorrect. "w" is any tkinter widget, "above_this" is another tkinter widget. The function places "w" above "above_this" in the stacking order.
You can use the following functions -
canvas.tag_raise(canvas_layer4) -> For bringing to front
canvas.tag_lower(canvas_layer4) -> For pushing back

How to link multiple buttons to one text widget in tkinter?

I'm trying to learn tkinter and I wanted to write a simple rock paper scissors game, where there is a window with 3 buttons and one text widget.
I'd like to be able to press any of the buttons and for the message to appear in the text field, then click a different button, the text field to clear and display a new message associated with the second button and so on.
From the tutorials I've watched, I know that I can pass the function housing text widget as an argument in button command parameter.I know I could make 3 functions with a text field, one for each button (displaying one at a time) but that's probably not the correct way. Here's what I have so far:
import tkinter as tk
root = tk.Tk()
root.title("Rock Paper Scissors")
root.geometry("420x200")
def Rock():
rockText = "Paper!"
return rockText
def Paper():
paperText = "Scissors!"
return paperText
def Scissors():
scissorsText = "Rock!"
return scissorsText
def display():
textDisplay = tk.Text(master = root, height = 10, width = 50)
textDisplay.grid(row = 1, columnspan = 5)
textDisplay.insert(tk.END, Rock())
buttonRock = tk.Button(text = "Rock", command = display).grid(row = 0, column = 1, padx = 10)
buttonPaper = tk.Button(text = "Paper").grid(row = 0, column = 2, padx = 10)
buttonScissors = tk.Button(text = "Scissors").grid(row = 0, column = 3, padx = 10)
root.mainloop()
Any help will be appreciated.
Edit: Second thought - I can imagine I'm complicating this for myself by trying to force the game to work this way. With the random module I'd be able to get away with one function for the computer choice with a list and saving the random pick in a parameter, then returning the value into the display function.
So if I got this right you just want to make a button click change the text in the Text-widget. For that you have two easy and quite similar options. First would be to define 3 functions, as you did, and let them change the text directly. The second option would be to make one function which changes the text according to whats given. Note that in the second case we will have to use lambda which works quite well in smaller projects but decreases the efficiency of your programs when they get bigger.
First option:
import tkinter as tk
class App:
def __init__(self):
root=tk.Tk()
root.title("Rock Paper Scissors")
root.geometry("420x200")
self.text=Text(root)
self.text.grid(row=1,columnspan=5)
tk.Button(root,text="Rock",command=self.Rock).grid(row=0,column=1,padx=10)
tk.Button(root,text="Paper",command=self.Paper).grid(row=0,column=2)
tk.Button(root,text="Scissors",command=self.Scissors).grid(row=0,column=3,padx=10)
root.mainloop()
def Rock(self):
text="Paper!"
self.text.delete(0,END) #delete everything from the Text
self.text.insert(0,text) #put the text in
def Paper(self):
text="Scissors!"
self.text.delete(0,END) #delete everything from the Text
self.text.insert(0,text) #put the text in
def Scissors(self):
text="Rock!"
self.text.delete(0,END) #delete everything from the Text
self.text.insert(0,text) #put the text in
if __name__=='__main__':
App()
Second option:
import tkinter as tk
class App:
def __init__(self):
root=tk.Tk()
root.title("Rock Paper Scissors")
root.geometry("420x200")
self.text=Text(root)
self.text.grid(row=1,columnspan=5)
tk.Button(root,text="Rock",command=lambda: self.updateText('Paper!')).grid(row=0,column=1,padx=10)
tk.Button(root,text="Paper",command=lambda: self.updateText('Scissors!')).grid(row=0,column=2)
tk.Button(root,text="Scissors",command=lambda: self.updateText('Rock!')).grid(row=0,column=3,padx=10)
root.mainloop()
def updateText(self,text):
self.text.delete(0,END) #delete everything from the Text
self.text.insert(0,text) #put the text in
if __name__=='__main__':
App()
Some little side notes from me here:
If you use grid, pack or place right on the widget itself you wont assign the widget to a variable but the return of the grid, pack or place function which is None. So rather first assign the widget to an variable and then use a geometry manager on it like I did for the Text-widget.
You don't have to extra set the title with the title function afterwards. You can set it with the className-argument in Tk.
If you're working with tkinter its fine to do it functionally but rather use a class to build up GUIs.
When creating new widgets always be sure to pass them the variable for the root window first. They will get it themselves too if you don't do that but that needs more unnecessary background activity and if you have more than one Tk-window open it will automatically chooses one which may not be the one you want it to take.
And one small tip in the end: If you want to learn more about all the tkinter widgets try http://effbot.org/tkinterbook/tkinter-index.htm#class-reference.
I hope its helpfull. Have fun programming!
EDIT:
I just saw your edit with the random module. In this case I would recommend the second option. Just remove the text-argument from updateText and replace lambda: self.updateText(...) with self.updateText(). In updateText itself you add that random of list thing you mentioned. :D

Tkinter: Grid containing frame containing grid

This is slightly exposing the entire program I'm building but it's just the front end anyways. If you guys want to make the app yourself, go ahead. :P
So, as the title says, I'm trying to make a grid that contains a frame that contains a grid, and I have looked at a few similar questions and don't completely get the answers, likely because I'm new to guis and tkinter and the solutions are often suited to their specific program. So here's a similar problem suited to mine.
What I'm trying to do is build something with the overall frame structure that looks like this: GUI Frame structure, where each frame besides the ioFrame is a 2x2 grid. The ioFrame contains two things: an input row and an output row. In short, this is a calculator that computes logic inputs and your only "numbers" are true and false.
Here's my code atm:
from tkinter import *;
class calculator:
#def update(self,
def __init__(self, window):
"""
Constructor method.
"""
self.toCompute = []; self.In = StringVar(); self.Out = StringVar();
# Window title
window.title("Logic Calculator");
# The set of 5 frames.
ioFrame = Frame(window, relief=GROOVE, borderwidth=3); ioFrame.grid(row=0); ioFrame.pack();
nwFrame = Frame(window); nwFrame.grid(row=1,column=0); nwFrame.pack();
neFrame = Frame(window); neFrame.grid(row=1,column=1); neFrame.pack();
swFrame = Frame(window); swFrame.grid(row=2,column=0); swFrame.pack();
seFrame = Frame(window); seFrame.grid(row=2,column=1); seFrame.pack();
# Top 2 rows: the IO portion
Label(ioFrame, textvariable=self.In, relief=SUNKEN).grid(row=0, sticky=W);
Label(ioFrame, textvariable=self.Out).grid(row=1, sticky=E);
# Top left 2x2 Frame: [ ( | ) ][ T | F ]
brlButton = Button(nwFrame, text='(', height=2, width=10).grid(row=0,column=0);
brrButton = Button(nwFrame, text=')', height=2, width=10).grid(row=0,column=1);
truButton = Button(nwFrame, text='T', height=2, width=10).grid(row=1,column=0);
falButton = Button(nwFrame, text='F', height=2, width=10).grid(row=1,column=1);
# Top right 2x2 Frame: [ AND | OOR ][ NND | NOR ]
andButton = Button(neFrame, text='and', height=2, width=10).grid(row=0,column=0);
oorButton = Button(neFrame, text='oor', height=2, width=10).grid(row=0,column=1);
nndButton = Button(neFrame, text='nnd', height=2, width=10).grid(row=1,column=0);
norButton = Button(neFrame, text='nor', height=2, width=10).grid(row=1,column=1);
# Bottom left 2x2 Frame: [ SSO | IIF ][ NSO | NIF ]
andButton = Button(swFrame, text='sso', height=2, width=10).grid(row=0,column=0);
oorButton = Button(swFrame, text='iif', height=2, width=10).grid(row=0,column=1);
nndButton = Button(swFrame, text='nso', height=2, width=10).grid(row=1,column=0);
norButton = Button(swFrame, text='nif', height=2, width=10).grid(row=1,column=1);
# Bottom right 2x2 Frame:[ EEQ | NEG ][ NEQ | === ]
eeqButton = Button(seFrame, text='eeq', height=2, width=10).grid(row=0,column=0);
negButton = Button(seFrame, text='neg', height=2, width=10).grid(row=0,column=1);
neqButton = Button(seFrame, text='neq', height=2, width=10).grid(row=1,column=0);
comButton = Button(seFrame, text='=', height=2, width=10).grid(row=1,column=1);
if __name__ == "__main__": # Only runs program if this specfic file is opened.
window = Tk(); # The window
calculator(window);
window.mainloop();
I still have to add the commands to these (which I've written in a separate file already), but I was just trying to get the gui done separately for now.
My problem, is that my code outputs this result instead: Current GUI. This is clearly way off from what I was going for. And being new to this, I probably have a list of problems.
Any help would be appreciated.
I'm going to assume here that you came to python from another language because of all the semicolons. In python, YOU DO NOT NEED SEMICOLONS. I understand at the top if you really want to keep those variables all on one line, but each line in your script should not end in a semicolon.
Secondly, if you print out the value of any button you you have put in a variable, you will see that it is None instead of a tkinter.Button object. This is because of that .grid(...) call at the end of the line (grid returns None). Typically, I only put widgets into a variable if I intend on using it later in the app. If not, I do what you did with the labels, just called them without some_variable_name = in front of it. If you ever try to do anything with those button variables later, it will not work because they are all None.
As mentioned in the comments, I was unable to get your provided code to run because you are using .grid(...) and .pack(...) to place the Frames on the window. You only need one. That was actually what caused the errors for me. Think of the window or Frame as a container; if you use grid to put something in that container, then you cannot use pack to put something else in the container. The different methods of placing widgets conflict with each other. I believe the output you got was because it would place the Frames using grid, then re-place them using pack which is why they are vertically aligned.
So taking all of this into consideration, this is what I have:
########## Frames ##########
# this is what the frames should look like in your code
frame = Frame(window, ...)
frame.grid(row=, column=)
########## Buttons ##########
# if you want to keep the buttons in variables
btn = Button(parent, text='', width=, height=)
btn.grid(row=, column=)
# or
# if the buttons don't have to be in variables, like your Labels
Button(parent, text='', width=, height=).grid(row=, column=)
There is also a typo with your button variables. In the Bottom left 2x2 Frame section, the button variables names should be changed to match the section title (if you are keeping the variables names):
ssoButton = ...
iifButton = ...
nsoButton = ...
nifButton = ...
The button variables are just references to the Button objects, so reusing a name doesn't matter, unless you want to use the buttons later in your code. In that case, it will cause issues, like I said before.
I updated part of the code so you can see what it should look like:
EDIT: I almost forgot about the ioFrame. It should be allowed to expand to fill the space provided at the top, but it's not. To fix this:
ioFrame = Frame(window, relief=GROOVE, borderwidth=3, bg='green')
ioFrame.grid(row=0, columnspan=2, sticky=N+E+S+W)
I set the background to green just to make sure it filled the space, and set the StringVars to some dummy text for testing (done like this: self.In.set('text here') to change the text). After all changes were made, this is my output:

Create a simple GUI for a minimalistic python script

I wrote a small python function, which takes several numerical input parameters and prints many lines with statements, which going to be used in an experiment, like this toy example:
def myADD(x,y,z):
res = x + y + z
print("the result is: {0}+{1}+{2}={3}").format(x,y,z,res)
I would like to create a minimalistic GUI, simply an overlay which calls my myADD.py script, where I can fill those parameters x,y,z and after clicking a "compute" button a text field occurs with the print statement.
Does anyone has a template, I was looking into the TKinter, but my attempts by manipulating other templates didn't succeed.
Would appreciate help, thanks.
Tkinter is a fantastic choice since it is built-in. It is ideally suited for this type of quick, minimalistic GUI.
Here's a basic framework for a Tkinter app to show you how simple it can be. All you need to do is add your function, either by importing it or including it in the same file:
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.parent = parent
self.entry = {}
# the basic layout is a form on the top, and
# a submit button on the bottom
form = tk.Frame(self)
submit = tk.Button(self, text="Compute", command=self.submit)
form.pack(side="top", fill="both", expand=True)
submit.pack(side="bottom")
# this fills in the form with input widgets for each parameter
for row, item in enumerate(("x", "y", "z")):
label = tk.Label(form, text="%s:"%item, anchor="w")
entry = tk.Entry(form)
label.grid(row=row, column=0, sticky="ew")
entry.grid(row=row, column=1, sticky="ew")
self.entry[item] = entry
# this makes sure the column with the entry widgets
# gets all the extra space when the window is resized
form.grid_columnconfigure(1, weight=1)
def submit(self):
'''Get the values out of the widgets and call the function'''
x = self.entry["x"].get()
y = self.entry["y"].get()
z = self.entry["z"].get()
print "x:", x, "y:", y, "z:", z
if __name__ == "__main__":
# create a root window
root = tk.Tk()
# add our example to the root window
example = Example(root)
example.pack(fill="both", expand=True)
# start the event loop
root.mainloop()
If you want the result to appear in the window, you can create another instance of a Label widget, and change it's value when you perform the computation by doing something like self.results_label.configure(text="the result")
Tkinter is usually a good start because it is bundled with Python (tutorial).
That said, Tk is pretty old and therefore "odd" at times. If you want a more modern UI, have a look at PyQt. It's based on Qt but it doesn't come with Python by default, so you have to install it manually.

Labels appearing over Entry's in Tkinter

When I run the following code the created labels appear over top of the Entry boxes as if they are not being added to the same grid.
class Application(Frame):
def __init__(self,master):
super(Application,self).__init__(master)
self.grid()
self.new_intervals()
def new_intervals(self):
self.int_label = Label(text="Interval Name")
self.int_label.grid(row=0, column=0,sticky=W)
self.int_time_label = Label(text="Time (HH:MM:SS)")
self.int_time_label.grid(row=0, column=1,sticky=W)
self.box1 = Entry(self)
self.box1.grid(row=1,column=0,sticky=W)
self.box2 = Entry(self)
self.box2.grid(row=1,column=1,sticky=W)
self.box3 = Entry(self)
self.box3.grid(row=2,column=0,sticky=W)
self.box4 = Entry(self)
self.box4.grid(row=2,column=1,sticky=W)
root = Tk()
root.title("Interval Timer")
root.geometry("400x500")
app=Application(root)
root.mainloop()
I know that i can add these boxes in a loop, however, I can't get it to work without the loop at the moment
The application frame is in row 0, column 0 of the main window. That is the default when you don't specify anything. Also as a default, they appear in the middle
This frame has four entry widgets spread across two rows, making the frame grow to fit around those entry widgets
The "Interval Name" label is also being placed in row 0, column 0 of the main window, because that's what you explicitly tell it to do, and because its parent is the main window.
The "Time" label is also in row 0 of the main window because, again, it's parent is the main window
both of these labels are appearing in the vertical center of the row because that is the default behavior which you haven't overridden, which is why they appear on top of the entry widgets.
So, because the labels and the application frame are in the same row of the main window, and because the labels default to being in the vertical center, they appear to be in the middle of the entry widgets.
I assume you intended for the labels to be children of the frame, so you need to specify "self" as the first parameter when creating them:
self.int_label = Label(self, text="Interval Name")
...
self.int_time_label = Label(self, text="Time (HH:MM:SS)")
I also recommend grouping all of your grid statements for a particular master window together, so it's easier to see the organization of your widgets. In my experience this makes the code easier to read and easier to maintain.
For example:
self.int_label = Label(...)
self.int_time_label = Label(...)
self.box1 = Entry(...)
...
self.int_label.grid(...)
self.int_time_label.grid(...)
self.box1.grid(...)
...

Categories