Monday, August 13, 2007

Example: Message Window


Here's a problem I thought about recently. I wanted to be able to display short messages to the user when events happen, but don't want MessageBoxes (very annoying) and don't want a standard status bar. Solution: Message Window

Requirements:
Fade-in and fade-out based on time requested (If the user wants automatic timing based on the length of the string, it's easy to make a function to do this)
Automatic resizing based on the length of the string.
Create and forget: Closes itself (thus freeing memory) after it's alloted time.
Since the naming is close to MessageBox, we should have a similiar way of using it.

Design:
As you can see in the picture above, I wanted my message to be in a rectangle, with rounded edges and a drop shadow effect. I did all this through Microsoft Expression Blend, but it's simple to do in the XAML:
<Window.BitmapEffect>
<DropShadowBitmapEffect/>
</Window.BitmapEffect>
<Grid Width="Auto">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Rectangle Fill="#FF191919" Stroke="#FF000000" StrokeThickness="1" RadiusX="20" RadiusY="20" Margin="8,8,8,8"/>
<Label Margin="0,0,0,0" x:Name="Message" Width="Auto" Height="25" Content="Label" Foreground="#FFFFFFFF" HorizontalAlignment="Center" VerticalAlignment="Center" FontFamily="Arial" FontSize="12" FontWeight="Normal"/>
</Grid>

RadiusX and RadiusY in Rectangle control the rounded corners. Here I've also placed the label which will hold the message.

Sizing The MessageWindow
The user is going to pass a String into the MessageWindow constructor, and based on how long that is, we need to resize. Luckily, in WPF, you can set a Label width to Auto, which will fix this problem. I want to take the ActualWidth of the Label after we put our String in, and adjust the Window's width and position based on that.

I want the Bottom and Right to be 20 away from the bottom right corner of the "Work Area" (The area above the taskbar). With a little math, here is our constructor:

1public MessageWindow(String message, double duration)
2{
3 InitializeComponent();
4
5 //Message to be displayed in the window
6 Message.Content = message;
7
8 //Begin closing the window after the specified duration has elapsed.
9 Timer closeTimer = new System.Timers.Timer(duration);
10 closeTimer.Elapsed += new System.Timers.ElapsedEventHandler(closeTimer_Elapsed);
11 closeTimer.Start();
12
13 //This cannot be in the constructor directly, because the ActualWidth of items
14 //does not get set until AFTER the constructor. Since I couldn't get the Initialize
15 //event to fire, this method seems to work good.
16 this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal, new VoidDelegate(delegate
17 {
18 this.Width = Message.ActualWidth + 50;
19
20 //Set the default placement to the bottom right corner, above the Taskbar.
21 this.Left = System.Windows.SystemParameters.WorkArea.Right - this.Width - 20;
22 this.Top = System.Windows.SystemParameters.WorkArea.Bottom - this.Height - 20;
23 }));
24}


I'm also creating a timer here, because after the duration elapses, I want to start to fade out our message, and then close it.

Fading In And Out
I created two storyboards in XAML (Using Blend), one that will fade our window in when the box is shown, and another that we can use to fade out.

<Storyboard x:Key="OnLoaded1">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="{x:Null}" Storyboard.TargetProperty="(UIElement.Opacity)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:01" Value=".7"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="FadeAway">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="{x:Null}" Storyboard.TargetProperty="(UIElement.Opacity)">
<SplineDoubleKeyFrame KeyTime="00:00:01" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>


In here, the OnLoaded1 event will fade in the window, going from 0 to .7 transparency, leaving the window slightly transparent when it's fully shown. Then when fading out, it'll go from the current value back to 0.

Using this, I set the Fading in event (OnLoaded1 was the default name for it) to trigger when the Window loads:

<Window.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard Storyboard="{StaticResource OnLoaded1}"/>
</EventTrigger>
</Window.Triggers>


Now all that's left is to fade the window out, and close. This takes a little code, since we want to wait until our timer elapses before we do it:

1void closeTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
2{
3 ((Timer)sender).Stop();
4
5 //We must begin the storyboard on the main window thread.
6 this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal, new VoidDelegate(delegate
7 {
8 Storyboard story = (Storyboard)this.FindResource("FadeAway");
9 story.Completed += new EventHandler(story_Completed);
10 this.BeginStoryboard(story);
11 }));
12}
13
14/// <summary>
15/// Closes the window after we're done fading out.
16/// </summary>
17void story_Completed(object sender, EventArgs e)
18{
19 this.Dispatcher.Invoke(System.Windows.Threading.DispatcherPriority.Normal, new VoidDelegate(Close));
20}


Here you can see once the Window fades away, the storyboard will say it's completed, which will cause the Window to close, with no work needed from the user.

Looking like a MessageBox - A Problem
Like the MessageBox class, I wanted a Show method similiar to MessageBox so I can create and display a MessageBox in 1 line of code. That was a simple add:

1public static void Show(String message, double duration)
2{
3 MessageWindow w = new MessageWindow(message, duration);
4 w.Show();
5}
6
7public static void Show(String message, double duration, Window parent)
8{
9 MessageWindow w = new MessageWindow(message, duration);
10 w.Show();
11 parent.Focus();
12}

Here's the problem I had: I wanted to automatically return the focus to the parent, because theres no reason this Window ever needs focus.

One solution is the one displayed above, where the parent must pass themselves in to recieve focus back.
The other option I saw was to do this.Owner.Focus() on the window. However, this relies on the Owner to actually set itself as the Owner before it shows the MessageWindow, which is a problem, especially with this Show() approach.

For now, the method I've shown works fine. I've asked around and never got a good alternative, so if you have a good alternate method for giving the parent focus again, let me know.

Finished MesssageWindow
And now you have a finished MessageWindow. You can call MessageWindow.Show(...) to show a MessageWindow, or if you want more control, you can create an instance of the Window yourself, then manually position it or do whatever you want.

Source (Executable is included inside)

2 comments:

Anonymous said...

This is a nice sample.

bruce said...

Very good! I like your article. It helped me a lot1