13

I have a C# WPF application using XAML and MVVM. My question is: How can I show a balloon tooltip above a text box for some invalid data entered by the user?

I want to use Microsoft's native balloon control for this. How would I implement this into my application?

qJake
  • 16,821
  • 17
  • 83
  • 135
  • 1
    Other SO post regarding this issue: http://stackoverflow.com/questions/2310102/how-to-implement-balloon-message-in-a-wpf-application. Might help you out. They also mention the same URL as you. – Christophe Geers Sep 24 '11 at 20:49
  • 2
    None of those use the native Windows balloon. I was already there, and it wasn't helpful, that's why I posted my own question. – qJake Sep 24 '11 at 20:57

4 Answers4

12

Just add a reference to System.Windows.Forms and C:\Program Files\Reference Assemblies\Microsoft\Framework.NETFramework\v4.0\WindowsFormsIntegration.dll and then:

    WindowsFormsHost host =new WindowsFormsHost();

    var toolTip1 = new System.Windows.Forms.ToolTip();

    toolTip1.AutoPopDelay = 5000;
    toolTip1.InitialDelay = 1000;
    toolTip1.ReshowDelay = 500;
    toolTip1.ShowAlways = true;
    toolTip1.IsBalloon = true;
    toolTip1.ToolTipIcon = System.Windows.Forms.ToolTipIcon.Info;
    toolTip1.ToolTipTitle = "Title:";

    System.Windows.Forms.TextBox tb = new System.Windows.Forms.TextBox();
    tb.Text="Go!";
    toolTip1.SetToolTip(tb, "My Info!");
    host.Child = tb;
    grid1.Children.Add(host);  //a container for windowsForm textBox 

and this is a sample for WinForm ToolTip Ballon in WPF:

enter image description here

Hope this help!

