Introduction to Toplevel Windows

Once in a while on comp.lang.tcl we get a question like the following, often from someone coming from the world of Visual Basic:

From: Tk Newbie
Subject: how do I create floating windows?
Newsgroups: comp.lang.tcl

Help! How do I create windows?

The not-so-obvious answer is to create a toplevel widget. Some toolkits call these "windows", "panels", "forms" or "dialogs". In Tk they are called toplevel widgets. Toplevel widgets are created with the toplevel [1] command. Tk gives you one toplevel widget (named ".") automatically; when this toplevel is destroyed your application will exit.

For many applications the default toplevel is the only toplevel you'll ever need. For some, though, it is necessary to create and manage additional toplevels. File selection dialog boxes, message boxes, help windows, "About..." windows, search and replace dialogs are all examples of toplevel windows.

Window Managers and Toplevel Windows

Since toplevel windows — including "." — aren't connected to any other windows, they are managed by the window manager. If you are coming from the Windows or Macintosh world you may not be familiar with the concept of a window manager. It is simply the part of the OS that gives windows a consistent window border, buttons for minimizing and maximizing, etc. On unix users have a choice from dozens of window managers with hundreds of different looks to them. Windows and Mac users typically have no choice, they simply get what the OS gives them (though there can be variations, such as the ability to get either a Win2k or WinXP style "look" when running WinXP).

The window manager is something that cannot be fully controlled from within a Tk application. You can't, for example, request that a custom button be placed on the titlebar. However, Tk gives us a way to provide hints to the window manager via the wm [2] command. Unfortunately many of these hints are just that, and window managers are free to ignore these hints.

Give your window a title

One thing Tk can control is the title that is displayed on the title bar. Tk will give every window a default title based on the name of the widget, but that is rarely sufficient if you wish to create a polished application. To create a title use the "wm title" command. For example, if you want to create an "About" window you might want to start by giving it a decent title:

toplevel .about
wm title .about "About My App"

The above code would give you a window that looks something like the following image, though the colors and look of the buttons depends on your specific window manager. The following screenshot was taking on a Macintosh:


toplevel window with a title (MacOSX)

Hint: Even though the window "." is created automatically and is given a default title, you should get in the habit of always giving it a better title.

Preparing for the worst

Most window managers will put a button on the titlebar the user can use to destroy the window. For normal windows we can't control whether that button appears or not. We can, however, control what it does. Because Tk was originally developed on a unix system, the solution is somewhat "unixy". Unix window managers have the notion of protocols which are sent to an application as a result of some event happening on the window. For the button that destroys the window this protocol is WM_DELETE_WINDOW. This is by far the most common protocol, but others are supported to a lesser extent on some windowing systems.

Tk lets us intercept this protocol and do whatever we want. If we don't specify anything, Tk will simply honor the request to destroy the window. Using the "wm protocol" command, however, we can request that a proc be called, and this proc can decide whether or not to destroy the window.

A common use of this is to give the user an opportunity to save their work, since many users prefer to click on the button on the titlebar rather than select "Quit" or "Exit" from the menubar. One way to do that is with code that looks something like the following:

wm protocol . WM_DELETE_WINDOW [list deleteWindow .]
proc deleteWindow {w} {
  # If no save is needed, destroy the window
  if {![saveNeeded $w]} {
    destroy $w
    return
  }

  # Prompt the user to save changes
  set response [tk_messageBox \
    -type yesnocancel -default yes -icon question \
    -title "Save Changes?" -message "Do you wish to save your changes?"]
    
  if {$response eq "yes"} {
    # Attempt to save; if we succeed, destroy the window
    if {[saveChanges $w]} {destroy $w}

  } elseif {$response eq "no"} {
    # User doesn't want to save; just destroy the window
    destroy $w

  } else {
    # User pressed cancel; don't save and don't destroy
    # the window
  }
}
# You wouldn't use these definitions in the real world, but you
# can use them to test out the above code:
proc saveNeeded {w} {return 1}
proc saveChanges {w} {return 1}

If you wanted to use the above code, you would of course have to supply your own definitions for the saveNeeded and saveChanges procedures. The important things to notice is that if no save is needed the window is destroyed, and if a save is needed but fails or the user cancels, the window is not destroyed. When I develop applications I often will have the "File->Exit" menu item call this same procedure so the same thing happens whether the user quits normally or via the window manager control.

It is important to note that this code only works in response to the specific window manager protocol. If the user quits the application by destroying the process (e.g. by issuing the unix kill command, or killing it from an administrative console) this code will not be called. It is a fact of life that processes can be killed without notification. If this were not so, malicious code could be written that could overtake the screen and not be killed even by an administrator.

For additional information about other protocols that might be supported, read up on 'wm protocol' on the wm man page [2]

Transient Windows

