How to make group of widgets in canvas responsible - python

First of all, I'm sorry for my English and lack of knowledge about tkinter. I have been trying to fix this this whole week, unsuccessfully.
So I am making my first GUI app and I have chosen tkinter to do this job. Right now I am completing my program's feature that shows all records from database (SQLite) and I am feeding these records into labels. The List is already scrollable and stuff, but my problem is that items inside frame in canvas are not responsive so they kinda underflow my frame.
I have already tried experimenting with weights, but nothing has changed the situation for better. I have also tried to reconfigure the frame to width of canvas, but it hasn't changed so I suppose that the canvas by itself "overflows" the root window. So I have tried giving it fixed width of parent by Configure but that just made thing worse.
class List(tk.Frame):
def __init__(self, parent, controller):
tk.Frame.__init__(self,parent)
createBackground(self)
def populate(object, list): # function to feed text from database to the grid
iterate = 0
for row in list:
count = 0
for element in row:
tk.Label(object, text=element,borderwidth=1, relief="solid", bg="yellow", fg="black", width=20).grid(row=iterate,column=count,sticky="NWES",ipady=8)
count += 1
iterate +=+ 1
def onFrameConfigure(event):
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
self.basic_frame = tk.Frame(self, bg="white")
self.basic_frame.grid(row=1,column=1, padx=30, pady=30, sticky="NWES")
# shanenigens with tkinter so I could scroll down
self.canvas = tk.Canvas(self.basic_frame, background="red", borderwidth=0)
self.frame = tk.Frame(self.canvas, background="blue")
self.scrollbar = tk.Scrollbar(self.basic_frame, orient=tk.VERTICAL, bg="white")
self.scrollbar.config(command=self.canvas.yview)
self.canvas.config(yscrollcommand=self.scrollbar.set, scrollregion=self.canvas.bbox(tk.ALL))
self.canvas.grid(row=1,column=0, columnspan=9, sticky="NWES")
self.scrollbar.grid(row=0,column=10, rowspan=2,sticky="NWES")
self.grid_rowconfigure(1,weight=1)
self.grid_columnconfigure(1,weight=1)
self.basic_frame.grid_rowconfigure(1,weight=1)
self.frame.grid_rowconfigure(1,weight=1)
self.frame.grid_columnconfigure(1,weight=1)
for x in range(9): # setting grid weights
self.basic_frame.grid_columnconfigure(x,weight=1)
self.canvas.grid_columnconfigure(x,weight=1)
label_array = ["Name","Author","Pages","Genre","ISBN","Language","Read","Owned","Added"] # making labels in List
for element in label_array:
label_name = tk.Label(self.basic_frame, text=element, bg="#0AB300",fg="black",borderwidth=1, relief="solid",width=20)
label_name.grid(row=0, column=label_array.index(element),sticky="NWES",ipady=8)
self.canvas.create_window((0,0), window=self.frame,anchor="nw",tags="feed_list")
self.frame.bind("<Configure>", onFrameConfigure)
books_list = select_all() # getting every element from database
populate(self.frame, books_list)
Here is a link for screenshot:
https://ctrlv.cz/qX7N

Related

Python tkinter window resizes, but widgets never change or move

