How Stuff Works: Mode Switch Buttons

| | Comments (14)

The nifty looking mode switching buttons in RB 2005 are nothing more than a fancy form of canvas -- something that's entirely possible for you to make on your own. Basically, there are three sets of images for each of three states -- normal, pushed and disabled. There's a ModeSwitch canvas objcet which is responsible for forwarding all messages onto a ModeSwitchImp class (remember the bridge pattern? This is used so that we can have multiple views of the mode switcher). The ModeSwitchImp is what's responsible for taking all these messages and translating them into actions. So, for example, when a MouseDown occurs, the ModeSwitch canvas alerts the imp that a mouse down has occurred. The imp then determines which button was pressed and changes its state to Pressed. Then it refreshes that button so that it can be redrawn to reflect the new state. Once the mouse is released, the imp is notified, checks to see which button the mouse was released above and if it was the same button the mouse went down on, then it fires the Action event. The Canvas simply relays this event up the call chain and the editor pane handles it. (And the editor pane uses the observer pattern to notify any listeners who care that the mode has been switched).

So let's make a poor-man's version of the mode switcher. For the first (and maybe only :: grins :: ) version of this, we'll use a Canvas subclass to handle everything (so we're removing the bridge pattern). We're also going to remove the multiple images and just use solid colors for simplicity's sake. Oh yeah, and one more thing -- we're also going to remove some of the more "correct" UI considerations such as proper mouse down/up behavior. Basically, a very simple version that we can add on to later to improve. But, we're not taking everything neat out -- I'm going to show you how to properly handle keyboard focus so that the keyboard people in the world (like myself) won't have anything to gripe about.

Alright! The first thing we need to do is make a canvas subclass. This subclass is going to be very simple -- it's going to assume that all the buttons are the same size and are simple rectangles (this makes the Paint event trivial to implement). So let's take a look at our Paint event just so we can get it out of the way.
[rbcode]
Sub FocusableSwitcher.Paint( g as Graphics )
dim i, oneThird as Integer = g.Width / mNumParts
dim colors(-1) as Color = Array( &cFF0000, &c00FF00, &c0000FF )

// Draw all the parts of the canvas
for i = 0 to mNumParts
g.ForeColor = colors( i mod 3 )
g.FillRect( oneThird * i, 0, oneThird, g.Height )
next i

// Now draw the focus rect on the properly
// focused index -- but only if we have a focus
// index selected.
dim pos as Integer = oneThird * mFocusIndex
if mFocusIndex <> -1 then
DrawFocusRing( g, pos, oneThird )
end if
End Sub
[/rbcode]
As you can see, the event is quite simple. We loop over all the "parts" of the control (which correspond to the buttons) and draw them in one of three colors. When the class was developed, I was hard-coding three parts to the control, which is why you see things labelled as oneThird. Nevermind that. ;-) We also check to see if there's a part which has the focus, and if it does, we draw the focus ring for it. As you can see, we've defined a handful of class properties and a method, so let's go into more details about what those are and what they do.
[rbcode]
Class FocusableSwitcher
// This holds the index (0-based) of which part of the
// control currently has focus. It will be -1 if no part has
// focus.
Protected Dim mFocusIndex as Integer

// This stores the last part index (0-based) that had the
// focus. This way the focus can be restored later if
// appropriate.
Protected Dim mLastFocusIndex as Integer

// The number of parts to our radio group.
Protected Dim mNumParts as Integer


// The method responsible for drawing the
// platform appropriate focus ring.
Sub DrawFocusRing( g as Graphics, pos as Integer, oneThird as Integer )
End Sub

Event Action( partNum as Integer )
End Class
[/rbcode]
That's the only special parts to the class, so as you can see, it's quite simple to do. But the proof is in the pudding, so let's take a look at how to properly handle all this focus magic (which is really the "tough" part). First, let's see what happens when the control is first loaded.
[rbcode]
Sub FocusableSwitcher.Open()
// Nothing has the focus yet
mFocusIndex = -1

// We want to accept the focus
me.AcceptFocus = true

// This is how many parts or "buttons"
// that the control has
mNumParts = 3
End Sub
[/rbcode]
We initialize everything (making sure to tell the framework that we want to accept the focus), and you'll notice that we start out with no focus index. That may seem kind of strange to you, because what if this control is the first one in the control order? That would mean this control starts off with the focus. That's ok because REALbasic will call the GotFocus event for us when the window opens, so we're given a notification that we're the focus control. So let's take a look at the GotFocus event to see what magic it does.
[rbcode]
Sub FocusableSwitcher.GotFocus()
// Restore our focus index so that we have
// the proper part selected
mFocusIndex = mLastFocusIndex

