Touchscreen scrolling Tkinter Python - python

I am making a kiosk app for a raspberry pi in python with a 7 inch touchscreen.
Everything works well and now, I am trying to make the scroll works like it does on touchscreens. I know that raspbian haven't got a properly touch interface, so, every touch on the screen work as a mouse-click, and if I move my finger touching the screen, works like the drag function.
To make this on python I use my modified version code of this Vertical Scrolled Frame using canvas and I need to add events binding <ButtonPress-1> and <B1-Motion>.
<ButtonPress-1> might save the y position of the click and enable the bind_all function for <B1-Motion>.
<B1-Motion> might set the scroll setting up or down the differrence between the y value saved by <ButtonPress-1> and the event.y of this event.
<ButtonRelease-1> might disable the bind_all function with <unbind_all> of the scroll.
My added code of the events is this, but I don't know how to make it work properly the .yview function of the canvas to make my function works as desired.
def moving(event):
#In this part I don't know how to make my effect
self.canvas.yview('scroll',event.y,"units")
def clicked(event):
global initialy
initialy = event.y
self.canvas.bind_all('<B1-Motion>', moving)
def released(event):
self.canvas.unbind_all('<B1-Motion>')
self.canvas.bind_all('<ButtonPress-1>',clicked)
self.canvas.bind_all('<ButtonRelease-1>', released)

Taking Saad's code as a base, I have modified it to make it work on every S.O. (win, linux,mac) using yview_moveto and I have applied some modifications as I explain here.
EDIT: I have edited the code to show the complete class.
class VerticalScrolledFrame(Frame):
"""A pure Tkinter scrollable frame that actually works!
* Use the 'interior' attribute to place widgets inside the scrollable frame
* Construct and pack/place/grid normally
* This frame only allows vertical scrolling
"""
def __init__(self, parent, bg,*args, **kw):
Frame.__init__(self, parent, *args, **kw)
# create a canvas object and a vertical scrollbar for scrolling it
canvas = Canvas(self, bd=0, highlightthickness=0,bg=bg)
canvas.pack(side=LEFT, fill=BOTH, expand=TRUE)
# reset the view
canvas.xview_moveto(0)
canvas.yview_moveto(0)
self.canvasheight = 2000
# create a frame inside the canvas which will be scrolled with it
self.interior = interior = Frame(canvas,height=self.canvasheight,bg=bg)
interior_id = canvas.create_window(0, 0, window=interior,anchor=NW)
# track changes to the canvas and frame width and sync them,
# also updating the scrollbar
def _configure_interior(event):
# update the scrollbars to match the size of the inner frame
size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
canvas.config(scrollregion="0 0 %s %s" % size)
if interior.winfo_reqwidth() != canvas.winfo_width():
# update the canvas's width to fit the inner frame
canvas.config(width=interior.winfo_reqwidth())
interior.bind('<Configure>', _configure_interior)
def _configure_canvas(event):
if interior.winfo_reqwidth() != canvas.winfo_width():
# update the inner frame's width to fill the canvas
canvas.itemconfigure(interior_id, width=canvas.winfo_width())
canvas.bind('<Configure>', _configure_canvas)
self.offset_y = 0
self.prevy = 0
self.scrollposition = 1
def on_press(event):
self.offset_y = event.y_root
if self.scrollposition < 1:
self.scrollposition = 1
elif self.scrollposition > self.canvasheight:
self.scrollposition = self.canvasheight
canvas.yview_moveto(self.scrollposition / self.canvasheight)
def on_touch_scroll(event):
nowy = event.y_root
sectionmoved = 15
if nowy > self.prevy:
event.delta = -sectionmoved
elif nowy < self.prevy:
event.delta = sectionmoved
else:
event.delta = 0
self.prevy= nowy
self.scrollposition += event.delta
canvas.yview_moveto(self.scrollposition/ self.canvasheight)
self.bind("<Enter>", lambda _: self.bind_all('<Button-1>', on_press), '+')
self.bind("<Leave>", lambda _: self.unbind_all('<Button-1>'), '+')
self.bind("<Enter>", lambda _: self.bind_all('<B1-Motion>', on_touch_scroll), '+')
self.bind("<Leave>", lambda _: self.unbind_all('<B1-Motion>'), '+')