Python beginner. I placed a scrollbar widget in window and that works, but no matter what I do I can't get the scrollbox widget to change size. Could go with a larger scrollbox or for it to resize when the window resizes, but can't figure out how to force either to happen. Tried lots of different solutions, but feels like the grid and canvas are defaulting to a size and can't figure out how to change that. Help would be appreciated. Code is below:
import tkinter as tk
from tkinter import ttk
import os
import subprocess
class Scrollable(tk.Frame):
"""
Make a frame scrollable with scrollbar on the right.
After adding or removing widgets to the scrollable frame,
call the update() method to refresh the scrollable area.
"""
def __init__(self, frame, width=16):
scrollbar = tk.Scrollbar(frame, width=width)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, expand=True)
self.canvas = tk.Canvas(frame, yscrollcommand=scrollbar.set)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.config(command=self.canvas.yview)
self.canvas.bind('<Configure>', self.__fill_canvas)
# base class initialization
tk.Frame.__init__(self, frame)
# assign this obj (the inner frame) to the windows item of the canvas
self.windows_item = self.canvas.create_window(0,0, window=self, anchor=tk.NW)
def __fill_canvas(self, event):
"Enlarge the windows item to the canvas width"
canvas_width = event.width
self.canvas.itemconfig(self.windows_item, width = canvas_width)
def update(self):
"Update the canvas and the scrollregion"
self.update_idletasks()
self.canvas.config(scrollregion=self.canvas.bbox(self.windows_item))
root = tk.Tk()
root.title("application")
root.geometry('750x800')
dbEnvs = ['a','b']
x = 1
header = ttk.Frame(root)
body = ttk.Frame(root)
footer = ttk.Frame(root)
header.pack(side = "top")
body.pack()
footer.pack(side = "top")
#setup Environment selection
envLabel = tk.Label(header, text="Environment:")
envLabel.grid(row=0,column=0,sticky='nw')
dbselection = tk.StringVar()
scrollable_body = Scrollable(body, width=20)
x = 1
for row in range(50):
checkboxVar = tk.IntVar()
checkbox = ttk.Checkbutton(scrollable_body, text=row, variable=checkboxVar)
checkbox.var = checkboxVar # SAVE VARIABLE
checkbox.grid(row=x, column=1, sticky='w')
x += 1
scrollable_body.update()
#setup buttons on the bottom
pullBtn = tk.Button(footer, text='Pull')
pullBtn.grid(row=x, column=2, sticky='ew')
buildBtn = tk.Button(footer, text='Build')
buildBtn.grid(row=x, column=3, sticky='ew')
compBtn = tk.Button(footer, text='Compare')
compBtn.grid(row=x, column=4, sticky='ew')
root.mainloop()
have tried anchoring, changing the window base size and multiple other things (8 or 19 different items, plus reading lots of posts), but they normally involve packing and since I used grids that normally and ends with more frustration and nothing changed.
If you want the whole scrollbox to expand to fill the body frame, you must instruct pack to do that using the expand and fill options:
body.pack(side="top", fill="both", expand=True)
Another problem is that you're setting expand to True for the scrollbar. That's probably not something you want to do since it means the skinny scrollbar will be allocated more space than is needed. So, remove that attribute or set it to False.
scrollbar.pack(side=tk.RIGHT, fill=tk.Y, expand=False)
tip: when debugging layout problems, the problems are easier to visualize when you temporarily give each widget a unique color. For example, set the canvas to one color, body to another, the instance of Scrollable to another, etc. This will let you see which parts are visible, which are growing or shrinking, which are inside the borders of others, etc.

Python - Passing a frame containing tkinter widgets into a class

I'm tring to build a scrollable GUI window containing ttk.Entry and ttk.Label.
The only way doing so (as noted in many questions here )- is by creating a Canvas, with a frame that contains all that widgets.
So- my goal is to make such class- that gets as a parameter a frame containing all needed widgets, and display it in a window with horizontal and vertical scroll bars ( since I need it in many displays inside my code ).
After coding successfully - I tried to make a class, but it shows only empty green canvas.
Any ideas what am I doing wrong?
import tkinter as tk
from tkinter import ttk
class CanvasWidgets(ttk.Frame):
def __init__(self, master, frame_in, width=100, height=100):
ttk.Frame.__init__(self, master)
self.master = master
self.frame = frame_in
self.width, self.height = width, height
self.build_gui()
def build_gui(self):
self.canvas = tk.Canvas(self.master, self.width, self.height, bg='light green')
# self.frame = ttk.Frame(self.frame_in)
self.frame.bind("<Configure>", self.onFrameConfigure)
self.vsb = tk.Scrollbar(self.frame, orient="vertical", command=self.canvas.yview)
self.hsb = tk.Scrollbar(self.frame, orient="horizontal", command=self.canvas.xview)
self.canvas.configure(yscrollcommand=self.vsb.set, xscrollcommand=self.hsb.set)
self.vsb.grid(row=0, column=1, sticky=tk.N + tk.S + tk.W)
self.hsb.grid(row=1, column=0, sticky=tk.W + tk.N + tk.E)
self.canvas.create_window((4, 4), window=self.frame, anchor="nw")
self.canvas.grid(row=0, column=0)
def onFrameConfigure(self, event):
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
root = tk.Tk()
frame = ttk.Frame(root)
rows, cols = 5, 5
for row in range(rows):
for col in range(cols):
ttk.Label(frame, text=[row, col], relief=tk.SUNKEN, width=5).grid(row=row, column=col, sticky=tk.E)
a = CanvasWidgets(root, frame)
a.grid()
root.mainloop()
The first problem is that you are placing the canvas in master when it needs to be in self. Think of the instance of CanvasWindow as a box in which you are going to put everything else.
The second problem is that, because the frame was created before the canvas, the frame has a lower stacking order than the canvas. You need to call lift on the frame to get it to be above the canvas.
The third problem is that you're putting the scrollbars in frame. You can't put them in the inner frame because they control the inner frame. Instead, they also need to be in self. Both the scrollbar and the canvas need to share a common parent.
The fourth problem is that the frame isn't a child of the canvas, so it won't be clipped by the borders of the canvas. It would be better if the CanvasWidgets created the frame, and then the caller can get the frame and add widgets to it.
For example:
a = CanvasWidgets(root)
rows, cols = 5, 5
for row in range(rows):
for col in range(cols):
label = ttk.Label(a.frame, text=[row, col], relief=tk.SUNKEN, width=5)
label.grid(row=row, column=col, sticky=tk.E)

