22

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?

tronda
  • 3,902
  • 4
  • 33
  • 55
  • I have already give answer in following link. Please follow it. [How Create Balloon for Caps Lock warning][1] [1]: http://stackoverflow.com/questions/1092808/wpf-warn-about-capslock/8060520#8060520 – NASSER Nov 09 '11 at 05:08

5 Answers5

10

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:

  • Fade in when the mouse enters.
  • Fade out when mouse leaves and close the window when the opacity reaches 0.
  • If the mouse is over the window, the opacity will be at 100% and not close.
  • The height of the Balloon window is dynamic.
  • Use event triggers instead of timers.
  • Position the balloon on the left or right side of the control.

Screnshotenter image description here

Here are the Help images that I used.

enter image description hereenter image description here

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();
        }
    }
}
LawMan
  • 3,469
  • 1
  • 29
  • 32
  • I've been looking all day for something that does exactly this. Unfortunately, although great, the tooltips in your CodePlex project are custom styled and do no go with a stock system look. Is there an easy way to get your control to look like a standard balloon tip? – Ani Nov 15 '14 at 00:40
  • Currently, I don't have this exposed to change. This control is starting to get some traction and I have been asked to add some other features as well. Please add your request to the CodePlex site, and I'll add it to my next release. In the meantime, you can download the source code, and alter the XAML, which should be pretty easy to do. Than you for your comments and support. – LawMan Nov 18 '14 at 16:22
9

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.

Community
  • 1
  • 1
Robert Rossney
  • 94,622
  • 24
  • 146
  • 218
  • Challenge is to trigger the ToolTip to show up when the cursor isn't over the field. I abandoned this solution. Thinking of a specific AdornerLayer which is more visual than the regular red border – tronda Feb 23 '10 at 10:11
  • I was also thinking about implementing it as a custom adorner implementation as the Validation.ErrorTemplate ControlTemplate, but not sure how to tackle it. – tronda Feb 25 '10 at 11:04
5

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.

tronda
  • 3,902
  • 4
  • 33
  • 55
3

I have made warning balloon to solve Caps Lock warning problem in my WPF-Project.

enter image description here

If you want to add this balloon warning in your project then follow these steps:

- Add new Window in your project and give name "WarningBalloon".
- Add following XAML code against new Window and add warning icon to image folder of project.

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

- Type the following code behind the 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;
        }
    }    
}
NASSER
  • 5,900
  • 7
  • 38
  • 57
  • For production this needs a lot of work. window resize and location changed event handlers for starters. If the control it is pointing to is on the bottom, right, whatever the arrow and size should adjust automatically otherwise it looks terrible. This code is just a starting point - not a solution. – Krafty Dec 21 '14 at 19:27
2

In our application we've implemented balloons as a simple WPF Window. The Window location is bounded to some of the parent control model properties. Here's a sample code (where BalloonContainerWindow inherits from 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();
Amittai Shapira
  • 3,749
  • 1
  • 30
  • 54