User Interface

Users Ignore Instructions? Animate them!

Posted on Updated on

You can imitate Movie makers and get users to read text by animating it. Somehow, the movement and action encourages users to read instructions. But, let’s just animate the instructions the first time they launch; thereafter, the users can just revisit the instructions when they need to. Benefit to you: your users actually read the instructions the first time, so they are less confused and like your app better.

Overview

The text doesn’t dance around, but it is displayed in sequence, as if being typed. I prefer to display whole words at a time, you can also add single letter at a time. In this sample, I will do the following actions in sequence:

  1. Animate my instruction string using a StringAnimationUsingKeyFrames
  2. Call attention to the instructions, one last time, by gently pulsing the background color
  3. Shrink-down the instructions
  4. Hide the instructions by closing the expander containing them
Screen Shot
4 Screen Shots Showing Each Animation Phase

The Animation Will only Run the First Time

The first time it runs, we will try hard to get the user to read the instructions. But the animation takes time, so let them use the expander if they need to read it again.

	if (AnimatedInstructions.Properties.Settings.Default.FirstTime) {
		AnimatedInstructions.Properties.Settings.Default.FirstTime = false;
		AnimatedInstructions.Properties.Settings.Default.Save();
		SetupInstructionAnimation();
	} else {
		expInstructions.Header = "Introduction/Instructions";
		expInstructions.IsExpanded = false;
	}

We will use the .NET properties functionality to record whether this is the first time user has run the app. We update the users’ config file to write ‘false’ to an xml variable called ‘FirstTime’. The method ‘SetupInstructionAnimation’ does the animation, as I am sure you can tell by the name!

Why am I doing this animation in code? You’ll see shortly, but basically, animating the text in XAML requires creating a lot of  KeyFrames; it is easier to generate them inside a loop.

The Method ‘SetupInstructionAnimation’ Builds Three Animations

private void SetupInstructionAnimation() {
	Storyboard sb = new Storyboard();
	string instructions = "These instructions tell you exactly how to run my new app. " +
		"You will get maximum benefit from the app if you read the instructions. " +
		"Users who don't read the instructions sometimes fail to capitalize on important " +
		"features. Read and avoid frustration! ";
	StringAnimationUsingKeyFrames stringAni;
	int stringAnilMs;
	BuildInstructionStringAnimation(instructions, out stringAni, out stringAnilMs);
	tbInstructions.BeginAnimation(TextBlock.TextProperty, stringAni);

	//now, a color animation for the background that starts after the string animation completes.
	ColorAnimation bgColorAni = BuildBackgroundColorAnimation(stringAnilMs);

	sb.Children.Add(bgColorAni);

	DoubleAnimation shrinkAni = BuildShrinkInstructionAnimation(stringAnilMs);
	sb.Children.Add(shrinkAni);
	sb.Completed += new EventHandler(Introductory_Animation_StoryBoard_Completed);

	tbInstructions.Loaded += delegate(object sender, RoutedEventArgs e) {
		sb.Begin(this);
	};
}

The first method we invoke is called ‘BuildInstructionStringAnimation’, and does exactly what you would think. The other methods generate the other animations.

 The String Animation

We use a StringAnimationUsingKeyFrames to make it look like we are typing the words. To set it up, we specify a “Key Frame” for every step of the animation. In our case, we will create a Key Frame for each word. As I mentioned, you can also provide a frame for every letter, but that didn’t look as nice to me.

private static void BuildInstructionStringAnimation(string instructions, 
                    out StringAnimationUsingKeyFrames stringAni, out int ms) {
    stringAni = new StringAnimationUsingKeyFrames();
    ms = 0;
    KeyTime kyTime = new KeyTime();
    int interval = 150;
    int wordIndex = 0;
    wordIndex = instructions.IndexOf(' ');
    while (wordIndex > 0) {
        string aWord = instructions.Substring(0, wordIndex);
        kyTime = TimeSpan.FromMilliseconds(ms);
        if (aWord.EndsWith("?") || aWord.EndsWith(".")) {
            ms += 1250;
        } else {
            ms += interval;
        }
        stringAni.KeyFrames.Add(new DiscreteStringKeyFrame(aWord, kyTime));

        wordIndex = instructions.IndexOf(' ', wordIndex + 1);
    }
    stringAni.Duration = TimeSpan.FromMilliseconds(ms);
}

