Tag Archives: PathGeometry

Creating a speech bubble with rounded corners

Following on from an earlier post I decided that the speech bubble in that post would look more like a … bubble if the corners were rounded. The relevant geometry to achieve this is a Cubic Bezier Curve. The MSDN page is here.

The abbreviated form is C. It works with 3 parameters:

C[start co-ordinates] [middle co-ordinate] [final co-ordinate]

bezier

The following is the geometry for the speech bubble:

<PathGeometry x:Key="SpeechBubble">M 10,10 L20,0 L30,10 L190,10 C190,10 200,10 200,20 L200,90 C200,90 200,100 190,100 L10,100 C10,100 0,100 0,90 L0,20 C0,20 0,10 10,10 Z</PathGeometry>

The idea is that instead of drawing the line right to the corner, I go to within 10 pixels and curve round the corner.

roundedtooltip

Tooltip Speech Bubbles

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.

WPF Drawing Application

The following is a XAML page that allows the user to draw on it. This was written and tested under a Windows 10 desktop application, but should work for WPF. Here’s the XAML:

        <Grid>
            <Border>
                <Canvas PointerPressed="Canvas_PointerPressed" PointerMoved="Canvas_PointerMoved"
                        PointerReleased="Canvas_PointerReleased"
                        Background="Orange" Name="Canvas" />
            </Border>
        </Grid>

The background is a different colour to identify the canvas.

There’s the code to allow drawing:

        
        Path _currentPath;

        private void Canvas_PointerPressed(object sender, Windows.UI.Xaml.Input.PointerRoutedEventArgs e)
        {            
            _currentPath = new Path
            {
                Data = new PathGeometry
                {
                    Figures = { new PathFigure
                    {
                        StartPoint = e.GetCurrentPoint((UIElement)sender).Position,
                        Segments = { new PolyLineSegment() }
                    }}
                },
                Stroke = new SolidColorBrush(Colors.Black),
                StrokeThickness = 5
            };

            Canvas.Children.Add(_currentPath);
        }

        private void Canvas_PointerMoved(object sender, PointerRoutedEventArgs e)
        {
            if (_currentPath == null) return;

            var pls = (PolyLineSegment)((PathGeometry)_currentPath.Data).Figures.Last().Segments.Last();
            pls.Points.Add(e.GetCurrentPoint((UIElement)sender).Position);
            
        }

        private void Canvas_PointerReleased(object sender, PointerRoutedEventArgs e)
        {
            _currentPath = null;
        }

As you can see, it doesn’t do anything for my drawing ability:

drawing

It’s all the code behind and, while I typically shy away from this, it seems to fit well for an application such as this (as the drawing relates more to the view than to anything else).