【问题标题】:Create popup "toaster" notifications in Windows with .NET使用 .NET 在 Windows 中创建弹出式“烤面包机”通知
【发布时间】:2011-03-03 08:39:47
【问题描述】:

我正在使用 .NET 并正在创建一个桌面应用程序/服务,当某些事件被触发时,它将在我的桌面角落显示通知。我不想使用过于打扰的常规消息框 b/c。我希望通知滑入视图,然后在几秒钟后淡出。我正在考虑一种非常类似于新邮件到达时收到的 Outlook 警报的东西。问题是:我应该为此使用 WPF 吗?我从来没有用 WPF 做过任何事情,但如果这是最好的方法,我会很乐意尝试。有没有办法使用常规的 .NET 库来实现这一点?

【问题讨论】:

标签: .net wpf windows desktop


【解决方案1】:

WPF 让这变得非常简单:可能需要十分钟或更短的时间。步骤如下:

  1. 创建一个窗口,设置 AllowsTransparency="true" 并向其中添加一个网格
  2. 将 Grid 的 RenderTransform 设置为原点为 0,1 的 ScaleTransform
  3. 在网格上创建动画,将 ScaleX 设置为 0 到 1 的动画,然后将不透明度从 1 设置为 0 的动画
  4. 在构造函数中计算 Window.Top 和 Window.Left 以将窗口放置在屏幕的右下角。

仅此而已。

使用 Expression Blend 我花了大约 8 分钟来生成以下工作代码:

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

后面有代码:

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;
    }));
  }
}

由于 WPF 是常规 .NET 库之一,答案是肯定的,可以通过“常规 .NET 库”来实现的。

如果您问是否有办法在不使用 WPF 的情况下做到这一点,答案仍然是肯定的,但它非常复杂,需要 5 天而不是 5 分钟。

【讨论】:

  • 不确定是否需要,但我在不透明动画中添加了 Completed 事件,并在后面的代码中添加了“this.Close();”。否则窗口将始终保持打开状态。可能会弄乱根据最后一个窗口关闭而关闭的应用程序。
  • 也可以使用System.Windows.SystemParameters.WorkArea,因此无需使用windows.forms dll(如本问题所述:stackoverflow.com/questions/7620488/…
  • 对重新运行此问题表示歉意;但是@Paul,你能~请~演示一下你是如何添加完成的活动的吗?
  • @NewAmbition,以防万一还没有弄清楚,我做了,有点,但不知道这是不是保罗用的。无论如何,就这样 - 使用动画的 Completed 事件来为 Opacity 设置动画
  • PresentationSource.FromVisual(this) 在这里返回 null,知道是什么原因造成的吗?
【解决方案2】:

我继续为此创建了一个 CodePlex 站点,其中包括“Toast Popups”和控制“Help Balloons”。这些版本具有比下面描述的更多的功能。 https://toastspopuphelpballoon.codeplex.com.

这是我正在寻找的解决方案的一个很好的起点。我做了一些修改以满足我的要求:

  • 我想在鼠标悬停时停止动画。
  • 鼠标离开时的“重置”动画。
  • 不透明度达到 0 时关闭窗口。
  • 堆叠Toast(如果窗口数量超过屏幕高度我还没有解决问题)
  • 从我的 ViewModel 调用加载

这是我的 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>

背后的代码

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();
        }
    }
}

来自 ViewModel 的调用:

    private void ShowNotificationExecute()
    {
        App.Current.Dispatcher.Invoke(DispatcherPriority.Normal, new Action(
            () =>
            {
                var notify = new NotificationWindow();
                notify.Show();
            }));
    }

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>

更新:我在窗体关闭时添加了这个事件处理程序以“删除”其他窗口。

    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;
                }
            }
        }
    }

【讨论】:

  • NotificationWindowClosed 是如何工作的?代码中的其他任何地方都没有引用它
  • 很抱歉。我添加了缺少的代码。这是一项正在进行的工作,我在上次更新中错过了。
  • 完美运行!您能否再解释一下“我在关闭窗体以“删除”其他窗口时添加了此事件处理程序”的含义。你是什​​么意思“下降”?
  • 基本上,当窗口达到 0 不透明度并关闭时,我希望上面的窗口“下降”到屏幕底部。
【解决方案3】:
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();
    }

}

以上代码是@Ray Burns 方法的精炼版。添加了时间间隔代码。这样通知窗口将在 4 秒后关闭..

将窗口称为,

NotificationWindow nfw = new NotificationWindow();
nfw.Show();

【讨论】:

  • @Gopichandar 我听不懂 Gopi
【解决方案4】:

在我看来,我使用上面的答案设计了我自己的通知窗口,它对用户更加友好,并使用了一些我花了一些时间才弄清楚的技术。分享以防万一它可以帮助其他人。:

  1. 添加了 MouseEnter 事件触发器以立即将窗口不透明度设置为 1,因此用户不必等待窗口淡入全视图。
  2. 添加了 MouseLeave 事件触发器,以在用户将鼠标移出窗口时将窗口不透明度淡化为 0。
  3. 添加了 MouseUp(鼠标点击)事件触发器,立即将窗口不透明度设置为 0 以隐藏通知窗口。
  4. 如果您必须多次 Show() 和 Hide() 通知窗口,那么下面的方法还会在最后重置 Storyboard,以便下一个 Show() 操作从头开始操作并且没有故障动画。

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>

后面的代码:

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;
        }));
    }
}

【讨论】:

    【解决方案5】:
    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();
    

    【讨论】:

      【解决方案6】:

      注意调用线程必须是sta,因为很多ui组件都需要这个,同时在system.timers.timer elapsed事件下编写如下代码

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

      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; 
          })); 
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2020-03-11
        • 2015-03-19
        • 1970-01-01
        • 1970-01-01
        • 2018-08-07
        • 1970-01-01
        • 2010-09-27
        相关资源
        最近更新 更多