Updated 2012-08-16 15:30:37 by tab

A recurring question on comp.lang.tcl is, ``How can I center a toplevel window on the screen?''

The usual answer is to use the update command to wait for the window to be configured, and then use the [wm] command to adjust its geometry.

There is an alternative way to do it that doesn't involve the many pitfalls of update: bind to the <Configure> event and use that to notify the process that the window has been configured and that it is time to compute the geometry. The code below gives the idea:

Martin Lemburg July 20th, 2002:

Every solution on this page for centering a window is nice, but they do not work completely satisfying. On windows all solutions don't respect the border and the title of the window. So the window is not really centered, but nearly.

Can anybody give me a hint how to fix this?

I don't want to ask the registry for its toplevel borderwidth and title height (display properties), because the values in the key HKEY_CURRENT_USER\Control Panel\Desktop\WindowMetrics are strange. They are mostly negative, eg.:

  • BorderWidth - REG_SZ - -12
  • CaptionWidth - REG_SZ - -276

The CaptionWidth and 2*BorderWidth must be added to the normal height of the window to be centered. The BorderWidth must be added twice to the normal width of the window to be centered.

Are there other ways?

Peter Newman 24 April 2004: See Total Window Geometry.

# KBK - 12 February 2001
    proc make_the_window {} {

        # Create the toplevel window

        toplevel .t
        grid [label .t.l -text "This window\nshould be centered."]
        grid [button .t.d -text Dismiss -command {destroy .t}]

        # Set things up to position the window when it is
        # configured.

        bind .t <Configure> {center_the_toplevel %W}

        return

    }

    proc center_the_toplevel { w } {

        # Callback on the <Configure> event for a toplevel
        # that should be centered on the screen

        # Make sure that we aren't configuring a child window

        if { [string equal $w [winfo toplevel $w]] } {

            # Calculate the desired geometry

            set width [winfo reqwidth $w]
            set height [winfo reqheight $w]
            set x [expr { ( [winfo vrootwidth  $w] - $width  ) / 2 }]
            set y [expr { ( [winfo vrootheight $w] - $height ) / 2 }]

            # Hand the geometry off to the window manager

            wm geometry $w ${width}x${height}+${x}+${y}

            # Unbind <Configure> so that this procedure is
            # not called again when the window manager finishes
            # centering the window

            bind $w <Configure> {}

        }

        return
    }

    grid [button .b -text "Test" -command make_the_window] \
        [button .q -text "Quit" -command exit]

Sergey Vlasov, 12-Jun-2005:

The code above does not work at least in Linux with Tcl/Tk 8.4.8 - apparently when the <Configure> event finally arrives, it is too late to call wm geometry. However, using
    after idle {center_the_toplevel .t}

instead of binding to the <Configure> event seems to work.

tclguy writes, "If you have 8.3, the ideal proc is defined in $tk_library/tk.tcl as ::tk::PlaceWindow. This is a private function, but you can copy it for your needs (or expect that it exists at least for 8.3)."

W. Rösler: I used the following, taken from the dialog.tcl file from the Tk distribution, which seems to work fine:
  proc center_window {w} {
    wm withdraw $w
    update idletasks
    set x [expr [winfo screenwidth $w]/2 - [winfo reqwidth $w]/2 \
          - [winfo vrootx [winfo parent $w]]]
    set y [expr [winfo screenheight $w]/2 - [winfo reqheight $w]/2 \
          - [winfo vrooty [winfo parent $w]]]
    wm geom $w +$x+$y
    wm deiconify $w
  }

Arjen Markus The following code is based partly on the above. It will bring up a "transient" window for displaying a logo, name whatever that eventually disappears. While the transient is visible, the main toplevel widget (.) is not.
  # Show a transient window, withdraw the usual window while that is visible
  #
  proc center_transient_window { w } {
     set width [winfo reqwidth $w]
     set height [winfo reqheight $w]
     set x [expr { ( [winfo vrootwidth  $w] - $width  ) / 2 }]
     set y [expr { ( [winfo vrootheight $w] - $height ) / 2 }]

     # Hand the geometry off to the window manager

     wm geometry $w ${width}x${height}+${x}+${y}
  }

  wm withdraw .
  toplevel .transient
  wm overrideredirect .transient 1
  wm transient        .transient
  center_transient_window .transient

  canvas   .transient.c
  pack     .transient.c -fill both
  .transient.c create rectangle 10 10 40 40 -fill green

  after 3000 {destroy .transient ; wm deiconify .}

