size and proportion of widgets

Aug 10, 2003

Common problems in the path of Tk beginners:

  • You design the default size and position of your app very carefully. It always starts with the same size. And at the very same spot on your screen. You provide an ideal amount of space between any two individual widgets. The whole set looks perfect. But all is lost in a different screen resolution.
  • You make a text widget in your app. After some time (minutes or days), you maximize the app for the first time. The app is maximized, but the text widget and everything else keep the same size and is now surrounded by a lot of gray empty space.

In HTML, the use of percentage measures is very common and trivial.

In Tk, use grid to do the same thing. E.g., to maintain a 10:90 ratio between two columns, try the following:

 entry .s -width 1
 entry .t -width 9
 grid .s .t -sticky ew
 grid columnconf . 0 -weight 10
 grid columnconf . 1 -weight 90
 wm geometry . 600x20

The widths on the entry widgets become minimum widths, but more importantly, they establish the initial ratio. The weights on the columns maintain the ratio through resizing. The geometry command sets the initial size. Rather than hard-coding initial size, you could use "winfo screenwidth" to base it on the screen width or "font measure" to base it on the number of columns of text to display by default.

Using pack or place geometry managers, you would have to calculate and generate the right sizes and proportions yourself.

That may not be a lot of fun. You'll spend quite some time working with the dreaded expr command, and probably also many levels of nested brackets... (why is expr "dreaded"?)

jmn 2003-08-10 As a Tk beginner, I find all my admittedly modest GUI resizing requirements admirably handled by the pack layout manager and the -expand option. Whilst sometimes it's easy to make a mistake and get the dreaded empty grey space attack when resizing - it only takes a little playing about with the pack options to sort it out. I'm not sure exactly what you're trying to achieve with the resizing that can't be simply handled by pack; perhaps you have more stringent requirements than I with respect to exact widget proportions, but it seems to me that the text above is a touch too negative about the situation for Tk beginners.

Suppose you make a notification window which, ideally, should be displayed at 20% the width of the screen and 10% the height (or something like that). You can't use absolute values and expect to get that same position in another screen resolution. You'll have to rely on percentages.

Also suppose you design at 800x600 and a text or entry widget looks just fine. Used by someone with higher resolutions, these widgets may look wiiiiiiiide, or looooooooong or aaaaaaaample... OK, designers don't design at 800x600. They like to boast at least 1280x1024. So the opposite is a lot more likely to happen: the app looks fine on your screen, but a user with a lower resolution, like 800x600, is likely to see everything bloated and crammed.

Many people have no problem just giving a big shrug to that. I find it terribly sloppy. I have that problem with Web pages very frequently. I inspect the source code and see that many designers use absolute values in tables! Golly, why?!! Why does HTML have percentage values anyway? Is it the designer's job to use the available tools to make their work look as good as possible in every screen or simply tell the user "get a larger screen"?

Although Tk doesn't have percentage values unless we calculate them and apply them in the code, maybe their use should be regarded as a very well recommended practice.

I guess.

US It's not so that Tk doesn't have percentage values, just pack doesn't have them. Use grid; the commands grid rowconfigure and grid columnconfigure provide a -weight option.

GJW An example using grid columnconfigure and grid rowconfigure:

toplevel  .t
label .t.l0 -text Name
entry .t.e0 -text {Last, First} -state disabled
label .t.l1 -text Number
entry .t.e1 -text 12345678 -state disabled
label .t.l2 -text Description
text  .t.t0 -height 20 -width 90

grid .t.l0 .t.e0 -sticky w
grid .t.l1 .t.e1 -sticky w
grid .t.l2 - -sticky w
grid .t.t0 - -sticky wens