Many window managers have the notion of "transient" windows -- windows that only appear for a brief amount of time. Typical examples include message dialogs, find-and-replace dialogs, etc. Again with the wm command, we can inform the window manager that a window is transient or not. This may or may not have any effect. Most window managers will treat transient windows differently from other windows; for example, transient windows may not get the same set of buttons on the title bar as regular windows.

Unfortunately, different window managers may do slightly different things for transient windows so it's best to test this out on any platforms on which you wish to deploy. The general rule, though, is to always mark short-lived windows as transient.

Here is a brief example of a transient window. Notice the slightly different appearance of the titlebar and the resize grip in the bottom right of the windows which is unique to the MacOSX platform; your platform may do something entirely different:

toplevel .transient
wm transient .transient .
wm title . "Normal Window"
wm title .transient "Transient Window"


normal and transient toplevels (MacOSX)

If you type that code in by hand you might not see the transient effect. The transient attribute needs to be set before the window is actually drawn on the screen. In some cases it is sufficient to withdraw then deiconify the window, and in some cases it's not. If the above code is put in a file and run you will see the effect. If you type it in interactively then the window will be drawn when you press <Return> after the toplevel command but before the window is marked as transient. If you are creating transient windows in an application just be sure that you call "wm transient" before the next update.

Event Bindings

Toplevel widgets are a little bit different than other widgets with respect to bindings. Tk has a notion of bindtags, and toplevel widgets play a special role with bindtags. If you are unfamiliar with bindtags you might want to read "Introduction to Bindtags" [3].

What makes toplevels special? Every widget gets a default list of bindtags, and one of the elements of that list is the name of the toplevel to which the widget belongs. Why is that important? It allows us to have bindings that only work within a specific window.

For example, message dialog boxes typically have an OK button, and pressing <Return> when the window is visible will do the same thing as if you clicked on the OK button. This result can be achieved even if there are other widgets in the window, by binding to the toplevel widget. In doing so, no matter which widget inside the toplevel has the focus, the binding will still fire.

toplevel .about
label .about.text -text "My App is Cool"
pack .about.text -side top -fill both -expand 1
button .about.ok -text OK -command about_ok
pack .about.ok -side bottom -expand 1
bind .about <Return> about_ok
proc about_ok {} {destroy .about}

Truly global bindings

When first starting out with Tk, it's easy to get into the habit of binding to "." whenever you want a global binding. For example, to have <F1> bring up a help window you might be tempted to do it like this:

bind . <F1> show_help

From what we just learned, however, that binding will only ever work if focus is in the main window. You might only have one window in which case the point is moot. If you get in the habit of doing this, though, you might find yourself one day scratching your head and wondering why <F1> doesn't work when you have a second window open.

The lesson is simple: if you If you want a truly global event, bind to "all" rather than ".". And if you want a binding that is global within the scope of one window only, bind to the toplevel window instead of to "all".

Creating toplevel widgets on demand

There are a couple of different ways to manage the creation of toplevel widgets. Once is to create them ahead of time and make them appear (wm deiconify) or disappear (wm withdraw) as needed. Another approach is to create them on demand on an as-needed basis. This second approach is arguably a bit more efficient with respect to memory, though the memory overhead of a toplevel is pretty darn small.

Creating the toplevel widgets on demand is hardly any different than creating them once up front. Write a procedure to create the toplevel, then simply check to see if the toplevel exists or not at the start of the procedure. Once created but no longer needed, it can either be hidden and reused, or destroyed and recreated depending whatever makes the most sense for this particular window.

proc about {} {
  if {![winfo exists .about]} {
    <code to create .about window>
  }
  wm deiconify .about
}

As you can see, the first time "about" is called it will create the window. If I have a button that dismisses the window I will typically have it just hide the window rather than destroy it. If the state of the application depends on the window being recreated, I can destroy it at will and the next time it needs to be displayed it will again be created.

Plain (Overrideredirected) Windows

Once in a great while it is useful to create a window that is completely devoid of all window manager decorations — no titlebar, no buttons, no border. Tk gives us the ability to do that by setting the overrideredirect flag on the window with the "wm overrideredirect" command. Be forewarned: when you do this you likely will lose all ability to move, resize or destroy the window with the mouse. You will be responsible for making bindings that let the user manage the window.

What are these windows good for? You can use them to create a kiosk-type application that takes over the whole screen, for example. These windows are also commonly used as mouse-over tooltips, for splash screens, and other instances where you the user doesn't need to directly manipulate the window.

Summary

Toplevel windows are simple to create and are a useful solution to many problems. Unlike most widgets, however, the look and feel of a toplevel is partially controlled by the window manager. Fortunately Tk gives us several mechanisms to interface with the window manager so that we can create floating windows that behave in a way that is consistent with other applications on the system, whether that system is some variant of Unix, Windows or the Mac OS.


References

  1. http://www.tcl.tk/man/tcl8.4/TkCmd/toplevel.htm
  2. http://www.tcl.tk/man/tcl8.4/TkCmd/wm.htm
  3. http://www.tclscripting.com/articles/mar06/article2.html

Further Reading