【问题标题】:How to implement Balloon message in a WPF application如何在 WPF 应用程序中实现气球消息
【发布时间】:2011-01-19 14:05:28
【问题描述】:

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

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

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

我应该知道在 WPF 下是否有为此用例构建的商业或开源控件?

【问题讨论】:

标签: .net wpf user-interface xaml wpf-controls


【解决方案1】:

用户体验指南指出气球和工具提示之间的区别是:

  • 气球可以独立于当前指针位置显示,因此它们有一个表明其来源的尾巴。

  • 气球有标题、正文和图标。

  • 气球可以是交互式的,但不可能点击提示。

就 WPF 而言,最后一点是唯一的症结所在。如果您需要用户能够与气球的内容进行交互,那么它需要是一个弹出窗口,而不是一个工具提示。 (如果你走这条路,你可能会从this 论坛帖子中受益。)

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

【讨论】:

  • 挑战是在光标不在字段上时触发工具提示显示。我放弃了这个解决方案。考虑比常规红色边框更直观的特定 AdornerLayer
  • 我也在考虑将其实现为自定义装饰器实现,如 Validation.ErrorTemplate ControlTemplate,但不知道如何解决。
【解决方案2】:

在我们的应用程序中,我们将气球实现为一个简单的 WPF 窗口。 Window 位置绑定到某些父控件模型属性。 这是一个示例代码(其中 BalloonContainerWindow 继承自 Window):

        BaloonContainterWindow newBalloon = new BaloonContainterWindow();
        newBalloon.CreateBaloon(balloonType, balloonData);

        // Allow input and output when theis window is on top of winforms window
        SetBalloonLocation(newBalloon, sequenceId, stepId, rulerModel);

        newBalloon.Show();
        newBalloon.CloseOnDeactivation = false;
        newBalloon.Activate();

【讨论】:

  • 这篇文章可以添加一些简化的示例代码吗?
【解决方案3】:

我最终在装饰层中放置了一个 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>

我还使用了每个 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>

不是最佳的(非常喜欢气球消息控件),但可以满足我们的需要。

