Crear notificaciones emergentes de "tostadora" en Windows with.NET


Estoy usando.NET y estoy creando una aplicación/servicio de escritorio que mostrará notificaciones en la esquina de mi escritorio cuando se activen ciertos eventos. No quiero usar un cuadro de mensaje regular b / c que sería demasiado intrusivo. Quiero que las notificaciones se deslicen a la vista y luego se desvanezcan después de unos segundos. Estoy pensando en algo que actuará muy parecido a las alertas de Outlook que uno recibe cuando llega un nuevo mensaje. La pregunta es: ¿Debo usar WPF para esto? Nunca he hecho nada con WPF, pero felizmente lo intentará si es el mejor medio para el final. ¿Hay alguna manera de lograr esto con bibliotecas. NET regulares?

Author: Antony, 2010-06-14

6 answers

WPF hace que esto sea absolutamente trivial: probablemente tomaría diez minutos o menos. Estos son los pasos:

  1. Cree una ventana, establezca AllowsTransparency="true" y agregue una cuadrícula
  2. Establecer el RenderTransform de la cuadrícula en un ScaleTransform con origen de 0,1
  3. Cree una animación en la cuadrícula que anime el scaleX 0 a 1 y luego anime la opacidad de 1 a 0
  4. En la ventana calcular del constructor.Arriba y ventana.Izquierda para colocar la ventana en la parte inferior esquina derecha de la pantalla.

Eso es todo lo que hay que hacer.

Usando la expresión Blend me tomó 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;
    }));
  }
}

Dado que 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ás preguntando si hay una manera de hacer esto sin usar WPF, la respuesta sigue siendo sí, pero lo es extremadamente complejo y tardará más de 5 días que 5 minutos.

 106
Author: Ray Burns,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2010-06-14 08:03:36

Seguí adelante y creé un sitio de CodePlex para esto que incluye " Ventanas emergentes de tostadas "y control"Globos de ayuda". 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 al pasar el ratón por encima.
  • Animación "Reset" cuando ratón dejar.
  • Cierre la ventana cuando la opacidad alcance 0.
  • Apilar la tostada (no he resuelto el problema si el número de ventanas supera la altura de la pantalla)
  • Carga de llamada 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 De

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 el 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>

ACTUALIZAR: He añadido este controlador de eventos cuando el formulario se cierra a " drop" 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;
                }
            }
        }
    }
 16
Author: LawMan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-09-08 16:33:07
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 enfoque. Añadido con código de intervalo de tiempo. Por lo que la ventana de notificación se cerraría después de 4 segundos..

Llama a la ventana como,

NotificationWindow nfw = new NotificationWindow();
nfw.Show();
 7
Author: Anees Deen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-06-18 06:37:44
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();
 2
Author: Mehul Sant,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-08-26 19:10:19

Tenga en cuenta que el subproceso de llamada debe ser sta porque muchos componentes de la interfaz de usuario lo requieren, mientras escriben el siguiente código bajo system.temporizador.timer elapsed event

Window1 notifyWin = new Window1();
bool? isOpen = notifyWin.ShowDialog();
if (isOpen != null && isOpen == true)
{
     notifyWin.Close();
}
System.Threading.Thread.Sleep(1000);
notifyWin.ShowDialog();

Bajo window1 constructor:

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; 
    })); 
}
 0
Author: PKBhaiya,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-03-08 03:46:32

He utilizado la respuesta anterior para diseñar mi propia ventana de notificación que es un poco más fácil de usar, en mi opinión, y utiliza algunas técnicas que me llevó un poco de tiempo para averiguar. Compartir en caso de que ayude a alguien más.:

  1. Se agregó MouseEnter event trigger para establecer inmediatamente la opacidad de la ventana en 1 para que el usuario no tenga que esperar a que la ventana se desvanezca a la vista completa.
  2. Se agregó el desencadenador de eventos MouseLeave para desvanecer la opacidad de la ventana a 0 cuando el usuario mueve el mouse por la ventana.
  3. Se agregó el desencadenador de eventos MouseUp (clic del ratón) para establecer inmediatamente la opacidad de la ventana en 0 para ocultar la ventana de notificación.
  4. Si tiene que Mostrar() y Ocultar() la ventana de notificación varias veces, entonces el método debajo también restablece el guion gráfico al final para que la siguiente operación Show() inicie la operación desde el principio y no haya ningún fallo 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;
        }));
    }
}
 0
Author: user1367200,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-07-12 23:46:58