Tooltip Speech Bubbles

April 01, 2016

I recently tried to introduce a concept of a tooltip that appeared as a speech bubble; that is, a box that has a pointer to the originating control.

Standing on the Shoulders of Giants

The following is a rundown of links that I used extensively during this investigation:

http://stackoverflow.com/questions/337181/how-do-i-create-a-custom-wpf-control-like-a-cartoon-bubble-with-constant-corners

http://stackoverflow.com/questions/337181/how-do-i-create-a-custom-wpf-control-like-a-cartoon-bubble-with-constant-corners

http://jobijoy.blogspot.co.uk/2008/12/xaml-balloon-comments-expression-blend.html

http://stackoverflow.com/questions/11446250/how-to-style-wpf-tooltip-like-a-speech-bubble

http://stevenhollidge.blogspot.co.uk/2012/04/custom-tooltip-and-popup.html

Possible Approaches

In investigating this, I created four separate projects; these essentially boiled down to three different approaches: 1. A styled tooltip with no arrow

tt1

2. A tooltip using the Expression Blend "Callout" method

tt2

3. A styled tooltip using the "PathGeometry" to define a pointer

tt3

(1) had the advantage that it looked much better, but doesn’t have a concept of an arrow to the source control. (2) was by far the easiest, but the arrow style makes it look a bit like a cartoon.

In the end I opted for (3), the solution looks a little like this:

    <Window.Resources>
        <LinearGradientBrush x:Key="LightBrush" StartPoint="0,0" EndPoint="0,1">
            <GradientBrush.GradientStops>
                <GradientStopCollection>
                    <GradientStop Color="#FFF" Offset="0.0"/>
                    <GradientStop Color="#EEE" Offset="1.0"/>
                </GradientStopCollection>
            </GradientBrush.GradientStops>
        </LinearGradientBrush>

        <SolidColorBrush x:Key="SolidBorderBrush" Color="#888" />
    
        <Style x:Key="{x:Type ToolTip}" TargetType="ToolTip">
            <Setter Property="OverridesDefaultStyle" Value="true" />
            <Setter Property="HorizontalOffset" Value="0" />
            <Setter Property="VerticalOffset" Value="0" />
            <Setter Property="Background" Value="GhostWhite" />
            <Setter Property="Foreground" Value="Gray" />
            <Setter Property="FontSize" Value="12" />
            <Setter Property="FontFamily" Value="Segoe UI" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ToolTip">
                        <Canvas Width="200" Height="100">
                            <Path x:Name="Container"
                          Canvas.Left="0"
                          Canvas.Top="0"
                          Margin="0"
                          Data="M 50,10 L60,0 L70,10 L100,10 L100,100 L0,100 L0,10 L50,10"
                          Fill="{TemplateBinding Background}"
                          Stroke="Black">
                                <Path.Effect>
                                    <DropShadowEffect BlurRadius="10"
                                              Opacity="0.5"
                                              ShadowDepth="4" />
                                </Path.Effect>
                            </Path>
                            <TextBlock Canvas.Left="50"
                               Canvas.Top="28"
                               Width="100"
                               Height="65"
                               Text="{TemplateBinding Content}"
                               TextWrapping="Wrapwithoverflow" />
                        </Canvas>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

You can style the bubble a bit:

        <LinearGradientBrush x:Key="LightBrush" StartPoint="0,0" EndPoint="0,1">
            <GradientBrush.GradientStops>
                <GradientStopCollection>
                    <GradientStop Color="#FFF" Offset="0.0"/>
                    <GradientStop Color="#EEE" Offset="1.0"/>
                </GradientStopCollection>
            </GradientBrush.GradientStops>
        </LinearGradientBrush>

        <SolidColorBrush x:Key="SolidBorderBrush" Color="#888" />
    
        <Style x:Key="{x:Type ToolTip}" TargetType="ToolTip">
            <Setter Property="OverridesDefaultStyle" Value="true" />
            <Setter Property="HorizontalOffset" Value="-50" />
            <Setter Property="VerticalOffset" Value="0" />
            <Setter Property="Background" Value="#BE1C1C1C" />
            <Setter Property="Foreground" Value="Gray" />
            <Setter Property="FontSize" Value="12" />
            <Setter Property="FontFamily" Value="Segoe UI" />
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="ToolTip">
                        <Canvas Width="200" Height="100">

                            <Path x:Name="Container"
                          Canvas.Left="0"
                          Canvas.Top="0"                                  
                          Margin="0"
                          Data="M 50,10 L60,0 L70,10 L200,10 L200,100 L0,100 L0,10 L50,10"                                  
                          Stroke="Black">
                                <Path.Effect>
                                    <DropShadowEffect BlurRadius="10"
                                              Opacity="0.5"
                                              ShadowDepth="4" />
                                </Path.Effect>
                                <Path.Fill>
                                    <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                                        <GradientStop Color="#CF181818" Offset="0"/>
                                        <GradientStop Color="#BE1C1C1C" Offset="1"/>
                                    </LinearGradientBrush>

                                </Path.Fill>
                            </Path>
                            <TextBlock Canvas.Left="50"
                               Canvas.Top="28"
                               Width="100"
                               Height="65"
                               Text="{TemplateBinding Content}"
                               TextWrapping="Wrapwithoverflow" />
                        </Canvas>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>

I then looked into the concept of centring the arrow, the following links looked like they might help:

http://stackoverflow.com/questions/14167763/wpf-path-geometry-is-there-a-way-to-bind-the-data-property

http://www.blackwasp.co.uk/WPFPathMarkupsyntax.aspx

And I did try expanding the syntax:


                                <Path.Data>
                                    <PathGeometry>
                                        <PathGeometry.Figures>
                                            <PathFigureCollection>
                                                <PathFigure IsClosed="True" StartPoint="{Binding RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource ControlToCentreConverter}}">
                                                    <PathFigure.Segments>
                                                        <PathSegmentCollection>
                                                            <LineSegment Point="60,0"/>
                                                            <LineSegment Point="70,10"/>
                                                            <LineSegment Point="200,10"/>
                                                            <LineSegment Point="200,100"/>
                                                            <LineSegment Point="0,100"/>
                                                            <LineSegment Point="0,10"/>
                                                        </PathSegmentCollection>
                                                    </PathFigure.Segments>
                                                </PathFigure>
                                            </PathFigureCollection>
                                        </PathGeometry.Figures>
                                    </PathGeometry>

The idea was to bind the line segments. I’m not saying it’s not possible; it clearly is, but it started to get prohibitively complex. If anyone comes up with a simple way of doing this (or even a complex one) then please add a link in the comments.



Profile picture

A blog about one man's journey through code… and some pictures of the Peak District
Twitter

© Paul Michaels 2024