Charles Petzold



An Easy Grid Splitter for Windows 8

June 3, 2013
Roscoe, N.Y.

Windows 8 doesn't have a GridSplitter control, most likely because the concept doesn't quite fit in with the Windows Store design paradigm. But sometimes you'd like to allow the user the freedom to change the relative sizes of two columns or rows in a Grid.

Fortunately it's easy to make your own grid splitter. It's so easy that you can do it on an ad hoc basis rather than creating a generalized custom control. The grid splitter I'll be showing you here is built around the Thumb control — a rather obscure control hidden away in the Windows.UI.Xaml.Controls.Primitives namespace and mostly found in control templates for ScrollBar, Slider, and ToggleSwitch. But the Thumb can come in handy when you need a control that responds to mouse or touch dragging. I discussed several uses of the pre-Windows 8 Thumb in the article "Silverlight, Windows Phone 7, and the Multi-Touch Thumb" in the December 2010 issue of MSDN Magazine.

Let's suppose you want a splitter between two Grid columns. Give those two columns initial star widths and put a column between them containing a Thumb control. For touch purposes, using little wider Thumb is advisable. Here's an example where the two sizeable columns both contain TextBox controls for easy experimentation:

<Grid Background="{StaticResource ApplicationPageBackgroundThemeBrush}">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="1*" />
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="2*" />
    </Grid.ColumnDefinitions>
        
    <TextBox Grid.Column="0"
             FontSize="48"
             TextWrapping="Wrap"
             AcceptsReturn="True" />
        
    <Thumb Grid.Column="1"
           Width="15"
           DragStarted="OnThumbDragStarted"
           DragDelta="OnThumbDragDelta" />
        
     <TextBox Grid.Column="2"
              FontSize="48"
              TextWrapping="Wrap"
              AcceptsReturn="True" />
</Grid>

The Thumb defines events named DragStarted, DragDelta, and DragCompleted that are fired when the user tries to drag the control using the mouse, pen, or touch. (Watch out! There are other events named DragOver, DragEnter, and DragLeave that are defined by the Control class and involve drag and drop!)

The Thumb does not move by itself. You'll need to move it from code in response to these events. Commonly, you'll move the Thumb using a TranslateTransform or by calling the Canvas.SetLeft or Canvas.SetTop static methods. But here I'm going to respond to Thumb events by changing the widths of the left and right Grid columns. Setting Grid column widths in code isn't common, but it can be done.

The DragStarted event occurs when a finger or pen first touches the Thumb, or the mouse button is pressed when the mouse is over the Thumb. In response to that event, the program sets the star widths of the first and last columns based on the actual pixel widths. (The code is somewhat generalized in case you want to use identical code for multiple grid splitters in the program.)

void OnThumbDragStarted(object sender, DragStartedEventArgs args)
{
    Thumb thumb = (Thumb)sender;
    Grid grid = (Grid)thumb.Parent;
    ColumnDefinition leftColDef = grid.ColumnDefinitions[0];
    ColumnDefinition rightColDef = grid.ColumnDefinitions[2];

    leftColDef.Width =
        new GridLength(leftColDef.ActualWidth, GridUnitType.Star);
    rightColDef.Width =
        new GridLength(rightColDef.ActualWidth, GridUnitType.Star);
}

For example, suppose the Grid is 1200 pixels wide. The Thumb is 15 pixels wide, so the first column might be 450 pixels and the third column 735 pixels. This code sets the width of the first column to "450*" and the third column to "735*". The actual column widths do not change, but now the star factors are the same as pixels.

So, for the DragDelta event, the handler can simply use the HorizontalChange value to adjust the star widths of the two columns:

void OnThumbDragDelta(object sender, DragDeltaEventArgs args)
{
    Thumb thumb = (Thumb)sender;
    Grid grid = (Grid)thumb.Parent;
    ColumnDefinition leftColDef = grid.ColumnDefinitions[0];
    ColumnDefinition rightColDef = grid.ColumnDefinitions[2];

    leftColDef.Width =
        new GridLength(leftColDef.Width.Value + args.HorizontalChange, 
                       GridUnitType.Star);
    rightColDef.Width =
        new GridLength(rightColDef.Width.Value - args.HorizontalChange, 
                       GridUnitType.Star);
}

And that's it!

