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.
Related
I'm trying to create a page that outputs a large amount of data, and wraps the text dynamically depending on window size. I started by setting wraplength = self.master.winfo_width(), which sets the text wrapping to the current window size, but it does not change when the window does. I found this answer, which seemed like it would solve the problem, but when trying to recreate it myself, something went wrong. I suspect that I'm misunderstanding something with .bind or <Configure>, but I can't be sure. My base code is as follows:
from tkinter import *
class Wrap_example(Frame):
def __init__(self):
Frame.__init__(self)
self.place(relx=0.5, anchor='n')
#Initalize list and variable that populates it
self.data_list = []
self.data = 0
#Button and function for creating a bunch of numbers to fill the space
self.button = Button(self, text = "Go", command = self.go)
self.button.grid()
def go(self):
for self.data in range(1, 20000, 100):
self.data_list.append(self.data)
#Label that holds the data, text = list, wraplength = current window width
self.data = Label(self, text = self.data_list, wraplength = self.master.winfo_width(), font = 'arial 30')
self.data.grid()
#Ostensibly sets the label to dynamically change wraplength to match new window size when window size changes
self.data.bind('<Configure>', self.rewrap())
def rewrap(self):
self.data.config(wraplength = self.master.winfo_width())
frame01 = Wrap_example()
frame01.mainloop()
A few things of note: I tried using the lambda directly as shown in the linked answer, but it didn't work. If I remove the rewrap function and use self.data.bind('<Configure>', lambda e: self.data.config(wraplength=self.winfo_width()), it throws a generic Syntax error, always targeting the first character after that line, (the d in def if the function is left in, the f in frame01 if it's commented out). Leaving rewrap as-is doesn't throw an error, but it doesn't perform any other apparent function, either. Clicking 'Go' will always spawn data that wraps at the current window size, and never changes.
There are few issues:
frame Wrap_example does not fill all the horizontal space when window is resized
label self.data does not fill all the horizontal space inside frame Wrap_example when the frame is resized
self.rewrap() will be executed immediately when executing the line self.data.bind('<Configure>', self.rewrap())
To fix the above issues:
set relwidth=1 in self.place(...)
call self.columnconfigure(0, weight=1)
use self.data.bind('<Configure>', self.rewrap) (without () after rewrap) and add event argument in rewrap()
from tkinter import *
class Wrap_example(Frame):
def __init__(self):
Frame.__init__(self)
self.place(relx=0.5, anchor='n', relwidth=1) ### add relwidth=1
self.columnconfigure(0, weight=1) ### make column 0 use all available horizontal space
#Initalize list and variable that populates it
self.data_list = []
self.data = 0
#Button and function for creating a bunch of numbers to fill the space
self.button = Button(self, text = "Go", command = self.go)
self.button.grid()
def go(self):
for self.data in range(1, 20000, 100):
self.data_list.append(self.data)
#Label that holds the data, text = list, wraplength = current window width
self.data = Label(self, text = self.data_list, wraplength = self.master.winfo_width(), font = 'arial 30')
self.data.grid()
#Ostensibly sets the label to dynamically change wraplength to match new window size when window size changes
self.data.bind('<Configure>', self.rewrap) ### remove () after rewrap
def rewrap(self, event): ### add event argument
self.data.config(wraplength = self.master.winfo_width())
frame01 = Wrap_example()
frame01.mainloop()
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()
I am creating a simple program using Tkinter. I want a function to be called every time xview property of entry changes. But there doesn't seem to be an event like this, at least not one that I can find.
The <Configure> event fires only on resize, which I already handled, but it doesn't fire when actual value I'm tracking changes in a different way, such as the user dragging his mouse to see the end of the entry.
Here is the code:
import Tkinter as Tk
import tkFileDialog
root = Tk.Tk()
class RepositoryFolderFrame(Tk.Frame):
def __init__(self, root):
Tk.Frame.__init__(self, root)
self.build_gui()
self.set_entry_text("Searching...")
#root.after(0, self.find_repo)
self.prev_entry_index = len(self.entry.get())
root.bind("<Configure>", self.on_entry_resize)
#self.entry.bind(???, self.on_entry_change)
#self.entry.bind("<Configure>", self.on_entry_change)
def on_entry_resize(self, event):
cur_entry_index = self.entry.xview()[1]
if cur_entry_index != self.prev_entry_index:
self.entry.xview(self.prev_entry_index)
def on_entry_change(self, event):
# This should be called when xview changes...
cur_entry_index = self.entry.xview()[1]
self.prev_entry_index = cur_entry_index
def set_entry_text(self, text):
self.entry_text.set(text)
self.entry.xview("end")
def build_gui(self):
label = Tk.Label(self, text = "Repository folder:")
label.pack(side = Tk.LEFT)
self.label = label
entry_text = Tk.StringVar()
self.entry_text = entry_text
entry = Tk.Entry(self, width = 50, textvariable = entry_text)
entry.configure(state = 'readonly')
entry.pack(side = Tk.LEFT, fill = Tk.X, expand = 1)
self.entry = entry
button = Tk.Button(self, text = "Browse...")
button.pack(side = Tk.LEFT)
self.button = button
repo_frame = RepositoryFolderFrame(root)
repo_frame.pack(fill = Tk.X, expand = 1)
root.mainloop()
There is no mechanism for getting notified when the xview changes. There are ways to do it by modifying the underlying tcl code, but it's much more difficult than it's worth.
A simple solution is to write a function that polls the xview every few hundred milliseconds. It can keep track of the most recent xview, compare it to the current, and if it has changed it can fire off a custom event (eg: <<XviewChanged>>) which you can bind to.
It would look something like this:
class RepositoryFolderFrame(Tk.Frame):
def __init__(self, root):
...
self.entry.bind("<<XviewChanged>>", self.on_entry_change)
# keep a cache of previous xviews. A dictionary is
# used in case you want to do this for more than
self._xview = {}
self.watch_xview(self.entry)
def watch_xview(self, widget):
xview = widget.xview()
prev_xview = self._xview.get(widget, "")
self._xview[widget] = xview
if xview != prev_xview:
widget.event_generate("<<XviewChanged>>")
widget.after(100, self.watch_xview, widget)
You'll need to modify that for the edge case that the entry widget is destroyed, though you can handle that with a simple try around the code. This should be suitably performant, though you might need to verify that if you have literally hundreds of entry widgets.
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()
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←