# This file provides a generic Multi-Column Listbox widget. It is derived
# from a heavily hacked version of Pmw.ScrolledFrame
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
# THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
# NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 675 Mass Ave, Cambridge, MA 02139, USA.
class MultiColumnListbox(Pmw
.MegaWidget
):
def __init__(self
, parent
= None, **kw
):
colors
= Pmw
.Color
.getdefaultpalette(parent
)
# Define the megawidget options.
#('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
),
('labellist', None, INITOPT
),
('selectbackground', colors
['selectBackground'], INITOPT
),
('selectforeground', colors
['selectForeground'], INITOPT
),
('background', colors
['background'], INITOPT
),
('foreground', colors
['foreground'], INITOPT
),
('dblclickcommand', None, None),
self
.defineoptions(kw
, optiondefs
)
# Initialise the base class (after defining the options).
Pmw
.MegaWidget
.__init
__(self
, parent
)
self
._numcolumns
= len(self
['labellist'])
self
._columnlabels
= self
['labellist']
self
._lineitemframes
= []
self
.origInterior
= Pmw
.MegaWidget
.interior(self
)
self
.origInterior
.grid_propagate(0)
# Create a frame widget to act as the border of the clipper.
self
._borderframe
= self
.createcomponent('borderframe',
self
._borderframe
.grid(row
= 2, column
= 2,
rowspan
= 2, sticky
= 'news')
# Create the clipping windows.
self
._hclipper
= self
.createcomponent('hclipper',
self
._hclipper
.pack(fill
= 'both', expand
= 1)
self
._hsframe
= self
.createcomponent('hsframe', (), None,
self
._vclipper
= self
.createcomponent('vclipper',
self
._vclipper
.grid(row
= 1, column
= 0,
columnspan
= self
._numcolumns
,
sticky
= 'news')#, expand = 1)
self
._hsframe
.grid_rowconfigure(1, weight
= 1)#, minsize = 300)
for labeltext
in self
._columnlabels
:
lframe
= self
.createcomponent(labeltext
+'frame', (), None,
label
= self
.createcomponent(labeltext
, (), None,
label
.pack(expand
= 0, fill
= 'y', side
= 'left')
lframe
.grid(row
= 0, column
= gridcolumn
, sticky
= 'ews')
self
._labelframe
[labeltext
] = lframe
#print lframe.winfo_reqwidth()
self
._hsframe
.grid_columnconfigure(gridcolumn
, weight
= 1)
gridcolumn
= gridcolumn
+ 1
self
._labelheight
= lframe
.winfo_reqheight()
self
.origInterior
.grid_rowconfigure(2, minsize
= self
._labelheight
+ 2)
self
.origInterior
.grid_rowconfigure(3, weight
= 1, minsize
= 0)
self
.origInterior
.grid_columnconfigure(2, weight
= 1, minsize
= 0)
# Create the horizontal scrollbar
self
._horizScrollbar
= self
.createcomponent('horizscrollbar',
# Create the vertical scrollbar
self
._vertScrollbar
= self
.createcomponent('vertscrollbar',
self
.createlabel(self
.origInterior
, childCols
= 3, childRows
= 4)
# Initialise instance variables.
self
._horizScrollbarOn
= 0
self
._vertScrollbarOn
= 0
self
._horizScrollbarNeeded
= 0
self
._vertScrollbarNeeded
= 0
self
._flexoptions
= ('fixed', 'expand', 'shrink', 'elastic')
# Create a frame in the clipper to contain the widgets to be
self
._vsframe
= self
.createcomponent('vsframe',
# Whenever the clipping window or scrolled frame change size,
self
._hsframe
.bind('<Configure>', self
._reposition
)
self
._vsframe
.bind('<Configure>', self
._reposition
)
self
._hclipper
.bind('<Configure>', self
._reposition
)
self
._vclipper
.bind('<Configure>', self
._reposition
)
#elf._vsframe.bind('<Button-1>', self._vsframeselect)
# Check keywords and initialise options.
if self
.scrollTimer
is not None:
self
.after_cancel(self
.scrollTimer
)
Pmw
.MegaWidget
.destroy(self
)
# ======================================================================
# Set timer to call real reposition method, so that it is not
# called multiple times when many things are reconfigured at the
if self
.scrollTimer
is None:
self
.scrollTimer
= self
.after_idle(self
._scrollBothNow
)
def insertrow(self
, index
, rowdata
):
#if len(rowdata) != self._numcolumns:
# raise ValueError, 'Number of items in rowdata does not match number of columns.'
if index
> self
._numrows
:
for columnlabel
in self
._columnlabels
:
celldata
= rowdata
.get(columnlabel
)
cellframe
= self
.createcomponent(('cellframeid.%d.%s'%(self
._lineid
,
(), ('Cellframerowid.%d'%self
._lineid
),
background
= self
['background'],
cellframe
.bind('<Double-Button-1>', self
._cellframedblclick
)
cellframe
.bind('<Button-1>', self
._cellframeselect
)
cell
= self
.createcomponent(('cellid.%d.%s'%(self
._lineid
,
(), ('Cellrowid.%d'%self
._lineid
),
background
= self
['background'],
foreground
= self
['foreground'],
cell
.bind('<Double-Button-1>', self
._celldblclick
)
cell
.bind('<Button-1>', self
._cellselect
)
cell
.pack(expand
= 0, fill
= 'y', side
= 'left', padx
= 1, pady
= 1)
rowframes
[columnlabel
] = cellframe
self
._lineitemdata
[self
._lineid
] = rowdata
self
._lineitems
.insert(index
, self
._lineid
)
self
._lineitemframes
.insert(index
, rowframes
)
self
._numrows
= self
._numrows
+ 1
self
._lineid
= self
._lineid
+ 1
def _placedata(self
, index
= 0):
for rowframes
in self
._lineitemframes
[index
:]:
for columnlabel
in self
._columnlabels
:
rowframes
[columnlabel
].grid(row
= gridy
,
def addrow(self
, rowdata
):
self
.insertrow(self
._numrows
, rowdata
)
rowframes
= self
._lineitemframes
.pop(index
)
for columnlabel
in self
._columnlabels
:
rowframes
[columnlabel
].destroy()
self
._numrows
= self
._numrows
- 1
del self
._lineitems
[index
]
if index
in self
._cursel
:
self
._cursel
.remove(index
)
# Return a tuple of just one element as this will probably be the
# interface used in a future implementation when multiple rows can
return tuple(self
._cursel
)
def getcurselection(self
):
# Return a tuple of just one row as this will probably be the
# interface used in a future implementation when multiple rows can
sellist
.append(self
._lineitemdata
[self
._lineitems
[sel
]])
# ======================================================================
# The horizontal scroll mode has been configured.
mode
= self
['hscrollmode']
if not self
._horizScrollbarOn
:
self
._toggleHorizScrollbar
()
if self
._horizScrollbarNeeded
!= self
._horizScrollbarOn
:
self
._toggleHorizScrollbar
()
if self
._horizScrollbarOn
:
self
._toggleHorizScrollbar
()
message
= 'bad hscrollmode option "%s": should be static, dynamic, or none' % mode
raise ValueError, message
# The vertical scroll mode has been configured.
mode
= self
['vscrollmode']
if not self
._vertScrollbarOn
:
self
._toggleVertScrollbar
()
if self
._vertScrollbarNeeded
!= self
._vertScrollbarOn
:
self
._toggleVertScrollbar
()
if self
._vertScrollbarOn
:
self
._toggleVertScrollbar
()
message
= 'bad vscrollmode option "%s": should be static, dynamic, or none' % mode
raise ValueError, message
# The horizontal flex mode has been configured.
if flex
not in self
._flexoptions
:
message
= 'bad horizflex option "%s": should be one of %s' % \
mode
, str(self
._flexoptions
)
raise ValueError, message
# The vertical flex mode has been configured.
if flex
not in self
._flexoptions
:
message
= 'bad vertflex option "%s": should be one of %s' % \
mode
, str(self
._flexoptions
)
raise ValueError, message
# ======================================================================
def _reposition(self
, event
):
for col
in self
._columnlabels
:
maxwidth
= self
._labelframe
[col
].winfo_reqwidth()
for row
in self
._lineitemframes
:
cellwidth
= row
[col
].winfo_reqwidth()
self
._hsframe
.grid_columnconfigure(gridx
, minsize
= maxwidth
)
gridwidth
= self
._hsframe
.grid_bbox(column
= gridx
, row
= 0)[2]
if self
['horizflex'] in ('expand', 'elastic') and gridwidth
> maxwidth
:
self
._vsframe
.grid_columnconfigure(gridx
, minsize
= maxwidth
)
self
._vclipper
.configure(height
= self
._hclipper
.winfo_height() - self
._labelheight
)
# 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
, value
, units
= None):
frameWidth
= self
._hsframe
.winfo_reqwidth()
self
.startX
= string
.atof(value
) * float(frameWidth
)
clipperWidth
= self
._hclipper
.winfo_width()
jump
= int(clipperWidth
* self
['horizfraction'])
self
.startX
= self
.startX
+ jump
self
.startX
= self
.startX
- jump
# 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
, value
, units
= None):
frameHeight
= self
._vsframe
.winfo_reqheight()
self
.startY
= string
.atof(value
) * float(frameHeight
)
clipperHeight
= self
._vclipper
.winfo_height()
jump
= int(clipperHeight
* self
['vertfraction'])
self
.startY
= self
.startY
+ jump
self
.startY
= self
.startY
- jump
clipperWidth
= self
._hclipper
.winfo_width()
frameWidth
= self
._hsframe
.winfo_reqwidth()
if frameWidth
<= clipperWidth
:
# The scrolled frame is smaller than the clipping window.
if self
['horizflex'] in ('expand', 'elastic'):
# The scrolled frame is larger than the clipping window.
if self
['horizflex'] in ('shrink', 'elastic'):
if self
.startX
+ clipperWidth
> frameWidth
:
self
.startX
= frameWidth
- clipperWidth
endScrollX
= (self
.startX
+ clipperWidth
) / float(frameWidth
)
# Position frame relative to clipper.
self
._hsframe
.place(x
= -self
.startX
, relwidth
= relwidth
)
return (self
.startX
/ float(frameWidth
), endScrollX
)
clipperHeight
= self
._vclipper
.winfo_height()
frameHeight
= self
._vsframe
.winfo_reqheight()
if frameHeight
<= clipperHeight
:
# The scrolled frame is smaller than the clipping window.
if self
['vertflex'] in ('expand', 'elastic'):
# The scrolled frame is larger than the clipping window.
if self
['vertflex'] in ('shrink', 'elastic'):
if self
.startY
+ clipperHeight
> frameHeight
:
self
.startY
= frameHeight
- clipperHeight
endScrollY
= (self
.startY
+ clipperHeight
) / float(frameHeight
)
# Position frame relative to clipper.
self
._vsframe
.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
def _scrollBothNow(self
):
# 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
._scrollRecurse
= self
._scrollRecurse
- 1
if self
._scrollRecurse
!= 0:
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
()
self
._toggleVertScrollbar
()
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
= 5, column
= 2, sticky
= 'news')
interior
.grid_rowconfigure(4, minsize
= self
['scrollmargin'])
self
._horizScrollbar
.grid_forget()
interior
.grid_rowconfigure(4, minsize
= 0)
def _toggleVertScrollbar(self
):
self
._vertScrollbarOn
= not self
._vertScrollbarOn
interior
= self
.origInterior
if self
._vertScrollbarOn
:
self
._vertScrollbar
.grid(row
= 3, column
= 4, sticky
= 'news')
interior
.grid_columnconfigure(3, minsize
= self
['scrollmargin'])
self
._vertScrollbar
.grid_forget()
interior
.grid_columnconfigure(3, minsize
= 0)
# ======================================================================
#def _vsframeselect(self, event):
# print 'vsframe event x: %d y: %d'%(event.x, event.y)
# col, row = self._vsframe.grid_location(event.x, event.y)
def _cellframeselect(self
, event
):
#print 'cellframe event x: %d y: %d'%(event.x, event.y)
x
= event
.widget
.winfo_x()
y
= event
.widget
.winfo_y()
#col, row = self._vsframe.grid_location(x + event.x, y + event.y)
self
._select
(x
+ event
.x
, y
+ event
.y
)#(col, row)
def _cellselect(self
, event
):
#print 'cell event x: %d y: %d'%(event.x, event.y)
lx
= event
.widget
.winfo_x()
ly
= event
.widget
.winfo_y()
parent
= event
.widget
.pack_info()['in']
#col, row = self._vsframe.grid_location(fx + lx + event.x, fy + ly + event.y)
self
._select
(fx
+ lx
+ event
.x
, fy
+ ly
+ event
.y
)#(col, row)
col
, row
= self
._vsframe
.grid_location(x
, y
)
#print 'Clicked on col: %d row: %d'%(col,row)
lineid
= self
._lineitems
[row
]
cfg
['Cellrowid.%d_foreground'%lineid
] = self
['selectforeground']
cfg
['Cellrowid.%d_background'%lineid
] = self
['selectbackground']
cfg
['Cellframerowid.%d_background'%lineid
] = self
['selectbackground']
#cfg['Cellframerowid%d_relief'%row] = 'raised'
lineid
= self
._lineitems
[cursel
]
if cursel
!= None and cursel
!= row
:
cfg
['Cellrowid.%d_foreground'%lineid
] = self
['foreground']
cfg
['Cellrowid.%d_background'%lineid
] = self
['background']
cfg
['Cellframerowid.%d_background'%lineid
] = self
['background']
#cfg['Cellframerowid%d_relief'%cursel] = 'flat'
apply(self
.configure
, (), cfg
)
def _cellframedblclick(self
, event
):
#print 'double click cell frame'
cmd
= self
['dblclickcommand']
def _celldblclick(self
, event
):
#print 'double click cell'
cmd
= self
['dblclickcommand']
if __name__
== '__main__':
rootWin
.title('MultiColumnListbox Demo')
rootWin
.configure(width
= 500, height
= 300)
print listbox
.getcurselection()
listbox
= MultiColumnListbox(rootWin
,
#print 'start adding item'
r
[('Column %d'%j
)] = 'Really long item name %d'%i
listbox
.pack(expand
= 1, fill
= 'both', padx
= 10, pady
= 10)
exitButton
= Tkinter
.Button(rootWin
, text
="Quit", command
=rootWin
.quit
)
exitButton
.pack(side
= 'left', padx
= 10, pady
= 10)