Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create popup "toaster" notifications in Windows with .NET

I am using .NET and am creating a desktop app/service that will display notifications in the corner of my Desktop when certain events are triggered. I don't want to use a regular Message Box b/c that would be too intrusive. I want notifications to slide into view and then fade out after a few seconds. I am thinking of something that will act very much like the Outlook alerts that one gets when a new message arrives. The question is: Should I use WPF for this? I've never done anything with WPF but will happily try it if that's best means to the end. Is there a way to accomplish this with regular .NET libraries?

like image 328
Antony Avatar asked Jun 14 '10 02:06

Antony


2 Answers

WPF makes this absolutely trivial: It would proably take ten minutes or less. Here are the steps:

  1. Create a Window, set AllowsTransparency="true" and add a Grid to it
  2. Set the Grid's RenderTransform to a ScaleTransform with origin of 0,1
  3. Create an animation on the grid that animates the ScaleX 0 to 1 then later animates the Opacity from 1 to 0
  4. 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.

like image 184
Ray Burns Avatar answered Oct 17 '22 06:10

Ray Burns


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;                 }             }         }     } 
like image 39
LawMan Avatar answered Oct 17 '22 06:10

LawMan