Skip to content

Commit 9ff879b

Browse files
Add scrollable frames
1 parent 6a2030a commit 9ff879b

File tree

4 files changed

+538
-2
lines changed

4 files changed

+538
-2
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from TkZero.Button import Button
2+
from TkZero.Frame import ScrollableFrame
3+
from TkZero.MainWindow import MainWindow
4+
5+
# Create the main window and set a title
6+
root = MainWindow()
7+
root.title = "Scrollable Frame Example"
8+
9+
# Create a scrollable frame
10+
scrollable_frame = ScrollableFrame(root, x_scrolling=True, y_scrolling=True)
11+
scrollable_frame.grid(row=0, column=0)
12+
13+
# Create a bunch of buttons in the scrollable frame
14+
for x in range(10):
15+
for y in range(20):
16+
# Note the usage of the frame attribute of the scrollable frame instead
17+
# of using it directly.
18+
b = Button(scrollable_frame.frame, text=f"{x}, {y}")
19+
b.grid(row=y, column=x)
20+
21+
# Start the mainloop like in Tkinter
22+
root.mainloop()

Tests/test_Frame.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import unittest
66

77
from TkZero import Style
8-
from TkZero.Frame import Frame
8+
from TkZero.Frame import Frame, ScrollableFrame
99
from TkZero.Label import Label
1010
from TkZeroUnitTest import TkTestCase
1111

@@ -14,6 +14,7 @@ class FrameTest(TkTestCase):
1414
def test_no_params(self):
1515
with self.assertRaises(TypeError):
1616
Frame()
17+
1718
def test_bad_params(self):
1819
with self.assertRaises(TypeError):
1920
Frame(parent=1)
@@ -63,5 +64,60 @@ def test_style(self):
6364
f.apply_style(123456789)
6465

6566

67+
class ScrollableFrameTest(TkTestCase):
68+
def test_no_params(self):
69+
with self.assertRaises(TypeError):
70+
ScrollableFrame()
71+
72+
def test_bad_params(self):
73+
with self.assertRaises(TypeError):
74+
ScrollableFrame(parent=1)
75+
76+
def test_good_params(self):
77+
ScrollableFrame(self.root, x_scrolling=True,
78+
y_scrolling=True).grid(row=0, column=0)
79+
80+
def test_width_height(self):
81+
f = ScrollableFrame(self.root)
82+
f.grid(row=0, column=0)
83+
f.width = 500
84+
f.height = 200
85+
self.root.update()
86+
self.assertEqual(f.width, 500)
87+
self.assertEqual(f.height, 200)
88+
with self.assertRaises(TypeError):
89+
f.width = "la"
90+
with self.assertRaises(ValueError):
91+
f.width = 0
92+
with self.assertRaises(TypeError):
93+
f.height = "lo"
94+
with self.assertRaises(ValueError):
95+
f.height = -1
96+
97+
def test_enabled(self):
98+
f = ScrollableFrame(self.root)
99+
f.grid(row=0, column=0)
100+
self.assertTrue(f.enabled)
101+
Label(f.frame).grid(row=0, column=0)
102+
Label(f.frame).grid(row=1, column=0)
103+
Frame(f.frame).grid(row=2, column=0)
104+
self.root.update()
105+
f.enabled = False
106+
self.assertFalse(f.enabled)
107+
with self.assertRaises(TypeError):
108+
f.enabled = []
109+
110+
def test_style(self):
111+
f = ScrollableFrame(self.root)
112+
f.grid(row=0, column=0)
113+
Style.define_style(Style.WidgetStyleRoots.Frame, "Test",
114+
background="red")
115+
f.apply_style("Test")
116+
self.assertEqual(f.frame.cget("style"), "Test.TFrame")
117+
self.root.update()
118+
with self.assertRaises(TypeError):
119+
f.apply_style(123456789)
120+
121+
66122
if __name__ == '__main__':
67123
unittest.main()

TkZero/Frame.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from tkinter import ttk
77
from typing import Union
88

9+
from TkZero import Platform
10+
911

