tkinter Canvas window size - python-3.x

I need to know when the Canvas is resized (eg when the master frame gets maximized) the new Canvas window size.
Unfortunately, if I try self.canvas['width'] it always seems to me I get back the width it had whenever I initialized it and not the current width.
How do I get the current Canvas window dimensions?

When you retrieve self.canvas['width'], you are asking tkinter to give you the configured width of the widget, not the actual width. For the actual width you can use .winfo_width().
If you want to know when the canvas is resized, you can add a binding to the <Configure> event on the widget. The event object that is passed to the binding has a width attribute which also has the actual width of the widget.
Here's an example:
import tkinter as tk
root = tk.Tk()
canvas = tk.Canvas(root, width=200, height=200, background="bisque")
canvas.pack(side="bottom", fill="both", expand=True)
canvas.create_text(10, 30, anchor="sw", tags=["event"])
canvas.create_text(10, 30, anchor="nw", tags=["cget"])
def show_width(event):
canvas.itemconfigure("event", text="event.width: %s" % event.width)
canvas.itemconfigure("cget", text="winfo_width: %s" % event.widget.winfo_width())
canvas.bind("<Configure>", show_width)
root.mainloop()

one possible solution:
try:
import Tkinter as tk
except:
import tkinter as tk
class myCanvas(tk.Frame):
def __init__(self, root):
#self.root = root
self.w = 600
self.h = 400
self.canvas = tk.Canvas(root, width=self.w, height=self.h)
self.canvas.pack( fill=tk.BOTH, expand=tk.YES)
root.bind('<Configure>', self.resize)
def resize(self, event):
self.w = event.width
self.h = event.height
print ('width = {}, height = {}'.format(self.w, self.h))
root = tk.Tk()
root.title('myCanvas')
myCanvas(root)
root.mainloop()
Notice that the size informed by the event is 2 pixels wider in either direction. That's the border, I suppose.

Related

Adding a Scrollbar to a Tkinter Graph of data which goes off the bottom for the screen [duplicate]