ARZ
  • 2,461
  • 3
  • 34
  • 56
  • 1
    Can you link to that ToolTip class? Is it http://msdn.microsoft.com/en-us/library/system.windows.controls.tooltip.aspx ? – Ian Boyd Sep 28 '11 at 12:04
  • This looks like exactly what I needed, I'll try it when I get home and award the bounty if it works. Thank you! – qJake Sep 28 '11 at 18:39
  • -1, this is the WinForms Tooltip. The question is about WPF, read the tags – Fredrik Hedblad Sep 29 '11 at 06:15
  • 9
    This is using not just a WinForms tooltip, it's also putting a WinForms _TextBox_ inside a WPF app. That seems like a pretty poor way to go. – Joe White Sep 29 '11 at 21:00
  • 1
    This doesn't work, and isn't what I'm looking for. Not only do I not want a reference to System.Windows.Forms and WindowsFormsIntegration, the code doesn't produce a toolip bubble like it should, it just fills my grid with a Forms text box. – qJake Sep 29 '11 at 22:22
  • Add WindowsFormsIntegration reference from C:\Program Files\Reference Assemblies\Microsoft\Framework\v4.0\WindowsFormsIntegration.dll and size your textbox. – ARZ Sep 30 '11 at 07:15
  • I resized it and it still didn't show. I don't want an extra text box on my form (Hiding it doesn't work, either). Also, when I call the Tooltip on the window itself (using `WindowInteropHandler`) a `NullReferenceException` gets thrown, and the balloon doesn't show. I don't think this is compatible with WPF. – qJake Sep 30 '11 at 13:44
  • Why "extra text box"?! you can deal with it as your main textbox. I just sized the textBox container and everything is OK! The answer has been updated. – ARZ Oct 01 '11 at 09:15
  • I can't use a Winforms textbox because I use WPF-specific properties on my textbox (styling, animations, etc). – qJake Oct 03 '11 at 14:16
  • If you need to have a tooltip like WinForm tooltip you must use WinForm textbox and if you need a WPF textbox you must use a native wpf tooltip not WinForm one. – ARZ Oct 03 '11 at 14:45
  • 1
    Except there is no native WPF tooltip that Microsoft has available. - Why hasn't anyone suggested that I just call the Tooltip natively with some P/Invokes? – qJake Oct 03 '11 at 17:59
  • @SpikeX for the work that you'd put into writing a WinAPI wrapper, I think you could get a better solution writing a WPF solution to your problem. Also, that solution could be used for Silverlight, not just WPF. Just my opinion... – Bahri Gungor Oct 04 '11 at 14:29
3

This BalloonDecorator Project is one that I am using on a current project to show help hints and error notifications. I know you could modify your error template to show this decorator, just like you could show an icon instead of the red borders. The benefit of using a decorator is you can make it look however you'd like, and won't have to depend on WinForms.

BalloonDecorator.cs

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace MyNamespace
{
public class BalloonDecorator : Decorator
{
    private static double _thickness = 0;
    private static int OpeningGap = 10;

    public static readonly DependencyProperty BackgroundProperty =
        DependencyProperty.Register("Background", typeof (Brush), typeof (BalloonDecorator));

    public static readonly DependencyProperty BorderBrushProperty =
        DependencyProperty.Register("BorderBrush", typeof (Brush), typeof (BalloonDecorator));

    public static readonly DependencyProperty PointerLengthProperty = 
        DependencyProperty.Register("PointerLength", typeof (double), typeof (BalloonDecorator),
        new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender |
        FrameworkPropertyMetadataOptions.AffectsMeasure));

    public static readonly DependencyProperty CornerRadiusProperty = 
        DependencyProperty.Register("CornerRadius", typeof (double), typeof (BalloonDecorator),
        new FrameworkPropertyMetadata(10.0, FrameworkPropertyMetadataOptions.AffectsRender |
        FrameworkPropertyMetadataOptions.AffectsMeasure));

    public Brush Background
    {
        get { return (Brush) GetValue(BackgroundProperty); }
        set { SetValue(BackgroundProperty, value); }
    }

    public Brush BorderBrush
    {
        get { return (Brush) GetValue(BorderBrushProperty); }
        set { SetValue(BorderBrushProperty, value); }
    }

    public double PointerLength
    {
        get { return (double) GetValue(PointerLengthProperty); }
        set { SetValue(PointerLengthProperty, value); }
    }

    public double CornerRadius
    {
        get { return (double) GetValue(CornerRadiusProperty); }
        set { SetValue(CornerRadiusProperty, value); }
    }

    protected override Size ArrangeOverride(Size arrangeSize)
    {
        UIElement child = Child;
        if (child != null)
        {
            double pLength = PointerLength;
            Rect innerRect =
                Rect.Inflate(new Rect(pLength, 0, Math.Max(0, arrangeSize.Width - pLength), arrangeSize.Height),
                             -1 * _thickness, -1 * _thickness);
            child.Arrange(innerRect);
        }

        return arrangeSize;
    }

    protected override Size MeasureOverride(Size constraint)
    {
        UIElement child = Child;
        Size size = new Size();
        if (child != null)
        {
            Size innerSize = new Size(Math.Max(0, constraint.Width - PointerLength), constraint.Height);
            child.Measure(innerSize);
            size.Width += child.DesiredSize.Width;
            size.Height += child.DesiredSize.Height;
        }

        Size borderSize = new Size(2 * _thickness, 2 * _thickness);
        size.Width += borderSize.Width + PointerLength;
        size.Height += borderSize.Height;

        return size;
    }

    protected override void OnRender(DrawingContext dc)
    {
        Rect rect = new Rect(0, 0, RenderSize.Width, RenderSize.Height);

        dc.PushClip(new RectangleGeometry(rect));
        dc.DrawGeometry(Background, new Pen(BorderBrush, _thickness), CreateBalloonGeometry(rect));
        dc.Pop();
    }

    private StreamGeometry CreateBalloonGeometry(Rect rect)
    {
        double radius = Math.Min(CornerRadius, rect.Height / 2);
        double pointerLength = PointerLength;

        // All the points on the path
        Point[] points =
            {
                new Point(pointerLength + radius, 0), new Point(rect.Width - radius, 0), // Top
                new Point(rect.Width, radius), new Point(rect.Width, rect.Height - radius), // Right
                new Point(rect.Width - radius, rect.Height), // Bottom
                new Point(pointerLength + radius, rect.Height), // Bottom
                new Point(pointerLength, rect.Height - radius), // Left
                new Point(pointerLength, radius) // Left
            };

        StreamGeometry geometry = new StreamGeometry();
        geometry.FillRule = FillRule.Nonzero;
        using (StreamGeometryContext ctx = geometry.Open())
        {
            ctx.BeginFigure(points[0], true, true);
            ctx.LineTo(points[1], true, false);
            ctx.ArcTo(points[2], new Size(radius, radius), 0, false, SweepDirection.Clockwise, true, false);
            ctx.LineTo(points[3], true, false);
            ctx.ArcTo(points[4], new Size(radius, radius), 0, false, SweepDirection.Clockwise, true, false);
            ctx.LineTo(points[5], true, false);

            ctx.ArcTo(points[6], new Size(radius, radius), 0, false, SweepDirection.Clockwise, true, false);

            // Pointer
            if (pointerLength > 0)
            {
                ctx.LineTo(rect.BottomLeft, true, false);
                ctx.LineTo(new Point(pointerLength, rect.Height - radius - OpeningGap), true, false);
            }
            ctx.LineTo(points[7], true, false);

            ctx.ArcTo(points[0], new Size(radius, radius), 0, false, SweepDirection.Clockwise, true, false);
        }
        return geometry;
    }
}
}

