What is it?A mass-widget is a widget that can handle from 1,000 to 1,000,000 items, and more. The best example is the listbox. Others examples are the listbox' derivatives:Why worry?Now that hard disks are very large, directories can contain many thousands of files. I remember becoming very unhappy when my new Columns megawidget died not so gracefully when I wanted to display a huge directory: my widget was busy a long time and suddently disappeared!So I tried to deal with that and wrote a listbox megawidget that can handle 1,000,000 items and more. You can find this Hugelist megawidget at http://perso.wanadoo.fr/maurice.ulis/tcl/Hugelist
. [Problems and solution with mass-widgetsThere are two main problems:- displaying, selecting, scrolling, moving, or resizing a mass-widget can hog all the CPU,
- a 1,000,000 item mass-widget can hog all the memory.
- when selecting, we have to do the mapping between the selected visible row and the currently displayed item,
- when displaying, we need to know if a visible row is displaying a selected item.
I can see where this might be an issue with the BWidgets tree widget. Conceptually I see both of these as an example where there is a view port used to display a current subset of a potentially very large set of data. Putting all of the data in a native widget or a megawidget might be possible, but it could also cause massive problems. Part of the problem is that the widget controls need to control the base set of data and not just the currently displayed subset. So there is a need for some indirection or virtualization between the controls and the display mechanisms. Only some of the data that might need to be displayed needs to be saved for data not currently being displayed. You could keep an array that tracked which nodes of a tree were not collapsed. As the user navigated around the tree and different pieces were swapped into the native widget, the state of the tree nodes would be recreated from the associated data instead of being all stored within the tree widget itself.--escargo 11/11/2002
# Displaying virtual items
# ------------------------
# parameters
# ------------------------
set visible_width 20 ; # visible area width, in chars
set visible_height 20 ; # visible area height, in rows
set char_width 8 ; # char width, in pixels
set char_height 16 ; # char height, in pixels
set items_count 1000000 ; # items count
# ------------------------
# items init
# ------------------------
wm title . "items init..."
update
for {set i 0} {$i < $items_count} {incr i} { lappend items item-$i }
wm title . "mass-widget"
update
# ------------------------
# widget init
# ------------------------
set canvas_width [expr {$visible_width * $char_width}]
set canvas_height [expr {$visible_height * $char_height}]
frame .f
canvas .f.c -width $canvas_width -height $canvas_height \
-bg white -bd 2 -relief sunken
set y 4
set width [expr {$canvas_width + 4}]
for {set i 0} {$i < $visible_height} {incr i} \
{
.f.c create text 6 $y -tags text$i -anchor nw
.f.c create rectangle 4 $y $width [incr y $char_height] \
-tags rect$i -outline ""
.f.c raise text$i
}
# ------------------------
# display proc
# ------------------------
proc display {} \
{
set item [.e get]
for {set i 0} {$i < $::visible_height} {incr i} \
{
.f.c itemconf text$i -text [lindex $::items $item]
.f.c itemconf rect$i -fill ""
incr item
}
}
# ------------------------
# interface
# ------------------------
label .l -text "enter an item number"
entry .e
.e insert end 0
button .b -text display -command display
# ------------------------
# start
# ------------------------
pack .f
pack .f.c -side left
pack .l .e .b -pady 5
display
focus -force .e# scrolling virtual items
# ------------------------
# scroll bar init
# ------------------------
scrollbar .f.s -command yview
pack .f.s -side left -fill y
# ------------------------
# scroll procs
# ------------------------
# setting the scroll bar
proc yset {} \
{
set first [.e get]
set last [expr {$first + $::visible_height}]
.f.s set [expr {double($first) / $::items_count}] \
[expr {double($last) / $::items_count}]
}
# setting the visible area
# parm1: scroll command (moveto or scroll)
# parm2: scroll command args
proc yview {cmd args} \
{
switch $cmd \
{
moveto \
{
# absolute movement
set first [expr {int($args * $::items_count)}]
}
scroll \
{
# relative movement
set count [lindex $args 0]
set units [lindex $args 1]
if {[string match p* $units]} \
{
# paging
set count [expr {$count * $::visible_height}]
}
set first [.e get]
incr first $count
}
}
# setting the scroll bar & displaying the visible area
if {$first < 0} { set first 0 }
if {$first > $::items_count - $::visible_height} \
{ set first [expr {$::items_count - $::visible_height}] }
.e delete 0 end
.e insert end $first
yset
display
}
# ------------------------
# button command
# ------------------------
.b config -command { yset; display }
yset# configuring virtual items
# ------------------------
# new config
# ------------------------
set defconf {"" black navy white} ; # default config
set configs [list $defconf] ; # registered configs
# parm1: config to register
# return: config ID
proc newconfig {config} \
{
set ID [llength $::configs]
lappend ::configs $config
return $ID
}
# ------------------------
# new range
# ------------------------
set ranges {{0 0}} ; # config ID associated with each item
# parm1: first item
# parm2: last item
# parm3: config ID
proc newrange {first last CID} \
{
if {$first == [lindex [lindex $::ranges end] 0]} \
{
# add a new range
set ::ranges [lreplace $::ranges end end [list $first $CID] [incr last]]
} \
else \
{
# replace a range
set step 1
foreach range $::ranges \
{
foreach {start newID} $range break
switch $step \
{
1 \
{
# before first
if {$start < $first} \
{
lappend newranges $range
if {$newID != ""} { set curID $newID }
} \
else \
{
lappend newranges [list $first $CID]
if {$newID != ""} { set curID $newID }
set step 2
}
}
2 \
{
# between first & last
if {$start > $last} \
{
lappend newranges [list [incr last] $curID]
if {$newID != ""} { set curID $newID }
set step 3
} \
elseif {$newID != ""} { set curID $newID }
}
3 \
{
# after last
lappend newranges $range
}
}
}
if {$step == 2} { lappend newranges [list [incr last] $curID] }
lappend newranges $::items_count
set ::ranges $newranges
}
}
# ------------------------
# retrieve a config
# ------------------------
# parm1: item ID
# return: registered config
proc retrieve {item} \
{
set CID 0
foreach range $::ranges \
{
foreach {start newID} $range break
if {$start > $item} { break }
set CID $newID
}
return [lindex $::configs $CID]
}
# ------------------------
# mass-configure items
# ------------------------
# parm1: first item to configure
# parm2: last item to configure (optional)
# parm3: modified options from last config
proc config {first last args} \
{
# get args
if {[string index $last 0] == "-"} \
{ set args [linsert $args 0 $last]; set last "" }
if {$last == ""} { set last $first }
# modify & register config
set CID [llength $::configs]; incr CID -1
if {$args != ""} \
{
set config [lindex $::configs $CID]
set changed 0
foreach {background foreground selbackground selforeground} $config break
foreach {key value} $args \
{
switch -glob -- $key \
{
-bg -
-bac* { set background $value; set changed 1 }
-fg -
-for* { set foreground $value; set changed 1 }
-selb* -
-selectb* { set selbackground $value; set changed 1 }
-self* -
-selectf* { set selforeground $value; set changed 1 }
-tex* { set ::items [lreplace $::items $first $last $value }
}
}
if {$changed} \
{
# create a new config
set config [list $background $foreground $selbackground $selforeground]
set CID [newconfig $config]
}
}
# associate items range and config ID
newrange $first $last $CID
}
# ------------------------
# new display proc
# ------------------------
proc display {} \
{
set item [.e get]
for {set i 0} {$i < $::visible_height} {incr i} \
{
# retrieve config
set config [retrieve $item]
foreach {background foreground selbackground selforeground} $config break
# map item
.f.c itemconf text$i -text [lindex $::items $item] -fill $foreground
.f.c itemconf rect$i -fill $background
incr item
}
}
# ------------------------
# configure & display
# ------------------------
# set global background
set last [expr {$items_count - 1}]
config 0 $last -bg beige
# change some items
set incr [expr {$items_count / 100}]
if {$incr == 0} { set incr 10 }
for {set i 0} {$i < $items_count} {incr i $incr} { config $i -fg red }
# display
display# selecting items
# ------------------------
# select binding
# ------------------------
bind .f.c <1> { select %y }
# ------------------------
# select proc
# ------------------------
set selection {} ; # selected items list
# parm1: y listbox coordinate
proc select {y} \
{
# get item
set item [expr {[.e get] + $y / $::char_height}]
# toggle select state
set n [lsearch $::selection $item]
if {$n == -1} { lappend ::selection $item } \
else { set ::selection [lreplace $::selection $n $n] }
# display
display
}
# ------------------------
# new display proc
# ------------------------
proc display {} \
{
# get first item
set item [.e get]
# display all visible items
for {set i 0} {$i < $::visible_height} {incr i} \
{
# retrieve config
set config [retrieve $item]
foreach {background foreground selbackground selforeground} $config break
# set colors
if {[lsearch $::selection $item] != -1} \
{
set background $selbackground
set foreground $selforeground
}
# map item
.f.c itemconf text$i -text [lindex $::items $item] -fill $foreground
.f.c itemconf rect$i -fill $background
incr item
}
}# editing items
# ------------------------
# edit binding
# ------------------------
bind .f.c <Double-1> { edit %y }
# ------------------------
# edit proc
# ------------------------
set edited "" ; # edited item
# parm1: y listbox coordinate
proc edit {y} \
{
# get row and mapped item
set row [expr {$y / $::char_height}]
set item [expr {[.e get] + $row}]
# save item
set ::edited $item
# create edit box
foreach {x y} [.f.c bbox text$row] break
set text [lindex $::items $item]
set width [expr {[string length $text] * 2}]
entry .edit -width $width \
-validate focusout -vcmd validate
.edit insert end $text
.edit selection range 0 end
place .edit -in .f.c -x $x -y $y
# focus management
bind .edit <Return> validate
focus -force .edit
set ::binding [bind .f.c <1>]
bind .f.c <1> {+ focus .f.c }
}
# ------------------------
# validate proc
# ------------------------
proc validate {} \
{
# get item
set item $::edited
# get new text
set text [.edit get]
# update item
set ::items [lreplace $::items $item $item $text]
# display
place forget .edit
display
# restore as before
bind .f.c <1> $::binding
focus -force .e
after idle destroy .edit
return 1
}Category GUI | Category Design | Category Performance | Category Widget