I am using Python to parse entries from a log file, and display the entry contents using Tkinter and so far it's been excellent. The output is a grid of label widgets, but sometimes there are more rows than can be displayed on the screen. I'd like to add a scrollbar, which looks like it should be very easy, but I can't figure it out.
The documentation implies that only the List, Textbox, Canvas and Entry widgets support the scrollbar interface. None of these appear to be suitable for displaying a grid of widgets. It's possible to put arbitrary widgets in a Canvas widget, but you appear to have to use absolute co-ordinates, so I wouldn't be able to use the grid layout manager?
I've tried putting the widget grid into a Frame, but that doesn't seem to support the scrollbar interface, so this doesn't work:
mainframe = Frame(root, yscrollcommand=scrollbar.set)
Can anyone suggest a way round this limitation? I'd hate to have to rewrite in PyQt and increase my executable image size by so much, just to add a scrollbar!
Overview
You can only associate scrollbars with a few widgets, and the root widget and Frame aren't part of that group of widgets.
There are at least a couple of ways to do this. If you need a simple vertical or horizontal group of widgets, you can use a text widget and the window_create method to add widgets. This method is simple, but doesn't allow for a complex layout of the widgets.
A more common general-purpose solution is to create a canvas widget and associate the scrollbars with that widget. Then, into that canvas embed the frame that contains your label widgets. Determine the width/height of the frame and feed that into the canvas scrollregion option so that the scrollregion exactly matches the size of the frame.
Why put the widgets in a frame rather than directly in the canvas? A scrollbar attached to a canvas can only scroll items created with one of the create_ methods. You cannot scroll items added to a canvas with pack, place, or grid. By using a frame, you can use those methods inside the frame, and then call create_window once for the frame.
Drawing the text items directly on the canvas isn't very hard, so you might want to reconsider that approach if the frame-embedded-in-a-canvas solution seems too complex. Since you're creating a grid, the coordinates of each text item is going to be very easy to compute, especially if each row is the same height (which it probably is if you're using a single font).
For drawing directly on the canvas, just figure out the line height of the font you're using (and there are commands for that). Then, each y coordinate is row*(lineheight+spacing). The x coordinate will be a fixed number based on the widest item in each column. If you give everything a tag for the column it is in, you can adjust the x coordinate and width of all items in a column with a single command.
Object-oriented solution
Here's an example of the frame-embedded-in-canvas solution, using an object-oriented approach:
import tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.canvas = tk.Canvas(self, borderwidth=0, background="#ffffff")
self.frame = tk.Frame(self.canvas, background="#ffffff")
self.vsb = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.vsb.set)
self.vsb.pack(side="right", fill="y")
self.canvas.pack(side="left", fill="both", expand=True)
self.canvas.create_window((4,4), window=self.frame, anchor="nw",
tags="self.frame")
self.frame.bind("<Configure>", self.onFrameConfigure)
self.populate()
def populate(self):
'''Put in some fake data'''
for row in range(100):
tk.Label(self.frame, text="%s" % row, width=3, borderwidth="1",
relief="solid").grid(row=row, column=0)
t="this is the second column for row %s" %row
tk.Label(self.frame, text=t).grid(row=row, column=1)
def onFrameConfigure(self, event):
'''Reset the scroll region to encompass the inner frame'''
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
if __name__ == "__main__":
root=tk.Tk()
example = Example(root)
example.pack(side="top", fill="both", expand=True)
root.mainloop()
Procedural solution
Here is a solution that doesn't use a class:
import tkinter as tk
def populate(frame):
'''Put in some fake data'''
for row in range(100):
tk.Label(frame, text="%s" % row, width=3, borderwidth="1",
relief="solid").grid(row=row, column=0)
t="this is the second column for row %s" %row
tk.Label(frame, text=t).grid(row=row, column=1)
def onFrameConfigure(canvas):
'''Reset the scroll region to encompass the inner frame'''
canvas.configure(scrollregion=canvas.bbox("all"))
root = tk.Tk()
canvas = tk.Canvas(root, borderwidth=0, background="#ffffff")
frame = tk.Frame(canvas, background="#ffffff")
vsb = tk.Scrollbar(root, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=vsb.set)
vsb.pack(side="right", fill="y")
canvas.pack(side="left", fill="both", expand=True)
canvas.create_window((4,4), window=frame, anchor="nw")
frame.bind("<Configure>", lambda event, canvas=canvas: onFrameConfigure(canvas))
populate(frame)
root.mainloop()
Make it scrollable
Use this handy class to make the frame containing your widgets scrollable. Follow these steps:
create the frame
display it (pack, grid, etc)
make it scrollable
add widgets inside it
call the update() method
import tkinter as tk
from tkinter import ttk
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=False)
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))
Usage example
root = tk.Tk()
header = ttk.Frame(root)
body = ttk.Frame(root)
footer = ttk.Frame(root)
header.pack()
body.pack()
footer.pack()
ttk.Label(header, text="The header").pack()
ttk.Label(footer, text="The Footer").pack()
scrollable_body = Scrollable(body, width=32)
for i in range(30):
ttk.Button(scrollable_body, text="I'm a button in the scrollable frame").grid()
scrollable_body.update()
root.mainloop()
Extends class tk.Frame to support a scrollable Frame
This class is independent from the widgets to be scrolled and can be used to replace a standard tk.Frame.
import tkinter as tk
class ScrollbarFrame(tk.Frame):
"""
Extends class tk.Frame to support a scrollable Frame
This class is independent from the widgets to be scrolled and
can be used to replace a standard tk.Frame
"""
def __init__(self, parent, **kwargs):
tk.Frame.__init__(self, parent, **kwargs)
# The Scrollbar, layout to the right
vsb = tk.Scrollbar(self, orient="vertical")
vsb.pack(side="right", fill="y")
# The Canvas which supports the Scrollbar Interface, layout to the left
self.canvas = tk.Canvas(self, borderwidth=0, background="#ffffff")
self.canvas.pack(side="left", fill="both", expand=True)
# Bind the Scrollbar to the self.canvas Scrollbar Interface
self.canvas.configure(yscrollcommand=vsb.set)
vsb.configure(command=self.canvas.yview)
# The Frame to be scrolled, layout into the canvas
# All widgets to be scrolled have to use this Frame as parent
self.scrolled_frame = tk.Frame(self.canvas, background=self.canvas.cget('bg'))
self.canvas.create_window((4, 4), window=self.scrolled_frame, anchor="nw")
# Configures the scrollregion of the Canvas dynamically
self.scrolled_frame.bind("<Configure>", self.on_configure)
def on_configure(self, event):
"""Set the scroll region to encompass the scrolled frame"""
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
Usage:
class App(tk.Tk):
def __init__(self):
super().__init__()
sbf = ScrollbarFrame(self)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
sbf.grid(row=0, column=0, sticky='nsew')
# sbf.pack(side="top", fill="both", expand=True)
# Some data, layout into the sbf.scrolled_frame
frame = sbf.scrolled_frame
for row in range(50):
text = "%s" % row
tk.Label(frame, text=text,
width=3, borderwidth="1", relief="solid") \
.grid(row=row, column=0)
text = "this is the second column for row %s" % row
tk.Label(frame, text=text,
background=sbf.scrolled_frame.cget('bg')) \
.grid(row=row, column=1)
if __name__ == "__main__":
App().mainloop()