if {[package require Tk] >= 8.5} {
    grid columnconfigure .t .t.t0 -weight 1
    grid rowconfigure .t .t.t0 -weight 1
} else {
    grid columnconfigure .t 1 -weight 1
    grid rowconfigure .t 3 -weight 1

24 Aug, 2003 - Here is a simple proportion challenge: I have a text editor with a status bar at the bottom that shows:

        /path/to/current/file.txt - 415 words in 121 lines

The size of the filepath name may cause the whole information not to fit the status bar. In such case, I would like to truncate the path:

        /path/to/extremely_long_path... file.txt - 415 words in 121 lines

How should I calculate how long the path can be so it doesn't need to be truncated, considering that the status bar's available length will vary in different screen resolutions?

Bryan Oakley 24 Aug 2003: The answer is to use [winfo width] to get the actual width of the window, and [font measure] to compute the width of the string, then do a little math. Here's a really quick hack. It's not particularly efficient but it doesn't have to be.

        set dir [pwd]
        set file [lindex [glob -nocomplain -type file *] 0]
        set currentPath [file join $dir $file]

        frame .statusbar -borderwidth 2 -relief groove
        label .statusbar.label -borderwidth 1 -relief sunken -width 1 \
            -anchor w
        label .statusbar.count -borderwidth 1 -relief sunken \
            -text "99,999 words in 99,000 lines"
        pack .statusbar.count -side right -fill y -expand n -ipadx 4 -padx 4
        pack .statusbar.label -side left -fill both -expand y
        pack .statusbar -side bottom -fill x -expand n
        bind .statusbar.label <Configure> showPath
        wm geometry . 600x300

        # usage: showPath ?path?
        proc showPath {args} {
                global currentPath

                # if given an arg, use it as the current path
                if {[llength $args] == 1} {
                        set currentPath [lindex $args 0]

                # compute interior width (width - borderwidth - highlightthickness)
                set width [winfo width .statusbar.label]
                set bd [.statusbar.label cget -borderwidth]
                set ht [.statusbar.label cget -highlightthickness]
                set width [expr {$width - 2 * ($bd + $ht)}]

                # compute how much space we need for label
                set font [.statusbar.label cget -font]
                set pixels [font measure $font $currentPath]

                # trim the string to fit...
                set text $currentPath
                if {$pixels > $width} {
                        set file [file tail $currentPath]
                        set dir [file dirname $currentPath]
                        while {$pixels > $width && [string length $dir] > 1} {
                                set dir [string range $dir 0 end-1]
                                set text "${dir}...$file"
                                set pixels [font measure $font $text]
                .statusbar.label configure -text $text

LES on Oct 28, 2013: I have been struggling with the following, please copy, paste and run to get a full explanation (look for the "bonus" in the source, it may be particularly interesting):

package require Tk
set w [toplevel .test]
wm withdraw .
wm resizable $w 1 1

set f $w.textframe; set t $f.text; set s $f.scrollbarv

ttk::frame $f; text $t; ttk::scrollbar $s
$t configure -font "Helvetica 16"
$t configure -wrap word 
$s configure -orient vertical 
$s configure -command [list $t yview]

pack $s -fill y -side right
pack $f -fill both -expand 1
pack $t -fill both -expand 1

bind $w <Alt_L><w>         {manytexts $t}
bind $w <Alt_L><q>         {exit 0}

# ---------------------------------------"
$t insert end "Here is a prototype that has been causing me headache.

The challenge here is to make an embedded frame with a few text widgets. Press Alt+w to make them appear. You may want to maximize the window after that.

Note that they don't fill the entire width of the parent text widget as desired. I have tried setting that width even manually, but I don't see how. I know that winfo can tell me the width of the parent text widget in pixels, but I can't use that information to resize the embedded frame, even if I set it manually/hardcode. I've tried, and nothing seems to happen.

What works manually is setting the size of the small text widgets individually to roughly the same size as the number of chars/columns in the parent text widget, but that only works because I count those chars/columns manually and hardcode that value into the CreateManyTexts proc to see what happens. What I need here to make this magic happen programatically is to know how many chars/columns the parent text widget has in each VISUAL line, i.e. before the line wraps, but I don't think there is such command, is there? I've searched the manuals high and low.

This is all using pack. I haven't tried grid yet. Either way, I think this looks like something that pack can't handle.
# ---------------------------------------"

focus $t
$t mark set insert 4.0

proc manytexts t {
    if {[winfo exists $::t.ttf]} {return}
    $::t window create insert -create {CreateManyTexts $::t}
#    $::t tag add center insert

proc CreateManyTexts {t} {
        set mtf $t.manytextsframe
        ttk::frame $mtf

        foreach i {1 2 3 4}        {
                set mt$i "$$i"
                text [set mt$i] -setgrid true 
                set h [expr [lindex [$t configure -height] end] / 5]
                [set mt$i] configure -height $h
                pack [set mt$i] -expand 1 -fill both

        $mt1 configure -bg lightblue
        $mt2 configure -bg #00ff00
        $mt3 configure -bg #ffff00
        $mt4 configure -bg bisque

        # BONUS: this next line makes the whole thing crash every time! Shell output is:
        # Job 1, "wish ./test.tcl" terminated by signal SIGSEGV (Address boundary error)
        $t mark set insert 6.0

    return $mtf

LES on Nov 03, 2013: Still struggling with the problem above, I have run into more issues: I tried another prototype with grid instead of pack and was unable to find a proper solution. I went back to pack and eventually came up with a creative hack that accomplishes the automatic width adjustment I wanted, and it seems to work pretty well. Still, it's an expr-based hack that may not work with other resolutions.

Now I struggle with height. I also want the height to adjust itself according to the amount of text the widget contains. I think there is a bug in Tk. I have a big text widget with 61 lines. Some of them are long, so there are 78 display lines. I run [$text count -lines 1.0 end] and the output is 61. Good. But then I run [$text count -displaylines 1.0 end], the output is 72 and that is wrong. There are 78, I counted. Then I create a smaller embedded text widget inside the bigger text widget. I run [$embtext count -lines 1.0 end] and the output is 1. Correct. But then I run [$embtext count -displaylines 1.0 end] and the output is 887. Very wrong. There are 887 characters in it, not 887 displaylines. There are 8 displaylines. It seems to me that the -displayline option is broken and very unpredictable.

The upshot is that there is no way for me to know what the width in characters of a text widget is except for an inaccurate guess using font measure, and I can't even come up with a way to venture a guess at how many displaylines there are in it. I've been trying, but the math for height is elusive. Maybe that has been a headache for me only, but if these problems with character-based width and accurate displaylines were solved, nobody would need this [L1 ] hack.