1012
class Frame(ttk.Frame):
1113
def __init__(self, parent: Union[tk.Widget, Union[tk.Tk, tk.Toplevel]]):
@@ -145,3 +147,142 @@ def apply_style(self, style_name: str) -> None:
145147
f"(type passed in: {repr(type(style_name))})"
146148
)
147149
self.configure(style=f"{style_name}.{self._style_root}")
150+
151+
152+
class ScrollableFrame(Frame):
153+
def __init__(
154+
self,
155+
parent: Union[tk.Widget, Union[tk.Tk, tk.Toplevel]],
156+
x_scrolling: bool = False,
157+
y_scrolling: bool = True,
158+
):
159+
"""
160+
Create a scrollable frame.
161+
MAKE SURE WHEN GRIDING WIDGETS TO THIS FRAME TO USE THE FRAME
162+
ATTRIBUTE INSTEAD OF THIS FRAME DIRECTLY.
163+
164+
:param parent: The parent of this frame.
165+
:param x_scrolling: A bool on whether to add a scrollbar in the x
166+
direction.
167+
:param y_scrolling: A bool on whether to add a scrollbar in the y
168+
direction.
169+
"""
170+
# https://blog.teclado.com/tkinter-scrollable-frames/
171+
super().__init__(parent)
172+
self.canvas = tk.Canvas(self)
173+
if x_scrolling:
174+
x_scrollbar = ttk.Scrollbar(
175+
self, orient=tk.HORIZONTAL, command=self.canvas.xview
176+
)
177+
if y_scrolling:
178+
y_scrollbar = ttk.Scrollbar(
179+
self, orient=tk.VERTICAL, command=self.canvas.yview
180+
)
181+
self.frame = Frame(self.canvas)
182+
self.frame.bind(
183+
"<Configure>",
184+
lambda _: self.canvas.configure(scrollregion=self.canvas.bbox(tk.ALL)),
185+
)
186+
self.canvas.create_window((0, 0), window=self.frame, anchor=tk.NW)
187+
if x_scrolling:
188+
self.canvas.configure(xscrollcommand=x_scrollbar.set)
189+
if y_scrolling:
190+
self.canvas.configure(yscrollcommand=y_scrollbar.set)
191+
self.canvas.grid(row=0, column=0)
192+
if x_scrolling:
193+
x_scrollbar.grid(row=1, column=0, sticky=tk.NSEW)
194+
if y_scrolling:
195+
y_scrollbar.grid(row=0, column=1, sticky=tk.NSEW)
196+
# https://stackoverflow.com/a/37858368/10291933
197+
self.frame.bind("<Enter>", self._bind_to_mousewheel)
198+
self.frame.bind("<Leave>", self._unbind_to_mousewheel)
199+
self._shift_pressed = False
200+
# https://stackoverflow.com/a/8089241/10291933
201+
# http://www.tcl.tk/man/tcl8.4/TkCmd/keysyms.htm
202+
self.frame.bind_all("<Shift_L>", lambda _: self._set_shift_pressed(True))
203+
self.frame.bind_all("<Shift_R>", lambda _: self._set_shift_pressed(True))
204+
self.frame.bind_all(
205+
"<KeyRelease-Shift_L>", lambda _: self._set_shift_pressed(False)
206+
)
207+
self.frame.bind_all(
208+
"<KeyRelease-Shift_R>", lambda _: self._set_shift_pressed(False)
209+
)
210+
211+
def _set_shift_pressed(self, is_pressed: bool) -> None:
212+
"""
213+
Set whether shift is pressed or not.
214+
215+
:param is_pressed: A bool.
216+
:return: None.
217+
"""
218+
self._shift_pressed = is_pressed
219+
220+
def _bind_to_mousewheel(self, event) -> None:
221+
"""
222+
Bind to the mousewheel.
223+
224+
:param event: An event that Tkinter passes in.
225+
:return: None
226+
"""
227+
self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)
228+
229+
def _unbind_to_mousewheel(self, event) -> None:
230+
"""
231+
Unbind to the mousewheel.
232+
233+
:param event: An event that Tkinter passes in.
234+
:return: None
235+
"""
236+
self.canvas.unbind_all("<MouseWheel>")
237+
238+
def _on_mousewheel(self, event) -> None:
239+
"""
240+
A callback for the mousewheel to scroll.
241+
242+
:param event: An event that Tkinter passes in.
243+
:return: None.
244+
"""
245+
if Platform.on_aqua(self):
246+
scroll = str(int(-1 * event.delta))
247+
else:
248+
scroll = str(int(-1 * (event.delta / 120)))
249+
if self._shift_pressed:
250+
self.canvas.xview_scroll(scroll, tk.UNITS)
251+
else:
252+
self.canvas.yview_scroll(scroll, tk.UNITS)
253+
254+
def _enable_children(
255+
self, parent: Union[tk.Widget, None] = None, enable: bool = True
256+
) -> None:
257+
"""
258+
Enable or disable the children.
259+
260+
:param parent: A tk.Widget that is our parent. If None then default to
261+
self.
262+
:param enable: Whether to enable or disable the children.
263+
:return: None.
264+
"""
265+
if parent is None:
266+
parent = self.frame
267+
for child in parent.winfo_children():
268+
if child.winfo_class() not in ("Frame", "LabelFrame"):
269+
try:
270+
child.state(["!disabled" if enable else "disabled"])
271+
except AttributeError:
272+
child.configure(state=tk.NORMAL if enable else tk.DISABLED)
273+
else:
274+
self._enable_children(parent, enable)
275+
276+
def apply_style(self, style_name: str) -> None:
277+
"""
278+
Apply a theme to this frame.
279+
280+
:param style_name: The name of the theme as a str, ex. "Warning"
281+
:return: None.
282+
"""
283+
if not isinstance(style_name, str):
284+
raise TypeError(
285+
f"style_name is not a str! "
286+
f"(type passed in: {repr(type(style_name))})"
287+
)
288+
self.frame.configure(style=f"{style_name}.{self._style_root}")

0 commit comments

Comments
 (0)