expand scrollable ttk.notebook in python3 / gui tkinter

coding a scrollable frame with a ttk.notebook inside works.
But the scrollbar / notebook has a fixed size. How can I change it?
from tkinter import Canvas, Scrollbar, Button, Tk
from tkinter.ttk import Frame, Notebook
class VerticalScrolledFrame(Frame):
"""A pure Tkinter scrollable frame that actually works!
* Use the 'interior' attribute to place widgets inside the scrollable frame
* Construct and pack/place/grid normally
* This frame only allows vertical scrolling
"""
def __init__(self, parent, *args, **kw):
Frame.__init__(self, parent, *args, **kw)
# create a canvas object and a vertical scrollbar for scrolling it
vscrollbar = Scrollbar(self, orient='vertical')
vscrollbar.pack(fill='y', side='right', expand='false')
canvas = Canvas(self, bd=0, highlightthickness=0,
yscrollcommand=vscrollbar.set)
canvas.pack(side='left', fill='both', expand='true')
vscrollbar.config(command=canvas.yview)
# reset the view
canvas.xview_moveto(0)
canvas.yview_moveto(0)
# create a frame inside the canvas which will be scrolled with it
self.interior = interior = Frame(canvas)
interior_id = canvas.create_window(0, 0, window=interior,
anchor='nw')
# track changes to the canvas and frame width and sync them,
# also updating the scrollbar
def _configure_interior(event):
# update the scrollbars to match the size of the inner frame
size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
canvas.config(scrollregion="0 0 %s %s" % size)
if interior.winfo_reqwidth() != canvas.winfo_width():
# update the canvas's width to fit the inner frame
canvas.config(width=interior.winfo_reqwidth())
interior.bind('<Configure>', _configure_interior)
def _configure_canvas(event):
if interior.winfo_reqwidth() != canvas.winfo_width():
# update the inner frame's width to fill the canvas
canvas.itemconfigure(interior_id, width=canvas.winfo_width())
canvas.bind('<Configure>', _configure_canvas)
root = Tk()
class Overview:
def __init__(self):
#mainframe to make a scrollable window
self.mainframe = VerticalScrolledFrame(root)
self.mainframe.grid(row=0, column=0)
# create a notebook
self.TNotebook_Overview = Notebook(self.mainframe.interior)
self.TNotebook_Overview.grid(row=0, column=0)
self.TNotebook_Overview.configure(takefocus="")
self.Frame_Overview = Frame(self.TNotebook_Overview)
self.TNotebook_Overview.add(self.Frame_Overview)
self.TNotebook_Overview.tab(0, text="Table", compound="left",underline="-1", )
buttons = []
for i in range(30):
buttons.append(Button(self.Frame_Overview, text="Button " + str(i)))
buttons[-1].grid(column=0, row=i)
if __name__ == "__main__":
ov = Overview()
root.title('Overview Items Database')
root.geometry('800x800+10+10')
root.configure(background="#4C7274")
root.grab_set()
root.mainloop()
I expect an expanded scrollable notebook/frame filled entire Tk() window.
Because the "ai" of stackoverflow don't allow use code from another thread, here is code https://pastebin.com/ykJGViAz
Inside init, do this instead
self.mainframe.grid(row=0, column=0, sticky="nsew")
self.TNotebook_Overview.grid(row=0, column=0, sticky="nsew")
This tells the frame to fill as much space as it needs
Just add this in the third line of init in the class overview:
self.mainframe.pack(fill = BOTH, expand = 1)