Change font size without messing with Tkinter button size

I am having trouble changing the font size of a button in Tkinter, when I attempt to do it the button also expands/contracts based on the size of the text. Is there a way I can alter the text size with the button's size anchored in place?
I ran across this while designing a tic-tac-toe application, however to save you the trouble, here is a very minimal example of the problem in practice:
import Tkinter as tk
MyWindow = tk.Tk()
MyWindow.geometry("500x550")
button = tk.Button(MyWindow,text="Hello!",width=17,height=10,font=('Helvetica', '20'))
button.grid(row=1, column=1)
MyWindow.mainloop()
The most important part here is font=('Helvetica', '15') or more specifically, the number 15. If you change that number and run this again, not only will the text be bigger/smaller, but so will the button! How do I get around this?
It's probably a really simple problem. I've just gotten started with Tkinter. Thanks in advance for any help I receive!
Typically, when you give a button a width, that width is measured in characters (ie: width=1 means the width of one average sized character). However, if the button has an image then the width specifies a size in pixels.
A button can contain both an image and text, so one strategy is to put a 1x1 pixel as an image so that you can specify the button size in pixels. When you do that and you change the font size, the button will not grow since it was given an absolute size.
Here is an example that illustrates the technique. Run the code, then click on "bigger" or "smaller" to see that the text changes size but the button does not.
import Tkinter as tk
import tkFont
def bigger():
size = font.cget("size")
font.configure(size=size+2)
def smaller():
size = font.cget("size")
size = max(2, size-2)
font.configure(size=size)
root = tk.Tk()
font = tkFont.Font(family="Helvetica", size=12)
toolbar = tk.Frame(root)
container = tk.Frame(root)
toolbar.pack(side="top", fill="x")
container.pack(side="top", fill="both", expand=True)
bigger = tk.Button(toolbar, text="Bigger", command=bigger)
smaller = tk.Button(toolbar, text="Smaller", command=smaller)
bigger.pack(side="left")
smaller.pack(side="left")
pixel = tk.PhotoImage(width=1, height=1)
for row in range(3):
container.grid_rowconfigure(row, weight=1)
for column in range(3):
container.grid_columnconfigure(column, weight=1)
button = tk.Button(container, font=font, text="x",
image=pixel, compound="center", width=20, height=20)
button.grid(row=row, column=column)
root.mainloop()
All of that being said, there is almost never a time when this is a good idea. If the user wants a larger font, the whole UI should adapt. Tkinter is really good at making that happen, to the point where it all mostly works by default.
The width of the button is defined in units of character width. In your case the button is defined to be 17 characters wide. So changing the character width by (ie changing the font size) changes the width of the button. AFAIK, the only way around that is to put the button into a Frame, because a Frame can define it's size in pixels. Here's a new kind of Button that does exactly that:
import Tkinter as tk
class Warspyking(tk.Frame):
'''A button that has it's width and height set in pixels'''
def __init__(self, master=None, **kwargs):
tk.Frame.__init__(self, master)
self.rowconfigure(0, minsize=kwargs.pop('height', None))
self.columnconfigure(0, minsize=kwargs.pop('width', None))
self.btn = tk.Button(self, **kwargs)
self.btn.grid(row=0, column=0, sticky="nsew")
self.config = self.btn.config
#example usage:
MyWindow = tk.Tk()
MyWindow.geometry("500x550")
from itertools import cycle
fonts = cycle((('Helvetica', '11'),('Helvetica', '15'),('Helvetica', '20')))
def chg():
button.config(font=next(fonts))
button = Warspyking(MyWindow,text="Click me!",width=200,height=100 ,font=next(fonts), command=chg)
button.grid(row=1, column=1)
MyWindow.mainloop()
EDIT: Based on what I learned from Bryan Oakley, here's a much neater implementation:
class Warspyking(tk.Button):
def __init__(self, master=None, **kwargs):
self.img = tk.PhotoImage()
tk.Button.__init__(self, master, image=self.img, compound='center', **kwargs)
I should also add that I very much agree with Bryan: Using this is probably a sign that you are doing something wrong. You should let tkinter handle sizing.
I found solution of this problem. I was trying to solve a similar problem: I want to put image on label. I set the image size equal to label size. When I have been trying to put it with command label.config(image=img) the label size grow. The image have the size I set to it, so it didn't cover label completely. I was using grid manager. All size were not entered in advanced but calculated by Tkinter. I was using grid_columnconfigure and grid_rowconfigure. The solution I found is to put this label with image (or button in Your case) to LabelFrame and set grid_propagate to False.
Code example:
MyWindow = tk.Tk()
MyWindow.geometry("500x550")
#create LabelFrame (200x200)
label = tk.LabelFrame(MyWindow, width=200, height=200)
#grid manager to set label localization
labelk.grid(row=0, column=0)
#label row and column configure: first argument is col or row id
label.grid_rowconfigure(0, weight=1)
label.grid_columnconfigure(0, weight=1)
#cancel propagation
label.grid_propagate(False)
#Create button and set it localization. You can change it font without changing size of button, but if You set too big not whole will be visible
button = t.Button(label, text="Hello!", font=('Helvetica', '20'))
#Use sticky to button took up the whole label area
button.grid(row=0, column=0, sticky='nesw')
MyWindow.mainloop()
Result for font size 40 and 20:
Example for creating button with dynamic size by grid manager:
MyWindow = tk.Tk()
MyWindow.geometry("500x550")
#Divide frame on 3x3 regions
for col in range(3):
MyWindow.grid_columnconfigure(col, weight=1)
for row in range(3):
MyWindow.grid_rowconfigure(row, weight=1)
label = tk.LabelFrame(MyWindow)
#Put label in the middle
label.grid(row=1, column=1, sticky='nesw')
label.grid_propagate(False)
label.grid_rowconfigure(0, weight=1)
label.grid_columnconfigure(0, weight=1)
button = tk.Button(label, text="Hello!", font=('Helvetica', '30'))
button.grid(row=0, column=0, sticky='nesw')
MyWindow.mainloop()
It is late answer, but maybe it will help someone.