Note: because the window is transient, it does not receive Configure events.

Arjen Markus Below is a full-featured proc for displaying a picture in the transient window (it does not delete the image though):
 # tkmisc.tcl --
 #    Package that implements various small Tk utilities
 #

 # tkmisc --
 #    Namespace for the commands
 #
 namespace eval ::tkmisc {
    namespace export showTransientWindow
 }

 # showTransientWindow
 #    Show a transient window, possibly with a bitmap (at start-up for
 #    instance)
 #
 # Arguments:
 #    time        Time it remains visible in seconds
 #    pictfile    Name of a picture file (may be empty)
 #    script      Script to be executed after the window has been created
 #                (optional)
 #
 # Return value:
 #    Widget name of the canvas created inside
 #
 # Note:
 #    If the name of the picture file is empty, the window is drawn at
 #    default size
 #    If a script is given, it should take "w" to mean the canvas in the
 #    transient window, for instance:
 #       showTransientWindow 3 {} {
 #          $w create text 10 10 -text "Hello World"
 #       }
 #
 proc ::tkmisc::showTransientWindow { time pictfile {script {}} } {

    #
    # Withdraw the default toplevel window, create a transient one
    # (centred) with a default size or determined from the picture
    #
    set t .transient
    set w ${t}.c

    wm withdraw .
    toplevel $t
    wm overrideredirect $t 1
    wm transient        $t

    if { $pictfile != "" } {
       set img [image create photo -file $pictfile]
       set height [image height $img]
       set width  [image width  $img]
       canvas $w -width $width -height $height
       $w create image 0 0 -anchor nw -image $img
    } else {
       canvas $w
       set width  [winfo reqwidth  $t]
       set height [winfo reqheight $t]
    }

    #
    # Centre the toplevel window
    #
    set x [expr { ( [winfo vrootwidth  $t] - $width  ) / 2 }]
    set y [expr { ( [winfo vrootheight $t] - $height ) / 2 }]

    # Hand the geometry off to the window manager

    wm geometry $t ${width}x${height}+${x}+${y}

    pack $w -fill both

    if { $script != {} } {
       eval $script
    }

    #
    # Now make it disappear in time
    # Note:
    #    The [list] command does not work for some reason.
    #after [expr {$time*1000}] [list destroy $t ; wm deiconify .]
    after [expr {$time*1000}] "destroy $t ; wm deiconify ."
 }

 #
 # Test code
 #
 if { [file tail [info script]] == [file tail $::argv0] } {
    namespace import ::tkmisc::*
    showTransientWindow 3 {} {$w create rectangle 10 10 30 30 -fill  green}
    after 4000 {
       showTransientWindow 3 "logoMed.gif"
    }
 }

EMJ
    #    The [list] command does not work for some reason.
    #after [expr {$time*1000}] [list destroy $t ; wm deiconify .]
    after [expr {$time*1000}] "destroy $t ; wm deiconify ."

But
    after [expr {$time*1000}] [list destroy $t \; wm deiconify .]