Adding padding query insert to Listbox

I have read a number of threads and other resources to try to find the correct way to handle this but I have not found anything that works with my application.
Here is what I am trying to accomplish.
When a query is completed and the insert of the data to a Listbox is done I cannot seem to get it to margin the data insert by 1 character space.
I am using pack() and I have read the tkinter manual for this and have tried each example available along with others found on various threads here.
The widget:
output = tkinter.Listbox(window_2, height = 20, font='Times 10',
width=42, bd=1, bg = '#FFD599', fg = '#9A0615', selectmode=SINGLE)
output.pack()
output.place(x=210, y=195)
I have tried padx and pady with pack() without success, although this works successfully with the Text widget. I have also attempted to use a few alternatives that I have found here on the site but all without success in margining the Listbox when the data is inserted.
Any advice?
pack's padx/pady and ipadx/ipady options don't affect the data that is inside the listbox. The listbox itself doesn't have any options to add an internal margin.
To get a margin around the inside of the listbox, what I normally do is give it a zero borderwidth and highlightthickness, and then place it in a frame with the same background color and let the frame be the border. You can then add any padding you want between the border and the listbox.
This is also convenient because you can put a scrollbar inside the frame, giving it the appearance that it is inside the listbox without actually being inside the listbox.
Example:
import tkinter as tk
root = tk.Tk()
root.configure(background="gray")
listbox_border = tk.Frame(root, bd=1, relief="sunken", background="white")
listbox_border.pack(padx=10, pady=10, fill=None, expand=False)
listbox = tk.Listbox(listbox_border, width=20, height=10,
borderwidth=0, highlightthickness=0,
background=listbox_border.cget("background"),
)
vsb = tk.Scrollbar(listbox_border, orient="vertical", command=listbox.yview)
listbox.configure(yscrollcommand=vsb)
vsb.pack(side="right", fill="y")
listbox.pack(padx=10, pady=10, fill="both", expand=True)
for i in range(100):
listbox.insert("end", "Item #{}".format(i))
root.mainloop()
here is a variation on the much appreciated answer by Bryan Oakley.
it uses ttk widgets instead of tk widgets
the scrollbar tracks your position in the list box when you scroll with the mouse
uses the oStyle.theme_use("clam") because it may look more modern...this can be commented out
'
import tkinter as tk
from tkinter import ttk
try: # allows the text to be more crisp on a high dpi display
from ctypes import windll
windll.shcore.SetProcessDpiAwareness(1)
except:
pass
root = tk.Tk()
oStyle = ttk.Style()
oStyle.theme_use("clam")
oStyle.configure('LB.TFrame', bd=1, relief="sunken", background="white")
listbox_border = ttk.Frame(root, style='LB.TFrame')
listbox_border.pack(padx=4, pady=4, fill=None, expand=False)
vsb = ttk.Scrollbar(listbox_border)
vsb.pack(side="right", fill="y")
listbox = tk.Listbox(listbox_border, width=20, height=10, borderwidth=0,
highlightthickness=0, selectmode=tk.SINGLE,
activestyle=tk.NONE)
listbox.pack(padx=6, pady=6, fill="y", expand=True)
listbox.config(yscrollcommand=vsb.set)
vsb.config(command=listbox.yview)
for i in range(100):
listbox.insert("end", "Item #{}".format(i))
root.mainloop()
'
first of all to format chars in a tkinter listbox you need to use a fixed font and .format python funcion....;
So you can do something this
Press Load to load data in the listbox and pay attention to this line code
s = '{0:>8}{1:5}'.format(i[0],i[1])
self.list.insert(tk.END, s)
import tkinter as tk
RS = (('Apple',10),
('Banana',20),
('Peack',8),
('Lemon',6),)
class App(tk.Frame):
def __init__(self,):
super().__init__()
self.master.title("Hello World")
self.init_ui()
def init_ui(self):
self.pack(fill=tk.BOTH, expand=1,)
f = tk.Frame()
sb = tk.Scrollbar(f,orient=tk.VERTICAL)
self.list = tk.Listbox(f,
relief=tk.GROOVE,
selectmode=tk.BROWSE,
exportselection=0,
background = 'white',
font='TkFixedFont',
yscrollcommand=sb.set,)
sb.config(command=self.list.yview)
self.list.pack(side=tk.LEFT,fill=tk.BOTH, expand =1)
sb.pack(fill=tk.Y, expand=1)
w = tk.Frame()
tk.Button(w, text="Load", command=self.on_callback).pack()
tk.Button(w, text="Close", command=self.on_close).pack()
f.pack(side=tk.LEFT, fill=tk.BOTH, expand=0)
w.pack(side=tk.RIGHT, fill=tk.BOTH, expand=0)
def on_callback(self,):
for i in RS:
s = '{0:>8}{1:5}'.format(i[0],i[1])
self.list.insert(tk.END, s)
def on_close(self):
self.master.destroy()
if __name__ == '__main__':
app = App()
app.mainloop()

