Setting the size of a textbox in pixels

I have an application with text boxes on a canvas, and I want to be able to resize them using the mouse; that means setting the height and width of the textbox in points rather than characters. Not having found anything on this subject here, I present my own humble effort:

  proc SizeText {w dx dy} {
    set font [$w cget -font]
    set width [expr ($dx-10)/[font measure $font "n"]]
    set height [expr ($dy-10)/[font metric $font -linespace]]
    $w config -width $width -height $height
  }

where w is the textbox, and dx and dy are the width and height of the box in points, respectively.

Notes:

  • This assumes that the text box only includes one font.
  • I use the width of the letter "n" as my conversion factor between characters and points. This seems to work but I have no idea what the real conversion factor should be.
  • The 10's should probably be replaced by twice the highlightthickness or something like that.

MG Tk uses the number "0" for measuring the size of a font's characters, IIRC.

Some Alternative Techniques

tml 20011-07-17: The text widget's width and height are integer values - even if floating point values seem to be accepted and converted. Thus, you'll never get pixel exact sizing doing font calculations as shown above. But there are easier ways to achieve your goal:

The place geometry manager allows you to assign exact pixel sizes to your subwindow, ignoring the subwindow's size requests - and it doesn't do any automatic positioning at all. If you try the following lines in an interactive wish, you'll notice that changing the text window's width or height won't show any visible effects after that:

. configure -width 500 -height 400
place [text .t] -x 100 -y 50 -width 300 -height 150

The canvas widget has the ability to manage its subwindows, letting you control the exact subwindow positioning in a manner much resembling the place geometry manager:

pack [canvas .c]
.c create window 100 50 -window [text .t] -anchor ne -width 300 -height 150 

For more complex window layouts you'll need to use grid or place. Create an intermediating frame widget and assign your required pixel sizes to that frame, then switch off this frame's geometry propagation, and be extremely careful about how you'd like to have the extra space treated, considering options like -anchor, -sticky, -expand, -fill etc for both the holder frame and the text widget:

pack [frame .textholder -width 300 -height 150] -fill y -expand yes
pack propagate .textholder no
pack [text .t] -in .textholder -fill both -expand yes

You might now decide that you'd like to specify the text widget's size in lines, again, while sticking to pixelwise sizing for its width. Having switched off the holder frame's propagation, you're intercepting propagation of both sizes - but when there's no automatism, you can propagate the subwindow's required height explicitly to the holder ... a simple proc does the trick:

proc newHeight {heightInLines} {
    .t configure -height $heightInLines
    .textholder configure -height [winfo reqheight .t]  
}

The downside of the above proc is that you'll always need to remember to set the width on the holder widget and the height through a special procedure. Instead of defining this procedure, you might intercept the text widget command (i.e. rename it and create a proc with the same name delegating to the original command) in a way that'll magically handle width and height according to your needs:

rename .t original_.t
bind .t <Destroy> {rename .t ""}
proc .t {subcommand args} {
    set result [uplevel original_.t $subcommand $args]
    if {[llength $args] > 1 && [string first "conf" $subcommand] > -1} {
        foreach {key value} $args {
            switch -- $key {
            -width  {.textholder configure -width  $value}
            -height {.textholder configure -height [winfo reqheight .t]}
    }   }   }
    return $result
}

Please don't delve to deep into this kind of interception - megawidget systems provided from packages like snit or itk are doing that for you, along with giving you a more powerful programming model each.

I've also experimented a bit to see how I'd implement the resizing through mouse events you mentioned, but the resulting code became longer and more frightening than I'd expected, so I'm placing that in the discussion section below.

Here's the full example including resizing:

#  Initialize the global variables needed to hold the subwindow
#  coordinates, and two more for temporary data we're gonna need.

foreach { x0  x1  y0  y1 xtmp ytmp
      } {150 350  50 150    0    0} {}

#  Calculate which mouse cursor to use for each of our resize
#  grips. xi must be 0/1/2 for left/middle/right (yi accordingly),
#  and we'll use that to determine the resulting name's details.  

proc gripCursor {xi yi} {
    return [join [concat [set _ [concat [
            lindex {top   ""   bottom} $yi
       ]   [lindex {left  ""   right } $xi
       ]]] [lindex {fleur side corner} [llength $_]
    ]] _]
}

#  Procedure used during resizing. It acts on our global variables,
#  parameters are the current mouse position and a list of the
#  affected global variables. (Note: Minimum sizes will be 40 pixels)

proc changeCoords {affected x y} {
    foreach     var {x y} {
        set changetmp no
        foreach i {0 1} {
            set a [llength $affected]
            if {$a == 0 || "$var$i" in $affected} {
                set old [set ::$var$i]
                incr ::$var$i [expr {[set $var] - [set ::${var}tmp]}]
                if {$a == 0 ||  [set ::${var}1] - [set ::${var}0] > 40
                   } {set changetmp yes} else {set ::${var}$i $old}
        }   }
        if {$changetmp} {set ::${var}tmp [set $var]}
    }
    reposition
} 

#  Procedure to apply the global coordinates to our canvas items,
#  including the text window item.

proc reposition {} {
    .c coords grip $::x0 $::y0 $::x1 $::y1
    foreach     xi {0 1 2} {
        foreach yi {0 1 2} {
            set radius 9
            .c coords grip$xi$yi [
               expr {int ($::x0 + ($::x1 - $::x0) * ($xi / 2.0) - $radius)}
            ] [expr {int ($::y0 + ($::y1 - $::y0) * ($yi / 2.0) - $radius)}
            ] [expr {int ($::x0 + ($::x1 - $::x0) * ($xi / 2.0) + $radius)}
            ] [expr {int ($::y0 + ($::y1 - $::y0) * ($yi / 2.0) + $radius)}] 

    }   }    
    .c coords window $::x0 $::y0
    .c itemconfigure window -width  [expr {$::x1 - $::x0}
                          ] -height [expr {$::y1 - $::y0}]   
}

#  Create bindings for our canvas items: to display the adequate mouse cursor
#  on each grip, and to handle resizing.  

proc createTagBindings {tag xi yi} {
    .c bind $tag <Leave> ".c configure -cursor {}                  ; break"
    .c bind $tag <Enter> ".c configure -cursor [gripCursor $xi $yi]; break"
    .c bind $tag <1>     "set xtmp %x; set ytmp %y                 ; break"
    set affected [concat [lindex {x0 "" x1} $xi] [lindex {y0 "" y1} $yi]]
    .c bind $tag <B1-Motion> "[list changeCoords $affected %x %y]  ; break"
}

#  Create a canvas containing a rectangle and some circles which will
#  serve as grips to resize and move the text window.

pack [canvas .c] -fill both -expand yes
.c create rect 1 2 3 4 -width 14 -outline blue -fill blue -tags grip
createTagBindings grip 1 1
foreach    xi {0 1 2} {
    foreach yi {0 1 2} {
        .c create oval 1 2 3 4 -width 2 -fill green \
                -outline black -tag grip$xi$yi
        createTagBindings grip$xi$yi $xi $yi
}  }

#  Add a text window to the canvas, and do the initial positioning

text .t -bd 2 -relief sunken
.t insert end "this\nis the\ntext\nwidget .t"
.c create window 0 0 -tags window -window .t -anchor nw
reposition