Version 8 of Multi-column display in text widget

Updated 2010-01-28 23:14:08 by WJG

Peter Berger wrote:

 > I have been charged with the task of presenting data.  I recieve a list in
 > alphabetical order such as: a b c d e f g h i j, and need to present it in a
 > dialog and organized in rows or columns such as:
 > a    e    i
 > b    f    j
 > c    g
 > d    h
 > If the dialog expands the number of columns should increase.  Currently I
 > use this code:
 > 
 > pack [text .text -wrap word -tabs "1.25i left" -setgrid 0]
 > set alist "apple bear cow dear elephant fine grape hello idol"
 > foreach a $alist {.text insert end "$a\t"}
 > 
 > but this orginizes data as:
 > a    b    c
 > d    e    ...
 > Anyone have any ideas on how I can switch to the other format?

KBK replies (20 Feb 2001):

Are you really trying, instead, to do a multi-column listbox? If so, then Bwidget and Tix both have them, and you should probably just pick up one of them. (DKF: In Tk 8.5 or Tile, you can also use the ttk::treeview widget.)

If you really need multi-column display in a text widget, read on...

I started to code this, and it turned out to be more interesting than I thought. I assume that you don't know the height of the text in advance, so you have to bind to the <Configure> event and adjust your columns on the fly. The code I came up with is shown below. Check out what happens when you resize the window.


 # Schedule to lay out the columns of the text from the idle loop

 proc repackWhenIdle { w args } {
     variable repackPending
     if { ! [info exists repackPending($w)] } {
         set repackPending($w) {}
         after idle [list [namespace code repack] $w]
     }
     return
 }

 # Lay out the columns of the text.

 proc repack { w } {

     variable repackPending
     variable list

     # Reset the flag that keeps this from being repeat-scheduled.

     catch { [unset repackPending($w)] }

     # Clear the old content of the widget

     $w configure -state normal
     $w delete 1.0 end

     # Calculate number of lines to display

     set lineHeight [font metrics [$w cget -font] -linespace]
     set textHeight [expr { [winfo height $w] - 2 * [$w cget -borderwidth] }]
     set numLines [expr { int( $textHeight / $lineHeight ) }]

     # Bail out if the widget is too small to display anything

     if { $numLines < 1 } {
         return
     }

     # Insert the requisite number of newlines, plus one

     for { set i 0 } { $i < $numLines } { incr i } {
         $w insert end \n {}
     }

     # Build up the list, in columns

     set line 1
     set sep {}
     foreach item $list {
         incr line
         $w insert "${line}.0-1c" $sep {} $item {}
         if { $line > $numLines } {
             set line 1
             set sep "\t"
         }
     }

     # Delete the excess newline

     $w delete end-1l end
     $w configure -state disabled

     return
 }

 # Set up the text

 grid [text .t \
           -state disabled \
           -wrap none \
           -font {Helvetica 12} \
           -width 40 -height 7 \
           -tabs {1.25i left} ] \
    -sticky nsew
 grid rowconfigure . 0 -weight 1
 grid columnconfigure . 0 -weight 1

 # Arrange to repack the text when either the text geometry or the
 # list content changes

 trace variable list w [list repackWhenIdle .t]
 bind .t <Configure> [list repackWhenIdle %W]

 # Set initial content of the list

 set list {
     apple blackberry blueberry cherry grape grapefruit kiwi kumquat
     lemon nectarine orange peach pear plum prune
     raisin raspberry strawberry tangerine
 }

WJG (28/Jan/10) This is a simple solution that I came up with. With a little adjustment it could be adapted to become a stand-alone proc.

#!/bin/sh
# the next line restarts using tclsh \
exec tclsh "$0" "$@"
#---------------

package require Gnocl

set txt1 [gnocl::text]
gnocl::window -child $txt1 -defaultWidth 320 -defaultHeight 200

set data {a b c d e f g h i j k l m n o p q r s t u v w x y z
          A B C D E F G H I J K L M N O P Q R S T U V W X Y Z}

set rows(max) 6

set r 0 ;# row counter

# initialise array to hold output strings
for {set i 0 } {$i < $rows(max) } {incr i} { set rows($i) {} }

# build up the output strings
for {set i 0} {$i < [llength $data] } {incr i} {
    set rows($r) "$rows($r)\t[lindex $data $i]"
    incr r
    if {$r ==  $rows(max) } {set r 0}
}

# insert int the text
for {set i 0 } {$i < $rows(max) } {incr i} {
    $txt1 insert end $rows($i)\n
}

And this is what it produces:

http://lh6.ggpht.com/_yaFgKvuZ36o/S2IZn5HoDXI/AAAAAAAAAN0/psR2JKiNa3g/s800/Screenshot-columns.tcl.png