Resize entry box in tkinter

The following MWE is for a window with horizontal and vertical scrollbars. The window contains an entry box in which the current working directory is displayed. However, the text in the entry box cannot all be seen as the box is too small. I would like to be able to display more of this text as the user enlarges the window. How can I adapt the following example so that the Entry box (defined in UserFileInput) resizes with the window? I have tried using window.grid_columnconfigure (see below), however this doesn't have any effect. It seems to be a problem with using the canvas, as previously I was able to get the Entry boxes to resize, however I need the canvas in order to place the horizontal and vertical scrollbars on the window.
window.grid(row=0, column=0, sticky='ew')
window.grid_columnconfigure(0, weight=1)
(and also with column = 1) but this doesn't have an effect.
import Tkinter as tk
import tkFileDialog
import os
class AutoScrollbar(tk.Scrollbar):
def set(self, lo, hi):
if float(lo) <= 0.0 and float(hi) >= 1.0:
# grid_remove is currently missing from Tkinter!
self.tk.call("grid", "remove", self)
else:
self.grid()
tk.Scrollbar.set(self, lo, hi)
class Window(tk.Frame):
def UserFileInput(self,status,name):
row = self.row
optionLabel = tk.Label(self)
optionLabel.grid(row=row, column=0, sticky='w')
optionLabel["text"] = name
text = status#str(dirname) if dirname else status
var = tk.StringVar(root)
var.set(text)
w = tk.Entry(self, textvariable= var)
w.grid(row=row, column=1, sticky='ew')
w.grid_columnconfigure(1,weight=1)
self.row += 1
return w, var
def __init__(self,parent):
tk.Frame.__init__(self,parent)
self.row = 0
currentDirectory = os.getcwd()
directory,var = self.UserFileInput(currentDirectory, "Directory")
if __name__ == "__main__":
root = tk.Tk()
root.grid_columnconfigure(0, weight=1)
root.grid_rowconfigure(0,weight=1)
vscrollbar = AutoScrollbar(root,orient=tk.VERTICAL)
vscrollbar.grid(row=0, column=1, sticky='ns')
hscrollbar = AutoScrollbar(root, orient=tk.HORIZONTAL)
hscrollbar.grid(row=1, column=0, sticky='ew')
canvas=tk.Canvas(root,yscrollcommand=vscrollbar.set,xscrollcommand=hscrollbar.set)
canvas.grid(row=0, column=0, sticky='nsew')
vscrollbar.config(command=canvas.yview)
hscrollbar.config(command=canvas.xview)
window = Window(canvas)
canvas.create_window(0, 0, anchor=tk.NW, window=window)
window.update_idletasks()
canvas.config(scrollregion=canvas.bbox("all"))
root.mainloop()
You have several problems in your code that are getting in your way. The biggest obstacle is that you're putting a frame inside a canvas. That's rarely necessary, and it makes your code more complex than it needs to be? Is there a reason you're using a canvas, and is there a reason you're using classes for part of your code but not for everything?
Regardless, you have two problems:
the frame isn't growing when the window grows, so the contents inside the window can't grow
the label won't grow because you're using grid_columnconfigure incorrectly.
Hints for visualizing the problem
When trying to solve layout problems, a really helpful technique is to temporarily give each containing widget a unique color so you can see where each widget is. For example, coloring the canvas pink and the Window frame blue will make it clear that the Window frame is also not resizing.
Resizing the frame
Because you're choosing to embed your widget in a canvas, you are going to have to manually adjust the width of the frame when the containing canvas changes size. You can do that by setting a binding on the canvas to call a function whenever it resizes. The event you use for this is <Configure>. Note: the configure binding fires for more than just size changes, but you can safely ignore that fact.
The function needs to compute the width of the canvas, and thus the desired width of the frame (minus any padding you want). You'll then need to configure the frame to have that width. To facilitate that, you'll need to either keep a reference to the canvas id of the frame, or give the frame a unique tag.
Here is a function that assumes the frame has the tag "frame":
def on_canvas_resize(event):
padding = 8
width = canvas.winfo_width() - padding
canvas.itemconfigure("frame", width=width)
You'll need to adjust how you create the canvas item to include the tag:
canvas.create_window(..., tags=["frame"])
Finally, set a binding to fire when the widget changes size:
canvas.bind("<Configure>", on_canvas_resize)
Using grid_columnconfigure to get the label to resize
You need to use grid_columnconfigure on the containing widget. You want the columns inside the frame to grow and shrink, not the columns inside the label.
You need to change this line:
w.grid_columnconfigure(...)
to this:
self.grid_columnconfigure(...)