Just make sure that this class's namespace is loaded into the XAML imports (I use a namespace called "Framework"), and it is simple to use:

    <Framework:BalloonDecorator  Background="#FFFF6600" PointerLength="50"
     CornerRadius="5" Opacity=".9" Margin="200,120,0,0"
     HorizontalAlignment="Left" VerticalAlignment="Top" Visibility="{Binding UnitPriceChangedBalloonVisibility}">
        <Border CornerRadius="2">
            <Border CornerRadius="2">
                <Button Height="Auto" Command="{Binding CloseUnitPriceChangedBalloonCommand}" Background="Transparent" BorderBrush="{x:Null}">
                <TextBlock Text="Please review the price. The Units have changed."
                     HorizontalAlignment="Left"
                     VerticalAlignment="Top"
                     FontStyle="Italic"
                     TextWrapping="Wrap"
                     Margin="10"
                     />
                     </Button>
            </Border>
        </Border>
    </Framework:BalloonDecorator>

Obviously, I tie the visibility to a binding, but you could just set it to true and put this inside your Validation.ErrorTemplate.

Hope this helps!

Bahri Gungor
  • 2,289
  • 15
  • 16
  • Yeah, this is a balloon, but I'd have to override the mouse hover style for the button that's being used in there because it looks horrible on Windows 7, and the positioning is all wrong (ideally it would not be part of the form layout, but rather, an independent, floating entity). It's just not what I'm looking for, sorry. – qJake Sep 29 '11 at 22:39
  • Sure, no problem. If you don't mind the hybrid WinForms/WPF solution, that's probably your best bet other than writing a WPF encapsulation of the WinAPI for the tooltip. – Bahri Gungor Sep 30 '11 at 14:00
  • Sure, I prefer it, but only when it doesn't throw NullReferenceExceptions when I try to invoke it. ;) – qJake Sep 30 '11 at 14:22
2

I've been searching for a better solution than the BalloonDecorator, and ran across the http://www.hardcodet.net/projects/wpf-notifyicon project. It is using the WinAPI at the lowest level, which might give you a head start on building your own solution. It appears that at first glance it could potentially solve it, but I haven't had enough time to verify that the BalloonTip can be made to behave as you've described.

Good luck on your project!

Bahri Gungor
  • 2,289
  • 15
  • 16
  • It's a possibility... I'd prefer a native Windows call, but I might have to settle for something like this. Thanks for the link. – qJake Oct 04 '11 at 14:45
  • Not a problem. If you find a suitable solution, please post it so the rest of us can see what you've accomplished. – Bahri Gungor Oct 04 '11 at 14:52
  • 1
    So, I'd rather have a custom WPF window as a tooltip rather than combining old Winforms stuff into my application, and since this project is the nicest example of that, it gets the bounty. Adding Winforms items into your WPF window is a viable option if you can handle the older text box (or whatever other control), but it wasn't what I was looking for. Another solution is to P/Invoke the tooltip calls in Win32, but doing a custom window in WPF is probably easier/quicker. – qJake Oct 04 '11 at 18:05
1

Maybe you can host a Windows Forms control in WPF using the WindowsFormsHost type.

There is a walkthrough available on MSDN on how to do this:

Hosting a Windows Forms Composite Control in WPF

Using this technique you could perhaps use the System.Windows.Forms.ToolTip control. If you set this control's IsBalloon property to true it will display as a balloon window.

Christophe Geers
  • 8,564
  • 3
  • 37
  • 53