如何在WPF应用程序中实现Balloon消息

tro*_*nda 22 .net wpf user-interface xaml wpf-controls

我们希望使用Microsoft 的UX指南中描述的气球消息.我发现了一些使用Windows Forms中的本机代码的示例,但是本机代码需要一个组件句柄,这对于WPF应用程序来说有点困难,因为它不遵循相同的概念.

我发现了一些使用WPF装饰器机制的示例代码,但我仍然不相信这是WPF应用程序最简单的方法.可能的实现是围绕工具提示实现装饰器吗?

我的具体案例是一个带有几个文本框的表单,需要输入验证和可能的错误输入值的通知 - 这似乎适用于气球消息.

在WPF下是否有为此用例构建的商业或开源控件,我应该注意哪些?

Rob*_*ney 9

UX指南指出气球和工具提示之间的区别是:

  • 气球可以独立于当前指针位置显示,因此它们具有指示其来源的尾部.

  • 气球有标题,正文和图标.

  • 气球可以是互动的,但是不可能点击小费.

就WPF而言,这是最后一个关键点.如果您需要用户能够与气球的内容进行交互,那么它将需要是Popup,而不是ToolTip.(如果你走这条路,你可能会从这个论坛帖子中受益.)

但是,如果你正在做的只是显示通知,你当然可以使用工具提示.你也不需要搞砸装饰者; 只需为ToolTip构建一个看起来像你想要的控件模板,创建一个使用该样式的ToolTip资源,并将目标控件的ToolTip属性设置为该模板ToolTip.使用它ToolTipService来控制它相对于放置目标的显示位置.


Law*_*Man 9

我继续为此创建了一个CodePlex网站,其中包括"Toast Popups"和控制"Help Balloons".这些版本具有比下面描述的更多功能. Code Plex项目.

这是Nuget包的链接

这是我的气球标题解决方案.我希望它做的一些事情有所不同:

  • 鼠标进入时淡入.
  • 当不透明度达到0时,鼠标离开时淡出并关闭窗口.
  • 如果鼠标位于窗口上方,则不透明度将为100%且不会关闭.
  • 气球窗口的高度是动态的.
  • 使用事件触发器而不是计时器.
  • 将气球放在控件的左侧或右侧.

Screnshot在此输入图像描述

这是我使用的帮助图像.

在此输入图像描述在此输入图像描述

我创建了一个带有简单"帮助"图标的UserControl.

<UserControl x:Class="Foundation.FundRaising.DataRequest.Windows.Controls.HelpBalloon"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         mc:Ignorable="d" 
         Name="HelpBalloonControl"
         d:DesignHeight="20" d:DesignWidth="20" Background="Transparent">
    <Image Width="20" Height="20" 
           MouseEnter="ImageMouseEnter" 
           Cursor="Hand"
           IsManipulationEnabled="True" 
           Source="/Foundation.FundRaising.DataRequest.Windows;component/Resources/help20.png" />
Run Code Online (Sandbox Code Playgroud)

并将此添加到后面的代码中.

public partial class HelpBalloon : UserControl
{
    private Balloon balloon = null;

    public HelpBalloon()
    {
        InitializeComponent();
    }

    public string Caption { get; set; }

    public Balloon.Position Position { get; set; }

    private void ImageMouseEnter(object sender, MouseEventArgs e)
    {
        if (balloon == null)
        {
            balloon = new Balloon(this, this.Caption);
            balloon.Closed += BalloonClosed;
            balloon.Show();
        }
    }

    private void BalloonClosed(object sender, EventArgs e)
    {
        this.balloon = null;
    }
}
Run Code Online (Sandbox Code Playgroud)

这是UserControl打开的气球窗口的XAML代码.