Tkinter Display varLabel and value cleanly

I've am new to Tkinter and have written a program to open a file and parse binary messages.
I am struggling on how best to display the results. My parsing class will have 300+ entries and I want something similar to a table.
var1Label : var1Val
var2Label : var2Val
I have played around with these widgets but have not gotten anything that I can be proud of: Label, Text, Message and probably others.
So I'd like the Labels to be justify Right, and the Var's to be justify left or anything else that would that would be a good idea on how to make this an attractive display, like having all the ':' aligned. The size of the Var's will be between 0-15 characters.
I'm using python 2.7.2 on windows.
Here's the grid method I was trying with dummy variables
self.lbVar1 = Label(self.pnDetails1, text="Var Desc:", justify=RIGHT, bd=1)
self.lbVar1.grid(sticky=N+W)
self.sVar1 = StringVar( value = self.binaryParseClass.Var1 )
self.Var1 = Label(self.pnDetails1, textvariable=self.sVar1)
self.Var1.grid(row=0, column=1, sticky=N+E)
The ttk.Treeview widget lets you create a list of objects with multiple columns. It will probably be the easiest thing for you to use.
Since you specifically asked about a grid of labels, here is a quick and dirty example showing how to create 300 items in a scrollable grid:
import Tkinter as tk
class ExampleApp(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
# create a canvas to act as a scrollable container for
# the widgets
self.container = tk.Canvas(self)
self.vsb = tk.Scrollbar(self, orient="vertical", command=self.container.yview)
self.container.configure(yscrollcommand=self.vsb.set)
self.vsb.pack(side="right", fill="y")
self.container.pack(side="left", fill="both", expand=True)
# the frame will contain the grid of labels and values
self.frame = tk.Frame(self)
self.container.create_window(0,0, anchor="nw", window=self.frame)
self.vars = []
for i in range(1,301):
self.vars.append(tk.StringVar(value="This is the value for item %s" % i))
label = tk.Label(self.frame, text="Item %s:" % i, width=12, anchor="e")
value = tk.Label(self.frame, textvariable=self.vars[-1], anchor="w")
label.grid(row=i, column=0, sticky="e")
value.grid(row=i, column=1, sticky="ew")
# have the second column expand to take any extra width
self.frame.grid_columnconfigure(1, weight=1)
# Let the display draw itself, the configure the scroll region
# so that the scrollbars are the proper height
self.update_idletasks()
self.container.configure(scrollregion=self.container.bbox("all"))
if __name__ == "__main__":
app = ExampleApp()
app.mainloop()

Categories

Resources