Tkinter not able to fill the text box to the frame using grid. Used styling to add shadow and focus

Description
I am creating a canvas with scrollbar and adding frames with a text box in the frame and to fill the entire frame with no border. This will make it look as if the frame is the textbox. I have added shadow and styling to the frame (as coded by Bryan). This is added dynamically in for loop.
When I am trying to expand the text box to the frame, it is not expanding to fill the entire frame. There are extra spaces left.
Question
How do I fill up the entire frame with the textbox using a grid?
Code
import tkinter as tk
from tkinter import ttk
focusBorderImageData = '''
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
Ry/99NIz//oGrZpUUEAAOw==
'''
borderImageData = '''
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
'''
root = tk.Tk()
style = ttk.Style()
borderImage = tk.PhotoImage("borderImage", data=borderImageData)
focusBorderImage = tk.PhotoImage("focusBorderImage", data=focusBorderImageData)
style.element_create("RoundedFrame",
"image", borderImage,
("focus", focusBorderImage),
border=16, sticky="nsew")
style.layout("RoundedFrame",
[("RoundedFrame", {"sticky": "nsew"})])
root.configure(background="white")
canvas = tk.Canvas(root)
scroll = tk.Scrollbar(root, orient='horizontal', command=canvas.xview)
canvas.configure(xscrollcommand=scroll.set)
frame = tk.Frame(canvas) # frame does not get pack() as it needs to be embedded into canvas throught canvas.
scroll.pack(side='bottom', fill='x')
canvas.pack(fill='both', expand='yes')
canvas.create_window((0,0), window=frame, anchor='nw')
frame.bind('<Configure>', lambda x: canvas.configure(scrollregion=canvas.bbox('all'))) # lambda function
for i in range(5):
frame1 = ttk.Frame(frame, style="RoundedFrame", padding=10)
journal1 = tk.Text(frame1, borderwidth=2, highlightthickness=0, width = 40, height = 38)
# journal1.configure(borderwidth="3")
journal1.configure(relief="groove")
journal1.configure(background="white")
journal1.grid(row=0, column=0, padx=(100, 10), sticky = 'nswe') # grid instead
journal1.bind("<FocusIn>", lambda event: frame.state(["focus"]))
journal1.bind("<FocusOut>", lambda event: frame.state(["!focus"]))
frame1.grid_columnconfigure(0, weight=1)
frame1.grid_rowconfigure(0, weight=1)
frame1.grid(row=0,column=i, sticky = 'nswe')
root.mainloop()
Output
I had troubles with the focus of the frame, it complained frame has no attribute 'state'. It works in Bryans original answer. I fixed it with closures.
def frameFocusCreator(frame, focusState):
def changeState(event):
frame.state([focusState])
return changeState
for i in range(5):
frame1 = ttk.Frame(frame, style="RoundedFrame", padding=10)
journal1 = tk.Text(frame1, borderwidth=0, highlightthickness=0, width = 40, height = 38)
journal1.configure(relief="groove")
journal1.configure(background="white")
journal1.pack(fill='both', expand=True)
journal1.bind("<FocusIn>", frameFocusCreator(frame1, "focus"))
journal1.bind("<FocusOut>", frameFocusCreator(frame1, "!focus"))
frame1.grid(row=0,column=i, sticky = 'nswe')

