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         # Initialise instance variables.
 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         # Create a frame in the clipper to contain the widgets to be
 195         # scrolled.
 196         self._frame = ttk.Frame(self._clipper)
 197 
 198         # Whenever the clipping window or scrolled frame change size,
 199         # update the scrollbars.
 200         self._frame.bind('<Configure>', self._reposition)
 201         self._clipper.bind('<Configure>', self._reposition)
 202 
 203         # Work around a bug in Tk where the value returned by the
 204         # scrollbar get() method is (0.0, 0.0, 0.0, 0.0) rather than
 205         # the expected 2-tuple.  This occurs if xview() is called soon
 206         # after the ScrolledFrame has been created.
 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     # hackish configure() implementation
 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     # Configuration methods.
 256 
 257     def _hscrollMode(self):
 258         # The horizontal scroll mode has been configured.
 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         # The vertical scroll mode has been configured.
 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         # The horizontal flex mode has been configured.
 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         # The vertical flex mode has been configured.
 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     # Public methods.
 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     # Set timer to call real reposition method, so that it is not
 332     # called multiple times when many things are reconfigured at the
 333     # same time.
 334     def reposition(self):
 335         if self.scrollTimer is None:
 336             self.scrollTimer = self.after_idle(self._scrollBothNow)
 337 
 338     # Called when the user clicks in the horizontal scrollbar.
 339     # Calculates new position of frame then calls reposition() to
 340     # update the frame and the scrollbar.
 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: # mode == 'scroll'
 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     # Called when the user clicks in the vertical scrollbar.
 360     # Calculates new position of frame then calls reposition() to
 361     # update the frame and the scrollbar.
 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: # mode == 'scroll'
 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     # Private methods.
 385 
 386     def _reposition(self, event):
 387         self.reposition()
 388 
 389     def _getxview(self):
 390 
 391         # Horizontal dimension.
 392         clipperWidth = self._clipper.winfo_width()
 393         frameWidth = self._frame.winfo_reqwidth()
 394         if frameWidth <= clipperWidth:
 395             # The scrolled frame is smaller than the clipping window.
 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             # The scrolled frame is larger than the clipping window.
 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         # Position frame relative to clipper.
 422         self._frame.place(x = -self.startX, relwidth = relwidth)
 423         return (self.startX / float(frameWidth), endScrollX)
 424 
 425     def _getyview(self):
 426 
 427         # Vertical dimension.
 428         clipperHeight = self._clipper.winfo_height()
 429         frameHeight = self._frame.winfo_reqheight()
 430         if frameHeight <= clipperHeight:
 431             # The scrolled frame is smaller than the clipping window.
 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             # The scrolled frame is larger than the clipping window.
 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         # Position frame relative to clipper.
 458         self._frame.place(y = -self.startY, relheight = relheight)
 459         return (self.startY / float(frameHeight), endScrollY)
 460 
 461     # According to the relative geometries of the frame and the
 462     # clipper, reposition the frame within the clipper and reset the
 463     # scrollbars.
 464     def _scrollBothNow(self):
 465         self.scrollTimer = None
 466 
 467         # Call update_idletasks to make sure that the containing frame
 468         # has been resized before we attempt to set the scrollbars.
 469         # Otherwise the scrollbars may be mapped/unmapped continuously.
 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         # If both horizontal and vertical scrollmodes are dynamic and
 485         # currently only one scrollbar is mapped and both should be
 486         # toggled, then unmap the mapped scrollbar.  This prevents a
 487         # continuous mapping and unmapping of the scrollbars.
 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#.origInterior
 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#.origInterior
 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 

tkinter: ThemedScrolledFrame (last edited 2010-12-19 18:18:05 by MichaelLange)