import string import types import Tkinter import Pmw class ScrolledFrame(Pmw.MegaWidget): def __init__(self, parent = None, **kw): # Define the megawidget options. INITOPT = Pmw.INITOPT optiondefs = ( ('borderframe', 1, INITOPT), ('horizflex', 'fixed', self._horizflex), ('horizfraction', 0.05, INITOPT), ('hscrollmode', 'dynamic', self._hscrollMode), ('labelmargin', 0, INITOPT), ('labelpos', None, INITOPT), ('scrollmargin', 2, INITOPT), ('usehullsize', 0, INITOPT), ('vertflex', 'fixed', self._vertflex), ('vertfraction', 0.05, INITOPT), ('vscrollmode', 'dynamic', self._vscrollMode), ) self.defineoptions(kw, optiondefs) # Initialise the base class (after defining the options). Pmw.MegaWidget.__init__(self, parent) # Create the components. self.origInterior = Pmw.MegaWidget.interior(self) if self['usehullsize']: self.origInterior.grid_propagate(0) if self['borderframe']: # Create a frame widget to act as the border of the clipper. self._borderframe = self.createcomponent('borderframe', (), None, Tkinter.Frame, (self.origInterior,), relief = 'sunken', borderwidth = 2, ) self._borderframe.grid(row = 2, column = 2, sticky = 'news') # Create the clipping window. self._clipper = self.createcomponent('clipper', (), None, Tkinter.Frame, (self._borderframe,), width = 400, height = 300, highlightthickness = 0, borderwidth = 0, ) self._clipper.pack(fill = 'both', expand = 1) else: # Create the clipping window. self._clipper = self.createcomponent('clipper', (), None, Tkinter.Frame, (self.origInterior,), width = 400, height = 300, relief = 'sunken', borderwidth = 2, ) self._clipper.grid(row = 2, column = 2, sticky = 'news') self.origInterior.grid_rowconfigure(2, weight = 1, minsize = 0) self.origInterior.grid_columnconfigure(2, weight = 1, minsize = 0) # Create the horizontal scrollbar self._horizScrollbar = self.createcomponent('horizscrollbar', (), 'Scrollbar', Tkinter.Scrollbar, (self.origInterior,), orient='horizontal', command=self.xview ) # Create the vertical scrollbar self._vertScrollbar = self.createcomponent('vertscrollbar', (), 'Scrollbar', Tkinter.Scrollbar, (self.origInterior,), orient='vertical', command=self.yview ) self.createlabel(self.origInterior, childCols = 3, childRows = 3) # Initialise instance variables. self._horizScrollbarOn = 0 self._vertScrollbarOn = 0 self.scrollTimer = None self._scrollRecurse = 0 self._horizScrollbarNeeded = 0 self._vertScrollbarNeeded = 0 self.startX = 0 self.startY = 0 self._flexoptions = ('fixed', 'expand', 'shrink', 'elastic') # Create a frame in the clipper to contain the widgets to be # scrolled. self._frame = self.createcomponent('frame', (), None, Tkinter.Frame, (self._clipper,) ) # Whenever the clipping window or scrolled frame change size, # update the scrollbars. self._frame.bind('', self._reposition) self._clipper.bind('', self._reposition) # Work around a bug in Tk where the value returned by the # scrollbar get() method is (0.0, 0.0, 0.0, 0.0) rather than # the expected 2-tuple. This occurs if xview() is called soon # after the Pmw.ScrolledFrame has been created. self._horizScrollbar.set(0.0, 1.0) self._vertScrollbar.set(0.0, 1.0) # Check keywords and initialise options. self.initialiseoptions() def destroy(self): if self.scrollTimer is not None: self.after_cancel(self.scrollTimer) self.scrollTimer = None Pmw.MegaWidget.destroy(self) # ====================================================================== # Public methods. def interior(self): return self._frame # Set timer to call real reposition method, so that it is not # called multiple times when many things are reconfigured at the # same time. def reposition(self): if self.scrollTimer is None: self.scrollTimer = self.after_idle(self._scrollBothNow) # Called when the user clicks in the horizontal scrollbar. # Calculates new position of frame then calls reposition() to # update the frame and the scrollbar. def xview(self, mode = None, value = None, units = None): if type(value) == types.StringType: value = string.atof(value) if mode is None: return self._horizScrollbar.get() elif mode == 'moveto': frameWidth = self._frame.winfo_reqwidth() self.startX = value * float(frameWidth) else: # mode == 'scroll' clipperWidth = self._clipper.winfo_width() if units == 'units': jump = int(clipperWidth * self['horizfraction']) else: jump = clipperWidth self.startX = self.startX + value * jump self.reposition() # Called when the user clicks in the vertical scrollbar. # Calculates new position of frame then calls reposition() to # update the frame and the scrollbar. def yview(self, mode = None, value = None, units = None): if type(value) == types.StringType: value = string.atof(value) if mode is None: return self._vertScrollbar.get() elif mode == 'moveto': frameHeight = self._frame.winfo_reqheight() self.startY = value * float(frameHeight) else: # mode == 'scroll' clipperHeight = self._clipper.winfo_height() if units == 'units': jump = int(clipperHeight * self['vertfraction']) else: jump = clipperHeight self.startY = self.startY + value * jump self.reposition() # ====================================================================== # Configuration methods. def _hscrollMode(self): # The horizontal scroll mode has been configured. mode = self['hscrollmode'] if mode == 'static': if not self._horizScrollbarOn: self._toggleHorizScrollbar() elif mode == 'dynamic': if self._horizScrollbarNeeded != self._horizScrollbarOn: self._toggleHorizScrollbar() elif mode == 'none': if self._horizScrollbarOn: self._toggleHorizScrollbar() else: message = 'bad hscrollmode option "%s": should be static, dynamic, or none' % mode raise ValueError, message def _vscrollMode(self): # The vertical scroll mode has been configured. mode = self['vscrollmode'] if mode == 'static': if not self._vertScrollbarOn: self._toggleVertScrollbar() elif mode == 'dynamic': if self._vertScrollbarNeeded != self._vertScrollbarOn: self._toggleVertScrollbar() elif mode == 'none': if self._vertScrollbarOn: self._toggleVertScrollbar() else: message = 'bad vscrollmode option "%s": should be static, dynamic, or none' % mode raise ValueError, message def _horizflex(self): # The horizontal flex mode has been configured. flex = self['horizflex'] if flex not in self._flexoptions: message = 'bad horizflex option "%s": should be one of %s' % \ (flex, str(self._flexoptions)) raise ValueError, message self.reposition() def _vertflex(self): # The vertical flex mode has been configured. flex = self['vertflex'] if flex not in self._flexoptions: message = 'bad vertflex option "%s": should be one of %s' % \ (flex, str(self._flexoptions)) raise ValueError, message self.reposition() # ====================================================================== # Private methods. def _reposition(self, event): self.reposition() def _getxview(self): # Horizontal dimension. clipperWidth = self._clipper.winfo_width() frameWidth = self._frame.winfo_reqwidth() if frameWidth <= clipperWidth: # The scrolled frame is smaller than the clipping window. self.startX = 0 endScrollX = 1.0 if self['horizflex'] in ('expand', 'elastic'): relwidth = 1 else: relwidth = '' else: # The scrolled frame is larger than the clipping window. if self['horizflex'] in ('shrink', 'elastic'): self.startX = 0 endScrollX = 1.0 relwidth = 1 else: if self.startX + clipperWidth > frameWidth: self.startX = frameWidth - clipperWidth endScrollX = 1.0 else: if self.startX < 0: self.startX = 0 endScrollX = (self.startX + clipperWidth) / float(frameWidth) relwidth = '' # Position frame relative to clipper. self._frame.place(x = -self.startX, relwidth = relwidth) return (self.startX / float(frameWidth), endScrollX) def _getyview(self): # Vertical dimension. clipperHeight = self._clipper.winfo_height() frameHeight = self._frame.winfo_reqheight() if frameHeight <= clipperHeight: # The scrolled frame is smaller than the clipping window. self.startY = 0 endScrollY = 1.0 if self['vertflex'] in ('expand', 'elastic'): relheight = 1 else: relheight = '' else: # The scrolled frame is larger than the clipping window. if self['vertflex'] in ('shrink', 'elastic'): self.startY = 0 endScrollY = 1.0 relheight = 1 else: if self.startY + clipperHeight > frameHeight: self.startY = frameHeight - clipperHeight endScrollY = 1.0 else: if self.startY < 0: self.startY = 0 endScrollY = (self.startY + clipperHeight) / float(frameHeight) relheight = '' # Position frame relative to clipper. self._frame.place(y = -self.startY, relheight = relheight) return (self.startY / float(frameHeight), endScrollY) # According to the relative geometries of the frame and the # clipper, reposition the frame within the clipper and reset the # scrollbars. def _scrollBothNow(self): self.scrollTimer = None # Call update_idletasks to make sure that the containing frame # has been resized before we attempt to set the scrollbars. # Otherwise the scrollbars may be mapped/unmapped continuously. self._scrollRecurse = self._scrollRecurse + 1 self.update_idletasks() self._scrollRecurse = self._scrollRecurse - 1 if self._scrollRecurse != 0: return xview = self._getxview() yview = self._getyview() self._horizScrollbar.set(xview[0], xview[1]) self._vertScrollbar.set(yview[0], yview[1]) self._horizScrollbarNeeded = (xview != (0.0, 1.0)) self._vertScrollbarNeeded = (yview != (0.0, 1.0)) # If both horizontal and vertical scrollmodes are dynamic and # currently only one scrollbar is mapped and both should be # toggled, then unmap the mapped scrollbar. This prevents a # continuous mapping and unmapping of the scrollbars. if (self['hscrollmode'] == self['vscrollmode'] == 'dynamic' and self._horizScrollbarNeeded != self._horizScrollbarOn and self._vertScrollbarNeeded != self._vertScrollbarOn and self._vertScrollbarOn != self._horizScrollbarOn): if self._horizScrollbarOn: self._toggleHorizScrollbar() else: self._toggleVertScrollbar() return if self['hscrollmode'] == 'dynamic': if self._horizScrollbarNeeded != self._horizScrollbarOn: self._toggleHorizScrollbar() if self['vscrollmode'] == 'dynamic': if self._vertScrollbarNeeded != self._vertScrollbarOn: self._toggleVertScrollbar() def _toggleHorizScrollbar(self): self._horizScrollbarOn = not self._horizScrollbarOn interior = self.origInterior if self._horizScrollbarOn: self._horizScrollbar.grid(row = 4, column = 2, sticky = 'news') interior.grid_rowconfigure(3, minsize = self['scrollmargin']) else: self._horizScrollbar.grid_forget() interior.grid_rowconfigure(3, minsize = 0) def _toggleVertScrollbar(self): self._vertScrollbarOn = not self._vertScrollbarOn interior = self.origInterior if self._vertScrollbarOn: self._vertScrollbar.grid(row = 2, column = 4, sticky = 'news') interior.grid_columnconfigure(3, minsize = self['scrollmargin']) else: self._vertScrollbar.grid_forget() interior.grid_columnconfigure(3, minsize = 0)