1 '''TScrolledFrame is a themed ScrolledFrame widget for Tkinter. Send feedback
2 and bug reports to Michael Lange <klappnase (at) freakmail (dot) de>
3
4 For the most part the code is stolen from the Python Mega Widget's
5 Pmw.ScrolledFrame widget. Pmw was originally written by Greg McFarlane and is
6 available at http://pmw.sourceforge.net .
7 Also most of the documentation is stolen from
8 http://pmw.sourceforge.net/doc/ScrolledFrame.html .
9
10 Pmw copyright:
11
12 Copyright 1997-1999 Telstra Corporation Limited, Australia Copyright 2000-2002
13 Really Good Software Pty Ltd, Australia
14
15 Permission is hereby granted, free of charge, to any person obtaining a copy of
16 this software and associated documentation files (the "Software"), to deal in
17 the Software without restriction, including without limitation the rights to
18 use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
19 of the Software, and to permit persons to whom the Software is furnished to do
20 so, subject to the following conditions:
21
22 The above copyright notice and this permission notice shall be included in all
23 copies or substantial portions of the Software.
24
25 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
27 FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
28 COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
29 IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
30 CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
31 '''
32
33 import ttk
34 from types import StringType
35
36 class TScrolledFrame(ttk.Frame):
37 '''A scrolled frame consists of a scrollable interior frame within a
38 clipping frame. The programmer can create other widgets within the interior
39 frame. If the frame becomes larger than the surrounding clipping frame, the
40 user can position the frame using the horizontal and vertical scrollbars.
41 The scrollbars can be dynamic, which means that a scrollbar will only be
42 displayed if it is necessary. That is, if the frame is smaller than the
43 surrounding clipping frame, the scrollbar will be hidden.
44 OPTIONS:
45 horizflex - Specifies how the width of the scrollable interior frame
46 should be resized relative to the clipping frame.
47 If 'fixed', the interior frame is set to the natural width,
48 as requested by the child widgets of the frame. If 'expand'
49 and the requested width of the interior frame is less than
50 the width of the clipping frame, the interior frame expands
51 to fill the clipping frame. If 'shrink' and the requested
52 width of the interior frame is more than the width of the
53 clipping frame, the interior frame shrinks to the width of
54 the clipping frame. If 'elastic', the width of the interior
55 frame is always set to the width of the clipping frame. The
56 default is 'expand'.
57 vertflex - Specifies how the height of the scrollable interior frame
58 should be resized relative to the clipping frame.
59 If 'fixed', the interior frame is set to the natural height,
60 as requested by the child widgets of the frame. If 'expand'
61 and the requested height of the interior frame is less than
62 the height of the clipping frame, the interior frame expands
63 to fill the clipping frame. If 'shrink' and the requested
64 height of the interior frame is more than the height of the
65 clipping frame, the interior frame shrinks to the height of
66 the clipping frame. If 'elastic', the height of the interior
67 frame is always set to the height of the clipping frame. The
68 default is 'expand'.
69 hscrollmode - The horizontal scroll mode. If 'none', the horizontal
70 scrollbar will never be displayed. If 'static', the
71 scrollbar will always be displayed. If 'dynamic', the
72 scrollbar will be displayed only if necessary. The default
73 is 'dynamic'.
74 vscrollmode - The vertical scroll mode. If 'none', the vertical
75 scrollbar will never be displayed. If 'static', the
76 scrollbar will always be displayed. If 'dynamic', the
77 scrollbar will be displayed only if necessary. The default
78 is 'dynamic'.
79 hfraction - The fraction of the width of the clipper frame to scroll the
80 interior frame when the user clicks on the horizontal
81 scrollbar arrows. The default is 0.05.
82 vfraction - The fraction of the height of the clipper frame to scroll
83 the interior frame when the user clicks on the vertical
84 scrollbar arrows. The default is 0.05.
85 scrollmargin - Initialisation option. The distance between the
86 scrollbars and the clipping frame. The default is 1.
87 usehullsize - Initialisation option. If true, the size of the megawidget
88 is determined solely by the width and height options of the
89 hull component. Otherwise, the size of the megawidget is
90 determined by the width and height of the clipper component,
91 along with the size and/or existence of the other components,
92 such as the scrollbars. All these affect the overall size of
93 the megawidget. The default is True.
94 COMPONENTS:
95 frame - The frame within the clipper to contain the widgets to be
96 scrolled.
97 clipper - The frame which is used to provide a clipped view of the
98 frame component.
99 hbar - The horizontal scrollbar.
100 vbar - The vertical scrollbar.
101 METHODS:
102 component(which) - Return the subwidget instance WHICH. WHICH may be one
103 of "frame", "clipper", "hbar", "vbar".
104 frame() - Return the frame within which the programmer may create
105 widgets to be scrolled. This is the same as component('frame').
106 reposition() - Update the position of the frame component in the clipper
107 and update the scrollbars.
108 Usually, this method does not need to be called explicitly,
109 since the position of the frame component and the scrollbars
110 are automatically updated whenever the size of the frame or
111 clipper components change or the user clicks in the
112 scrollbars. However, if horizflex or vertflex is 'expand',
113 the megawidget cannot detect when the requested size of the
114 frame increases to greater than the size of the clipper.
115 Therefore, this method should be called when a new widget
116 is added to the frame (or a widget is increased in size)
117 after the initial megawidget construction.
118 xview(mode = None, value = None, units = None) - Query or change the
119 horizontal position of the scrollable interior frame. If
120 mode is None, return a tuple of two numbers, each between
121 0.0 and 1.0. The first is the position of the left edge of
122 the visible region of the contents of the scrolled frame,
123 expressed as a fraction of the total width of the contents.
124 The second is the position of the right edge of the visible
125 region.
126 If mode == 'moveto', adjust the view of the interior so that
127 the fraction value of the total width of the contents is
128 off-screen to the left. The value must be between 0.0 and 1.0.
129 If mode == 'scroll', adjust the view of the interior left or
130 right by a fixed amount. If what is 'units', move the view
131 in units of horizfraction. If what is pages, move the view
132 in units of the width of the scrolled frame. If value is
133 positive, move to the right, otherwise move to the left.
134 yview(mode = None, value = None, units = None) - Query or change the
135 vertical position of the scrollable interior frame. If mode
136 is None, return a tuple of two numbers, each between 0.0 and
137 1.0. The first is the position of the top edge of the
138 visible region of the contents of the scrolled frame,
139 expressed as a fraction of the total height of the contents.
140 The second is the position of the bottom edge of the visible
141 region.
142 If mode == 'moveto', adjust the view of the interior so that
143 the fraction value of the total height of the contents is
144 off-screen to the top. The value must be between 0.0 and 1.0.
145 If mode == 'scroll', adjust the view of the interior up or
146 down by a fixed amount. If what is 'units', move the view in
147 units of vertfraction. If what is pages, move the view in
148 units of the height of the scrolled frame. If value is
149 positive, move to down, otherwise move up.
150 '''
151
152 def __init__(self, master=None, hscrollmode='dynamic', vscrollmode='dynamic',
153 horizflex='expand', vertflex='expand',
154 hfraction=0.05, vfraction=0.05,
155 usehullsize=True, scrollmargin=1, **kw):
156 if not kw.has_key('width'):
157 kw['width'] = 400
158 if not kw.has_key('height'):
159 kw['height'] = 300
160 ttk.Frame.__init__(self, master, **kw)
161
162 self._scrolledframe_opts = {'hscrollmode' : hscrollmode,
163 'vscrollmode' : vscrollmode,
164 'horizflex' : horizflex,
165 'vertflex' : vertflex,
166 'hfraction' : hfraction,
167 'vfraction' : vfraction,
168 'usehullsize' : usehullsize,
169 'scrollmargin' : scrollmargin
170 }
171
172 self._clipper = ttk.Frame(self, width=400, height=300)
173 self._clipper.grid(row=2, column=2, sticky='news')
174
175 self.grid_rowconfigure(2, weight=1, minsize=0)
176 self.grid_columnconfigure(2, weight=1, minsize=0)
177 if usehullsize:
178 self.grid_propagate(0)
179
180 self._hbar = ttk.Scrollbar(self, orient='horizontal', command=self.xview)
181 self._vbar = ttk.Scrollbar(self, orient='vertical', command=self.yview)
182
183
184 self._hbarOn = 0
185 self._vbarOn = 0
186 self.scrollTimer = None
187 self._scrollRecurse = 0
188 self._hbarNeeded = 0
189 self._vbarNeeded = 0
190 self.startX = 0
191 self.startY = 0
192 self._flexoptions = ('fixed', 'expand', 'shrink', 'elastic')
193
194
195
196 self._frame = ttk.Frame(self._clipper)
197
198
199
200 self._frame.bind('<Configure>', self._reposition)
201 self._clipper.bind('<Configure>', self._reposition)
202
203
204
205
206
207 self._hbar.set(0.0, 1.0)
208 self._vbar.set(0.0, 1.0)
209
210 self._hscrollMode()
211 self._vscrollMode()
212 self._horizFlex()
213 self._vertFlex()
214
215
216 def _configure_scrolledframe(self, option, value):
217 if not self._scrolledframe_opts.has_key(option):
218 raise KeyError, 'unknown option: %s' % option
219 if option in ('scrollmargin', 'usehullsize'):
220 raise KeyError, 'Option cannot be configured: %s' % option
221 self._scrolledframe_opts[option] = value
222 if option == 'hscrollmode':
223 self._hscrollMode()
224 elif option == 'vscrollmode':
225 self._vscrollMode()
226 elif option == 'horizflex':
227 self._horizFlex()
228 elif option == 'vertflex':
229 self._vertFlex()
230
231 def configure(self, cnf=None, **kw):
232 for opt in self._scrolledframe_opts.keys():
233 if not cnf is None and cnf.has_key(opt):
234 self._configure_scrolledframe(opt, cnf[opt])
235 del cnf[opt]
236 if kw.has_key(opt):
237 self._configure_scrolledframe(opt, kw[opt])
238 del kw[opt]
239 return ttk.Frame.configure(self, cnf, **kw)
240 config = configure
241
242 def cget(self, key):
243 if key in self._scrolledframe_opts.keys():
244 return self._scrolledframe_opts[key]
245 return ttk.Frame.cget(self, key)
246 __getitem__ = cget
247
248 def destroy(self):
249 if self.scrollTimer is not None:
250 self.after_cancel(self.scrollTimer)
251 self.scrollTimer = None
252 ttk.Frame.destroy(self)
253
254
255
256
257 def _hscrollMode(self):
258
259
260 mode = self['hscrollmode']
261
262 if mode == 'static':
263 if not self._hbarOn:
264 self._toggleHorizScrollbar()
265 elif mode == 'dynamic':
266 if self._hbarNeeded != self._hbarOn:
267 self._toggleHorizScrollbar()
268 elif mode == 'none':
269 if self._hbarOn:
270 self._toggleHorizScrollbar()
271 else:
272 message = 'bad hscrollmode option "%s": should be static, dynamic, or none' % mode
273 raise ValueError, message
274
275 def _vscrollMode(self):
276
277
278 mode = self['vscrollmode']
279
280 if mode == 'static':
281 if not self._vbarOn:
282 self._toggleVertScrollbar()
283 elif mode == 'dynamic':
284 if self._vbarNeeded != self._vbarOn:
285 self._toggleVertScrollbar()
286 elif mode == 'none':
287 if self._vbarOn:
288 self._toggleVertScrollbar()
289 else:
290 message = 'bad vscrollmode option "%s": should be static, dynamic, or none' % mode
291 raise ValueError, message
292
293 def _horizFlex(self):
294
295
296 flex = self['horizflex']
297
298 if flex not in self._flexoptions:
299 message = 'bad horizflex option "%s": should be one of %s' % \
300 (flex, str(self._flexoptions))
301 raise ValueError, message
302
303 self.reposition()
304
305 def _vertFlex(self):
306
307
308 flex = self['vertflex']
309
310 if flex not in self._flexoptions:
311 message = 'bad vertflex option "%s": should be one of %s' % \
312 (flex, str(self._flexoptions))
313 raise ValueError, message
314
315 self.reposition()
316
317
318
319
320 def component(self, which):
321 if not which in ('hbar', 'vbar', 'clipper', 'frame'):
322 raise KeyError, 'Bad value for component: %s' % which
323 if which == 'hbar': return self._hbar
324 if which == 'vbar': return self._vbar
325 if which == 'clipper': return self._clipper
326 if which == 'frame': return self._frame
327
328 def frame(self):
329 return self._frame
330
331
332
333
334 def reposition(self):
335 if self.scrollTimer is None:
336 self.scrollTimer = self.after_idle(self._scrollBothNow)
337
338
339
340
341 def xview(self, mode = None, value = None, units = None):
342 if type(value) == StringType:
343 value = float(value)
344 if mode is None:
345 return self._hbar.get()
346 elif mode == 'moveto':
347 frameWidth = self._frame.winfo_reqwidth()
348 self.startX = value * float(frameWidth)
349 else:
350 clipperWidth = self._clipper.winfo_width()
351 if units == 'units':
352 jump = int(clipperWidth * self['hfraction'])
353 else:
354 jump = clipperWidth
355 self.startX = self.startX + value * jump
356
357 self.reposition()
358
359
360
361
362 def yview(self, mode = None, value = None, units = None):
363
364 if type(value) == StringType:
365 value = float(value)
366 if mode is None:
367 return self._vbar.get()
368 elif mode == 'moveto':
369 frameHeight = self._frame.winfo_reqheight()
370 self.startY = value * float(frameHeight)
371 else:
372 clipperHeight = self._clipper.winfo_height()
373 if units == 'units':
374 jump = int(clipperHeight * self['vfraction'])
375 else:
376 jump = clipperHeight
377 self.startY = self.startY + value * jump
378
379 self.reposition()
380
381
382
383
384
385
386 def _reposition(self, event):
387 self.reposition()
388
389 def _getxview(self):
390
391
392 clipperWidth = self._clipper.winfo_width()
393 frameWidth = self._frame.winfo_reqwidth()
394 if frameWidth <= clipperWidth:
395
396
397 self.startX = 0
398 endScrollX = 1.0
399
400 if self['horizflex'] in ('expand', 'elastic'):
401 relwidth = 1
402 else:
403 relwidth = ''
404 else:
405
406
407 if self['horizflex'] in ('shrink', 'elastic'):
408 self.startX = 0
409 endScrollX = 1.0
410 relwidth = 1
411 else:
412 if self.startX + clipperWidth > frameWidth:
413 self.startX = frameWidth - clipperWidth
414 endScrollX = 1.0
415 else:
416 if self.startX < 0:
417 self.startX = 0
418 endScrollX = (self.startX + clipperWidth) / float(frameWidth)
419 relwidth = ''
420
421
422 self._frame.place(x = -self.startX, relwidth = relwidth)
423 return (self.startX / float(frameWidth), endScrollX)
424
425 def _getyview(self):
426
427
428 clipperHeight = self._clipper.winfo_height()
429 frameHeight = self._frame.winfo_reqheight()
430 if frameHeight <= clipperHeight:
431
432
433 self.startY = 0
434 endScrollY = 1.0
435
436 if self['vertflex'] in ('expand', 'elastic'):
437 relheight = 1
438 else:
439 relheight = ''
440 else:
441
442
443 if self['vertflex'] in ('shrink', 'elastic'):
444 self.startY = 0
445 endScrollY = 1.0
446 relheight = 1
447 else:
448 if self.startY + clipperHeight > frameHeight:
449 self.startY = frameHeight - clipperHeight
450 endScrollY = 1.0
451 else:
452 if self.startY < 0:
453 self.startY = 0
454 endScrollY = (self.startY + clipperHeight) / float(frameHeight)
455 relheight = ''
456
457
458 self._frame.place(y = -self.startY, relheight = relheight)
459 return (self.startY / float(frameHeight), endScrollY)
460
461
462
463
464 def _scrollBothNow(self):
465 self.scrollTimer = None
466
467
468
469
470 self._scrollRecurse = self._scrollRecurse + 1
471 self.update_idletasks()
472 self._scrollRecurse = self._scrollRecurse - 1
473 if self._scrollRecurse != 0:
474 return
475
476 xview = self._getxview()
477 yview = self._getyview()
478 self._hbar.set(xview[0], xview[1])
479 self._vbar.set(yview[0], yview[1])
480
481 self._hbarNeeded = (xview != (0.0, 1.0))
482 self._vbarNeeded = (yview != (0.0, 1.0))
483
484
485
486
487
488 if (self['hscrollmode'] == self['vscrollmode'] == 'dynamic' and
489 self._hbarNeeded != self._hbarOn and
490 self._vbarNeeded != self._vbarOn and
491 self._vbarOn != self._hbarOn):
492 if self._hbarOn:
493 self._toggleHorizScrollbar()
494 else:
495 self._toggleVertScrollbar()
496 return
497
498 if self['hscrollmode'] == 'dynamic':
499 if self._hbarNeeded != self._hbarOn:
500 self._toggleHorizScrollbar()
501
502 if self['vscrollmode'] == 'dynamic':
503 if self._vbarNeeded != self._vbarOn:
504 self._toggleVertScrollbar()
505
506 def _toggleHorizScrollbar(self):
507
508 self._hbarOn = not self._hbarOn
509
510 interior = self
511 if self._hbarOn:
512 self._hbar.grid(row = 4, column = 2, sticky = 'news')
513 interior.grid_rowconfigure(3, minsize = self['scrollmargin'])
514 else:
515 self._hbar.grid_forget()
516 interior.grid_rowconfigure(3, minsize = 0)
517
518 def _toggleVertScrollbar(self):
519
520 self._vbarOn = not self._vbarOn
521
522 interior = self
523 if self._vbarOn:
524 self._vbar.grid(row = 2, column = 4, sticky = 'news')
525 interior.grid_columnconfigure(3, minsize = self['scrollmargin'])
526 else:
527 self._vbar.grid_forget()
528 interior.grid_columnconfigure(3, minsize = 0)
529
530
531
532
533 if __name__ == '__main__':
534 import Tkinter
535 root = Tkinter.Tk()
536 sf = TScrolledFrame(root)
537 sf.pack(fill='both', expand=1)
538 for x in range(30):
539 for y in range(20):
540 Tkinter.Entry(sf.frame(), bd=0, highlightthickness=1,
541 highlightbackground='gray50').grid(row=x, column=y)
542 root.mainloop()
543