resizeable scrollable canvas with tkinter - python

Here my code for a very simple gui:
from Tkinter import *
class my_gui(Frame):
def __init__(self):
# main tk object
self.root = Tk()
# init Frame
Frame.__init__(self, self.root)
# create frame (gray window)
self.frame=Frame(self.root,width=100,height=100)
self.frame.grid(row=0,column=0)
self.__add_scroll_bars()
self.__create_canvas()
self.__add_plot()
def __create_canvas(self):
# create white area in the window for plotting
# width and height are only the visible size of the white area, scrollregion is the area the user can see by scrolling
self.canvas = Canvas(self.frame,bg='#FFFFFF',width=300,height=300,scrollregion=(0,0,500,500))
# with this command the window is filled with the canvas
self.canvas.pack(side=LEFT,expand=True,fill=BOTH)
# position and size of the canvas is used for configuration of the scroll bars
self.canvas.config(xscrollcommand=self.hbar.set, yscrollcommand=self.vbar.set)
# add command to the scroll bars to scroll the canvas
self.hbar.config(command = self.canvas.xview)
self.vbar.config(command = self.canvas.yview)
def __add_scroll_bars(self):
# add scroll bars
self.hbar=Scrollbar(self.frame,orient=HORIZONTAL)
self.hbar.pack(side=BOTTOM,fill=X)
self.vbar=Scrollbar(self.frame,orient=VERTICAL)
self.vbar.pack(side=RIGHT,fill=Y)
def __add_plot(self):
# create a rectangle
self.canvas.create_polygon(10, 10, 10, 150, 200, 150, 200, 10, fill="gray", outline="black")
def mainLoop(self):
# This function starts an endlos running thread through the gui
self.root.mainloop()
def __quit(self):
# close everything
self.root.quit()
def mainLoop(self):
# This function starts an endlos running thread through the gui
self.root.mainloop()
# init gui
my_gui = my_gui()
# execute gui
my_gui.mainLoop()
I have two questions:
1) I want if I resize the gui, that then the scrollbars are always on the Ends of the gui and I resize the canvas.
2) If I resize the GUI and the canvas, then the rectangle in the canvas shall be resized (for example if the new size of gui and canvas is four times the old size, then the new size of rectangle is twize the old size).
I search a solution for the first problem and for the second problem seperately.
Thanks for help.

You could use the following way to integrate my frame into your gui class:
from Tkinter import *
class ScrollableFrame(Frame):
def __init__(self, parent, *args, **kw):
'''
Constructor
'''
Frame.__init__(self, parent, *args, **kw)
# create a vertical scrollbar
vscrollbar = Scrollbar(self, orient = VERTICAL)
vscrollbar.pack(fill = Y, side = RIGHT, expand = FALSE)
# create a horizontal scrollbar
hscrollbar = Scrollbar(self, orient = HORIZONTAL)
hscrollbar.pack(fill = X, side = BOTTOM, expand = FALSE)
#Create a canvas object and associate the scrollbars with it
self.canvas = Canvas(self, bd = 0, highlightthickness = 0, yscrollcommand = vscrollbar.set, xscrollcommand = hscrollbar.set)
self.canvas.pack(side = LEFT, fill = BOTH, expand = TRUE)
#Associate scrollbars with canvas view
vscrollbar.config(command = self.canvas.yview)
hscrollbar.config(command = self.canvas.xview)
# set the view to 0,0 at initialization
self.canvas.xview_moveto(0)
self.canvas.yview_moveto(0)
# create an interior frame to be created inside the canvas
self.interior = interior = Frame(self.canvas)
interior_id = self.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())
self.canvas.config(scrollregion='0 0 %s %s' % size)
if interior.winfo_reqwidth() != self.canvas.winfo_width():
# update the canvas's width to fit the inner frame
self.canvas.config(width = interior.winfo_reqwidth())
interior.bind('<Configure>', _configure_interior)
class my_gui(Frame):
def __init__(self):
# main tk object
self.root = Tk()
# init Frame
Frame.__init__(self, self.root)
# create frame (gray window)
self.frame = ScrollableFrame(self.root)
self.frame.pack(fill=BOTH, expand=YES)
#self.__add_scroll_bars()
#self.__create_canvas()
self.__add_plot()
def __add_plot(self):
# create a rectangle
self.frame.canvas.create_polygon(10, 10, 10, 150, 200, 150, 200, 10, fill="gray", outline="black")
def mainLoop(self):
# This function starts an endlos running thread through the gui
self.root.mainloop()
def __quit(self):
# close everything
self.root.quit()
# init gui
my_gui = my_gui()
# execute gui
my_gui.mainLoop()
This should essentially solve your first problem. As for the second problem you'll need to create a function to re-render the canvas every time you resize it. In a way similar to the _configure_interior function.

You could use this following example, or integrate it in your class.
You could create a frame like this by calling.
self.frame = ScrollableFrame(self.root)
self.frame.pack(fill=BOTH, expand=YES)
Create a class like this for your frame:
from Tkinter import *
class ScrollableFrame(Frame):
'''
Creates a scrollable frame
'''
def __init__(self, parent, *args, **kw):
'''
Constructor
'''
Frame.__init__(self, parent, *args, **kw)
# create a vertical scrollbar
vscrollbar = Scrollbar(self, orient = VERTICAL)
vscrollbar.pack(fill = Y, side = RIGHT, expand = FALSE)
# create a horizontal scrollbar
hscrollbar = Scrollbar(self, orient = HORIZONTAL)
hscrollbar.pack(fill = X, side = BOTTOM, expand = FALSE)
#Create a canvas object and associate the scrollbars with it
canvas = Canvas(self, bd = 0, highlightthickness = 0, yscrollcommand = vscrollbar.set, xscrollcommand = hscrollbar.set)
canvas.pack(side = LEFT, fill = BOTH, expand = TRUE)
#Associate scrollbars with canvas view
vscrollbar.config(command = canvas.yview)
hscrollbar.config(command = canvas.xview)
# set the view to 0,0 at initialization
canvas.xview_moveto(0)
canvas.yview_moveto(0)
# create an interior frame to be created inside the canvas
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)
You could use this to obtain the result you want. Horizontal and Vertical scrolling are both enabled for this frame and scrollbar positions can be set using 'side' field.
For the second part of your question, could you elucidate further.
Reference: Gonzo's answer
Python Tkinter scrollbar for frame

