Splitter Bar

Download the complete source code of the application that was used in this example.

A splitter bar is a useful control that is not part of the Windows' common bag of controls. How difficult is it to implement it? Not so difficult, as it turns out, once you know the basics of Windows API. The description that follows might seem complicated at first, but you'll be learning several very important techniques that can be used over and over in a variety of situations. Working with child windows, mouse capture, drawing using xor mode, just to mention a few.

A splitter bar is a window. More precisely, it's a child window. It is positioned between two other child windows--we'll call these left and right panes, respectively (or top and bottom, for a horizontal splitter). There must also be a main window that fathers the three children.

Without further ado, here's the code in WinMain that sets up the stage.

First, we register classes. The top window class is associated with its window procedure WndProcMain, which we'll examine in a moment. The two child panes share the same window class associated with WndProcPane. Next, our own splitter class is registered (we'll see the code soon). Finally, the top window is created and displayed. Child windows are created dynamically during the initialization of the parent window.

It's a pretty standard window procedure, except for one message, MSG_MOVESPLITTER. This is our own, user-defined message that is sent by the splitter control to the parent window. But first, let's have a look at the main window controller.

When the user drags the splitter bar, the parent receives the MSG_MOVESPLITTER messages. The parameter wParam contains the new distance of the splitter bar from the left edge of the parent window. In response to such a message, the parent has to resize both child panes and move the splitter. It does it by calling the Size method.

When the left mouse button is clicked over the client area of the splitter, we perform the following tasks. First, we capture the mouse. The user might, and probably will, drag the mouse cursor outside of the splitter bar. Capturing the mouse will ensure that all mouse messages will now be directed to us, even though the mouse cursor may wander all over the screen.

Next, we convert the local splitter-bar coordinates to the parent window coordinates. We do it by converting the parent's origin to screen coordinates and converting our (splitter's) origin to screen coordinates. The difference gives us our origin with respect to parent's client area. We do some more arithmetics in order to find the x coordinate of the center of the splitter, because that's what we'll be dragging.

To give the user dragging feedback, we draw a single vertical line that will be dragged across the client area of the parent window. Notice two important details. The canvas that we are creating are associated with the parent--we are using the parent's window handle. The drawing mode is logical xor (exclusive or). That means that the pixels we are drawing are xor'd with the original pixels. Logical xor has this useful property that if you apply it twice, you restore the original. It's the oldest trick in computer graphics--the poor man's animation technique. You draw something using the xor mode and erase it simply by drawing it again in the xor mode. Just look at the dragging code below.

We draw the vertical line in the xor mode using the previous remembered position. Since this is the second time we draw this line in the same place, the net result will be the restoration of the original pixels from under the line. Then we draw the new line in the new position, still in the xor mode. We remember this position, so that next time LButtonDrag is called we'll erase it too. And so on, until finally the user releases the mouse button.

At this point we should erase the vertical line for the last time. However, we don't do it directly--we use a little trick here. We release the mouse capture. As a side effect, Windows sends us the WM_CAPTURECHANGED message. It's during the processing of that message that we actually erase the vertical line.

Why do we do that? It's because Windows can force us to release the mouse capture before the user releases the mouse button. It might happen, for instance, when another application suddenly decides to pop up its window while the user is in the middle of dragging. In such a case our window would never get the button-up message and, if we weren't clever, wouldn't be able to cleanly finish the drag. Fortunately, before taking away the mouse capture, Windows manages to send us the WM_CAPTURECHANGED message, and we take it as a clue to do our cleanup.

Going back to LButtonUp--once the user finishes dragging, we send the parent our special message MSG_MOVESPLITTER, passing it the new position of the center of the splitter bar measured in parent's client area coordinates. You've already seen the client code response to this message.

Finally, here's an excerpt from a very useful class HWnd that encapsulates a lot of basic Windows APIs that deal with windows. In particular, look at the methods MoveDelayPaint and ForceRepaint that we used in repainting the splitter bar.