The advantage of keeping the two Grid column widths as star widths rather than pixel widths is evident when you turn a Windows 8 tablet sideways into portrait mode, or go into a snapped mode. If the two Grid widths are star widths rather than pixel widths, the relative column widths remain the same.

The code I've just shown you adapted from a SplitContainer control that I made for the XamlCruncher program shown in Chapter 8 of Programming Windows, 6th edition. But let me show you some enhancements not in my book:

Suppose you want to limit how narrow a particular column can be. That's an easy setting right in the Grid column definitions:

<Grid.ColumnDefinitions>
    <ColumnDefinition Width="1*" MinWidth="100" />
    <ColumnDefinition Width="Auto" />
    <ColumnDefinition Width="2*" MinWidth="100" />
</Grid.ColumnDefinitions>

Suppose you want a special cursor to appear when the mouse is over the grid splitter. Attach handlers for the PointerEntered and PointerExited events:

<Thumb Grid.Column="1"
       Width="15"
       DragStarted="OnThumbDragStarted"
       DragDelta="OnThumbDragDelta"
       PointerEntered="OnThumbPointerEntered"
       PointerExited="OnThumbPointerExited" />

Setting the pointer cursor in response to those events in Windows 8 is not quite obvious but it becomes rather easy when you know where the API is located:

void OnThumbPointerEntered(object sender, PointerRoutedEventArgs args)
{
    Window.Current.CoreWindow.PointerCursor = 
        new CoreCursor(CoreCursorType.SizeWestEast, 0);
}

void OnThumbPointerExited(object sender, PointerRoutedEventArgs args)
{
    Window.Current.CoreWindow.PointerCursor = 
        new CoreCursor(CoreCursorType.SizeWestEast, 0);
}

Suppose you want to add a keyboard interface. You'll probably want to make the Thumb a tab stop as well. You can then handle the GotFocus event to perform initialization, and the KeyDown event to adjust relative column widths:

<Thumb Grid.Column="1"
       Width="15"
       IsTabStop="True"
       DragStarted="OnThumbDragStarted"
       DragDelta="OnThumbDragDelta"
       PointerEntered="OnThumbPointerEntered"
       PointerExited="OnThumbPointerExited"
       GotFocus="OnThumbGotFocus"
       KeyDown="OnThumbKeyDown" />

You can then refactor the code just a bit to consolidate handling of keyboard and pointer events:

void OnThumbDragStarted(object sender, DragStartedEventArgs args)
{
    OnDragStart((Thumb)sender);
}

void OnThumbDragDelta(object sender, DragDeltaEventArgs args)
{
    OnDragDelta((Thumb)sender, args.HorizontalChange);
}

void OnThumbGotFocus(object sender, RoutedEventArgs args)
{
    OnDragStart((Thumb)sender);
}

void OnThumbKeyDown(object sender, KeyRoutedEventArgs args)
{
    if (args.Key == VirtualKey.Left || args.Key == VirtualKey.Right)
        OnDragDelta((Thumb)sender, args.Key == VirtualKey.Left ? -5 : 5);
}

void OnDragStart(Thumb thumb)
{
    Grid grid = (Grid)thumb.Parent;
    ColumnDefinition leftColDef = grid.ColumnDefinitions[0];
    ColumnDefinition rightColDef = grid.ColumnDefinitions[2];

    leftColDef.Width = 
        new GridLength(leftColDef.ActualWidth, GridUnitType.Star);
    rightColDef.Width = 
        new GridLength(rightColDef.ActualWidth, GridUnitType.Star);
}

void OnDragDelta(Thumb thumb, double change)
{
    Grid grid = (Grid)thumb.Parent;
    ColumnDefinition leftColDef = grid.ColumnDefinitions[0];
    ColumnDefinition rightColDef = grid.ColumnDefinitions[2];

    leftColDef.Width = 
        new GridLength(leftColDef.Width.Value + change, GridUnitType.Star);
    rightColDef.Width = 
        new GridLength(rightColDef.Width.Value - change, GridUnitType.Star);
}

Now you can use the Tab key to change input focus from the first TextBox to the Thumb (whereupon you can move the Thumb using the Left and Right arrow keys) and then Tab again to go to the second TextBox. Here's the complete project.

And hey! Check it out: In celebration of TechEd, all Microsoft Press ebooks (including Programming Windows 6th edition) are 50% off through June 6:


Programming Windows, 6th edition