Toolbars and Grayscale Images

I was reading the Microsoft UI guidelines the other day to get some specifics about toolbar look & feel. Even though Microsoft isn't particularly consistent even within their own apps, they at least publish guidelines as a starting point. What I learned was that toolbar buttons, in addition to being flat when the mouse is not over them, should also be "neutral or grayscale" [1] (look for the section named "Hot-Tracked Appearance"). Of course, what Microsoft says and what Microsoft does can somtimes be two different things. A quick survey of my windows desktop shows that a few apps behave this way, and many don't.

Tk button widgets have built-in support (via -relief and -overrelief) for changing the relief when the cursor hovers over them, so that requirement is easy. Even without built in support we could accomplish the same thing with the use of <Enter> and <Leave> bindings. But what about the notion of changing the images to grayscale? Obviously we could have two versions of every icon — one grayscale and one full color — and swap them in and out, but that is inefficient.

What you may not know is that the Tk photo image has an option with which you can change the color palette of an image on the fly. This is done, not surprisingly, through the -palette option of each image. If given a single number it designates a number of shades of gray to use. Thus, to convert an image to grayscale we can configure the palette to be, say "256" meaning 256 shades of gray. This is all documented in the photo man page [2].

What is not documented is how to revert an image that has been converted to grayscale back to its original colors. For that we need to read between the lines and do a little experimentation. If you create an image and then use the cget method to get the value of the -palette option you'll see that by default it is the empty string. It is therefore reasonable to assume that setting the -palette option to the empty string will return the image to its original form, and so it does.

A simple example

To illustrate the technique, first we need a couple of color images that we want to use in a toolbar. We want to be able to reference the images with symbolic names rather than "image1", "image2", etc but it's generally considered to be bad practice to hard-code image names. There's a possibility that any name we pick could clash with a command name or an image created by a library procedure. The best choice is to let the image command pick a name which we'll store in an array. That guarantees us a unique image name yet lets us reference it with a symbolic name.

set image(file_open) [image create photo -data {
  R0lGODlhEgASAPIAAAAAAICAAMDAwPj8APj8+AAAAAAAAAAAACH5BAEAAAIALAAAAAASABIA
  AAM7KLrc/jAKQCUDC2N7t6JeA2YDMYDoA5hsWYZk28LfjN4b4AJB7/ue1elHDOl4RKAImQzQ
  cDeOdEp1JAAAO///
}]

set image(file_save) [image create photo -data {
  R0lGODlhEgASAPEAAAAAAICAAMDAwAAAACH5BAEAAAIALAAAAAASABIAAAI3lI+pywYPY0Qg
  AHbvqVpBamHhNnqlwIkdeoJrZUlPcML0jde0DOnxxHrtJA6frLhC8YiNpnNRAAA7////
}]

Now that we have our images, let's create a simple toolbar that uses these images. We'll use the built‑in "‑relief" and "‑overrelief" options to cause the button to become raised or go flat when the mouse hovers over them.

frame .toolbar
pack .toolbar -side top -fill x

button .toolbar.open -image $image(file_open) \
    -relief flat -overrelief raised -borderwidth 1
button .toolbar.save -image $image(file_save) \
    -relief flat -overrelief raised -borderwidth 1

pack .toolbar.open .toolbar.save -side left

If you combine the above code into a single file, or copy and paste it into a running wish process you should see a toolbar with two simple icons

toolbar with colored buttons
toolbar with colored buttons

Starting with the code from the previous example, we can give ourselves a new version of the toolbar that has grayscale images:

$image(file_open) configure -palette 256
$image(file_save) configure -palette 256

toolbar with grayscale buttons
toolbar with grayscale buttons

This is a step in the right direction but we need to restore the color when the cursor hovers over the button. To do that we'll reset the palette to the empty string to get the default color palette. Since we need to switch the colors as the cursor moves, let's put the code into a procedure.

# Usage: changeColor <color> button ?button ...?
# If <color> is "grayscale" the images will be converted to grayscale,
# otherwise they will be in full color
proc changeColor {color args} {
  set palette [expr {$color eq "grayscale" ? "256" : ""}]
  foreach widget $args {
    set image [$widget cget -image]
    $image configure -palette $palette
  }
}

Now, we must modify the toolbar to call this procedure when the mouse hovers over the icons. Also, because the images were initially created in color we need to convert them to grayscale to reflect the initial state of not having the mouse cursor hovering over them:

bind .toolbar.open <Enter> [list changeColor fullcolor %W]
bind .toolbar.open <Leave> [list changeColor grayscale %W]
bind .toolbar.save <Enter> [list changeColor fullcolor %W]
bind .toolbar.save <Leave> [list changeColor grayscale %W]

changeColor grayscale .toolbar.open .toolbar.save

That's all there is to it. If you put all that code in a file and execute it you should have a toolbar where each button is raised and colored or flat and gray depending on whether or not the mouse cursor is hovering over the button.

The complete example

set image(file_open) [image create photo -data {
  R0lGODlhEgASAPIAAAAAAICAAMDAwPj8APj8+AAAAAAAAAAAACH5BAEAAAIALAAAAAASABIA
  AAM7KLrc/jAKQCUDC2N7t6JeA2YDMYDoA5hsWYZk28LfjN4b4AJB7/ue1elHDOl4RKAImQzQ
  cDeOdEp1JAAAO///
}]

set image(file_save) [image create photo -data {
  R0lGODlhEgASAPEAAAAAAICAAMDAwAAAACH5BAEAAAIALAAAAAASABIAAAI3lI+pywYPY0Qg
  AHbvqVpBamHhNnqlwIkdeoJrZUlPcML0jde0DOnxxHrtJA6frLhC8YiNpnNRAAA7////
}]

# Usage: changeColor <color> button ?button ...?
# If <color> is "grayscale" the images will be converted to grayscale,
# otherwise they will be in full color
proc changeColor {color args} {
  set palette [expr {$color eq "grayscale" ? "256" : ""}]
  foreach widget $args {
    set image [$widget cget -image]
    $image configure -palette $palette
  }
}

frame .toolbar
pack .toolbar -side top -fill x

button .toolbar.open -image $image(file_open) \
    -relief flat -overrelief raised -borderwidth 1
button .toolbar.save -image $image(file_save) \
    -relief flat -overrelief raised -borderwidth 1

pack .toolbar.open .toolbar.save -side left

bind .toolbar.open <Enter> [list changeColor fullcolor %W]
bind .toolbar.open <Leave> [list changeColor grayscale %W]
bind .toolbar.save <Enter> [list changeColor fullcolor %W]
bind .toolbar.save <Leave> [list changeColor grayscale %W]

changeColor grayscale .toolbar.open .toolbar.save

Summary

This article uses a toolbar and buttons to illustrate a simple technique for changing the colors of an image. Should you use this technique for the toolbars in the apps you create? That's a design decision I can't answer. If you want this sort of behavior it comes at virtually no cost; Tk has been designed in a way that makes this simple to implement. Even if you don't want this look for your toolbars, the technique can be applied any time you have a color image that you want to display in grayscale, such as to imply that it is disabled or unavailable, etc. One of the great things about Tk is that it doesn't try to inforce any sort of policy, but gives us the tools to make very rich applications.

References

  1. http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnwue/html/ch14c.asp
  2. http://www.tcl.tk/man/tcl8.4/TkCmd/photo.htm#M11