I'm trying to insert a scrollbar into a canvas filled with buttons (where, buttons can be switched to clickable text as well).
With the code below, the window seems OK.
However, when I uncomment the lines with the scrollbar, the window geometry scrambles.
Can anybody give a hand to figure out what's wrong with the code?
System: Windows 10, python 3.9.6.
import tkinter as tk
class tWindow(tk.Tk):
frame = None
canvas = None
scrollbar = None
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.geometry("640x480+50+50")
self.setup_frame()
self.setup_canvas()
def setup_frame(self):
self.frame = tk.Frame(master=self, width=640, height=480)
self.frame.configure(bg="#003163")
self.frame.place(x=0, y=0)
def setup_canvas(self):
self.update()
self.canvas = tk.Canvas(master=self.frame, bg="#006331",
width=int(self.frame.winfo_width() / 4), height=self.frame.winfo_height())
self.canvas.place(x=0, y=0)
self.update()
# self.scrollbar = tk.Scrollbar(master=self.canvas, orient=tk.VERTICAL)
# self.scrollbar.pack(side=tk.RIGHT, fill=tk.BOTH, expand=tk.TRUE)
# self.canvas.configure(yscrollcommand=self.scrollbar.set)
# self.scrollbar.configure(command=self.canvas.yview)
# self.update()
tmp_pos = 0
for i in range(20):
btn_tmp = tk.Button(master=self.canvas, text=f"testing testing testing testing testing {i:02} ...",
justify=tk.LEFT, wraplength=self.canvas.winfo_width(), bg="#00c0c0", fg="#000000")
btn_tmp.place(x=0, y=tmp_pos)
self.update()
tmp_pos = btn_tmp.winfo_y() + btn_tmp.winfo_height()
def main():
app = tWindow()
app.mainloop()
if __name__ == "__main__":
main()
See comments in code as additional to the link I have provided. Also see why update is considered harmfull and how do I organize my tkinter gui for more details.
import tkinter as tk
class tWindow(tk.Tk):
frame = None
canvas = None
scrollbar = None
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.geometry("640x480+50+50")
self.setup_frame()
self.setup_canvas()
def setup_frame(self):
self.frame = tk.Frame(master=self, width=640, height=480,bg="#003163")
self.frame.pack(side='left',fill='both',expand=True)##pack left
## self.frame.place(x=0, y=0) ##place requiers too think to much
def setup_canvas(self):
## self.update() ##update is harmfull
self.canvas = tk.Canvas(master=self.frame, bg='orange',#"#006331",
width=int(self.frame.winfo_reqwidth() / 4), height=self.frame.winfo_reqheight())
##use reqwidth/hight to avoid the use of update.
##It takes the requested size instead the actual
self.canvas.pack(side='left')
self.canvas_frame = tk.Frame(self.canvas,bg="#006331")
##frame to pack buttons in canvas
self.canvas.create_window((0,0), window=self.canvas_frame, anchor="nw")
##show frame in canvas
self.scrollbar = tk.Scrollbar(master=self.frame, orient=tk.VERTICAL)
self.scrollbar.pack(side='left',fill='y')
self.canvas.configure(yscrollcommand=self.scrollbar.set)
self.scrollbar.configure(command=self.canvas.yview)
self.canvas_frame.bind('<Configure>',self.oncanvasconfigure)
##bind event configure to update the scrollregion
for i in range(20):
btn_tmp = tk.Button(master=self.canvas_frame, text=f"testing testing testing testing testing {i:02} ...",
justify=tk.LEFT, wraplength=self.canvas.winfo_reqwidth(), bg="#00c0c0", fg="#000000")
btn_tmp.pack()
def oncanvasconfigure(self,event):
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
def main():
app = tWindow()
app.mainloop()
if __name__ == "__main__":
main()
Related
I have created few windows using Tkinter. I need help in the implementation of switching from one window to another when the button has been clicked.
All windows that are created should have the same size.
And also I want to clear existing window data and show next window data.
If you want to have multiple windows opened and want to switch between each window with all of their widgets intact then I don't think destroying a window each time you switch is a good idea instead you can try to withdraw and deiconify the windows.
I've created something like this which can switch between windows and maintain the same geometry of the previous window as you said.
import tkinter as tk
class Window(tk.Toplevel):
# List to keep the reference of all the toplevel windows
_info_pages = []
def __init__(self, master=None, cnf={}, **kw):
kw = tk._cnfmerge( (cnf,kw) )
width = kw.pop('width', master.winfo_width()) # 250x250 will be the standard size of the window
height = kw.pop('height', master.winfo_height())
title = kw.pop('title', 'Win %s' %(len(self._info_pages)+1) )
super(Window, self).__init__(master=master, cnf=cnf, **kw)
for i in self._info_pages: i.wm_withdraw() # Hide the previous windows
if self._info_pages and width == master.winfo_width():
self.wm_geometry(self._info_pages[-1].winfo_geometry())
else:
self.wm_geometry("%dx%d+%d+%d" % (width, height,
master.winfo_rootx()+master.winfo_width(), master.winfo_rooty()))
self._info_pages.append(self)
self.title(title)
self.B1 = tk.Button(self, text='◀ Prev', padx=5, command=self.switch_to_prev)
self.B1.place(relx=0, rely=1, anchor='sw')
self.B2 = tk.Button(self, text='Next ▶', padx=5, command=self.switch_to_next)
self.B2.place(relx=1, rely=1, anchor='se')
self.enable_disable_button()
def enable_disable_button(self):
"""Enable and disable the buttons accordingly if there is no window."""
for i in self._info_pages:
if i == self._info_pages[0]: i.B1['state'] = 'disabled'
else: i.B1['state'] = 'normal'
if i == self._info_pages[-1]: i.B2['state'] = 'disabled'
else: i.B2['state'] = 'normal'
def switch_to_prev(self):
"""Switch to the previous window"""
index = self._info_pages.index(self)
if index != 0:
for i in self._info_pages:
i.wm_withdraw()
self._info_pages[index-1].geometry(self.winfo_geometry())
self._info_pages[index-1].wm_deiconify()
def switch_to_next(self):
"""Switch to the next window"""
index = self._info_pages.index(self)
if index+1 != len(self._info_pages):
for i in self._info_pages:
i.wm_withdraw()
self._info_pages[index+1].geometry(self.winfo_geometry())
self._info_pages[index+1].wm_deiconify()
def destroy(self):
"""if a window is destroyed this will open the last window in the list"""
self._info_pages.remove(self)
if self._info_pages:
self._info_pages[-1].geometry(self.winfo_geometry())
self._info_pages[-1].wm_deiconify()
self.enable_disable_button()
return super().destroy()
# This is just a demo
if __name__ == '__main__':
import random as rnd
root = tk.Tk()
root.geometry('250x250')
root.title("I'm the main window")
colorlist = ['beige','bisque','black','blanchedalmond','blue','blueviolet',
'burlywood', 'cadetblue','chartreuse','chocolate' ]
def create_window():
Window(root, bg=rnd.choice(colorlist))
tk.Button(root, text='Create Window', command=create_window).pack()
root.mainloop()
I create a simple application interface in python with tkinter.
[you can run the code]
It has 2 frames inside [self.CON] frame.
Frame one is for header [self.HeaderFrame] and second frame [self.CFrame] is a dynamically populated list inside scrolling canvas [self.Content].
Since the number of items in scrolling canvas [self.Content] will change, I need to resize the canvas each time list was populated or window resized.
from tkinter import *
import tkinter.font as font
from PIL import Image, ImageTk
class wmGUI (object):
def __init__ (self,tittle,itemnum):
self.tittle=tittle
self.GUI = wmWINDOW(self.tittle,itemnum)
class wmWINDOW(Tk):
windowwidth,windowheight,sliderresizer = 250,600,100
def __init__ (self,tittle,itemnum):
super().__init__()
WW = self.winfo_screenwidth()
WH = self.winfo_screenheight()
print('WW : %d & WH : %d' % (WW,WH))
geometry = '{width}x{height}+{pos_x}+{pos_y}'.format(height=WH, width=self.windowwidth, pos_y=0, pos_x=WW-self.windowwidth-20)
self.geometry(geometry)
self.configure(background='grey')
self.title("GUI")
self.itemnum=itemnum
self.resizable(width=False, height=True)
self.attributes('-topmost',True)
self.CreateMenus()
self.Myfont= font.Font(family="Arial", size=8, weight=font.NORMAL)
self.Myfont2 = font.Font(family="Helvetica", size=9, weight=font.BOLD)
self.Myfont3 = font.Font(family="Helvetica", size=7, weight=font.BOLD)
self.CON=Frame(self,width=self.windowwidth,height=WH)
self.MakeHeader()
self.MakeContent()
self.CON.pack(fill=BOTH, expand=YES)
self.RUN()
def MakeHeader (self):
self.HeaderFrame= Frame(self.CON, width=self.windowwidth,height=60, background='Blue',borderwidth=0 )
self.HeaderFrame.propagate(1)
hImage = Image.open("images/header.jpg")
headerImage=ImageTk.PhotoImage(hImage, width= self.windowwidth, height=60)
self.headerLabel=Label(self.HeaderFrame,image=headerImage, width= self.windowwidth, height=60, borderwidth=0, highlightthickness=0)
self.headerLabel.image=headerImage
self.headerLabel.pack( )
self.HeaderFrame.pack( padx=0,pady=0,ipadx=0,ipady=0)
def MakeContent (self):
self.CFrame = Frame(self.CON, width=self.windowwidth,bg="cyan")
self.Content = Canvas(self.CFrame, width=self.windowwidth-15, bg="green",borderwidth=0, highlightthickness=0)
self.ContentFrame = Frame(self.Content, bg="#EBEBEB",height=100)
self.Content.create_window(0, 0, window=self.ContentFrame, anchor='nw')
self.ContentFrame.bind("<Configure>", self.on_change)
self.Items()
self.CScrollbar = Scrollbar(self.CFrame, orient=VERTICAL , width=15)
self.CScrollbar.config(command=self.Content.yview)
self.Content.config(yscrollcommand=self.CScrollbar.set)
self.CScrollbar.grid(row=0, column=1, sticky="ns")
self.Content.grid(row=0, column=0, sticky="nsew")
self.CFrame.pack(fill=BOTH, expand=YES)
self.Content.configure(height=self.CON.winfo_reqheight() - 60)
def on_change(self,event):
self.Content.configure(scrollregion=self.Content.bbox("all"))
def Items(self):
for n in range(self.itemnum):
LabelFrame(self.ContentFrame, text='Example Item {}'.format(n) , font=self.Myfont3, bg="yellow", fg="black",width=self.windowwidth,height=40).grid()
def RUN (self):
self.mainloop()
def CreateMenus(self):
#MAIN MENUBAR
MenuBar = Menu(self) #Creates Menu bar
self.config(menu=MenuBar)
# FILE MENU
FileMenu=Menu(MenuBar,tearoff=0)
FileMenu.add_command(label="Open",command=None) #command=bewritten
FileMenu.add_command(label="Close",command=None)
FileMenu.add_separator()
FileMenu.add_command(label="Reset", accelerator="Ctrl + Shift + r")
FileMenu.add_separator()
FileMenu.add_command(label="Quit", command= None,accelerator="Ctrl q")
MenuBar.add_cascade(label="File",menu=FileMenu)
WMGUI=wmGUI('My Window',50)
I assume to resize the canvas [self.Content] I should retrieve the window inner height. But I can not find a method for it.
How can i do this ?
I found the solution . The example here really helps..
LINK
The format of frames in the window was like following
[Frame CON] -[Frame Header] -[Frame Content] -[Content Canvas] > [/Frame CON]
I binded on_change function to self.CON ; container frame since the container frame will change each time CON content changed.
self.CON.bind("<Configure>", self.on_change)
I changed on_change function to :
def on_change(self,event):
self.Content.configure(scrollregion=self.Content.bbox("all"))
self.Content.configure(height=self.CON.winfo_height()-60)
So on an event, on_change event gets the height of CON container frame and applies height-60 to canvas each time CON changed.
This really worked for the scrollbar to be resize when content created...
Also added an event handler to the root window ..This will resize the scrollbar when the window size changes.
self.bind('<Configure>', self.on_change)
Seems like everything works fine now ...
Forgive me in advance for my code, but I'm simply making this for friend of mine to automatically populate a GUI interface with song information, channel information for each song, and things such as images attached to the songs. Right now I'm only scraping from a playlist on Youtube and a playlist on Soundcloud. I have all of that properly working, but me being new to frontend development left me in a horrible spot to make a decent application for him. I had a lot in mind that I could have done, but now I'm simply creating buttons with each song title as the text. Here is an image of my progress. I still have to find a way to attach each image to each button for the on_enter event, but that is for later on. As you can see, I have a on_leave function commented out. I was using that to delete the self.image_window each time I left a button. Problem is even a minuscule amount of mouse movement would cause the window to be delete and recreated dozens of times. How do I make it static so when I am hovering over a button it doesn't spam create/delete the window?
Thanks!
from Tkinter import *
import json
import os
import webbrowser
class GUIPopulator(Frame):
def __init__(self, parent):
Frame.__init__(self)
self.parent = parent
self.configure(bg='PeachPuff2')
self.columnconfigure(20, weight=1)
self.rowconfigure(30, weight=1)
self.curtab = None
self.tabs = {}
self.pack(fill=BOTH, expand=1, padx=5, pady=5)
self.column = 0
self.row = 0
def on_enter(self, event):
self.image_window = Toplevel(self)
self.img_path = os.getcwd() + '/Rotating_earth_(large).gif'
self.img = PhotoImage(file=self.img_path)
#self.image_window.minsize(width=200, height=250)
self.image_window.title("Preview")
canvas = Canvas(self.image_window, width=200, height=200)
canvas.pack()
canvas.create_image(100, 100, image=self.img)
#def on_leave(self, enter):
def addTab(self, id):
tabslen = len(self.tabs)
tab = {}
if self.row < 30:
btn = Button(self, text=id,highlightbackground='PeachPuff2' ,command=lambda: self.raiseTab(id))
btn.grid(row=self.row, column=self.column, sticky=W+E)
btn.bind("<Enter>", self.on_enter)
#btn.bind("<Leave>", self.on_leave)
tab['id']=id
tab['btn']=btn
self.tabs[tabslen] = tab
self.raiseTab(id)
self.row +=1
else:
self.row = 0
self.column +=1
btn = Button(self, text=id,highlightbackground='PeachPuff2' ,command=lambda: self.raiseTab(id))
btn.grid(row=self.row, column=self.column, sticky=W+E)
tab['id']=id
tab['btn']=btn
self.tabs[tabslen] = tab
self.raiseTab(id)
def raiseTab(self, tabid):
with open(os.getcwd() + '/../PlaylistListener/CurrentSongs.json') as current_songs:
c_songs = json.load(current_songs)
print(tabid)
if self.curtab!= None and self.curtab != tabid and len(self.tabs)>1:
try:
#webbrowser.open(c_songs[tabid]['link'])
webbrowser.open_new('http://youtube.com')
except:
pass
def main():
root = Tk()
root.title('Playlist Scraper')
root.geometry("1920x1080+300+300")
t = GUIPopulator(root)
with open(os.getcwd() + '/../PlaylistListener/CurrentSongs.json') as current_songs:
c_songs = json.load(current_songs)
for song in c_songs:
t.addTab(song)
root.mainloop()
if __name__ == '__main__':
main()
Example of JSON file provided:
{
"F\u00d8RD - Shadows (feat. Samsaruh)": {
"page_title": "youtube",
"link": "youtube.com/watch?v=CNiV6Pne50U&index=32&list=PLkx04k4VGz1tH_pnRl_5xBU1BLE3PYuzd",
"id": "CNiV6Pne50U",
"channel": "youtube.com/watch?v=CNiV6Pne50U&index=32&list=PLkx04k4VGz1tH_pnRl_5xBU1BLE3PYuzd",
"image_path": [
"http://i4.ytimg.com/vi/CNiV6Pne50U/hqdefault.jpg",
"CNiV6Pne50U.jpg"
]
},
"Katelyn Tarver - You Don't Know (tof\u00fb remix)": {
"page_title": "youtube",
"link": "youtube.com/watch?v=7pPNv38JzD4&index=43&list=PLkx04k4VGz1tH_pnRl_5xBU1BLE3PYuzd",
"id": "7pPNv38JzD4",
"channel": "youtube.com/watch?v=7pPNv38JzD4&index=43&list=PLkx04k4VGz1tH_pnRl_5xBU1BLE3PYuzd",
"image_path": [
"http://i4.ytimg.com/vi/7pPNv38JzD4/hqdefault.jpg",
"7pPNv38JzD4.jpg"
]
},
"Illenium - Crawl Outta Love (feat. Annika Wells)": {
"page_title": "youtube",
"link": "youtube.com/watch?v=GprXUDZrdT4&index=7&list=PLkx04k4VGz1tH_pnRl_5xBU1BLE3PYuzd",
"id": "GprXUDZrdT4",
"channel": "youtube.com/watch?v=GprXUDZrdT4&index=7&list=PLkx04k4VGz1tH_pnRl_5xBU1BLE3PYuzd",
"image_path": [
"http://i4.ytimg.com/vi/GprXUDZrdT4/hqdefault.jpg",
"GprXUDZrdT4.jpg"
]
}
}
After some testing I have come up with some code I think you can use or is what you are looking for.
I have changes a few things and add some others.
1st we needed to create a place holder for the top window that we can use later in the code. So in the __init__ section of GUIPopulatior add self.image_window = None. We will talk about this part soon.
next I created the image path as a class attribute on init this can be changed later with update if need be.
next I added a bind() to the init that will help us keep the self.image_window placed in the correct location even if we move the root window. so we add self.parent.bind("<Configure>", self.move_me) to the __init__ section as well.
next we create the method move_me that we just created a bind for.
the way it is written it will only take effect if self.image_window is not equal to None this should prevent any errors while self.image_window is being used however I have not created the error handling to deal with what happens after the toplevel window is closed by the user. Its not difficult but I wanted to answer for the main issue at hand.
Here is the move_me method:
def move_me(self, event):
if self.image_window != None:
h = self.parent.winfo_height() # gets the height of the window in pixels
w = self.parent.winfo_width() # gets the width of the window in pixels
# gets the placement of the root window then uses height and width to
# calculate where to place the window to keep it at the bottom right.
self.image_window.geometry('+{}+{}'.format(self.parent.winfo_x() + w - 250,
self.parent.winfo_y() + h - 250))
next we need to modify the on_enter method to create the toplevel window if out class attribute self.image_window is equal to None if it is not equal to None then we can use the else portion of the if statement to just update the image.
Here is the modified on_enter method:
def on_enter(self, event):
if self.image_window == None:
self.image_window = Toplevel(self)
#this keeps the toplevel window on top of the program
self.image_window.attributes("-topmost", True)
h = self.parent.winfo_height()
w = self.parent.winfo_width()
self.image_window.geometry('+{}+{}'.format(self.parent.winfo_x() + w - 250,
self.parent.winfo_y() + h - 250))
self.img = PhotoImage(file=self.img_path)
self.image_window.title("Preview")
self.canvas = Canvas(self.image_window, width=200, height=200)
self.canvas.pack()
self.canv_image = self.canvas.create_image(100, 100, image=self.img)
else:
self.img = PhotoImage(file= self.img_path)
self.canvas.itemconfig(self.canv_image, image = self.img)
all that being said there are some other issues with your code that need to be addressed however this answer should point you in the right direction.
Below is a section of your code you need to replace:
class GUIPopulator(Frame):
def __init__(self, parent):
Frame.__init__(self)
self.parent = parent
self.configure(bg='PeachPuff2')
self.columnconfigure(20, weight=1)
self.rowconfigure(30, weight=1)
self.curtab = None
self.tabs = {}
self.pack(fill=BOTH, expand=1, padx=5, pady=5)
self.column = 0
self.row = 0
def on_enter(self, event):
self.image_window = Toplevel(self)
self.img_path = os.getcwd() + '/Rotating_earth_(large).gif'
self.img = PhotoImage(file=self.img_path)
#self.image_window.minsize(width=200, height=250)
self.image_window.title("Preview")
canvas = Canvas(self.image_window, width=200, height=200)
canvas.pack()
canvas.create_image(100, 100, image=self.img)
#def on_leave(self, enter):
With this:
class GUIPopulator(Frame):
def __init__(self, parent):
Frame.__init__(self)
self.parent = parent
self.configure(bg='PeachPuff2')
self.columnconfigure(20, weight=1)
self.rowconfigure(30, weight=1)
self.curtab = None
self.image_window = None
self.img_path = os.getcwd() + '/Rotating_earth_(large).gif'
self.tabs = {}
self.pack(fill=BOTH, expand=1, padx=5, pady=5)
self.parent.bind("<Configure>", self.move_me)
self.column = 0
self.row = 0
def move_me(self, event):
if self.image_window != None:
h = self.parent.winfo_height()
w = self.parent.winfo_width()
self.image_window.geometry('+{}+{}'.format(self.parent.winfo_x() + w - 250,
self.parent.winfo_y() + h - 250))
def on_enter(self, event):
if self.image_window == None:
self.image_window = Toplevel(self)
self.image_window.attributes("-topmost", True)
h = self.parent.winfo_height()
w = self.parent.winfo_width()
self.image_window.geometry('+{}+{}'.format(self.parent.winfo_x() + w - 250,
self.parent.winfo_y() + h - 250))
self.img = PhotoImage(file=self.img_path)
self.image_window.title("Preview")
self.canvas = Canvas(self.image_window, width=200, height=200)
self.canvas.pack()
self.canv_image = self.canvas.create_image(100, 100, image=self.img)
else:
self.img = PhotoImage(file= self.img_path)
self.canvas.itemconfig(self.canv_image, image = self.img)
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←