Tkinter scrollbars let user scroll more than they should

I'm developing a "cell" matrix using python 3.6 and tkinter, I've modified a scrolled frame class (code below) because scrollbars let me scroll though there isn't space left in the scrollbar. The result is an undesired space (see images below) . I used the class AutoScrollbar to gray out the scrollbar when it's not needed and worked fine under Windows, but under Linux still let me scroll further than it should and doesn't gray out.
is there any way to prevent tkinter scrollbars to allow scroll further then they should?
Under Windows:
Under Linux:
import tkinter as tk
from tkinter import ttk
class AutoScrollbar(tk.Scrollbar):
def set(self, lo, hi):
if float(lo) <= 0.0 and float(hi) >= 1.0:
tk.Scrollbar.set(self, 0.0, 1.0)
else:
tk.Scrollbar.set(self, lo, hi)
class ScrolledFrame(tk.Frame):
def __init__(self, parent, *args, **kw):
tk.Frame.__init__(self, parent, *args, **kw)
vscrollbar = AutoScrollbar(self, orient=tk.VERTICAL)
vscrollbar.pack(fill=tk.Y, side=tk.RIGHT, expand=False)
hscrollbar = AutoScrollbar(self, orient=tk.HORIZONTAL)
hscrollbar.pack(fill=tk.X, side=tk.BOTTOM, expand=False)
canvas = tk.Canvas(self, bd=0, highlightthickness=0,
yscrollcommand=vscrollbar.set,
xscrollcommand=hscrollbar.set,
bg ='gray')
canvas.pack(fill=tk.BOTH, expand=True)
vscrollbar.config(command=canvas.yview)
hscrollbar.config(command=canvas.xview)
# reset the view
canvas.xview_moveto(0)
canvas.yview_moveto(0)
# create a frame inside the canvas which will be scrolled with it
self.interior = interior = tk.Frame(canvas)
interior_id = canvas.create_window(0, 0, window=interior,
anchor=tk.NW)
# detect changes on interior size
def _configure_interior(event):
# adjust size of scroll region
size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
#print('size interior = canvas: '+str(size))
canvas.config(scrollregion="0 0 %s %s" % size)
interior.bind('<Configure>', _configure_interior)

Resources