We would like to use balloon messages as described in the UX Guide from Microsoft. I found some samples which uses native code from Windows Forms, but the native code requires a handle to the component which a bit difficult for a WPF application since it doesn't follow the same concept.
I found some sample code which uses WPF's decorator mechanism, but I'm still not convinced that this is the easiest approach for WPF application. Could a possible implementation be to implement a decorator around a tooltip?
The concrete case I have is a form with several text boxes which need input validation and notification on possible wrong input values - something which seems appropriate for balloon messages.
Is there a commercial or open source control built for this use case under WPF that I should be aware of?
I went ahead and created a CodePlex site for this that includes "Toast Popups" and control "Help Balloons". These versions have more features than what's described below. Code Plex Project.
Here's the link to the Nuget Package
Here's my solution for balloon caption. Some of the things that I wanted it to do differently:
Here are the Help images that I used.
I created a UserControl with a simple "Help" icon.
<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" />
And added this to the code behind.
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;
}
}
Here's the XAML Code for the Balloon Window that the UserControl opens.
<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>
And the code behind of the Balloon window.
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();
}
}
}
The UX Guide points out that the differences between a balloon and a tool tip are:
Balloons can be displayed independently of the current pointer location, so they have a tail that indicates their source.
Balloons have a title, body text, and an icon.
Balloons can be interactive, whereas it is impossible to click on a tip.
That last is the only sticking point as far as WPF is concerned. If you need the user to be able to interact with the contents of the balloon, then it'll need to be a Popup, not a ToolTip. (You might benefit from this forum post if you go that route.)
But if all you're doing is displaying notifications, you can certainly use a ToolTip. You don't need to mess around with decorators either; just build a control template for the ToolTip that looks like what you want, create a ToolTip resource that uses that style, and set the target control's ToolTip
property to that ToolTip
. Use the ToolTipService
to control where it displays relative to the placement target.
I ended up putting a TextBlock in the adorner layer:
<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>
I also used the tooltip as shown in every WPF examples out there:
<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>
Not optimal (would really like a Balloon Message control), but works good enough for the need we have.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With