tutorial - Crear notificaciones emergentes de "tostadora" en Windows con.NET
wpf vs winforms (6)
Tenga en cuenta que el hilo que realiza la llamada debe estar en sta porque muchos componentes de la interfaz de usuario lo requieren, al escribir el siguiente código en el evento system.timers.timer transcurrido.
Window1 notifyWin = new Window1();
bool? isOpen = notifyWin.ShowDialog();
if (isOpen != null && isOpen == true)
{
notifyWin.Close();
}
System.Threading.Thread.Sleep(1000);
notifyWin.ShowDialog();
bajo el constructor window1:
public Window1()
{
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;
}));
}
Estoy usando .NET y estoy creando una aplicación de escritorio / servicio que mostrará notificaciones en la esquina de mi Escritorio cuando se desencadenen ciertos eventos. No quiero utilizar un cuadro de mensaje normal b / c que sería demasiado intrusivo. Quiero que las notificaciones se deslicen a la vista y luego desaparezcan después de unos segundos. Estoy pensando en algo que se parecerá mucho a las alertas de Outlook que uno recibe cuando llega un nuevo mensaje. La pregunta es: ¿Debería usar WPF para esto? Nunca he hecho nada con WPF, pero lo intentaré felizmente si es lo mejor para el final. ¿Hay alguna manera de lograr esto con las librerías .NET regulares?
Utilicé la respuesta anterior para diseñar mi propia ventana de notificación que es un poco más amigable para el usuario, en mi opinión, y utiliza algunas técnicas que me tomaron un poco de tiempo descifrar. Compartir en caso de que ayude a alguien más por ahí .:
- Se ha agregado el desencadenador del evento MouseEnter para establecer inmediatamente la opacidad de la ventana en 1 para que el usuario no tenga que esperar a que la ventana se desvanezca en vista completa.
- Se ha agregado el activador de eventos MouseLeave para atenuar la opacidad de la ventana a 0 cuando el usuario mueve el mouse fuera de la ventana.
- Activador de evento MouseUp (clic con el mouse) para establecer inmediatamente la opacidad de la ventana en 0 para ocultar la ventana de notificación.
- Si tiene que Mostrar () y Ocultar () la ventana de notificación varias veces, el método siguiente también restablece el Guión gráfico al final para que la siguiente operación Mostrar () inicie la operación desde el principio y no haya ningún problema en la animación.
XAML:
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ToastNotificationWindow"
Title="Notification Popup"
Width="480"
Height="140"
WindowStyle="None"
AllowsTransparency="True"
Background="Transparent"
BorderThickness="0"
Topmost="True"
>
<Grid Background="Transparent" Name="ToastWindowGrid" RenderTransformOrigin="0,1">
<Border Name="ToastWindowBorder" BorderThickness="0" Background="#333333">
<StackPanel Name="ToastWindowStackPanel" Margin="10" Orientation="Horizontal">
<Image Name="ToastLogo" Width="100" Height="100" Source="D:/Development/ToastNotification/resources/Logo-x100.png"/>
<StackPanel Name="ToastMessageStackPanel" Width="359">
<TextBox Name="ToastTitleTextBox" Margin="5" MaxWidth="340" Background="#333333" BorderThickness="0" IsReadOnly="True" Foreground="White" FontSize="20" Text="Toast Title" FontWeight="Bold" HorizontalContentAlignment="Center" Width="Auto" HorizontalAlignment="Stretch" IsHitTestVisible="False"/>
<TextBox Name="ToastMessageTextBox" Margin="5" MaxWidth="340" Background="#333333" BorderThickness="0" IsReadOnly="True" Foreground="LightGray" FontSize="16" Text="Toast title message. Click to start something." HorizontalContentAlignment="Left" TextWrapping="Wrap" IsHitTestVisible="False"/>
</StackPanel>
</StackPanel>
</Border>
<Grid.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<EventTrigger.Actions>
<BeginStoryboard Name="StoryboardLoad">
<Storyboard Name="ToastAnimationStoryboard">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)" FillBehavior="HoldEnd">
<SplineDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
<SplineDoubleKeyFrame KeyTime="0:0:0.5" Value="1"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" FillBehavior="HoldEnd">
<SplineDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
<SplineDoubleKeyFrame KeyTime="0:0:0.5" Value="1"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" FillBehavior="HoldEnd">
<SplineDoubleKeyFrame KeyTime="0:0:20" Value="1"/>
<SplineDoubleKeyFrame KeyTime="0:0:23" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent="Mouse.MouseEnter">
<EventTrigger.Actions>
<StopStoryboard BeginStoryboardName="StoryboardMouseLeaveFadeOut"/>
<BeginStoryboard Name="StoryboardMouseEnterFadeIn">
<Storyboard Name="ToastAnimationStoryboardMouseEnterFadeIn">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" FillBehavior="HoldEnd">
<DiscreteDoubleKeyFrame KeyTime="0:0:0.1" Value="1"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent="Mouse.MouseLeave">
<EventTrigger.Actions>
<RemoveStoryboard BeginStoryboardName="StoryboardMouseLeaveFadeOut"/>
<BeginStoryboard Name="StoryboardMouseLeaveFadeOut">
<Storyboard Name="ToastAnimationStoryboardMouseLeaveFadeOut">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" FillBehavior="HoldEnd">
<SplineDoubleKeyFrame KeyTime="0:0:0" Value="1"/>
<SplineDoubleKeyFrame KeyTime="0:0:3" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent="Mouse.MouseUp">
<EventTrigger.Actions>
<StopStoryboard BeginStoryboardName="StoryboardMouseEnterFadeIn"/>
<RemoveStoryboard BeginStoryboardName="StoryboardMouseEnterFadeIn"/>
<StopStoryboard BeginStoryboardName="StoryboardMouseLeaveFadeOut"/>
<RemoveStoryboard BeginStoryboardName="StoryboardMouseLeaveFadeOut"/>
<BeginStoryboard Name="StoryboardMouseClickFadeOut">
<Storyboard Name="ToastAnimationStoryboardMouseClickFadeOut">
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Opacity)" FillBehavior="HoldEnd">
<DiscreteDoubleKeyFrame KeyTime="0:0:0.1" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
<SeekStoryboard BeginStoryboardName="StoryboardLoad"/>
<PauseStoryboard BeginStoryboardName="StoryboardLoad"/>
</EventTrigger.Actions>
</EventTrigger>
</Grid.Triggers>
<Grid.RenderTransform>
<ScaleTransform ScaleY="1" />
</Grid.RenderTransform>
</Grid>
</Window>
Código detrás:
using System;
using System.Windows;
using System.Windows.Threading;
public partial class ToastNotificationWindow
{
public ToastNotificationWindow()
{
InitializeComponent();
Dispatcher.BeginInvoke(DispatcherPriority.ApplicationIdle, new Action(() =>
{
var workingArea = System.Windows.SystemParameters.WorkArea;
var transform = PresentationSource.FromVisual(this).CompositionTarget.TransformFromDevice;
var corner = transform.Transform(new Point(workingArea.Right, workingArea.Bottom));
this.Left = corner.X - this.ActualWidth - 10;
this.Top = corner.Y - this.ActualHeight;
}));
}
}
WPF lo hace absolutamente trivial: probablemente tomaría diez minutos o menos. Estos son los pasos:
- Cree una ventana, establezca AllowsTransparency = "true" y agregue una cuadrícula a ella
- Establezca RenderTransform de la cuadrícula en una ScaleTransform con origen de 0,1
- Cree una animación en la cuadrícula que anima el ScaleX 0 a 1 y luego anima la Opacidad de 1 a 0
- En el constructor, calcule Window.Top y Window.Left para colocar la ventana en la esquina inferior derecha de la pantalla.
Eso es todo al respecto.
Usando Expression Blend me llevó unos 8 minutos generar el siguiente código de trabajo:
<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>
Con código detrás
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;
}));
}
}
Como WPF es una de las bibliotecas regulares de .NET, la respuesta es sí, es posible lograr esto con las "bibliotecas regulares de .NET".
Si está preguntando si hay una forma de hacerlo sin usar WPF, la respuesta es sí, pero es extremadamente compleja y tomará más de 5 días que 5 minutos.
Seguí adelante y creé un sitio CodePlex para esto que incluye "Toast Popups" y control de "Help Balloons". Estas versiones tienen más características que las que se describen a continuación. https://toastspopuphelpballoon.codeplex.com .
Este fue un gran punto de partida para la solución que estaba buscando. He hecho un par de modificaciones para cumplir con mis requisitos:
- Quería detener la animación sobre el mouse.
- Animación "Restablecer" cuando el mouse se va.
- Cierre la ventana cuando la opacidad llegue a 0.
- Apila el Toast (no he resuelto el problema si el número de ventanas excede la altura de la pantalla)
- Carga de llamadas desde mi ViewModel
Aquí está mi 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>
El código detrás
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();
}
}
}
La llamada desde ViewModel:
private void ShowNotificationExecute()
{
App.Current.Dispatcher.Invoke(DispatcherPriority.Normal, new Action(
() =>
{
var notify = new NotificationWindow();
notify.Show();
}));
}
Los estilos a los que se hace referencia en el 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>
ACTUALIZACIÓN: agregué este controlador de eventos cuando el formulario está cerrado para "soltar" las otras ventanas.
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;
}
}
}
}
NotifyIcon notifyIcon = new NotifyIcon();
Stream iconStream = System.Windows.Application.GetResourceStream(new Uri("pack://application:,,,/Assets/ic_instant_note_tray.ico")).Stream;
notifyIcon.Icon = new System.Drawing.Icon(iconStream);
notifyIcon.Text = string.Format(Properties.Resources.InstantNoteAppName, Constants.Application_Name);
notifyIcon.Visible = true;
notifyIcon.ShowBalloonTip(5000, "tooltiptitle", "tipMessage", ToolTipIcon.Info);
notifyIcon.Visible = false;
notifyIcon.Dispose();
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();
}
}
El código anterior es versión refinada @Ray Burns approach. Agregado con código de intervalo de tiempo. Entonces esa ventana de notificación se cerraría después de 4 segundos ...
Llamar a la ventana como,
NotificationWindow nfw = new NotificationWindow();
nfw.Show();