I modified the VerticalScrolledFrame with few lines of code which does scroll how you want. I tested the code with the VerticalScrolledFrame and it works fine with the mouse. Add the below code to the VerticalScrolledFrame.
self.offset_y = 0
def on_press(evt):
self.offset_y = evt.y_root
def on_touch_scroll(evt):
if evt.y_root-self.offset_y<0:
evt.delta = -1
else:
evt.delta = 1
# canvas.yview_scroll(-1*(evt.delta), 'units') # For MacOS
canvas.yview_scroll( int(-1*(evt.delta/120)) , 'units') # For windows
self.bind("<Enter>", lambda _: self.bind_all('<Button-1>', on_press), '+')
self.bind("<Leave>", lambda _: self.unbind_all('<Button-1>'), '+')
self.bind("<Enter>", lambda _: self.bind_all('<B1-Motion>', on_touch_scroll), '+')
self.bind("<Leave>", lambda _: self.unbind_all('<B1-Motion>'), '+')
I hope you can find this useful.

Related

Tkinter auto hiding scroll bar not working

I want to create a text box with two auto hiding scroll bars, horizontally and vertically. The thing is, I'm struggling to set them correctly. Here's an example code:
from tkinter import *
class AutoScrollbar(Scrollbar):
# a scrollbar that hides itself if it's not needed. only
# works if you use the pack geometry manager.
def set(self, lo, hi):
if float(lo) <= 0.0 and float(hi) >= 1.0:
self.pack_forget()
else:
if self.cget("orient") == HORIZONTAL:
self.pack(fill=X, side=BOTTOM)
else:
self.pack(fill=Y, side=RIGHT)
Scrollbar.set(self, lo, hi)
def grid(self, **kw):
raise (TclError, "cannot use grid with this widget")
def place(self, **kw):
raise (TclError, "cannot use place with this widget")
class Listboxapp(Tk):
def __init__(self):
super().__init__()
self.geometry("400x400")
# Text box frame
self.frame1 = Frame(self)
self.frame1.pack(pady=5)
# Create vertical scrollbar
self.text_scroll = AutoScrollbar(self.frame1)
self.text_scroll.pack()
# Create Horizontal AutoScrollbar
self.hor_scroll = AutoScrollbar(self.frame1, orient=HORIZONTAL)
self.hor_scroll.pack()
# Create text box
self.my_text = Text(self.frame1, yscrollcommand=self.text_scroll.set,
wrap=NONE, xscrollcommand=self.hor_scroll.set)
self.my_text.pack()
# Configure scrollbars
self.text_scroll.config(command=self.my_text.yview)
self.hor_scroll.config(command=self.my_text.xview)
if __name__ == '__main__':
w = Listboxapp()
w.mainloop()
This is what it looks like the horizontal scroll bar:
It works fine, however, once the vertical one appears, it looks like this:
It doesn't set correctly on the right side, besides the horizontal one dissapears.
I also tried self.my_text(side=LEFT) but it looks like this:

Python Tkinter Scrollbar and Frame not showing all Checkboxes