Note that each word is displayed after a pause of 150 milliseconds, using the variable ‘ms’. Except, after the end of each sentence, we wait 1-1/4 seconds (1250 milliseconds) so the user can read the whole sentence. To find word boundaries in the instruction string, we search for spaces using the ‘IndexOf’ method. Then we use the Substring method to grab all of the instructions up to the current space and build a KeyFrame with it.

Note that we return ms to the caller, because we will use it for the BeginTime of the next animation.

The Remainder of the Code

The rest of the code is fairly standard if you have done any other animation.

private ColorAnimation BuildBackgroundColorAnimation(int ms) {
    ColorAnimation bgColorAni = new ColorAnimation();
    bgColorAni.BeginTime = TimeSpan.FromMilliseconds(ms);

    bgColorAni.From = Colors.White;
    bgColorAni.To = Colors.LightYellow;
    bgColorAni.Duration = TimeSpan.FromSeconds(1);
    bgColorAni.RepeatBehavior = new RepeatBehavior(2);

    Storyboard.SetTarget(bgColorAni, tbInstructions);
    Storyboard.SetTargetProperty(bgColorAni, new PropertyPath("Background.Color"));
    bgColorAni.AutoReverse = true;
    return bgColorAni;
}

private DoubleAnimation BuildShrinkInstructionAnimation(int stringAnilMs) {
    ScaleTransform scale = new ScaleTransform(1.0, 1.0);
    tbInstructions.RenderTransformOrigin = new Point(0, 0);
    tbInstructions.RenderTransform = scale;

    DoubleAnimation shrinkAni = new DoubleAnimation(1.0, 0.35, TimeSpan.FromMilliseconds(500), FillBehavior.Stop);
    shrinkAni.BeginTime = TimeSpan.FromMilliseconds(stringAnilMs + 4000);
    Storyboard.SetTargetProperty(shrinkAni, new PropertyPath("RenderTransform.ScaleY"));
    Storyboard.SetTarget(shrinkAni, tbInstructions);
    return shrinkAni;
}

void Introductory_Animation_StoryBoard_Completed(object sender, EventArgs e) {
    expInstructions.IsExpanded = false;
    expInstructions.Header = "Show Introduction/Instructions";
}

And Finally, the XAML

<Expander Grid.Row="1" Grid.ColumnSpan="3" Name="expInstructions" IsExpanded="True" >
    <TextBlock FontSize="18" Name="tbInstructions" TextWrapping="Wrap" >
        <TextBlock.Background>
            <SolidColorBrush Color="White" />
        </TextBlock.Background>
        These instructions tell you exactly how to run my new app.
        You will get maximum benefit from the app if you read the instructions.
        Users who don't read the instructions sometimes fail to capitalize on important
        features. Read and avoid frustration!
    </TextBlock>
</Expander>

Summary

Easy-to-learn applications are cheaper to maintain! They will generate fewer support calls from confused users. You can help your users learn to run your app by animating the instructions. Your boss and users will like you better for it!

Make the WPF GridSplitter Work Correctly!

Posted on Updated on

You can find lots of articles explaining how to use a WPF GridSplitter, but the ones I’ve seen never work quite right. Actually, they seem to work until you add some more realistic code, and then things start going haywire! In this post I explain a couple of gotcha’s, plus one nifty trick to give your splitter a 3-d appearance.

I dragged the splitter to the right, squashing the right-hand side
Now I have dragged the splitter to the right, squashing the right-hand side
GridSplitter sample the left-hand content is squashed
GridSplitter sample: the left-hand content is squashed when I drag my splitter left

 

 

