Create popup "toaster" notifications in Windows with .NET
WPF makes this absolutely trivial: It would proably take ten minutes or less. Here are the steps:
- Create a Window, set AllowsTransparency="true" and add a Grid to it
- Set the Grid's RenderTransform to a ScaleTransform with origin of 0,1
- Create an animation on the grid that animates the ScaleX 0 to 1 then later animates the Opacity from 1 to 0
- In the constructor calculate Window.Top and Window.Left to place the window in the lower right-hand corner of the screen.
That's all there is to it.
Using Expression Blend it took about 8 minutes me to generate the following working code:
<Window x:Class="NotificationWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Notification Popup" Width="300" SizeToContent="Height" WindowStyle="None" AllowsTransparency="True" Background="Transparent"> <Grid RenderTransformOrigin="0,1" > <!-- Notification area --> <Border BorderThickness="1" Background="Beige" BorderBrush="Black" CornerRadius="10"> <StackPanel Margin="20"> <TextBlock TextWrapping="Wrap" Margin="5"> <Bold>Notification data</Bold><LineBreak /><LineBreak /> Something just happened and you are being notified of it. </TextBlock> <CheckBox Content="Checkable" Margin="5 5 0 5" /> <Button Content="Clickable" HorizontalAlignment="Center" /> </StackPanel> </Border> <!-- Animation --> <Grid.Triggers> <EventTrigger RoutedEvent="FrameworkElement.Loaded"> <BeginStoryboard> <Storyboard> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)"> <SplineDoubleKeyFrame KeyTime="0:0:0" Value="0"/> <SplineDoubleKeyFrame KeyTime="0:0:0.5" Value="1"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)"> <SplineDoubleKeyFrame KeyTime="0:0:2" Value="1"/> <SplineDoubleKeyFrame KeyTime="0:0:4" Value="0"/> </DoubleAnimationUsingKeyFrames> </Storyboard> </BeginStoryboard> </EventTrigger> </Grid.Triggers> <Grid.RenderTransform> <ScaleTransform ScaleY="1" /> </Grid.RenderTransform> </Grid></Window>
With code behind:
using System;using System.Windows;using System.Windows.Threading;public partial class NotificationWindow{ public NotificationWindow() { InitializeComponent(); Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(() => { var workingArea = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea; var transform = PresentationSource.FromVisual(this).CompositionTarget.TransformFromDevice; var corner = transform.Transform(new Point(workingArea.Right, workingArea.Bottom)); this.Left = corner.X - this.ActualWidth - 100; this.Top = corner.Y - this.ActualHeight; })); }}
Since WPF is one of the regular .NET libraries, the answer is yes, it is possible to accomplish this with the "regular .NET libraries".
If you're asking if there is a way to do this without using WPF the answer is still yes, but it is extremely complex and will take more like 5 days than 5 minutes.
I went ahead and created a CodePlex site for this that includes "Toast Popups" and control "Help Balloons". These versions have more features than what's described below. https://toastspopuphelpballoon.codeplex.com.
This was a great jumping off point for the solution that I was looking for. I've made a couple of modifications to meet my requirements:
- I wanted to stop the animation on mouse over.
- "Reset" animation when mouse leave.
- Close the Window when opacity reached 0.
- Stack the Toast (I have not solved the problem if the number of windows exceeds the screen height)
- Call Load from my ViewModel
Here's my XAML
<Window x:Class="Foundation.FundRaising.DataRequest.Windows.NotificationWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:telerik="http://schemas.telerik.com/2008/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="NotificationWindow" Height="70" Width="300" ShowInTaskbar="False" WindowStyle="None" AllowsTransparency="True" Background="Transparent"><Grid RenderTransformOrigin="0,1" > <Border BorderThickness="2" Background="{StaticResource GradientBackground}" BorderBrush="DarkGray" CornerRadius="7"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="60"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="24"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="30"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Image Grid.Column="0" Grid.RowSpan="2" Source="Resources/data_information.png" Width="40" Height="40" VerticalAlignment="Center" HorizontalAlignment="Center"/> <Image Grid.Column="2" Source="Resources/error20.png" Width="20" Height="20" VerticalAlignment="Center" ToolTip="Close" HorizontalAlignment="Center" Cursor="Hand" MouseUp="ImageMouseUp"/> <TextBlock Grid.Column="1" Grid.Row="0" VerticalAlignment="Center" HorizontalAlignment="Center" FontWeight="Bold" FontSize="15" Text="A Request has been Added"/> <Button Grid.Column="1" Grid.Row="1" FontSize="15" Margin="0,-3,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" Content="Click Here to View" Style="{StaticResource LinkButton}"/> </Grid> </Border> <!-- Animation --> <Grid.Triggers> <EventTrigger RoutedEvent="FrameworkElement.Loaded"> <BeginStoryboard x:Name="StoryboardLoad"> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="(UIElement.Opacity)" From="0.0" To="1.0" Duration="0:0:2" /> <DoubleAnimation Storyboard.TargetProperty="(UIElement.Opacity)" From="1.0" To="0.0" Duration="0:0:8" BeginTime="0:0:5" Completed="DoubleAnimationCompleted"/> </Storyboard> </BeginStoryboard> </EventTrigger> <EventTrigger RoutedEvent="Mouse.MouseEnter"> <EventTrigger.Actions> <RemoveStoryboard BeginStoryboardName="StoryboardLoad"/> <RemoveStoryboard BeginStoryboardName="StoryboardFade"/> </EventTrigger.Actions> </EventTrigger> <EventTrigger RoutedEvent="Mouse.MouseLeave"> <BeginStoryboard x:Name="StoryboardFade"> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="(UIElement.Opacity)" From="1.0" To="0.0" Duration="0:0:8" BeginTime="0:0:2" Completed="DoubleAnimationCompleted"/> </Storyboard> </BeginStoryboard> </EventTrigger> </Grid.Triggers> <Grid.RenderTransform> <ScaleTransform ScaleY="1" /> </Grid.RenderTransform></Grid>
The Code Behind
public partial class NotificationWindow : Window{ public NotificationWindow() : base() { this.InitializeComponent(); this.Closed += this.NotificationWindowClosed; } public new void Show() { this.Topmost = true; base.Show(); this.Owner = System.Windows.Application.Current.MainWindow; this.Closed += this.NotificationWindowClosed; var workingArea = Screen.PrimaryScreen.WorkingArea; this.Left = workingArea.Right - this.ActualWidth; double top = workingArea.Bottom - this.ActualHeight; foreach (Window window in System.Windows.Application.Current.Windows) { string windowName = window.GetType().Name; if (windowName.Equals("NotificationWindow") && window != this) { window.Topmost = true; top = window.Top - window.ActualHeight; } } this.Top = top; } private void ImageMouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e) { this.Close(); } private void DoubleAnimationCompleted(object sender, EventArgs e) { if (!this.IsMouseOver) { this.Close(); } }}
The call from the ViewModel:
private void ShowNotificationExecute() { App.Current.Dispatcher.Invoke(DispatcherPriority.Normal, new Action( () => { var notify = new NotificationWindow(); notify.Show(); })); }
The Styles referenced in the XAML:
<Style x:Key="LinkButton" TargetType="Button"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="Button"> <TextBlock> <ContentPresenter /> </TextBlock> </ControlTemplate> </Setter.Value> </Setter> <Setter Property="Foreground" Value="Blue"/> <Setter Property="Cursor" Value="Hand"/> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="ContentTemplate"> <Setter.Value> <DataTemplate> <TextBlock TextDecorations="Underline" Text="{TemplateBinding Content}"/> </DataTemplate> </Setter.Value> </Setter> </Trigger> </Style.Triggers> </Style> <LinearGradientBrush x:Key="GradientBackground" EndPoint="0.504,1.5" StartPoint="0.504,0.03"> <GradientStop Color="#FFFDD5A7" Offset="0"/> <GradientStop Color="#FFFCE79F" Offset="0.567"/> </LinearGradientBrush>
UPDATE: I added this event handler when the form is closed to "drop" the other windows.
private void NotificationWindowClosed(object sender, EventArgs e) { foreach (Window window in System.Windows.Application.Current.Windows) { string windowName = window.GetType().Name; if (windowName.Equals("NotificationWindow") && window != this) { // Adjust any windows that were above this one to drop down if (window.Top < this.Top) { window.Top = window.Top + this.ActualHeight; } } } }
public partial class NotificationWindow : Window{ DispatcherTimer timer = new System.Windows.Threading.DispatcherTimer(); public NotificationWindow() : base() { this.InitializeComponent(); Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(() => { var workingArea = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea; var transform = PresentationSource.FromVisual(this).CompositionTarget.TransformFromDevice; var corner = transform.Transform(new Point(workingArea.Right, workingArea.Bottom)); this.Left = corner.X - this.ActualWidth; this.Top = corner.Y - this.ActualHeight; })); timer.Interval = TimeSpan.FromSeconds(4d); timer.Tick += new EventHandler(timer_Tick); } public new void Show() { base.Show(); timer.Start(); } void timer_Tick(object sender, EventArgs e) { //set default result if necessary timer.Stop(); this.Close(); }}
The above code is refined version @Ray Burns approach. Added with time interval code. So that Notification window would close after 4 seconds..
Call the Window as,
NotificationWindow nfw = new NotificationWindow();nfw.Show();