【讨论】:

    【解决方案4】:

    我制作了警告气球来解决我的 WPF 项目中的 Caps Lock 警告问题。

    如果您想在项目中添加此气球警告,请按照以下步骤操作:

    - 在您的项目中添加新窗口并命名为“WarningBalloon”。
    - 针对新窗口添加以下 XAML 代码并将警告图标添加到图像文件夹项目。

    <Window x:Class="MyNameSpace.WarningBalloon"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                Height="160" Width="469" WindowStyle="None" ResizeMode="NoResize" ShowInTaskbar="False" Topmost="True" IsTabStop="False" OverridesDefaultStyle="False" AllowsTransparency="True" Background="Transparent" Opacity="1" >
            <Grid Height="126" Width="453">
                <Grid.RowDefinitions>
                    <RowDefinition Height="81" />
                    <RowDefinition Height="45*" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="177*" />
                    <ColumnDefinition Width="72*" />
                    <ColumnDefinition Width="0*" />
                    <ColumnDefinition Width="170*" />
                </Grid.ColumnDefinitions>
                <Border Margin="12,32,0,0"
              CornerRadius="10,10,10,10" Grid.ColumnSpan="4" HorizontalAlignment="Left" Width="429" Height="82" VerticalAlignment="Top" Grid.RowSpan="2">
                    <Border.Effect>
                        <DropShadowEffect
                  Color="#FF474747" />
                    </Border.Effect>
                    <Border.Background>
                        <LinearGradientBrush
                  EndPoint="0.5,1"
                  StartPoint="0.5,0">
                            <GradientStop
                    Color="#FF58C2FF"
                    Offset="0" />
                            <GradientStop
                    Color="#FFFFFFFF"
                    Offset="1" />
                        </LinearGradientBrush>
                    </Border.Background>
                    <Grid Height="76" Name="grid1" Width="441">
                        <Image Height="35" HorizontalAlignment="Left" Margin="6,6,0,0" Name="image1" Stretch="Fill" VerticalAlignment="Top" Width="35" Source="/MyNameSpace;component/Images/warning-icon.png" />
                        <Label Content="Caps Lock is ON" Height="31" HorizontalAlignment="Left" Margin="125,-6,0,0" Name="lblWarningHeader" VerticalAlignment="Top" FontSize="16" FontWeight="Bold" />
                        <TextBlock HorizontalAlignment="Right" Margin="0,22,17,-1" Name="txbMessage" Width="379">Having Caps Lock on may cause you to enter your password incorrectly. <LineBreak/> <LineBreak/> You should press Caps Lock to turn it of before entering your password. VerticalAlignment="Top" Width="346" FontSize="11"</TextBlock>
                    </Grid>
                </Border>
                <Image
                Source="{Binding Path=IconSource}" Width="16" HorizontalAlignment="Left" Margin="-56,0,0,-38" Height="16" VerticalAlignment="Bottom" Grid.Row="1" />
                <Path Data="M10402.99154,55.5381L10.9919,0.64 0.7,54.9" Fill="LightSkyBlue" HorizontalAlignment="Left" Margin="32,3,0,0" Stretch="Fill" Stroke="Black" Width="22" Height="31" VerticalAlignment="Top" />
            </Grid>
        </Window>
    

    - 在 LoginForm 后面键入以下代码。

        private Point location;
        public static  bool balloonVisFlag = false;
        private DispatcherTimer timer;
        WarningBalloon Balloon = null;
    
        private void ShowHideBalloon()
        {            
            if (System.Windows.Forms.Control.IsKeyLocked(System.Windows.Forms.Keys.CapsLock))
            {
                if (timer == null)
                {
                    timer = new DispatcherTimer();
                }
                location = GetControlPosition(psbPassword);
                Balloon.Left = location.X;
                Balloon.Top = location.Y;
                Balloon.Show();
                balloonVisFlag = true;
                timer.Interval = TimeSpan.FromMilliseconds(5000);
                timer.IsEnabled = true;
                timer.Tick += new EventHandler(Timer_Tick);
                psbPassword.Focus();
            }
            else
            {
                Balloon.Hide();
                balloonVisFlag = false;
                psbPassword.Focus();
            }
        }
    
        Point GetControlPosition(Control myControl)
        {
            Point locationToScreen = myControl.PointToScreen(new Point(0, 0));
            PresentationSource source = PresentationSource.FromVisual(myControl);
            return source.CompositionTarget.TransformFromDevice.Transform(locationToScreen);
        }     
    
        private void psbPassword_KeyDown(object sender, KeyEventArgs e)
        {
            ShowHideBalloon();
        }
    
        private void Window_LocationChanged(object sender, EventArgs e)
        {
            if (balloonVisFlag == true)
            {
                ShowHideBalloon();
            }
        }
    
        private void Timer_Tick(object sender, EventArgs e)
        {
            if (balloonVisFlag == true)
            {
                Balloon.Hide();
                balloonVisFlag = false;
            }
        }    
    }
    

    【讨论】:

    • 对于生产来说,这需要大量的工作。用于初学者的窗口调整大小和位置更改事件处理程序。如果它指向的控件位于右下方,则无论箭头和大小都应自动调整,否则看起来很糟糕。这段代码只是一个起点 - 不是解决方案。
    【解决方案5】:

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

    这是Nuget Package的链接

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

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

    这是我使用的帮助图片。

    我创建了一个带有简单“帮助”图标的用户控件。

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

    并将其添加到后面的代码中。

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

    这是用户控件打开的气球窗口的 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>
    

    还有气球窗口背后的代码。

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

    【讨论】:

    • 我整天都在寻找能做到这一点的东西。不幸的是,虽然很棒,但您的 CodePlex 项目中的工具提示是自定义样式的,并且与库存系统外观不符。有没有一种简单的方法可以让您的控件看起来像一个标准的气球提示?
    • 目前,我没有对此进行更改。这个控件开始获得一些牵引力,我也被要求添加一些其他功能。请将您的请求添加到 CodePlex 站点,我会将其添加到我的下一个版本中。同时,您可以下载源代码并更改 XAML,这应该很容易做到。感谢您的 cmets 和支持。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-12-30
    • 1970-01-01
    • 1970-01-01
    • 2023-04-01
    • 1970-01-01
    相关资源
    最近更新 更多