// And we want to refresh our drawing
// so that the focus rect is shown
me.Refresh( false )
End Sub
[/rbcode]
That's it -- the only magic involved is restoring the current focus index to be whatever the last value was. Since the last value defaults to 0, then the initial focus (when tabbed into, or when at the 0th control order) is the 0th part. So let's peek at what needs to happen when we lose focus.
[rbcode]
Sub FocusableSwitcher.LostFocus()
// Save the last focus index so that
// we can restore it
mLastFocusIndex = mFocusIndex
mFocusIndex = -1

// Now refresh our drawing so that the
// focus rect is removed
me.Refresh( false )
End Sub
[/rbcode]
Again, no real magic. We simply store whatever our focus index was, and set the current focus to -1. You'll notice that in both cases we call Refresh (and don't bother to erase the background). That's because the Paint event is what's responsible for drawing the focus highlight, and since the focus has changed, we want to make sure our control reflects that.

You must be wondering where the tough code is at. Sorry, there really is none. The only thing that comes close is how to handle the keyboard input to navigate the control properly. Because this control is really like a radio button group, it makes sense for the control to behave as such. So we can tab into the control, and hitting the tab key tabs back out of the control (note: not to another button within the control!). The arrow keys navigate to and from the different buttons. The space or enter key selects a button. That's it! So let's see how this all happens.
[rbcode]
Function FocusableSwitcher.KeyDown( key as String ) as Boolean
dim focusChange as Boolean

// Check for arrow navigation keys or the
// action key
select case Asc( key )
case kLeftArrow
mFocusIndex = mFocusIndex - 1
focusChange = true

case kRightArrow
mFocusIndex = mFocusIndex + 1
focusChange = true

case kSpace, kReturn
// We might also want to put a "pushed"
// graphic in here so that there's more
// visual feedback. But that's left as an
// exercise for the reader.
Action( mFocusIndex )

end select

// If the focus changed, then we need to
// make sure it's constrained properly and
// it gets redrawn
if focusChange then
if mFocusIndex = -1 then
mFocusIndex = mNumParts - 1
elseif mFocusIndex = mNumParts then
mFocusIndex = 0
end if

me.Refresh( false )