The First Gotcha – The Wrong Column Stretches!

If you are trying your first GridSplitter, chances are you will omit a tag that looks completely unnecessary: the HorizontalAlignment tag (or, if you are splitting rows, the VerticalAlignment tag). If you omit that tag, your screen will look like this when you drag the splitter:

This is how it looks when you omit the HorizontalAlignment tag
This is how it looks when you omit the HorizontalAlignment tag

The correct XAML  looks like this:


<GridSplitter Grid.Column="1" ResizeDirection="Columns" Width="10" ToolTip="Drag left-right to resize" 
HorizontalAlignment="Stretch" />

Why do you need that tag? I wish I knew, I’ve never seen it discussed.

Gotcha Number Two

The next problem you’ll need to fight is when the wrong columns (or rows) get resized, or when nothing is resized whatsoever! Generally this happens when

  • You have 3 columns…
  • Your middle column holds the splitter…
  • And you auto-size either column zero, column 2, or both!

Important principle:If you specify the size of any row or column, even autosizing it, the GridSplitter cannot change it’s size!

For column zero, I specified 'width="auto". You can see that it's size is not affected by my GridSplitter, whereas column 2, with no width setting, responds correctly
For column zero, I specified ‘width=”auto”. You can see that its size is not affected by my GridSplitter, whereas column 2, with no width setting at all, responds correctly.

Further, if you have more than three columns, you should make sure that exactly two of them lack any width specification; they will be resized. If you have 4, 5, or more columns, please specify a width, typically ‘width=Auto”‘.

Tip: Give Your GridSplitter a 3-d Appearance

Even with a tool tip, your users might not realize that they should drag your GridSplitter around. (But they might get frustrated try knocking it around!) But, make it look 3-d and they will cotton-on pretty quick. The best way to do that is to

  • Use an ImageBrush for your background
  • If you’re using an older version of WPF, specify TileMode=”Tile” for the brush
  • I like to save a bit on resources and use the smallest possible image for my background (such as 8×1 pixels), letting the tile mode spread it across the available area.

Here is the XAML:

<GridSplitter Grid.Column="1" ResizeDirection="Columns" Width="10" ToolTip="Drag left-right to resize" 
               HorizontalAlignment="Center">
     <GridSplitter.Background>
         <ImageBrush ImageSource="/SplitterSample;component/Images/Splitter8x1.jpg" />
     </GridSplitter.Background>
</GridSplitter>

Here is what my background image looks like (magnified 8x):

This image is only 1 pixel high; WPF stretches it to tile the whole range necessary
This image is only 1 pixel high; WPF stretches it to tile the whole range necessary.

WPF will stretch (tile) your image to fill the available space, causing your image to magically appear like a 3-d bar!

Code Listing

Finally, here is the full XAML. If you wish to try it, make sure you create a sub-folder in your project named ‘Images’ and place your background image in that folder. Also, replace ‘SplitterSample’ with your project name where the ImageBrush is referenced.

<Window x:Class="SplitterSample.MainWindow"
 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 WindowStartupLocation="CenterScreen"
 Title="Grid Splitter Sample" Height="150" Width="325">
 <Grid>
     <Grid.ColumnDefinitions>
         <ColumnDefinition />
         <ColumnDefinition Width="auto" />
         <ColumnDefinition />
     </Grid.ColumnDefinitions>

     <TextBlock Background="LightGoldenrodYellow" Text="This is the left-hand side" />
     <GridSplitter Grid.Column="1" ResizeDirection="Columns" Width="10" ToolTip="Drag left-right to resize" 
                 HorizontalAlignment="Center" > 
          <GridSplitter.Background>
              <ImageBrush ImageSource="/SplitterSample;component/Images/Splitter8x1.jpg" />
          </GridSplitter.Background>
     </GridSplitter>

     <TextBlock Grid.Column="2" Background="Linen" Text="This is the right-hand side" />
 </Grid>
</Window>