having an issue with some tkinter code and I believe I am just too close to it and can't see the issue in front of my face. I am loading checkboxes into a frame and attaching a scrollbar to that location.
This works well until I get to a little over 1000 checkboxes. It then seems to cut off and even though the frame extends a height appropriate for all checkboxes it is not showing them in the gui. You can see in the image here where they stop showing Checkbox Malfunction
Here is my code: (Please excuse how messy it looks, it is a subset of a much larger code set, I've just isolated the error)
from tkinter import *
build_vars = {}
build_Radios = []
parent = Tk()
center_container = Frame(parent, width=5, height=5)
center_container.grid(row=1, sticky="nsew")
# Center Row Columns
center_center_container = Frame(center_container, width=150, height=200)
center_center_container.grid(row=0, column=2, sticky="ns")
build_canvas = Canvas(center_center_container, background='green')
build_canvas.grid(row=0, column=0, sticky=N+E+W+S)
# Create a vertical scrollbar linked to the canvas.
vsbar = Scrollbar(center_center_container, orient=VERTICAL, command=build_canvas.yview)
vsbar.grid(row=0, column=1, sticky=NS)
build_canvas.configure(yscrollcommand=vsbar.set)
# Create a frame on the canvas to contain the buttons.
frame_buttons = Frame(build_canvas, bd=2, background='red')
def create_build_radios():
# for index, item in enumerate(filtered_builds):
for index, item in enumerate(list(range(3000))):
build_vars[item] = IntVar()
radio = Checkbutton(frame_buttons, text=item, variable=build_vars[item], onvalue=1,
offvalue=0,
command=lambda item=item: sel(item))
radio.grid(row=index, column=0, sticky=W)
build_Radios.append(radio)
# Create canvas window to hold the buttons_frame.
build_canvas.create_window((0, 0), window=frame_buttons, anchor=NW)
build_canvas.update_idletasks() # Needed to make bbox info available.
bbox = build_canvas.bbox(ALL) # Get bounding box of canvas with Buttons.
build_canvas.configure(scrollregion=bbox, width=150, height=400)
def sel(item):
print(item)
create_build_radios()
parent.mainloop()
So here is the better solution (way better than the other, can easily put way more widgets on this, tho note that there probably is some kind of limit (at least the CPU's capabilities):
from tkinter import Tk, Canvas, Frame, Label, Scrollbar, Button, DoubleVar, StringVar, Entry
from tkinter.ttk import Progressbar
class PagedScrollFrame(Frame):
def __init__(self, master, items_per_page=100, **kwargs):
Frame.__init__(self, master, **kwargs)
self.master = master
self.items_per_page = items_per_page
self.pages = None
self.id_list = []
self.bbox_tag = 'all'
self._loading_frame = Frame(self)
self.__load_progress_tracker = DoubleVar(master=self.master, value=0.0)
self.__percent_tracker = StringVar(master=self.master, value='0.00%')
self.frame = Frame(self)
self.frame.pack(side='top', padx=20, pady=20)
self.canvas = Canvas(self.frame)
self.canvas.pack(side='left')
self.bg_label = Label(self.canvas)
self.bg_label.place(x=0, y=0, relwidth=1, relheight=1)
self.scrollbar = Scrollbar(self.frame, orient='vertical', command=self.canvas.yview)
self.scrollbar.pack(side='right', fill='y')
self.canvas.config(yscrollcommand=self.scrollbar.set)
self.canvas.bind('<Configure>',
lambda e: self.canvas.config(
scrollregion=self.canvas.bbox(self.bbox_tag)
))
self.button_frame = Frame(self)
self.button_frame.pack(fill='x', side='bottom', padx=20, pady=20)
self.canvas_frame = Frame(self.button_frame)
self.button_canvas = Canvas(self.canvas_frame, height=20)
self.button_canvas.pack(expand=True)
self.inner_frame = Frame(self.button_canvas)
self.button_canvas.create_window(0, 0, window=self.inner_frame, anchor='nw')
self.button_scrollbar = Scrollbar(self.canvas_frame,
orient='horizontal',
command=self.button_canvas.xview)
self.button_scrollbar.pack(fill='x')
self.button_canvas.config(xscrollcommand=self.button_scrollbar.set)
self.button_canvas.bind(
'<Configure>', lambda e: self.button_canvas.config(
scrollregion=self.button_canvas.bbox('all')
)
)
def pack_items(self):
if not self.pages:
return
self._loading_frame.place(x=0, y=0, relwidth=1, relheight=1)
self._loading_frame.lift()
self._loading_frame.update_idletasks()
self.after(100, self._pack_items)
def _pack_items(self):
Label(self._loading_frame, text='Loading...').pack(expand=True)
Progressbar(self._loading_frame,
orient='horizontal',
variable=self.__load_progress_tracker,
length=self._loading_frame.winfo_width()
- self._loading_frame.winfo_width() // 10).pack(expand=True)
Label(self._loading_frame, textvariable=self.__percent_tracker).pack(expand=True)
self.update_idletasks()
widgets = [widget for page in self.pages for widget in page.winfo_children()]
length = len(widgets)
self.after(100, self.__pack_items, widgets, 0, length)
def __pack_items(self, widgets, index, length):
if index >= length:
self._loading_frame.destroy()
self.canvas.config(scrollregion=self.canvas.bbox('all'))
return
widgets[index].pack()
percent = (index + 1) * 100 / length
self.__load_progress_tracker.set(value=percent)
self.__percent_tracker.set(value=f'{percent: .2f}%')
self.after(1, self.__pack_items, widgets, index + 1, length)
def change_frame(self, index):
if not self.pages:
return
self.bbox_tag = self.id_list[index]
self.canvas.config(scrollregion=self.canvas.bbox(self.bbox_tag))
self.bg_label.lift()
self.pages[index].lift()
def create_pages(self, num_of_items, items_per_page=None):
self.pages = None
if not items_per_page:
items_per_page = self.items_per_page
num_of_pages = num_of_items // items_per_page
if num_of_items % items_per_page != 0:
num_of_pages += 1
start_indexes = [items_per_page * page_num for page_num in range(num_of_pages)]
end_indexes = [num + items_per_page for num in start_indexes]
end_indexes[-1] += (num_of_items % items_per_page
- (items_per_page if num_of_items % items_per_page != 0 else 0))
self.pages = [Frame(self.canvas) for _ in range(num_of_pages)]
self.id_list = []
for page, frame in enumerate(self.pages):
self.id_list.append(self.canvas.create_window(0, 0, window=frame, anchor='nw'))
self.pages[0].lift()
if num_of_pages >= 2:
Button(self.button_frame, text='1',
command=lambda: self.change_frame(0)).pack(
side='left', expand=True, fill='both', ipadx=5
)
if num_of_pages > 2:
self.canvas_frame.pack(fill='x', expand=True, side='left')
for page_num in range(1, num_of_pages - 1):
Button(self.inner_frame, text=page_num + 1,
command=lambda index=page_num: self.change_frame(index)).pack(
expand=True, fill='both', side='left', ipadx=5
)
Button(self.button_frame, text=num_of_pages,
command=lambda: self.change_frame(num_of_pages - 1)).pack(
side='right', fill='both', expand=True, ipadx=5
)
return zip(start_indexes, end_indexes, self.pages)
def create_paged_canvas():
scroll = PagedScrollFrame(root)
scroll.pack()
lst = tuple(range(3000))
for start, end, parent in scroll.create_pages(len(lst)):
for i in lst[start:end]:
frame_ = Frame(parent)
Label(frame_, text=str(i).zfill(4)).pack(side='left')
scroll.pack_items()
root = Tk()
root.protocol('WM_DELETE_WINDOW', exit)
create_paged_canvas()
root.mainloop()
Main info:
Basically this creates paged scrollable canvas. All that is needed is to adjust the inner loop and the iterable in the create_paged_canvas() function. You can also adjust how many items per page to show (that also allows for later configuration for example in a menu you could call a similar to create_paged_canvas() function and change the items_per_page argument to sth else (will have to load everything again but... tkinter is tkinter, it is pretty slow and even worse it doesn't allow directly using threads, not even talking about processes (that stuff would speed things up very much but simply can't be done)))
Important (suggestion):
I strongly advise against using wildcard (*) when importing something, You should either import what You need, e.g. from module import Class1, func_1, var_2 and so on or import the whole module: import module then You can also use an alias: import module as md or sth like that, the point is that don't import everything unless You actually know what You are doing; name clashes are the issue.
Misc:
for better performance it would be better to instead of creating labels simply create text directly on canvas (using the other solution) or use a listbox, that is in case if you need to display huge amounts of data because it will speed things up since no widgets have to be created (it also means you can only view the data pretty much)
If you have any questions, be sure to ask those!
Here comes the solution:
Very Important
There is some bizzare bug that I have no idea how to fix (yet at least) regarding performance and stuff, which makes this solution pretty unusable, I mean theoretically you will be able to put more widgets on this and scroll than the usual method but depending on the CPU it may work or it may not work that well. I will simply write a pagination answer
from tkinter import (Tk, Frame, Label, Scrollbar, Canvas,
DoubleVar, Entry, Button, StringVar, TclError)
from tkinter.ttk import Progressbar
class EndlessScroll(Canvas):
def __init__(self, master, update_mode='passive', scrollbar=None, **kwargs):
"""update_mode:
'passive' (recommended and default): widgets have to be accessed (Text, Entry...)
or using Frame, DON'T use if only single widget per line,
USE Frame with this
'active': recommended only if single widget
per line (best with Label
but can use Button too)"""
Canvas.__init__(self, master, **kwargs)
self.master = master
self._widget_heights = []
self._start_index = 0
self._widgets = []
self.temp_frame = None
self.__got_height = False
self.__initialised_placement = False
self.__height_counter = 0
self.yscrollincrement = None
self._update_mode = update_mode
self.scrollbar = scrollbar
self.__load_progress_tracker = DoubleVar(master=self.master, value=0.0)
self.__percent_tracker = StringVar(master=self.master, value='0.0%')
self._allow_mouse_control = False
self.bind('<Enter>', lambda e: setattr(self, '_allow_mouse_control', True))
self.bind('<Leave>', lambda e: setattr(self, '_allow_mouse_control', False))
self.bind('<MouseWheel>', self.__scroll_with_mouse)
for key, value in kwargs.items():
setattr(self, key, value)
def __scroll_with_mouse(self, event):
if not self._allow_mouse_control:
return
self.yscroll('scroll', (-1 * (event.delta/120)), 'units')
def set_scrollbar(self, scrollbar):
self.scrollbar = scrollbar
def init_after_widgets(self):
self.temp_frame = Frame(self)
self._widgets = self.winfo_children()[:-1]
self.temp_frame.place(x=0, y=0, relwidth=1, relheight=1)
self.update()
Label(self.temp_frame, text='Loading...').pack(expand=True)
Progressbar(self.temp_frame,
orient='horizontal',
variable=self.__load_progress_tracker,
length=self.temp_frame.winfo_width()
- self.temp_frame.winfo_width() // 10).pack(expand=True)
Label(self.temp_frame, textvariable=self.__percent_tracker).pack(expand=True)
self.after(100, self._get_widget_heights, 0)
def _get_widget_heights(self, index):
length = len(self._widgets)
self.after(100, self.__get_widget_heights, index, self._widgets, length)
def __get_widget_heights(self, index, widgets, length):
if index >= length:
self.__got_height = True
self.temp_frame.place_forget()
self._initial_placement(widgets)
self.update_idletasks()
self.__update()
self.yscroll(None, 0)
return
id_ = self.create_window(0, 0, window=widgets[index], anchor='nw')
self.update()
try:
self._widget_heights.append(widgets[index].winfo_height())
self.delete(id_)
except TclError:
pass
percent = (index + 1) * 100 / length
self.__load_progress_tracker.set(value=percent)
self.__percent_tracker.set(value=f'{percent: .2f}%')
self.after(1, self.__get_widget_heights, index + 1, widgets, length)
def yscroll(self, *args):
if not self.__initialised_placement or not self.scrollbar:
return
type_, fraction = args[0], args[1]
parent_height = self.winfo_height()
if type_ == 'scroll':
units = args[2]
k = float(fraction)
if k < -1.0:
k = -1.0
elif k > 1.0:
k = 1.0
top, bottom, *_ = self.scrollbar.get()
length = (bottom - top) if not self.yscrollincrement else self.yscrollincrement
if (top == 0.0 and k < 0) or (bottom == 1.0 and k > 0):
return
if 0 < top < length:
length = top
if 0 < (1 - bottom) < length:
length = 1 - bottom
if units == 'units':
fraction = top + (k * length)
elif units == 'pages':
return
# fraction = top + (k * (0.9 * parent_height) * length)
sum_height = sum(self._widget_heights)
scroll_height = float(fraction) * sum_height
height_counter = 0
for index_, height in enumerate(self._widget_heights):
height_counter += height
if height_counter > scroll_height:
self._start_index = index_
self.__height_counter = -(height - (height_counter - scroll_height))
break
self.scrollbar.set(float(fraction), float(fraction) + parent_height / sum_height)
if self._update_mode == 'passive':
self.__update()
def __update(self, _call_from_self=False):
if _call_from_self and self._update_mode == 'passive':
self.after(10, self.__update, True)
return
parent_height = self.winfo_height()
height_counter = self.__height_counter
self.delete('all')
for height, widget in zip(self._widget_heights[self._start_index:], self._widgets[self._start_index:]):
if height_counter > parent_height:
break
self.create_window(0, height_counter, window=widget, anchor='nw')
height_counter += height
self.after(10, self.__update, True)
def _initial_placement(self, widgets):
parent_height = self.winfo_height()
height_counter = 0
for height, widget in zip(self._widget_heights, widgets):
if height_counter > parent_height:
self.__initialised_placement = True
return
self.create_window(0, height_counter, window=widget, anchor='nw')
height_counter += height
def create_scroller():
endless_scroll = EndlessScroll(root, yscrollincrement=0.1)
endless_scroll.pack(side='left')
for i in range(100):
frame = Frame(endless_scroll)
Label(frame, text=str(i).zfill(4)).pack()
scrollbar = Scrollbar(root, command=endless_scroll.yscroll)
scrollbar.pack(side='right', fill='y')
endless_scroll.set_scrollbar(scrollbar)
endless_scroll.init_after_widgets()
root = Tk()
create_scroller()
root.mainloop()
MAIN_EDIT:
I polished the widget a bit, rest of the instructions are still valid, but I fixed the below issue with having a widget so that you can see the last widget added (was an index problem), now you can see all of them. Added mouse scrolling and scrolling with the scrollbar's arrows (still can't move it by clicking on scrollbar, no idea how to do that either (yet)). Also now shows percentage, reduced some waiting time. Also now it catches the error which gets raised if you close the window while loading is in progress.
Basically you just have to go to the create_scroller() function definition and adjust the loop for your needs.
Important (suggestion)
I strongly advise against using wildcard (*) when importing something, You should either import what You need, e.g. from module import Class1, func_1, var_2 and so on or import the whole module: import module then You can also use an alias: import module as md or sth like that, the point is that don't import everything unless You actually know what You are doing; name clashes are the issue.
The main instruction here is that you have to place the .init_after_widgets() method after you have added widgets to the EndlessScroll widget (don't pack the or anything just add them (as you can see in the example)). Note the special widget, will try to fix that too so that it works properly and also very important is that it currently only works with dragging the slider of the scrollbar (will fix that too (at least try)).
The thing is that it now works, it can theoretically show any number of widgets (tried with 50000 but that was very laggy and I had to close it but with the 3000 it worked like a charm), also great thing is that the window is responsive while "loading" the widgets (loading is necessary for getting the height of the widgets which is crucial) so yeah, you can try improving this yourself but I will also try doing that myself too but a bit later.
EDIT1:
For example if you wanted to have more widgets together you can use frames:
for i in range(100):
frame = Frame(endless_scroll)
Label(frame, text=f'Entry {i}:').pack(side='left')
Entry(frame).pack(side='right')
Solved this to an extent. See edit below.
Tho this specific example has a slight issue with the entries because of the .__update() loop, otherwise widgets that are static should work completely fine (Buttons have less of an issue but it might be harder to press them, Entry and Text widgets are probably unusable)
EDIT2:
added option to change update mode to deal with the above mentioned problem (read docstring in the EndlessScroll class). The issue is using Frame, it basically breaks the now "active" mode loop (probably because of amount of data) so with it you have to use "passive" mode however that means less smooth updates (also "passive" mode horribly updates if you don't use a Frame). So you can also use "active" mode but only when you have one widget per line (hopefully this will be solved by #TheLizzard by making some kind of custom .grid() method or sth, in comments) and then it updates very smoothly. So currently unless there really is a need to have a lot of widgets in a column and have them extend to other columns, I would suggest using the usual methods of adding scrolling. That is of course unless you are fine with a little bit of flickering when dragging the slider, other methods don't produce that issue.
If you have any questions, ask them!

Custom tkinter slider [duplicate]

I'm creating a game launcher specially in Python 3.7 tkInter, and I want to make my own styled Scrollbar (in Windows 10 (version 1903)).
I've tried adding a hidden Scrollbar, and hiding works but i can't simulate it:
def scroll(self, i, reqHeight, vbarValue):
print("vbarValue", vbarValue)
value = -i / 1.4
a1 = int(self.canvass.coords(self.scroll2)[1]) == 5
a2 = value > 0
a = not(a1 ^ a2)
b1 = ((self.canvass.coords(self.scroll2)[3] > self.cHeight))
b2 = value < 0
b = not(b1 ^ b2)
print(value, value < 0)
print(a1, 5)
print("====")
print(a1, a2)
print(a)
print("----")
print(b1, b2)
print(b)
print("====\n\n")
print("OK")
x1, y1, x2, y2 = self.canvass.coords(self.scroll2)
_y1, _y2 = vbarValue
print("1:",y1, y2)
print("2:",_y1, _y2)
print("3:",(_y2 - _y1) / 2 - y2)
print("4:",(_y1 + (_y2 - _y1) / 120) * self.cHeight)
print("5:",(_y1 + (_y2 - _y1) / 120) * self.cHeight - (y2 / y1))
print("6:",((_y2 - _y1) / 120) * self.cHeight - y2* -i)
print("7:",(_y1 + (_y2 - _y1) / 120))
value = (_y1 + (_y2 - _y1) / 120) * self.cHeight / (y1 / y2)
print("8:",(y2 / y1))
# value = value - (y1 / y2)
print("Dynamic Canvas Region Height:")
print("DCRH:", self.cHeight)
print("Value: %s", value)
self.canvass.move(self.scroll2, 0, -y2)
self.canvass.move(self.scroll2, 0, value)
print("coords: %s" % self.canvass.coords(self.scroll2))
print("reqHeight: %s" % reqHeight)
Event:
def _bound_to_mousewheel(self, event): # <Enter> Event
self.canv.bind_all("<MouseWheel>", self._on_mousewheel)
def _unbound_to_mousewheel(self, event): # <Leave> Event
self.canv.unbind_all("<MouseWheel>")
def _on_mousewheel(self, event): # <Configure> Event
self.canv.yview_scroll(int(-1 * (event.delta / 120)), "units")
self.scrollCommand(int(-1 * (event.delta / 120)), self.scrollwindow.winfo_reqheight(), self.vbar.get())
def _configure_window(self, event):
# update the scrollbars to match the size of the inner frame
size = (self.scrollwindow.winfo_reqwidth(), self.scrollwindow.winfo_reqheight()+1)
self.canv.config(scrollregion='0 0 %s %s' % size)
# if self.scrollwindow.winfo_reqwidth() != self.canv.winfo_width():
# # update the canvas's width to fit the inner frame
# # self.canv.config(width=self.scrollwindow.winfo_reqwidth())
# if self.scrollwindow.winfo_reqheight() != self.canv.winfo_height():
# # update the canvas's width to fit the inner frame
# # self.canv.config(height=self.scrollwindow.winfo_reqheight())
By the way, self.scrollCommand(...) is the same as scroll on the first code.
I expect to get some x and y output for the canvas.move method.
How do i simulate a Scrollbar in Tkinter Canvas
The scrollbar has a well defined interface. To simulate a scrollbar all you need to do is implement this interface. This is most easily done by creating a class that has the following attributes:
you need to define the set method, which is called whenever the widget being scrolled wants to update the scrollbar
you need to add mouse bindings to call the yview method of the widget being controlled by the scrollbar (or xview if creating a horizontal widget).
If you do those two things, your scrollbar can be used exactly like a built-in scrollbar.
For the rest of this answer, I'm going to assume you want to simulate a vertical scrollbar. Simulating a horizontal scrollbar works identically, but instead of 'top' and 'bottom', you are dealing with 'left' and right'.
Defining the set method
The set method will be called with two fractions. The canonical documentation describes it like this:
This command is invoked by the scrollbar's associated widget to tell the scrollbar about the current view in the widget. The command takes two arguments, each of which is a real fraction between 0 and 1. The fractions describe the range of the document that is visible in the associated widget. For example, if first is 0.2 and last is 0.4, it means that the first part of the document visible in the window is 20% of the way through the document, and the last visible part is 40% of the way through.
Defining the bindings
The other half of the equation is when the user interacts with the scrollbar to scroll another widget. The way this happens is that the scrollbar should call the yview command of the widget to be controlled (eg: canvas, text, listbox, etc).
The first argument you must pass to the command is either the string "moveto" or "scroll".
If "moveto", the second argument is a fraction which represents the amount that has been scrolled off of the top. This is typically called when clicking on the scrollbar, to immediately move the scrollbar to a new position
if "scroll", the second argument is an integer representing an amount, and the third argument is either the string "units" or "pages". The definition of "units" refers to the value of the yscrollincrement option. "pages" represents 9/10ths of the window height. This is typically called when dragging the mouse over the scrollbar.
The options are spelled out in the man pages for each scrollable widget.
Example
The following is an example of a uses a text widget, so that you can see that when you type, the scrollbar properly grows and shrinks. If you click anywhere in the scrollbar, it will scroll to that point in the documentation.
To keep the example short, this code doesn't handle dragging the scrollbar, and it hard-codes a lot of values that probably ought to be configurable. The point is to show that all you need to do to simulate a scrollbar is create a class that has a set method and which calls the yview or xview method of the connected widget.
First, the scrollbar class
import tkinter as tk
class CustomScrollbar(tk.Canvas):
def __init__(self, parent, **kwargs):
self.command = kwargs.pop("command", None)
tk.Canvas.__init__(self, parent, **kwargs)
# coordinates are irrelevant; they will be recomputed
# in the 'set' method
self.create_rectangle(0,0,1,1, fill="red", tags=("thumb",))
self.bind("<ButtonPress-1>", self.on_click)
def set(self, first, last):
first = float(first)
last = float(last)
height = self.winfo_height()
x0 = 2
x1 = self.winfo_width()-2
y0 = max(int(height * first), 0)
y1 = min(int(height * last), height)
self.coords("thumb", x0, y0, x1, y1)
def on_click(self, event):
y = event.y / self.winfo_height()
self.command("moveto", y)
Using the class in a program
You would use this class exactly like you would a native scrollbar: instantiate it, and set the command to be the yview command of a scrollable widget.
This example uses a text widget so you can see the scrollbar updating as you type, but the exact same code would work with a Canvas, or any other scrollable window.
root = tk.Tk()
text = tk.Text(root)
sb = CustomScrollbar(root, width=20, command=text.yview)
text.configure(yscrollcommand=sb.set)
sb.pack(side="right", fill="y")
text.pack(side="left", fill="both", expand=True)
with open(__file__, "r") as f:
text.insert("end", f.read())
root.mainloop()

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.

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