This works very well, to get what I want with the minimal scrollable canvas size. But there is still the bug, when the gui was made larger and when it seems so, that one can not scroll, there is the possibility to click on the left or upper arrow of the scroll bars and so to scroll the canvas, what sould not be possible.
from Tkinter import *
class ScrollableFrame(Frame):
def __init__(self, parent, minimal_canvas_size, *args, **kw):
'''
Constructor
'''
Frame.__init__(self, parent, *args, **kw)
self.minimal_canvas_size = minimal_canvas_size
# create a vertical scrollbar
vscrollbar = Scrollbar(self, orient = VERTICAL)
vscrollbar.pack(fill = Y, side = RIGHT, expand = FALSE)
# create a horizontal scrollbar
hscrollbar = Scrollbar(self, orient = HORIZONTAL)
hscrollbar.pack(fill = X, side = BOTTOM, expand = FALSE)
#Create a canvas object and associate the scrollbars with it
self.canvas = Canvas(self, bd = 0, highlightthickness = 0, yscrollcommand = vscrollbar.set, xscrollcommand = hscrollbar.set)
self.canvas.pack(side = LEFT, fill = BOTH, expand = TRUE)
#Associate scrollbars with canvas view
vscrollbar.config(command = self.canvas.yview)
hscrollbar.config(command = self.canvas.xview)
# set the view to 0,0 at initialization
self.canvas.xview_moveto(0)
self.canvas.yview_moveto(0)
self.canvas.config(scrollregion='0 0 %s %s' % self.minimal_canvas_size)
# create an interior frame to be created inside the canvas
self.interior = interior = Frame(self.canvas)
interior_id = self.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 = (max(interior.winfo_reqwidth(), self.minimal_canvas_size[0]), max(interior.winfo_reqheight(), self.minimal_canvas_size[1]))
self.canvas.config(scrollregion='0 0 %s %s' % size)
if interior.winfo_reqwidth() != self.canvas.winfo_width():
# update the canvas's width to fit the inner frame
self.canvas.config(width = interior.winfo_reqwidth())
interior.bind('<Configure>', _configure_interior)
class my_gui(Frame):
def __init__(self):
# main tk object
self.root = Tk()
# init Frame
Frame.__init__(self, self.root)
minimal_canvas_size = (500, 500)
# create frame (gray window)
self.frame = ScrollableFrame(self.root, minimal_canvas_size)
self.frame.pack(fill=BOTH, expand=YES)
self.__add_plot()
def __add_plot(self):
# create a rectangle
self.frame.canvas.create_polygon(10, 10, 10, 150, 200, 150, 200, 10, fill="gray", outline="black")
def mainLoop(self):
# This function starts an endlos running thread through the gui
self.root.mainloop()
def __quit(self):
# close everything
self.root.quit()
# init gui
my_gui = my_gui()
# execute gui
my_gui.mainLoop()

Related

Tkinter buttons getting hidden behind other frames

