6 class ScrolledFrame(Pmw.MegaWidget):
7 def __init__(self, parent = None, **kw):
9 # Define the megawidget options.
12 ('borderframe', 1, INITOPT),
13 ('horizflex', 'fixed', self._horizflex),
14 ('horizfraction', 0.05, INITOPT),
15 ('hscrollmode', 'dynamic', self._hscrollMode),
16 ('labelmargin', 0, INITOPT),
17 ('labelpos', None, INITOPT),
18 ('scrollmargin', 2, INITOPT),
19 ('usehullsize', 0, INITOPT),
20 ('vertflex', 'fixed', self._vertflex),
21 ('vertfraction', 0.05, INITOPT),
22 ('vscrollmode', 'dynamic', self._vscrollMode),
24 self.defineoptions(kw, optiondefs)
26 # Initialise the base class (after defining the options).
27 Pmw.MegaWidget.__init__(self, parent)
29 # Create the components.
30 self.origInterior = Pmw.MegaWidget.interior(self)
32 if self['usehullsize']:
33 self.origInterior.grid_propagate(0)
35 if self['borderframe']:
36 # Create a frame widget to act as the border of the clipper.
37 self._borderframe = self.createcomponent('borderframe',
39 Tkinter.Frame, (self.origInterior,),
43 self._borderframe.grid(row = 2, column = 2, sticky = 'news')
45 # Create the clipping window.
46 self._clipper = self.createcomponent('clipper',
48 Tkinter.Frame, (self._borderframe,),
51 highlightthickness = 0,
54 self._clipper.pack(fill = 'both', expand = 1)
56 # Create the clipping window.
57 self._clipper = self.createcomponent('clipper',
59 Tkinter.Frame, (self.origInterior,),
65 self._clipper.grid(row = 2, column = 2, sticky = 'news')
67 self.origInterior.grid_rowconfigure(2, weight = 1, minsize = 0)
68 self.origInterior.grid_columnconfigure(2, weight = 1, minsize = 0)
70 # Create the horizontal scrollbar
71 self._horizScrollbar = self.createcomponent('horizscrollbar',
73 Tkinter.Scrollbar, (self.origInterior,),
78 # Create the vertical scrollbar
79 self._vertScrollbar = self.createcomponent('vertscrollbar',
81 Tkinter.Scrollbar, (self.origInterior,),
86 self.createlabel(self.origInterior, childCols = 3, childRows = 3)
88 # Initialise instance variables.
89 self._horizScrollbarOn = 0
90 self._vertScrollbarOn = 0
91 self.scrollTimer = None
92 self._scrollRecurse = 0
93 self._horizScrollbarNeeded = 0
94 self._vertScrollbarNeeded = 0
97 self._flexoptions = ('fixed', 'expand', 'shrink', 'elastic')
99 # Create a frame in the clipper to contain the widgets to be
101 self._frame = self.createcomponent('frame',
103 Tkinter.Frame, (self._clipper,)
106 # Whenever the clipping window or scrolled frame change size,
107 # update the scrollbars.
108 self._frame.bind('<Configure>', self._reposition)
109 self._clipper.bind('<Configure>', self._reposition)
111 # Work around a bug in Tk where the value returned by the
112 # scrollbar get() method is (0.0, 0.0, 0.0, 0.0) rather than
113 # the expected 2-tuple. This occurs if xview() is called soon
114 # after the Pmw.ScrolledFrame has been created.
115 self._horizScrollbar.set(0.0, 1.0)
116 self._vertScrollbar.set(0.0, 1.0)
118 # Check keywords and initialise options.
119 self.initialiseoptions()
122 if self.scrollTimer is not None:
123 self.after_cancel(self.scrollTimer)
124 self.scrollTimer = None
125 Pmw.MegaWidget.destroy(self)
127 # ======================================================================
134 # Set timer to call real reposition method, so that it is not
135 # called multiple times when many things are reconfigured at the
137 def reposition(self):
138 if self.scrollTimer is None:
139 self.scrollTimer = self.after_idle(self._scrollBothNow)
141 # Called when the user clicks in the horizontal scrollbar.
142 # Calculates new position of frame then calls reposition() to
143 # update the frame and the scrollbar.
144 def xview(self, mode = None, value = None, units = None):
146 if type(value) == types.StringType:
147 value = string.atof(value)
149 return self._horizScrollbar.get()
150 elif mode == 'moveto':
151 frameWidth = self._frame.winfo_reqwidth()
152 self.startX = value * float(frameWidth)
153 else: # mode == 'scroll'
154 clipperWidth = self._clipper.winfo_width()
156 jump = int(clipperWidth * self['horizfraction'])
159 self.startX = self.startX + value * jump
163 # Called when the user clicks in the vertical scrollbar.
164 # Calculates new position of frame then calls reposition() to
165 # update the frame and the scrollbar.
166 def yview(self, mode = None, value = None, units = None):
168 if type(value) == types.StringType:
169 value = string.atof(value)
171 return self._vertScrollbar.get()
172 elif mode == 'moveto':
173 frameHeight = self._frame.winfo_reqheight()
174 self.startY = value * float(frameHeight)
175 else: # mode == 'scroll'
176 clipperHeight = self._clipper.winfo_height()
178 jump = int(clipperHeight * self['vertfraction'])
181 self.startY = self.startY + value * jump
185 # ======================================================================
187 # Configuration methods.
189 def _hscrollMode(self):
190 # The horizontal scroll mode has been configured.
192 mode = self['hscrollmode']
195 if not self._horizScrollbarOn:
196 self._toggleHorizScrollbar()
197 elif mode == 'dynamic':
198 if self._horizScrollbarNeeded != self._horizScrollbarOn:
199 self._toggleHorizScrollbar()
201 if self._horizScrollbarOn:
202 self._toggleHorizScrollbar()
204 message = 'bad hscrollmode option "%s": should be static, dynamic, or none' % mode
205 raise ValueError, message
207 def _vscrollMode(self):
208 # The vertical scroll mode has been configured.
210 mode = self['vscrollmode']
213 if not self._vertScrollbarOn:
214 self._toggleVertScrollbar()
215 elif mode == 'dynamic':
216 if self._vertScrollbarNeeded != self._vertScrollbarOn:
217 self._toggleVertScrollbar()
219 if self._vertScrollbarOn:
220 self._toggleVertScrollbar()
222 message = 'bad vscrollmode option "%s": should be static, dynamic, or none' % mode
223 raise ValueError, message
225 def _horizflex(self):
226 # The horizontal flex mode has been configured.
228 flex = self['horizflex']
230 if flex not in self._flexoptions:
231 message = 'bad horizflex option "%s": should be one of %s' % \
232 (flex, str(self._flexoptions))
233 raise ValueError, message
238 # The vertical flex mode has been configured.
240 flex = self['vertflex']
242 if flex not in self._flexoptions:
243 message = 'bad vertflex option "%s": should be one of %s' % \
244 (flex, str(self._flexoptions))
245 raise ValueError, message
249 # ======================================================================
253 def _reposition(self, event):
258 # Horizontal dimension.
259 clipperWidth = self._clipper.winfo_width()
260 frameWidth = self._frame.winfo_reqwidth()
261 if frameWidth <= clipperWidth:
262 # The scrolled frame is smaller than the clipping window.
267 if self['horizflex'] in ('expand', 'elastic'):
272 # The scrolled frame is larger than the clipping window.
274 if self['horizflex'] in ('shrink', 'elastic'):
279 if self.startX + clipperWidth > frameWidth:
280 self.startX = frameWidth - clipperWidth
285 endScrollX = (self.startX + clipperWidth) / float(frameWidth)
288 # Position frame relative to clipper.
289 self._frame.place(x = -self.startX, relwidth = relwidth)
290 return (self.startX / float(frameWidth), endScrollX)
294 # Vertical dimension.
295 clipperHeight = self._clipper.winfo_height()
296 frameHeight = self._frame.winfo_reqheight()
297 if frameHeight <= clipperHeight:
298 # The scrolled frame is smaller than the clipping window.
303 if self['vertflex'] in ('expand', 'elastic'):
308 # The scrolled frame is larger than the clipping window.
310 if self['vertflex'] in ('shrink', 'elastic'):
315 if self.startY + clipperHeight > frameHeight:
316 self.startY = frameHeight - clipperHeight
321 endScrollY = (self.startY + clipperHeight) / float(frameHeight)
324 # Position frame relative to clipper.
325 self._frame.place(y = -self.startY, relheight = relheight)
326 return (self.startY / float(frameHeight), endScrollY)
328 # According to the relative geometries of the frame and the
329 # clipper, reposition the frame within the clipper and reset the
331 def _scrollBothNow(self):
332 self.scrollTimer = None
334 # Call update_idletasks to make sure that the containing frame
335 # has been resized before we attempt to set the scrollbars.
336 # Otherwise the scrollbars may be mapped/unmapped continuously.
337 self._scrollRecurse = self._scrollRecurse + 1
338 self.update_idletasks()
339 self._scrollRecurse = self._scrollRecurse - 1
340 if self._scrollRecurse != 0:
343 xview = self._getxview()
344 yview = self._getyview()
345 self._horizScrollbar.set(xview[0], xview[1])
346 self._vertScrollbar.set(yview[0], yview[1])
348 self._horizScrollbarNeeded = (xview != (0.0, 1.0))
349 self._vertScrollbarNeeded = (yview != (0.0, 1.0))
351 # If both horizontal and vertical scrollmodes are dynamic and
352 # currently only one scrollbar is mapped and both should be
353 # toggled, then unmap the mapped scrollbar. This prevents a
354 # continuous mapping and unmapping of the scrollbars.
355 if (self['hscrollmode'] == self['vscrollmode'] == 'dynamic' and
356 self._horizScrollbarNeeded != self._horizScrollbarOn and
357 self._vertScrollbarNeeded != self._vertScrollbarOn and
358 self._vertScrollbarOn != self._horizScrollbarOn):
359 if self._horizScrollbarOn:
360 self._toggleHorizScrollbar()
362 self._toggleVertScrollbar()
365 if self['hscrollmode'] == 'dynamic':
366 if self._horizScrollbarNeeded != self._horizScrollbarOn:
367 self._toggleHorizScrollbar()
369 if self['vscrollmode'] == 'dynamic':
370 if self._vertScrollbarNeeded != self._vertScrollbarOn:
371 self._toggleVertScrollbar()
373 def _toggleHorizScrollbar(self):
375 self._horizScrollbarOn = not self._horizScrollbarOn
377 interior = self.origInterior
378 if self._horizScrollbarOn:
379 self._horizScrollbar.grid(row = 4, column = 2, sticky = 'news')
380 interior.grid_rowconfigure(3, minsize = self['scrollmargin'])
382 self._horizScrollbar.grid_forget()
383 interior.grid_rowconfigure(3, minsize = 0)
385 def _toggleVertScrollbar(self):
387 self._vertScrollbarOn = not self._vertScrollbarOn
389 interior = self.origInterior
390 if self._vertScrollbarOn:
391 self._vertScrollbar.grid(row = 2, column = 4, sticky = 'news')
392 interior.grid_columnconfigure(3, minsize = self['scrollmargin'])
394 self._vertScrollbar.grid_forget()
395 interior.grid_columnconfigure(3, minsize = 0)