does work - "Think like the interpreter" (quote from somebody, can't find it right now).

Arjen Markus Yes, but the effect was rather surprising - there was no error message but the command (and seemingly) some earlier commands did not work.

DGP Uhhhh... if
    after [expr {$time*1000}] [list destroy $t \; wm deiconify .]

works, then you've got a buggy [list] in your Tcl library. [list] should quote the semicolon so that it isn't special to [eval]. I can't find such a buggy Tcl. What version of Tcl are you using?

AM I am using for instance Tcl 8.3.1 on Solaris, but also Tcl 8.3.4 from ActiveTcl on Windows, they all give (when typed into the console):
   % set a [list b ; c]

ambiguous command name "c": case catch cd clock close concat continue

Isn't it the interpreter that sees ; as a command separator, before [list] can deal with it?

DGP
   % set a [list b ; c]
   ambiguous command name "c": case catch cd clock close concat continue

In that example, the [list] command is ended by the semicolon, and the next command [c] is evalauted, raising the error seen. The [set a] never gets a chance to run because of errors during command substitution of its arguments.

Now look at:
   % set a [list b \; c]
   b {;} c

No error, but the semi-colon is quoted. If I pass that list to [eval] it will see the command [b] with arguments ; and c. It will not see the two commands [b] and [c] separated by a semi-colon.

Finally consider:
   % set a "b ; c"
   b ; c

That's what you need to pass to [eval] to get two commands evaluated in sequence.

Clear?

From the Tcl chatroom:

suchenwi: If you want to use list to keep whitespacy words together, you could use it locally:
 after [expr {$time*1000}] "destroy [list $t] ; wm deiconify ."

list on a non-whitespacy argument doesn't hurt (or add braces); but if $t is ".my Toplevel", it would come as "destroy {.my Toplevel};" in the after script.

dgp: or,
 [after $delay "[list destroy $t]; [list wm deiconify .]"]

DGP One final note. While the discussion above may be useful for understanding the fine points of Tcl substitution rules, the right answer to the problem posed is to define a [proc].

DKF notes: The expected behaviour of
 after [expr {$time*1000}] [list destroy $t \; wm deiconify .]

is probably quite surprising to newbies. What it will do is it will kill Tk completely (in the current interpreter) after $time seconds. Probably not what was desired!

This is because the list invokation forms a single command (which is destroy) with 5 arguments, two of which happen to look like widget names (those are $t and ., presumably.) Now destroy is a helpful command, which just ignores requests to destroy widgets that don't exist; hence it ignores the \; wm deiconify and destroys $t and then .

Destroying . stops the current interpreter from being Tk-enabled...

WJG 25-FEB-07. Centered window with a specific size.
 #-----------------------------------------
 # centre the window, with specified height and width, defaults to 640 x 480
 #-----------------------------------------
 proc centre_window {w {width 640} {height 480} } {
      set x [expr { ( [winfo vrootwidth  $w] - $width  ) / 2 }]
      set y [expr { ( [winfo vrootheight $w] - $height ) / 2 }]
      wm geometry $w ${width}x${height}+${x}+${y}
 }

MHo 2007, Sep. 23: Here's my try, a kind of combination of what I've read here and there:
 #-------------------------------------------------------------------------------
 # - takes the path of a window as the only optional arg
 # - returns 6 elements which are either 0 or the following, in that order:
 #    winOffSetX winOffsetY borderWidth borderHeight taskbarWidth taskbarHeight
 #
 proc taskbar {{w .taskBarSz_tmp}} {
      catch {destroy $w}
      toplevel $w
      # wm overrideredirect $w 1; # this ignores the TaskBar!
      wm state $w zoomed
      update
      foreach {x y oX oY} [list [winfo x $w] [winfo y $w] 0 0] {break}
      # Width of the ver. border must be added to width:
      set wAdd [expr {[winfo rootx $w] - $x}]
      # Height of the hor. border incl. window title bar must be added to height:
      set hAdd [expr {[winfo rooty $w] - $y}]
      # TaskBar Width (if on the LEFT or RIGHT side):
      set tW [expr {[winfo screenwidth  $w] - ([winfo width  $w] + $wAdd)}];# or 'reqwidth'?
      # TaskBar Height (if at the TOP or BOTTOM):
      set tH [expr {[winfo screenheight $w] - ([winfo height $w] + $hAdd)}];# or 'reqheight'?
      if {$y < $x} {
         set oX $tW; # TaskBar on the LEFT
      } elseif {$x < $y} {
         set oY $tH; # TaskBar on the RIGHT
      }
      destroy $w
      return [list $oX $oY $wAdd $hAdd $tW $tH]
 }
 #-------------------------------------------------------------------------------
 # centers the given window on screen, respecting TaskBar size and position
 #
 proc center_window {w} {
      wm withdraw $w
      update idletasks
      foreach {oX oY wAdd hAdd tW tH} [taskbar] {break}
      set workAreaWidth  [expr {[winfo screenwidth  $w] - $tW}]
      set workAreaHeight [expr {[winfo screenheight $w] - $tH}]
      set windowWidth    [expr {[winfo reqwidth  $w] + $wAdd}]
      set windowHeight   [expr {[winfo reqheight $w] + $hAdd}]
      set x [expr {($workAreaWidth  - $windowWidth)  / 2 + $oX}]
      set y [expr {($workAreaHeight - $windowHeight) / 2 + $oY}]
      wm geom $w +$x+$y
      wm deiconify $w
    # ::tk::PlaceWindow $w; # aus tk.tcl. Does not respect the taskbar.
 }
 # Remark: the two routines above, and PlaceWindow, could and should be combined.

MHo 2008-02-11: reworked and simplified the code from above:
 #-------------------------------------------------------------------------------
 # centers the given window on screen, respecting TaskBar size and position
 #
 proc center_window {w} {
     set tw ${w}_temp_
     catch {destroy $tw}
     toplevel $tw
     wm attributes $tw -alpha 0.0 ; # avoid flicker through wm ... zoomed
     wm state $tw zoomed
     update
     set areaW [winfo width  $tw] ; # border thickness not relevant (?)
     set areaH [winfo height $tw] ; # titlebar not relevant
     set offsX [winfo x      $tw]
     set offsY [winfo y      $tw]
     destroy $tw
     set winW [winfo width  $w]
     set winH [winfo height $w]
     wm withdraw $w
     set x [expr { (($areaW - $winW) / 2) + $offsX }]
     set y [expr { (($areaH - $winH) / 2) + $offsY }]
     wm geom $w +$x+$y
     wm deiconify $w
     update
 }

I don't really know why, but on my system it works....

MHo 2008-02-13: Found (more) bugs and simplified code again, see Centering a window on windows.

TAB August 2012 This is my version to center a child toplevel over a parant toplevel. I use mostly overrideredirect true and this prevents for moving the window. The result of a center (i.e. iwidget) is, that the child could be hidden left/right/top/bottom if the child is to big. This handful lines handle the new position.
 ############################################
 # Center the dialog child over dialog parent
 proc center {child parent} {
     after idle _center $child $parent
 }
 proc _center {child parent} {

     if {[winfo exists $child] && [winfo exists $parent] } {
         # border offset
         set bo 2

         # We must make an update befor we calculate the new postion
         # the update does the resizing
         # (reqheigh, reqwidth does not do what is expected)
         update

         # we get current widget position and size
         # and determin the center of the widget
         foreach w {child parent} {
             set wg [set $w]
             set x$w [winfo x $wg]
             set y$w [winfo y $wg]
             set h$w [winfo height $wg]
             set w$w [winfo width $wg]

             # Find center of the Windows
             set zx$w [expr [winfo x $wg] + ([winfo width $wg] /2)]
             set zy$w [expr [winfo y $wg] + ([winfo height $wg] /2)]
         }

         # what is the maximum of a new postion
         set wmax [winfo screenwidth $child]
         set hmax [winfo screenheight $child]

         # first possible new position if the parent
         set newx [expr $zxparent - $wchild/2]
         set newy [expr $zyparent - $hchild/2]

         # think about border left
         if {$newx < 0} { set newx $bo}
         # think about border top
         if {$newy < 0} { set newy $bo}
         # think about border right
         if {($newx + $wchild) > $wmax} { set newx [expr $wmax - $wchild - $bo]}
         # think about border bottom
         if {($newy + $hchild) > $hmax} { set newy [expr $hmax - $hchild - $bo]}

        # put the child to the new position
        wm geometry $child +$newx+$newy

     }
  }