我们希望使用 Microsoft 的 UX Guide 中描述的气球消息。我发现一些示例使用 Windows 窗体中的本机代码,但本机代码需要组件的句柄,这对于 WPF 应用程序来说有点困难,因为它不遵循相同的概念。
我发现了一些使用WPF装饰器机制的示例代码,但我仍然不相信这是WPF应用程序最简单的方法。可能的实现是围绕工具提示实现装饰器吗?
我的具体案例是一个带有多个文本框的表单,需要输入验证并通知可能的错误输入值 - 这似乎适合气球消息。
我应该了解 WPF 下是否有为此用例构建的商业或开源控件?
**我继续为此创建了一个 GitHub 存储库,其中包括“Toast Popups”和控件“Help Balloons”。这些版本具有比下面描述的更多的功能。
这是 Nuget 包
的链接这是我的气球标题解决方案。我希望它做一些不同的事情:
这是我使用的帮助图像。
我创建了一个带有简单“帮助”图标的用户控件。
<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;
}
}
这是 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>
以及气球窗口后面的代码。
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();
}
}
}
用户体验指南指出气球和工具提示之间的区别是:
气球可以独立于当前指针位置显示,因此它们有一个表明其来源的尾巴。
气球有标题、正文和图标。
气球可以互动,但无法点击提示。
对于 WPF 而言,最后一点是唯一的症结所在。如果您需要用户能够与气球的内容进行交互,那么它需要是弹出窗口,而不是工具提示。 (如果您选择这条路线,您可能会从this论坛帖子中受益。)
但是,如果您所做的只是显示通知,那么您当然可以使用工具提示。你也不需要乱搞装饰器;只需为 ToolTip 构建一个看起来像您想要的那样的控件模板,创建一个使用该样式的 ToolTip 资源,并将目标控件的
ToolTip
属性设置为该 ToolTip
。使用 ToolTipService
控制其相对于放置目标的显示位置。
我最终在装饰层中放置了一个 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>
不是最优的(真的想要气球消息控件),但足以满足我们的需要。
我制作了警告气球来解决我的 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;
}
}
}
在我们的应用程序中,我们将气球实现为一个简单的 WPF 窗口。 窗口位置受限于某些父控件模型属性。 下面是一个示例代码(其中 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();