return true
end if
End Function
[/rbcode]
We check to see which key was pressed (and notice that I made some constants for the key presses -- that's because I hate magic numbers with a passion). If it's an arrow key, then we advance or retreat the focus as appropriate and make sure we redraw the control. If the space or enter key is pressed, we fire the Action event for the currently focused part of the control. You'll notice that we don't handle the Tab key. This is very important because we want the Tab key to move the focus to the next control on the window. This means that we don't have to worry about how the user is going to tab thru the control group since the framework handles it automatically for us. Neat, eh?

The only event left to deal with is the MouseDown event. The purpose of this event is to make sure the proper part has its Action event called, and the focus is setup properly. So we determine which part is the hit item, and then work our magic.
[rbcode]
Function FocusableSwitcher.MouseDown( x as Integer, y as Integer ) as Boolean
// Note: to have a truly decent control, you should never
// fire the action in the MouseDown event. You should always
// display the button in a pushed state when the mouse is
// down and over the control. Once it leaves the control, you
// should show it in a non-pushed state. You should handle
// the action in the MouseUp event only if the mouse is released
// aboved the pushed control. This is a UI standard on all platforms,
// and is an exercise left for the reader.

// We need to figure out which part the user selected
// so that it gets the focus properly

dim partWidth as Integer = me.Width \ mNumParts
dim i as Integer

for i = 0 to mNumParts
if x > partWidth * i and x < partWidth * (i + 1) then
// This works around a strange bug where a
// MsgBox steals the focus but doesn't return
// it to a canvas properly
me.SetFocus

// Set the focus index
mFocusIndex = i

// Redraw with our new state
me.Refresh( false )

// And since we have a focus index, let's fire
// the action event as well
Action( mFocusIndex )

// We're done looking
exit
end if
next i
End Function
[/rbcode]
I'm sure there's an easier way to determine which part was the hit item, but this code is functional (which is what counts). So we figure out which item has been hit and then set the focus to it. We need to redraw immediately so that the focus ring is displayed before the Action event is fired. Once we've shown the new state, we fire the Action event. Simple, right?

There's one function which I've been leaving out, which is how to draw the focus ring properly. I left it for last for a reason -- I only know how to draw the focus ring on Windows. I'm positive there's a function to do it for GTK, and I'm guessing on the Mac as well, but I didn't take the time to look either of them up (sorry). So on non-Windows platforms, we just draw a rectangle on the inside of the focused part (which is abhorrent, but will work for a first pass).
[rbcode]
Sub FocusableSwitcher.DrawFocusRing( g as Graphics, pos as Integer, oneThird as Integer )
#if TargetWin32
Declare Sub DrawFocusRect Lib "User32" ( hdc as Integer, rect as Ptr )
Declare Function REALGraphicsDC Lib "" ( gfx as Graphics ) as Integer

// One thing to note about the DrawFocusRect function
// is that it needs the coordinates to draw that are local
// to the window, not the canvas itself. So we have to be
// sure to include the Left and Top properties when setting
// the left and top on the rect. Also, Win32 RECT structures
// are left, top, right, bottom -- which is different than usual
// RB rects which are left, top, width and height.
dim rect as new MemoryBlock( 16 )
rect.Long( 0 ) = me.Left + pos + 3
rect.Long( 4 ) = me.Top + 3
rect.Long( 8 ) = rect.Long( 0 ) + oneThird - 6
rect.Long( 12 ) = rect.Long( 4 ) + g.Height - 6

dim hdc as Integer = REALGraphicsDC( g )
DrawFocusRect( hdc, rect )
#else
g.ForeColor = &c000000
g.DrawRect( pos + 3, 3, oneThird - 6, g.Height - 6 )
#endif
End Sub
[/rbcode]
You'll notice that I am using a nasty hack to get into the plugins API. As much as I tell people "don't do this, it may break," I find myself needing it over and over again whenever dealing with graphics on Windows. It's an unfortunate hack, and it probably will break at some point in the future, but there it is -- take it or leave it.

There you have it -- a first pass at a mode switch button group that handles keyboard focus properly. As you can see, it's not too difficult to accomplish. It's just a matter of keeping track of things that you normally don't need to think about. Perhaps next time we'll look at cleaning it up into a better design with support for images, proper mouse handling and other goodies. But for now, you can check out the project here.

14 Comments

Thanks for the tip about REALGraphicsDC. I've been looking for that in my own project.

BTW, if you'd like to stop using that nasty hack, here's my feature request for a Graphics.Handle method:

http://www.realsoftware.com/feedback/viewreport.php?reportid=mwdsfmtk

Thanks.

Keep in mind that nasty hack is exactly that -- a nasty hack. It may very well disappear in a future version of REALbasic. Hopefully by then we'll have another solution though.

And btw, the problem with Graphics.Handle is that it's ambiguous -- you can't be certain of what you're getting back. Is it an HDC? Is it a Graphics object for GDI+? Is it a QuickDraw handle thinger or CoreGraphics? I think it needs to be Graphics.RequestHandle( handleType as Integer ) as Integer so that it's clear what you're requesting and that you may not get a handle back.

I'm just curious, is this call somewhat special? I tried to call other functions of the plugin API just for the hell of it and most gave me symbol not found errors when I went to launch it.

-- SirG3

Well, I guess the cat's out of the bag now.

@SirG3 -- And there's nothing special about it -- I'm calling the exported symbol. There's a call resolver that can translate plugin APIs into their exported names, so some APIs in the plugin aren't exported by the name they're usually called by.

@Charles -- Cat's always been out of the bag, I need to use this call all over the Windows Functionality Suite (with big disclaimers about its use everywhere I use it).

Bad Aaron, no biscuit.

Pffft -- we had this discussion (internally) ages ago about this very topic. So long as there's a disclaimer about what a horrible hack it is, we figured it's ok. And honestly, the fact that I've found no less than a DOZEN places where this hack is needed should be a clue that we need to do something about it.

Still... Bad Aaron, No cookie! ;)

LoL, can you tell I'm frustrated by our not putting this into any release even though I've been bringing it up since 5.0? ;-)

In both RB 5.5.4 and 5.5.5, the following line:

dim hdc as Integer = REALGraphicsDC( g )

produces the error, while highlighting REALGraphicsDC:

"This method or property does not exist."

I'd love to try this out, as I might have a real use for this.

You didn't include the declare line (most likely) -- this works in 5.5.5.

Nevermind. I'd actually created my own DrawFocusRing method, when the parent class had already provided one.

So long as there’s a disclaimer about what a horrible hack it is, we figured it’s ok.

Errr.... I was unaware we had decided it was OK to publish it. In fact I was still under the impression that it was a deep dark secret. I guess I'm stuck supporting it now.

No no, *break it*, please! That means we'd have a supported way to accomplish these things. And we did discuss it (heavily) and said with a disclaimer is ok -- because there is simply *NO* other way to accomplish anything with graphics on Windows.

Leave a comment

Disclaimer

I'm currently an employee of REAL Software. My blog is mine. The opinions represented in this blog are mine as well and may not represent my employer's opinions. All original material is copyrighted and property of the author.

REALbasic® is a registered trademark of REAL Software, Inc. REAL SQL Server™ and Lingua™ are pending trademarks of REAL Software, Inc. All rights reserved.