I am trying to create a battlemap for dnd (picture) with adjustable grid and movable enemy/creature tokens. The idea is to drag one of the token from the right onto the map on the left.
The window is made of 3 frames. The frame for the map, the frame for the "new map" button and slider. And then frame for the tokens, which are buttons tiled using button.grid()
I found a drag and drop system here that I'm using to drag the tokens. However, when I bring them over the map, they go behind it and you can't see them (I know they go behind because they can be partially visible between the two frames). Is there any way to bring them to the front?
import tkinter as tk
class DragManager():
def add_dragable(self, widget):
widget.bind("<ButtonPress-1>", self.on_start)
widget.bind("<B1-Motion>", self.on_drag)
widget.bind("<ButtonRelease-1>", self.on_drop)
widget.configure(cursor="hand1")
def on_start(self, event):
# you could use this method to create a floating window
# that represents what is being dragged.
pass
def on_drag(self, event):
# you could use this method to move a floating window that
# represents what you're dragging
event.widget.place(x=event.x_root + event.x, y= event.y_root + event.y)
#when button is dropped, create a new one where this one originally was
def on_drop(self, event):
# find the widget under the cursor
x,y = event.widget.winfo_pointerxy()
target = event.widget.winfo_containing(x,y)
try:
target.configure(image=event.widget.cget("image"))
except:
pass
if x > window.winfo_screenwidth() - 200:
del event.widget
return
if not event.widget.pure:
return
button = tk.Button(master=entity_select_frame, text = "dragable", borderwidth=1, compound="top")
#avoiding garbage collection
button.gridx = event.widget.gridx
button.gridy = event.widget.gridy
button.grid(row = event.widget.gridx, column = event.widget.gridy)
button.grid()
button.pure = True
dnd.add_dragable(button)
window = tk.Tk()
window.geometry("1000x800")
map_frame = tk.Frame()
controls_frame = tk.Frame(width=200, borderwidth=1, relief=tk.RAISED)
tk.Label(master=controls_frame, text="controls here").pack()
entity_select_frame = tk.Frame(width=200, relief=tk.RAISED, borderwidth=1)
dnd = DragManager()
button = tk.Button(master=entity_select_frame, text = "dragable", borderwidth=1)
button.gridx = 0
button.gridy = 0
button.grid(row = 0, column = 0)
button.pure = True
dnd.add_dragable(button)
map_frame.pack(fill=tk.BOTH, side=tk.LEFT, expand=True)
controls_frame.pack(fill=tk.BOTH)
entity_select_frame.pack(fill=tk.BOTH)
window.mainloop()
I played around a little bit and used stuff from this post. I did not structure it as a class and I used the picture frame as my root-frame and put the control-frame inside that. I'm not sure how this would be best combined with your "draw-grid", "token" functionalities etc., however I hope it helps. I did not find a way to drag widgets across frames though (tried to set a new master for the button, recreate it after dropping it etc.). Get the image used in my code from here.
from tkinter import Tk, Frame, Label, Button, Canvas, font
from tkinter import ttk
from PIL import Image, ImageTk
root = Tk()
""" ####################### Configuration parameters ###################### """
image_file_path = "Island_AngelaMaps-1024x768.jpg"
resize_img = False # set to True if you want to resize the image > window size
resize_to = (600, 600) # resolution to rescale image to
""" ####################### Drag and drop functionality ################### """
def make_draggable(widget):
widget.bind("<Button-1>", on_drag_start)
widget.bind("<B1-Motion>", on_drag_motion)
def on_drag_start(event):
widget = event.widget
widget._drag_start_x = event.x
widget._drag_start_y = event.y
def on_drag_motion(event):
widget = event.widget
x = widget.winfo_x() - widget._drag_start_x + event.x
y = widget.winfo_y() - widget._drag_start_y + event.y
widget.place(x=x, y=y)
""" ################################# Layout ############################## """
# picture frame with picture as background
picture_frame = Frame(root)
picture_frame.pack(side="left", anchor="w", fill="both", expand=True)
# load the image
if resize_img:
img = ImageTk.PhotoImage(Image.open(image_file_path).resize(resize_to, Image.ANTIALIAS))
else:
img = ImageTk.PhotoImage(Image.open(image_file_path))
# create canvas, set canvas background to the image
canvas = Canvas(picture_frame, width=img.width(), height=img.height())
canvas.pack(side="left")
canvas.background = img # Keep a reference in case this code is put in a function.
bg = canvas.create_image(0, 0, anchor="nw", image=img)
# subframe inside picture frame for controls
ctrl_subframe = Frame(picture_frame)
ctrl_subframe.pack(side="right", anchor="n")
# separator between picture and controls, inside picture frame
ttk.Separator(picture_frame, orient="vertical").pack(side="right", fill="y")
# underlined label 'Controls' in subframe
ctrl_header = Label(ctrl_subframe, text="Controls", font=("Arial", 10, "bold"))
f = font.Font(ctrl_header, ctrl_header.cget("font"))
f.configure(underline=True)
ctrl_header.configure(font=f)
ctrl_header.pack(side="top", pady=2)
# update window to get proper sizes from widgets
root.update()
# a draggable button, placed below ctrl_header
# (based on X of ctrl_subframe and height of ctrl_header, plus padding)
drag_button = Button(picture_frame, text="Drag me", bg="green", width=6)
drag_button.place(x=ctrl_subframe.winfo_x()+2, y=ctrl_header.winfo_height()+10)
make_draggable(drag_button)
""" ################################ Mainloop ############################# """
root.mainloop()

Handling visual overflow in a tkinter frame?

