Version 10 of Multi-column display in text widget

Updated 2010-01-29 09:26:13 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 into 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

WJG Reworked this whilst drinking my first cup of tea of the (working) day. Here's it in proc form. It will return a list of the strings which can be subsequently inserted or processed elsewhere. The leading tabs on the first column are not included which, of course, can be added if needed on insertion.

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

package require Gnocl

proc tabulate_Columns {data nrows} {

    set r 0 ;# row counter
    set str {} ;# list contain final, formatted list, returned by proc

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

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

    # insert int the text
    for {set i 0 } {$i < $nrows } {incr i} {
        lappend str $rows($i)
    }

    return $str
}

# the uniquitous demo script
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

foreach row [tabulate_Columns $data 5] {
    $txt1 insert end ${row}\n
}