<Window x:Class="Foundation.FundRaising.DataRequest.Windows.Balloon"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Height="90" Width="250" WindowStyle="None" 
    ResizeMode="NoResize" ShowInTaskbar="False"
    Topmost="True" IsTabStop="False" 
    OverridesDefaultStyle="False" 
    SizeToContent="Height"
    AllowsTransparency="True" 
    Background="Transparent" >
   <Grid RenderTransformOrigin="0,1" >        
    <StackPanel Orientation="Vertical">
        <StackPanel Orientation="Horizontal">
            <StackPanel.Resources>
                <Style TargetType="Path">
                    <Setter Property="Fill" Value="#fdfdfd"/>
                    <Setter Property="Stretch" Value="Fill"/>
                    <Setter Property="Width" Value="22"/>
                    <Setter Property="Height" Value="31"/>
                    <Setter Property="Panel.ZIndex" Value="99"/>
                    <Setter Property="VerticalAlignment" Value="Top"/>
                    <Setter Property="Effect">
                        <Setter.Value>
                            <DropShadowEffect Color="#FF757575" Opacity=".7"/>
                        </Setter.Value>
                    </Setter>
                </Style>
            </StackPanel.Resources>
            <Path  
              HorizontalAlignment="Left"  
              Margin="15,3,0,0" 
                Data="M10402.99154,55.5381L10.9919,0.64 0.7,54.9"
              x:Name="PathPointLeft"/>
            <Path  
                HorizontalAlignment="Right"  
                Margin="175,3,0,0"
                Data="M10402.992,55.5381 L10284.783,3.2963597 0.7,54.9"
                x:Name="PathPointRight">
            </Path>
        </StackPanel>

        <Border Margin="5,-3,5,5" 
                CornerRadius="7" Panel.ZIndex="100"
                VerticalAlignment="Top">
            <Border.Background>
                <LinearGradientBrush StartPoint="0,0" EndPoint="1,0">
                    <LinearGradientBrush.RelativeTransform>
                        <RotateTransform Angle="90" CenterX="0.7" CenterY="0.7" />
                    </LinearGradientBrush.RelativeTransform>
                    <GradientStop Color="#FFFDFDFD" Offset=".2"/>
                    <GradientStop Color="#FFB6FB88" Offset=".8"/>
                </LinearGradientBrush>
            </Border.Background>
            <Border.Effect>
                <DropShadowEffect Color="#FF757575" Opacity=".7"/>
            </Border.Effect>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>

                <Image Grid.Column="0" 
                       Width="35" 
                       Margin="5"
                       VerticalAlignment="Top" Height="35" 
                       Source="Resources/help.png" />

                <TextBlock Grid.Column="1" 
                           TextWrapping="Wrap"
                           Margin="0,10,10,10" 
                           TextOptions.TextFormattingMode="Display"
                           x:Name="textBlockCaption"
                           Text="This is the caption"/>
            </Grid>
        </Border>
    </StackPanel>

    <!-- 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:3" BeginTime="0:0:3" 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:2" BeginTime="0:0:1" Completed="DoubleAnimationCompleted"/>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Grid.Triggers>

    <Grid.RenderTransform>
        <ScaleTransform ScaleY="1" />
    </Grid.RenderTransform>
</Grid>
Run Code Online (Sandbox Code Playgroud)

气球窗口背后的代码.

public partial class Balloon : Window
{
    public enum Position
    {
        Left,

        Right
    }

    public Balloon(Control control, string caption, Position position)
    {
        InitializeComponent();

        this.textBlockCaption.Text = caption;

        // Compensate for the bubble point
        double captionPointMargin = this.PathPointLeft.Margin.Left;

        Point location = GetControlPosition(control);

        if (position == Position.Left)
        {
            this.PathPointRight.Visibility = Visibility.Hidden;
            this.Left = location.X + (control.ActualWidth / 2) - captionPointMargin;
        }
        else
        {
            this.PathPointLeft.Visibility = Visibility.Hidden;
            this.Left = location.X - this.Width + control.ActualWidth + (captionPointMargin / 2);
        }

        this.Top = location.Y + (control.ActualHeight / 2);
    }

    private static Point GetControlPosition(Control control)
    {
        Point locationToScreen = control.PointToScreen(new Point(0, 0)); 
        var source = PresentationSource.FromVisual(control);
        return source.CompositionTarget.TransformFromDevice.Transform(locationToScreen);
    }

    private void DoubleAnimationCompleted(object sender, EventArgs e)
    {
        if (!this.IsMouseOver)
        {
            this.Close();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


tro*_*nda 5

我最终将TextBlock放置在装饰层中:

<Setter Property="Validation.ErrorTemplate">
    <Setter.Value>
        <ControlTemplate>
            <StackPanel Orientation="Vertical">
                <Border>
                    <AdornedElementPlaceholder  x:Name="adorner"/>
                </Border>
                <TextBlock 
                    Height="20" Margin="10 0" Style="{StaticResource NormalColorBoldWeightSmallSizeTextStyle}"
                    Text="{Binding ElementName=adorner, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"/>
            </StackPanel>
        </ControlTemplate>
    </Setter.Value>
</Setter>
Run Code Online (Sandbox Code Playgroud)

我还使用了每个WPF示例中所示的工具提示:

<Style.Triggers>
    <Trigger Property="Validation.HasError" Value="True">
        <Setter Property="ToolTip"
                Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}">
        </Setter>
    </Trigger>
</Style.Triggers>
Run Code Online (Sandbox Code Playgroud)

不是最佳选择(确实会像气球消息控件一样),但是可以满足我们的需求。