I'm writing a class that adds a scrolling frame. It detects when the frame's contents exceed its height, and then configures the scrollbar. The problem is that when I scroll down, the items in the frame scroll outside of the top of the frame and appear above it.
I've tested it out using plain labels, and it worked fine, but I'm using a class object that has some nested frames, and the child objects are what show up above the scrolling frame.
This is the gist of the code that's giving me problems (please note that the layout doesn't match the full project. I used this as my reference for the ScrollFrame() class.)
Just running this, pressing the button, and scrolling down will show you what's wrong with it.
import tkinter as tk
import tkinter.simpledialog as sd
class ScrollFrame(tk.Frame):
def __init__(self, master):
tk.Frame.__init__(self, master)
### setting up the objects used ###
self.canvas = tk.Canvas(master)
self.frame = tk.Frame(self.canvas)
self.scrollbar = tk.Scrollbar(master, orient = 'vertical',
command = self.canvas.yview)
### scrollbar moves with current canvas scrollamount ###
self.canvas.configure(yscrollcommand = self.scrollbar.set)
self.scrollbar.pack(side = 'right', fill = 'y')
self.canvas.pack(side = 'left', fill = 'both', expand = True)
### creating frame to pack widgets onto ###
self.canvas.create_window((4, 4), window = self.frame,
anchor = 'nw', tags = 'self.frame')
### setting scrollbar height on load ###
self.frame.bind('<Configure>', self.frameConfig)
### scroll when a user's mouse wheel is used inside the canvas ###
def scrollCanvas(event):
self.canvas.yview_scroll(-1*(event.delta//120), 'units')
self.canvas.bind('<MouseWheel>', scrollCanvas)
### set the scrollregion of the canvas ###
def frameConfig(self, event):
self.canvas.configure(scrollregion = self.canvas.bbox('all'))
class OptionSet(tk.Frame):
def __init__(self, master, **kwargs):
super().__init__()
self.all = tk.Frame(master)
self.all.configure(bd = 1, relief = 'solid')
# independent label
self.text = '' if not kwargs['text'] else kwargs['text']
self.label = tk.Label(text = self.text)
# list of all buttons
self.buttons = tk.Frame()
buttons = [] if not kwargs['buttons'] else kwargs['buttons']
self.button_list = []
if buttons:
for button in buttons:
self.button_list.append(
tk.Button(self.buttons, text = button)
)
self.style()
def style(self, default = 1, **kwargs):
if default:
self.label.pack(in_ = self.all, side = 'left')
for button in self.button_list:
button.pack(side = 'left')
self.buttons.pack(in_ = self.all, side = 'right')
root = tk.Tk()
list_items = []
current = {
'month': 'August'
# ...
}
def btn_fcn(num):
for i in list_items:
i.grid_forget()
'''
# get event as input
event = sd.askstring('Event Input',
f"What is happening on {current['month']} {num}?")
# insert new list_item
list_items.append(OptionSet(event_list.frame, text = event,
buttons = ['Edit', 'Delete']))
print(event)
'''
for i in range(10):
list_items.append(OptionSet(event_list.frame, text = 'test',
buttons = ['Edit', 'Delete']))
for i in list_items:
i.all.grid(sticky = 'we')
tk.Button(root, text = 'Add', command = lambda: btn_fcn(22)).pack()
event_list = ScrollFrame(root)
event_list.pack()
root.mainloop()
I want the buttons and labels to cut off outside of the ScrollFrame. I don't know whether they're overflowing from the frame or the canvas, but they should cut off normally if all goes according to plan.
Thanks.
The problem is that you're being very sloppy with where you create your widgets. For example, you aren't putting the canvas and scrollbar in the ScrollFrame, you're putting it in master. Every widget defined in ScrolLFrame needs to be in self or a child of self.
The same is true with OptionSet - you're putting the inner frame (self.all) a d the other widgets in master rather than inside the OptionSet itself.
My recommendation is to temporarily remove all of the OptionSet code (or just don't use it) and instead, just add labels to the scroll frame. Focus on getting that working without the complexity of a custom class being added to a custom class. Once you are able to scroll just labels, the you can add back in the OptionSet code.

Scrollable Frame with fill in Python Tkinter

I am trying to implement a scrollable frame in Python with Tkinter:
if the content changes, the size of the widget is supposed to stay constant (basically, I don't really care whether the size of the scrollbar is subtracted from the frame or added to the parent, although I do think that it would make sense if this was consistent but that does not seem to be the case currently)
if the content becomes too big a scrollbar shall appear so that one can scroll over the entire content (but not further)
if the content fits entirely into the widget the scrollbar shall disappear and it shall not be possible to scroll anymore (no need to scroll, because everything is visible)
if the req size of the content becomes smaller than the widget, the content shall fill the widget
I am surprised how difficult it seems to get this running, because it seems like a pretty basic functionality.
The first three requirements seem relatively easy but I am having a lot of trouble since trying to fill the widget.
The following implementation has the following problems:
first time a scrollbar appears, frame does not fill canvas (seems to depend on available space):
add one column. The horizontal scrollbar appears. Between the scrollbar and the white background of the frame the red background of the canvas becomes visible. This red area looks around as high as the scrollbar.
When adding or removing a row or column or resizing the window the red area disappears and does not seem to appear again.
size jumps:
add elements until horizontal scrollbar becomes visible. make window wider (not higher). the height [!] of the window increases with a jump.
infinite loop:
add rows until the vertical scrollbar appears, remove one row so that vertical scrollbar disappears again, add one row again. The window's size is rapidly increasing and decreasing. The occurence of this behaviour depends on the size of the window. The loop can be broken by resizing or closing the window.
What am I doing wrong?
Any help would be appreciated.
#!/usr/bin/env python
# based on https://stackoverflow.com/q/30018148
try:
import Tkinter as tk
except:
import tkinter as tk
# I am not using something like vars(tk.Grid) because that would override too many methods.
# Methods like Grid.columnconfigure are suppossed to be executed on self, not a child.
GM_METHODS_TO_BE_CALLED_ON_CHILD = (
'pack', 'pack_configure', 'pack_forget', 'pack_info',
'grid', 'grid_configure', 'grid_forget', 'grid_remove', 'grid_info',
'place', 'place_configure', 'place_forget', 'place_info',
)
class AutoScrollbar(tk.Scrollbar):
'''
A scrollbar that hides itself if it's not needed.
Only works if you use the grid geometry manager.
'''
def set(self, lo, hi):
if float(lo) <= 0.0 and float(hi) >= 1.0:
self.grid_remove()
else:
self.grid()
tk.Scrollbar.set(self, lo, hi)
def pack(self, *args, **kwargs):
raise TclError('Cannot use pack with this widget.')
def place(self, *args, **kwargs):
raise TclError('Cannot use place with this widget.')
#TODO: first time a scrollbar appears, frame does not fill canvas (seems to depend on available space)
#TODO: size jumps: add elements until horizontal scrollbar becomes visible. make widget wider. height jumps from 276 to 316 pixels although it should stay constant.
#TODO: infinite loop is triggered by
# - add rows until the vertical scrollbar appears, remove one row so that vertical scrollbar disappears again, add one row again (depends on size)
# was in the past triggered by:
# - clicking "add row" very fast at transition from no vertical scrollbar to vertical scrollbar visible
# - add columns until horizontal scrollbar appears, remove column so that horizointal scrollbar disappears again, add rows until vertical scrollbar should appear
class ScrollableFrame(tk.Frame):
def __init__(self, master, *args, **kwargs):
self._parentFrame = tk.Frame(master)
self._parentFrame.grid_rowconfigure(0, weight = 1)
self._parentFrame.grid_columnconfigure(0, weight = 1)
# scrollbars
hscrollbar = AutoScrollbar(self._parentFrame, orient = tk.HORIZONTAL)
hscrollbar.grid(row = 1, column = 0, sticky = tk.EW)
vscrollbar = AutoScrollbar(self._parentFrame, orient = tk.VERTICAL)
vscrollbar.grid(row = 0, column = 1, sticky = tk.NS)
# canvas & scrolling
self.canvas = tk.Canvas(self._parentFrame,
xscrollcommand = hscrollbar.set,
yscrollcommand = vscrollbar.set,
bg = 'red', # should not be visible
)
self.canvas.grid(row = 0, column = 0, sticky = tk.NSEW)
hscrollbar.config(command = self.canvas.xview)
vscrollbar.config(command = self.canvas.yview)
# self
tk.Frame.__init__(self, self.canvas, *args, **kwargs)
self._selfItemID = self.canvas.create_window(0, 0, window = self, anchor = tk.NW)
# bindings
self.canvas.bind('<Enter>', self._bindMousewheel)
self.canvas.bind('<Leave>', self._unbindMousewheel)
self.canvas.bind('<Configure>', self._onCanvasConfigure)
# geometry manager
for method in GM_METHODS_TO_BE_CALLED_ON_CHILD:
setattr(self, method, getattr(self._parentFrame, method))
def _bindMousewheel(self, event):
# Windows
self.bind_all('<MouseWheel>', self._onMousewheel)
# Linux
self.bind_all('<Button-4>', self._onMousewheel)
self.bind_all('<Button-5>', self._onMousewheel)
def _unbindMousewheel(self, event):
# Windows
self.unbind_all('<MouseWheel>')
# Linux
self.unbind_all('<Button-4>')
self.unbind_all('<Button-5>')
def _onMousewheel(self, event):
if event.delta < 0 or event.num == 5:
dy = +1
elif event.delta > 0 or event.num == 4:
dy = -1
else:
assert False
if (dy < 0 and self.canvas.yview()[0] > 0.0) \
or (dy > 0 and self.canvas.yview()[1] < 1.0):
self.canvas.yview_scroll(dy, tk.UNITS)
return 'break'
def _onCanvasConfigure(self, event):
self._updateSize(event.width, event.height)
def _updateSize(self, canvWidth, canvHeight):
hasChanged = False
requWidth = self.winfo_reqwidth()
newWidth = max(canvWidth, requWidth)
if newWidth != self.winfo_width():
hasChanged = True
requHeight = self.winfo_reqheight()
newHeight = max(canvHeight, requHeight)
if newHeight != self.winfo_height():
hasChanged = True
if hasChanged:
print("update size ({width}, {height})".format(width = newWidth, height = newHeight))
self.canvas.itemconfig(self._selfItemID, width = newWidth, height = newHeight)
return True
return False
def _updateScrollregion(self):
bbox = (0,0, self.winfo_reqwidth(), self.winfo_reqheight())
print("updateScrollregion%s" % (bbox,))
self.canvas.config( scrollregion = bbox )
def updateScrollregion(self):
# a function called with self.bind('<Configure>', ...) is called when resized or scrolled but *not* when widgets are added or removed (is called when real widget size changes but not when required/requested widget size changes)
# => useless for calling this function
# => this function must be called manually when adding or removing children
# The content has changed.
# Therefore I need to adapt the size of self.
# I need to update before measuring the size.
# It does not seem to make a difference whether I use update_idletasks() or update().
# Therefore according to Bryan Oakley I better use update_idletasks https://stackoverflow.com/a/29159152
self.update_idletasks()
self._updateSize(self.canvas.winfo_width(), self.canvas.winfo_height())
# update scrollregion
self._updateScrollregion()
def setWidth(self, width):
print("setWidth(%s)" % width)
self.canvas.configure( width = width )
def setHeight(self, height):
print("setHeight(%s)" % width)
self.canvas.configure( height = height )
def setSize(self, width, height):
print("setSize(%sx%s)" % (width, height))
self.canvas.configure( width = width, height = height )
# ==================== TEST ====================
if __name__ == '__main__':
class Test(object):
BG_COLOR = 'white'
PAD_X = 1
PAD_Y = PAD_X
# ---------- initialization ----------
def __init__(self):
self.root = tk.Tk()
self.buttonFrame = tk.Frame(self.root)
self.buttonFrame.pack(side=tk.TOP)
self.scrollableFrame = ScrollableFrame(self.root, bg=self.BG_COLOR)
self.scrollableFrame.pack(side=tk.TOP, expand=tk.YES, fill=tk.BOTH)
self.scrollableFrame.grid_columnconfigure(0, weight=1)
self.scrollableFrame.grid_rowconfigure(0, weight=1)
self.contentFrame = tk.Frame(self.scrollableFrame, bg=self.BG_COLOR)
self.contentFrame.grid(row=0, column=0, sticky=tk.NSEW)
self.labelRight = tk.Label(self.scrollableFrame, bg=self.BG_COLOR, text="right")
self.labelRight.grid(row=0, column=1)
self.labelBottom = tk.Label(self.scrollableFrame, bg=self.BG_COLOR, text="bottom")
self.labelBottom.grid(row=1, column=0)
tk.Button(self.buttonFrame, text="add row", command=self.addRow).grid(row=0, column=0)
tk.Button(self.buttonFrame, text="remove row", command=self.removeRow).grid(row=1, column=0)
tk.Button(self.buttonFrame, text="add column", command=self.addColumn).grid(row=0, column=1)
tk.Button(self.buttonFrame, text="remove column", command=self.removeColumn).grid(row=1, column=1)
self.row = 0
self.col = 0
def start(self):
self.addRow()
widget = self.contentFrame.grid_slaves()[0]
width = widget.winfo_width() + 2*self.PAD_X + self.labelRight.winfo_width()
height = 4.9*( widget.winfo_height() + 2*self.PAD_Y ) + self.labelBottom.winfo_height()
#TODO: why is size saved in event different from what I specify here?
self.scrollableFrame.setSize(width, height)
# ---------- add ----------
def addRow(self):
if self.col == 0:
self.col = 1
columns = self.col
for col in range(columns):
button = self.addButton(self.row, col)
self.row += 1
self._onChange()
def addColumn(self):
if self.row == 0:
self.row = 1
rows = self.row
for row in range(rows):
button = self.addButton(row, self.col)
self.col += 1
self._onChange()
def addButton(self, row, col):
button = tk.Button(self.contentFrame, text = '--------------------- %d, %d ---------------------' % (row, col))
# note that grid(padx) seems to behave differently from grid_columnconfigure(pad):
# grid : padx = "Optional horizontal padding to place around the widget in a cell."
# grid_rowconfigure: pad = "Padding to add to the size of the largest widget in the row when setting the size of the whole row."
# http://effbot.org/tkinterbook/grid.htm
button.grid(row=row, column=col, sticky=tk.NSEW, padx=self.PAD_X, pady=self.PAD_Y)
# ---------- remove ----------
def removeRow(self):
if self.row <= 0:
return
self.row -= 1
columns = self.col
if columns == 0:
return
for button in self.contentFrame.grid_slaves():
info = button.grid_info()
if info['row'] == self.row:
button.destroy()
self._onChange()
def removeColumn(self):
if self.col <= 0:
return
self.col -= 1
rows = self.row
if rows == 0:
return
for button in self.contentFrame.grid_slaves():
info = button.grid_info()
if info['column'] == self.col:
button.destroy()
self._onChange()
# ---------- other ----------
def _onChange(self):
print("=========== user action ==========")
print("new size: %s x %s" % (self.row, self.col))
self.scrollableFrame.updateScrollregion()
def mainloop(self):
self.root.mainloop()
test = Test()
test.start()
test.mainloop()
EDIT: I do not think that this is a duplicate of this question. The answer to that question is certainly a good starting point if you don't know how to start. It explains the basic concept of how to handle scrollbars in Tkinter. That however, is not my problem. I think that I am aware of the basic idea and I think that I have implemented that.
I have noticed that the answer mentions the possibility of directly drawing on the canvas instead of putting a frame on it. However, I would like to have a reusable solution.
My problem is that when I tried to implement that the content shall fill the frame (like with pack(expand=tk.YES, fill=tk.BOTH)) if the req size is smaller than the size of the canvas the three above listed weird effects occured which I do not understand. Most importantly that is that the program runs into an infinite loop when I add and remove rows as described (without changing the window size).
EDIT 2: I have reduced the code even further:
# based on https://stackoverflow.com/q/30018148
try:
import Tkinter as tk
except:
import tkinter as tk
class AutoScrollbar(tk.Scrollbar):
def set(self, lo, hi):
if float(lo) <= 0.0 and float(hi) >= 1.0:
self.grid_remove()
else:
self.grid()
tk.Scrollbar.set(self, lo, hi)
class ScrollableFrame(tk.Frame):
# ---------- initialization ----------
def __init__(self, master, *args, **kwargs):
self._parentFrame = tk.Frame(master)
self._parentFrame.grid_rowconfigure(0, weight = 1)
self._parentFrame.grid_columnconfigure(0, weight = 1)
# scrollbars
hscrollbar = AutoScrollbar(self._parentFrame, orient = tk.HORIZONTAL)
hscrollbar.grid(row = 1, column = 0, sticky = tk.EW)
vscrollbar = AutoScrollbar(self._parentFrame, orient = tk.VERTICAL)
vscrollbar.grid(row = 0, column = 1, sticky = tk.NS)
# canvas & scrolling
self.canvas = tk.Canvas(self._parentFrame,
xscrollcommand = hscrollbar.set,
yscrollcommand = vscrollbar.set,
bg = 'red', # should not be visible
)
self.canvas.grid(row = 0, column = 0, sticky = tk.NSEW)
hscrollbar.config(command = self.canvas.xview)
vscrollbar.config(command = self.canvas.yview)
# self
tk.Frame.__init__(self, self.canvas, *args, **kwargs)
self._selfItemID = self.canvas.create_window(0, 0, window = self, anchor = tk.NW)
# bindings
self.canvas.bind('<Configure>', self._onCanvasConfigure)
# ---------- setter ----------
def setSize(self, width, height):
print("setSize(%sx%s)" % (width, height))
self.canvas.configure( width = width, height = height )
# ---------- listen to GUI ----------
def _onCanvasConfigure(self, event):
self._updateSize(event.width, event.height)
# ---------- listen to model ----------
def updateScrollregion(self):
self.update_idletasks()
self._updateSize(self.canvas.winfo_width(), self.canvas.winfo_height())
self._updateScrollregion()
# ---------- internal ----------
def _updateSize(self, canvWidth, canvHeight):
hasChanged = False
requWidth = self.winfo_reqwidth()
newWidth = max(canvWidth, requWidth)
if newWidth != self.winfo_width():
hasChanged = True
requHeight = self.winfo_reqheight()
newHeight = max(canvHeight, requHeight)
if newHeight != self.winfo_height():
hasChanged = True
if hasChanged:
print("update size ({width}, {height})".format(width = newWidth, height = newHeight))
self.canvas.itemconfig(self._selfItemID, width = newWidth, height = newHeight)
return True
return False
def _updateScrollregion(self):
bbox = (0,0, self.winfo_reqwidth(), self.winfo_reqheight())
print("updateScrollregion%s" % (bbox,))
self.canvas.config( scrollregion = bbox )
# ==================== TEST ====================
if __name__ == '__main__':
labels = list()
def createLabel():
print("========= create label =========")
l = tk.Label(frame, text="test %s" % len(labels))
l.pack(anchor=tk.W)
labels.append(l)
frame.updateScrollregion()
def removeLabel():
print("========= remove label =========")
labels[-1].destroy()
del labels[-1]
frame.updateScrollregion()
root = tk.Tk()
tk.Button(root, text="+", command=createLabel).pack()
tk.Button(root, text="-", command=removeLabel).pack()
frame = ScrollableFrame(root, bg="white")
frame._parentFrame.pack(expand=tk.YES, fill=tk.BOTH)
createLabel()
frame.setSize(labels[0].winfo_width(), labels[0].winfo_height()*5.9)
#TODO: why is size saved in event object different from what I have specified here?
root.mainloop()
procedure to reproduce the infinite loop is unchanged:
click "+" until the vertical scrollbar appears, click "-" once so that vertical scrollbar disappears again, click "+" again. The window's size is rapidly increasing and decreasing. The occurence of this behaviour depends on the size of the window. The loop can be broken by resizing or closing the window.
to reproduce the jump in size:
click "+" until horizontal [!] scrollbar appears (the window height then increases by the size of the scrollbar, which is ok). Increase width of window until horizontal scrollbar disappears. The height [!] of the window increases with a jump.
to reproduce that the canvas is not filled:
comment out the line which calls frame.setSize. Click "+" until vertical scrollbar appears.
Between the scrollbar and the white background of the frame the red background of the canvas becomes visible. This red area looks around as wide as the scrollbar. When clicking "+" or "-" or resizing the window the red area disappears and does not seem to appear again.

When Tkinter Frame class too long, event "configure" stop tracking it

Hey guys I'm working on a image reading GUI using Tkinter, trying to make a auto resize frame in canvas, and it's scrollbar should renew when new image add into frame.
I found a really helpful example on web and I modified it to a more general one:
import Tkinter
from PIL import Image, ImageTk
root = Tkinter.Tk()
root.geometry("%dx%d+%d+%d" % (1400,807,0,0))
scrollable = ScrollableFrame(root)
scrollable.pack( fill = Tkinter.BOTH)
image_container = []
for i in range (50):
# Here's the problem! Scrollbar works properly, but after scroll down to 50000
# units, is screen stop at 50000, and when scroll up back to lower than 50000,
# it works again !!!!
img = ImageTk.PhotoImage( Image.open("%s.jpg"%(i)) ) )
image_container.append( Tkinter.Label( scrollable.interior, image = img )
image_container[ len( image_container ) -1 ].image = img
class ScrollableFrame(Tkinter.Frame):
def __init__(self, parent, *args, **kw):
self.set_parameter( kw )
Tkinter.Frame.__init__(self, parent, *args, **kw)
# create a canvas object and a vertical scrollbar for scrolling it
self.scrollbar = Tkinter.Scrollbar(self, orient= self.par["orient"])
self.scrollbar.pack(fill= self.par["scroll_fill"], side= self.par["scroll_side"], expand=Tkinter.FALSE)
self.canvas = Tkinter.Canvas(self, bd=0, highlightthickness=0, bg = self.par["canvas_bg"] )
if self.par["orient"] in ("vertical",Tkinter.VERTICAL):
self.canvas.config(yscrollcommand = self.scrollbar.set )
self.scrollbar.config(command=self.canvas.yview)
else:
self.canvas.config(xscrollcommand = self.scrollbar.set )
self.scrollbar.config(command=self.canvas.xview)
self.canvas.pack(side= self.par["canvas_side"], fill=Tkinter.BOTH, expand=Tkinter.TRUE)
# reset the view
self.canvas.xview_moveto(0)
self.canvas.yview_moveto(0)
# create a frame inside the canvas which will be scrolled with it
self.interior = interior = Tkinter.Frame(self.canvas, bg = self.par["canvas_bg"])
interior_id = self.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())
print size, "interior"
self.canvas.config(scrollregion="0 0 %s %s" % size)
if self.par["orient"] in ("vertical",Tkinter.VERTICAL):
if interior.winfo_reqwidth() != self.canvas.winfo_width():
# update the canvas's width to fit the inner frame
self.canvas.config(width=interior.winfo_reqwidth())
else:
if interior.winfo_reqheight() != self.canvas.winfo_height():
# update the canvas's width to fit the inner frame
self.canvas.config(height=interior.winfo_reqheight())
interior.bind('<Configure>', _configure_interior)
def _configure_canvas(event):
print "canvas"
if self.par["orient"] in ("vertical",Tkinter.VERTICAL):
if interior.winfo_reqwidth() != self.canvas.winfo_width():
# update the inner frame's width to fill the canvas
self.canvas.itemconfigure(interior_id, width=self.canvas.winfo_width())
else:
if interior.winfo_reqheight() != self.canvas.winfo_height():
# update the inner frame's height to fill the canvas
self.canvas.itemconfigure(interior_id, height=self.canvas.winfo_height())
self.canvas.bind('<Configure>', _configure_canvas)
def set_parameter(self, options):
self.par = {"orient": "vertical",
"canvas_bg":"white"}
if "orient" in options.keys():
self.par["orient"] = options["orient"]
options.pop("orient")
if "canvas_bg" in options.keys():
self.par["canvas_bg"] = options["canvas_bg"]
options.pop("canvas_bg")
if self.par["orient"] in ("vertical",Tkinter.VERTICAL):
self.par["scroll_fill"] = Tkinter.Y
self.par["scroll_side"] = Tkinter.RIGHT
self.par["canvas_side"] = Tkinter.LEFT
else:
self.par["scroll_fill"] = Tkinter.X
self.par["scroll_side"] = Tkinter.BOTTOM
self.par["canvas_side"] = Tkinter.TOP
This work fine at first, but when I load about 40 images, the frame height goes over 50000( units) and the configure don't works! Just can't find out what's wrong here, is there a system limit height?

Tkinter custom window

I want to make a window in Tk that has a custom titlebar and frame. I have seen many questions on this website dealing with this, but what I'm looking for is to actually render the frame using a canvas, and then to add the contents to the canvas. I cannot use a frame to do this, as the border is gradiented.
According to this website: http://effbot.org/tkinterbook/canvas.htm#Tkinter.Canvas.create_window-method, I cannot put any other canvas items on top of a widget (using the create_window method), but I need to do so, as some of my widgets are rendered using a canvas.
Any suggestions on how to do this? I'm clueless here.
EDIT: Bryan Oakley confirmed that rendering with a canvas would be impossible. Would it then be possible to have a frame with a custom border color? And if so, could someone give a quick example? I'm sort of new with python.
You can use the canvas as if it were a frame in order to draw your own window borders. Like you said, however, you cannot draw canvas items on top of widgets embedded in a canvas; widgets always have the highest stacking order. There is no way around that, though it's not clear if you really need to do that or not.
Here's a quick and dirty example to show how to create a window with a gradient for a custom border. To keep the example short I didn't add any code to allow you to move or resize the window. Also, it uses a fixed color for the gradient.
import Tkinter as tk
class GradientFrame(tk.Canvas):
'''A gradient frame which uses a canvas to draw the background'''
def __init__(self, parent, borderwidth=1, relief="sunken"):
tk.Canvas.__init__(self, parent, borderwidth=borderwidth, relief=relief)
self._color1 = "red"
self._color2 = "black"
self.bind("<Configure>", self._draw_gradient)
def _draw_gradient(self, event=None):
'''Draw the gradient'''
self.delete("gradient")
width = self.winfo_width()
height = self.winfo_height()
limit = width
(r1,g1,b1) = self.winfo_rgb(self._color1)
(r2,g2,b2) = self.winfo_rgb(self._color2)
r_ratio = float(r2-r1) / limit
g_ratio = float(g2-g1) / limit
b_ratio = float(b2-b1) / limit
for i in range(limit):
nr = int(r1 + (r_ratio * i))
ng = int(g1 + (g_ratio * i))
nb = int(b1 + (b_ratio * i))
color = "#%4.4x%4.4x%4.4x" % (nr,ng,nb)
self.create_line(i,0,i,height, tags=("gradient",), fill=color)
self.lower("gradient")
class SampleApp(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
self.wm_overrideredirect(True)
gradient_frame = GradientFrame(self)
gradient_frame.pack(side="top", fill="both", expand=True)
inner_frame = tk.Frame(gradient_frame)
inner_frame.pack(side="top", fill="both", expand=True, padx=8, pady=(16,8))
b1 = tk.Button(inner_frame, text="Close",command=self.destroy)
t1 = tk.Text(inner_frame, width=40, height=10)
b1.pack(side="top")
t1.pack(side="top", fill="both", expand=True)
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
Here is a rough example where the frame, titlebar and close button are made with canvas rectangles:
import Tkinter as tk
class Application(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
# Get rid of the os' titlebar and frame
self.overrideredirect(True)
self.mCan = tk.Canvas(self, height=768, width=768)
self.mCan.pack()
# Frame and close button
self.lFrame = self.mCan.create_rectangle(0,0,9,769,
outline='lightgrey', fill='lightgrey')
self.rFrame = self.mCan.create_rectangle(760,0,769,769,
outline='lightgrey', fill='lightgrey')
self.bFrame = self.mCan.create_rectangle(0,760,769,769,
outline='lightgrey', fill='lightgrey')
self.titleBar = self.mCan.create_rectangle(0,0,769,20,
outline='lightgrey', fill='lightgrey')
self.closeButton = self.mCan.create_rectangle(750,4,760, 18,
activefill='red', fill='darkgrey')
# Binds
self.bind('<1>', self.left_mouse)
self.bind('<Escape>', self.close_win)
# Center the window
self.update_idletasks()
xp = (self.winfo_screenwidth() / 2) - (self.winfo_width() / 2)
yp = (self.winfo_screenheight() / 2) - (self.winfo_height() / 2)
self.geometry('{0}x{1}+{2}+{3}'.format(self.winfo_width(),
self.winfo_height(),
xp, yp))
def left_mouse(self, event=None):
obj = self.mCan.find_closest(event.x,event.y)
if obj[0] == self.closeButton:
self.destroy()
def close_win(self, event=None):
self.destroy()
app = Application()
app.mainloop()
If I were going to make a custom GUI frame I would consider creating it with images,
made with a program like Photoshop, instead of rendering canvas objects.
Images can be placed on a canvas like this:
self.ti = tk.PhotoImage(file='test.gif')
self.aImage = mCanvas.create_image(0,0, image=self.ti,anchor='nw')
More info →here←

Categories

Resources