0

I have an WPF speech bubble tooltip which is working fine.

<Style x:Key="{x:Type ToolTip}" TargetType="ToolTip">
    <Setter Property="OverridesDefaultStyle" Value="true" />
    <Setter Property="HorizontalOffset" Value="1" />
    <Setter Property="VerticalOffset" Value="1" />
    <Setter Property="Background" Value="White" />
    <Setter Property="Foreground" Value="Black" />
    <Setter Property="FontSize" Value="12" />
    <Setter Property="FontFamily" Value="Segoe UI" />
    <Setter Property="DataContext" Value="{Binding Path=PlacementTarget.DataContext, RelativeSource={x:Static RelativeSource.Self}}"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ToolTip">
                <Canvas Width="225" Height="131">
                    <Path x:Name="Container"
                          Canvas.Left="0"
                          Canvas.Top="0"
                          Margin="0"
                          Data="M8,7.41 L15.415,0 L22.83,7.41 L224,7.41 L224,130 L0,130 L0,7.41 L8,7.41"
                          Fill="{TemplateBinding Background}"
                          Stroke="Gray">
                        <Path.Effect>
                            <DropShadowEffect BlurRadius="10"
                                              Opacity="0.5"
                                              ShadowDepth="4" />
                        </Path.Effect>
                    </Path>
                    <TextBlock Canvas.Left="10"
                               Canvas.Top="10"
                               Width="100"
                               Height="65"
                               Text="{TemplateBinding Content}"
                               TextWrapping="WrapWithOverflow" />
                </Canvas>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

The problem with above approach is that the arrow/pointer of the speech bubble tooltip (path) is always placed in the same position regardless the situation and I would like it to adapt to the situation and use one of the following (above style implements the arrow placed at the top left, first tooltip in the screenshot below):

enter image description here

How can I do this? Is it possible?

Willy
  • 9,848
  • 22
  • 141
  • 284
  • 1
    Hi,@Rodri . Does the custom control in bazsisz's answer solve your problem? If not, could you describe the problem and provide more details? – Hui Liu-MSFT Dec 06 '22 at 06:24

2 Answers2

1

Here it is, the full code for this task:

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

namespace Decorators
{
public enum Position 
{
    None,
    Top,
    Bottom,
    RightSide,
    LeftSide,
}

public enum SpecificPosition
{
    None,
    LeftOrTop = 25,
    Center = 50,
    RightOrBottom = 75,
}

internal class BubbleTextDecorator : Decorator
{


    #region DependencyProperties
    public static readonly DependencyProperty VerticalMarginProperty = DependencyProperty.Register("VerticalMargin", 
                                                                                                   typeof(double), 
                                                                                                   typeof(BubbleTextDecorator), 
                                                                                                   new FrameworkPropertyMetadata(0.0, 
                                                                                                                                 FrameworkPropertyMetadataOptions.AffectsMeasure | 
                                                                                                                                 FrameworkPropertyMetadataOptions.AffectsRender));

    public double VerticalMargin
    {
        get { return (double)GetValue(VerticalMarginProperty); }
        set { SetValue(VerticalMarginProperty, value); }
    }

    public static readonly DependencyProperty HorizontalMarginProperty = DependencyProperty.Register("HorizontalMargin", 
                                                                                                     typeof(double),
                                                                                                     typeof(BubbleTextDecorator),
                                                                                                     new FrameworkPropertyMetadata(0.0,
                                                                                                                                   FrameworkPropertyMetadataOptions.AffectsMeasure |
                                                                                                                                   FrameworkPropertyMetadataOptions.AffectsRender));

    public double HorizontalMargin
    {
        get { return (double)GetValue(HorizontalMarginProperty); }
        set { SetValue(HorizontalMarginProperty, value); }
    }



    public static readonly DependencyProperty PointerPositionProperty = DependencyProperty.Register("PointerPosition", 
                                                                                                    typeof(Position), 
                                                                                                    typeof(BubbleTextDecorator), 
                                                                                                    new FrameworkPropertyMetadata(Position.None, 
                                                                                                                                  FrameworkPropertyMetadataOptions.AffectsRender |
                                                                                                                                  FrameworkPropertyMetadataOptions.AffectsMeasure));

    public Position PointerPosition
    {
        get { return (Position)GetValue(PointerPositionProperty); }
        set { SetValue(PointerPositionProperty, value); }
    }

    public static readonly DependencyProperty AlignmentPositionProperty = DependencyProperty.Register("AlignmentPosition",
                                                                                            typeof(SpecificPosition),
                                                                                            typeof(BubbleTextDecorator),
                                                                                            new FrameworkPropertyMetadata(SpecificPosition.None,
                                                                                                                          FrameworkPropertyMetadataOptions.AffectsRender |
                                                                                                                          FrameworkPropertyMetadataOptions.AffectsMeasure));

    public SpecificPosition AlignmentPosition
    {
        get { return (SpecificPosition)GetValue(AlignmentPositionProperty); }
        set { SetValue(AlignmentPositionProperty, value); }
    }


    public static readonly DependencyProperty PointerHeightProperty = DependencyProperty.Register("PointerHeight", 
                                                                                                  typeof(double), 
                                                                                                  typeof(BubbleTextDecorator), 
                                                                                                  new FrameworkPropertyMetadata(0.0, 
                                                                                                      FrameworkPropertyMetadataOptions.AffectsMeasure |
                                                                                                      FrameworkPropertyMetadataOptions.AffectsRender));

    public double PointerHeight
    {
        get { return (double)GetValue(PointerHeightProperty); }
        set { SetValue(PointerHeightProperty, value); }
    }

    public static readonly DependencyProperty PointerWidthProperty = DependencyProperty.Register("PointerWidth", 
                                                                                                 typeof(double), 
                                                                                                 typeof(BubbleTextDecorator), 
                                                                                                 new FrameworkPropertyMetadata(0.0,
                                                                                                     FrameworkPropertyMetadataOptions.AffectsMeasure |
                                                                                                     FrameworkPropertyMetadataOptions.AffectsArrange |
                                                                                                     FrameworkPropertyMetadataOptions.AffectsRender));

    public double PointerWidth
    {
        get { return (double)GetValue(PointerWidthProperty); }
        set { SetValue(PointerWidthProperty, value); }
    }

    #endregion

    protected override Size ArrangeOverride(Size arrangeSize)
    {
        Size desiredSize = base.ArrangeOverride(arrangeSize);
        if (Child != null) 
        {

            switch (PointerPosition)
            {
                case Position.Top:
                    Child.Arrange(new Rect(new Point(0.0, PointerHeight), new Point(desiredSize.Width, desiredSize.Height)));
                    break;
                case Position.Bottom:
                    Child.Arrange(new Rect(new Point(0.0, 0.0), new Point(desiredSize.Width, desiredSize.Height - PointerHeight)));
                    break;
                case Position.LeftSide:
                    Child.Arrange(new Rect(new Point(PointerHeight, 0.0), new Point(desiredSize.Width, desiredSize.Height)));
                    break;
                case Position.RightSide:
                    Child.Arrange(new Rect(new Point(0.0, 0.0), new Point(desiredSize.Width - PointerHeight, desiredSize.Height)));
                    break;
            }
        }
        return arrangeSize;
    }

    protected override Size MeasureOverride(Size constraint)
    {
        Size desiredSize = base.MeasureOverride(constraint);
        Size size = (PointerPosition == Position.Top || PointerPosition == Position.Bottom)
            ? new Size(desiredSize.Width + (HorizontalMargin * 2), desiredSize.Height + (VerticalMargin * 2) + PointerHeight)
            : new Size(desiredSize.Width + (HorizontalMargin * 2) + PointerHeight, desiredSize.Height + (VerticalMargin * 2));

        return size;
    }

    protected override void OnRender(DrawingContext drawingContext)
    {
        Brush renderBrush = Brushes.Transparent;
        Pen renderPen = new Pen(Brushes.Black, 1);
        StreamGeometry geom = new StreamGeometry();

        switch (PointerPosition) 
        {
            case Position.Top:
                DrawTop(geom);
                break;
            case Position.Bottom:
                DrawBottom(geom);
                break;
            case Position.RightSide:
                DrawRight(geom);
                break;
            case Position.LeftSide:
                DrawLeft(geom);
                break;

        }
        // Some arbitrary drawing implements.
        drawingContext.DrawGeometry(renderBrush, renderPen, geom);
    }

    private void DrawLeft(StreamGeometry geom)
    {
        using (StreamGeometryContext ctx = geom.Open())
        {
            ctx.BeginFigure(
                new Point(PointerHeight, 0.0),
                true,
                true);
            ctx.LineTo(
                new Point(ActualWidth, 0.0),
                true,
                false);
            ctx.LineTo(
                new Point(ActualWidth, ActualHeight),
                true,
                false);
            ctx.LineTo(
                new Point(PointerHeight, ActualHeight),
                true,
                false);
            ctx.LineTo(
                new Point(PointerHeight, (ActualHeight * (double)AlignmentPosition / 100) + (PointerWidth / 2)),
                true,
                false);
            ctx.LineTo(
                new Point(0.0, ActualHeight * (double)AlignmentPosition / 100),
                true,
                false);
            ctx.LineTo(
                new Point(PointerHeight, (ActualHeight * (double)AlignmentPosition / 100) - (PointerWidth / 2)),
                true,
                false);
            ctx.LineTo(
                new Point(PointerHeight, 0.0),
                true,
                false);
        }
    }

    private void DrawRight(StreamGeometry geom)
    {
        using (StreamGeometryContext ctx = geom.Open())
        {
            ctx.BeginFigure(
                new Point(0.0, 0.0),
                true,
                true);
            ctx.LineTo(
                new Point(ActualWidth - PointerHeight, 0.0),
                true,
                false);
            ctx.LineTo(
                new Point(ActualWidth - PointerHeight, (ActualHeight * (double)AlignmentPosition / 100) - (PointerWidth / 2)),
                true,
                false);
            ctx.LineTo(
                new Point(ActualWidth, ActualHeight * (double)AlignmentPosition / 100),
                true,
                false);
            ctx.LineTo(
                new Point(ActualWidth - PointerHeight, (ActualHeight * (double)AlignmentPosition / 100) + (PointerWidth / 2)),
                true,
                false);
            ctx.LineTo(
                new Point(ActualWidth - PointerHeight, ActualHeight),
                true,
                false);
            ctx.LineTo(
                new Point(0.0, ActualHeight),
                true,
                false);
            ctx.LineTo(
                new Point(0.0, 0.0),
                true,
                false);
        }
    }

    private void DrawBottom(StreamGeometry geom)
    {
        using (StreamGeometryContext ctx = geom.Open())
        {
            ctx.BeginFigure(
                new Point(0.0, 0.0),
                true,
                true);
            ctx.LineTo(
                new Point(ActualWidth, 0.0),
                true,
                false);
            ctx.LineTo(
               new Point(ActualWidth, ActualHeight - PointerHeight),
               true,
               false);
            ctx.LineTo(
                new Point((ActualWidth * (double)AlignmentPosition / 100) + (PointerWidth / 2), ActualHeight - PointerHeight),
                true,
                false);
            ctx.LineTo(
                new Point(ActualWidth * (double)AlignmentPosition / 100, ActualHeight),
                true,
                false);
            ctx.LineTo(
                new Point((ActualWidth * (double)AlignmentPosition / 100) - (PointerWidth / 2), ActualHeight - PointerHeight),
                true,
                false);
            ctx.LineTo(
                new Point(0.0, ActualHeight - PointerHeight),
                true,
                false);
            ctx.LineTo(
                new Point(0.0, 0.0),
                true,
                false);
        }
    }

    private void DrawTop(StreamGeometry geom)
    {
        using (StreamGeometryContext ctx = geom.Open())
        {
            ctx.BeginFigure(
                new Point(0.0, PointerHeight),
                true,
                true);
            ctx.LineTo(
                new Point((ActualWidth * (double)AlignmentPosition / 100) - (PointerWidth / 2), PointerHeight),
                true,
                false);
            ctx.LineTo(
                new Point(ActualWidth * (double)AlignmentPosition / 100, 0.0),
                true,
                false);
            ctx.LineTo(
                new Point((ActualWidth * (double)AlignmentPosition / 100) + (PointerWidth / 2), PointerHeight),
                true,
                false);
            ctx.LineTo(
                new Point(ActualWidth, PointerHeight),
                true,
                false);
            ctx.LineTo(
               new Point(ActualWidth, ActualHeight),
               true,
               false);
            ctx.LineTo(
                new Point(0.0, ActualHeight),
                true,
                false);
            ctx.LineTo(
                new Point(0.0, PointerHeight),
                true,
                false);
        }
    }
}
}

And this is how you use it:

<localdecorators:BubbleTextDecorator PointerHeight="10"
                                    PointerWidth="20"
                                    PointerPosition="LeftSide"
                                    AlignmentPosition="Center"
                                    VerticalMargin="30"
                                    HorizontalMargin="30"
                                    HorizontalAlignment="Left">
<TextBlock Text="this"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"/>
</localdecorators:BubbleTextDecorator>

Result

E_net4
  • 27,810
  • 13
  • 101
  • 139
bazsisz
  • 394
  • 2
  • 11
  • I like the idea, the problem with this approach is that you need to tell it explicitly in what position the arrow/pointer should be drawn but this is a thing that it is decided and calculated automatically by OS i guess which it know if there is enough space in the screen and based on this it decides where to draw it. – Willy Mar 13 '23 at 17:27
  • @Willy I will try to find the solution for you, if i have spare time. I think with this approch is not a lot more work to achieve what you want. – bazsisz Mar 17 '23 at 07:58
-1

This is a typical case for creating a Decorator. I once made a customizable ArrowBorder around a Text. You need to inherit from the Decorator class.

internal class ArrowBorderDecorator : Decorator

Then you need some DependencyProperties so that it will be easy to customize. In my case that was the ArrowTipToArrowTriangleBaseDistance property which means how "pointy" the arrow should be.In your case where should the bubble text arrows should be.

    public static readonly DependencyProperty ArrowTipToArrowTriangleBaseDistanceProperty = DependencyProperty.Register("ArrowTipToArrowTriangleBaseDistance", typeof(double), typeof(ArrowBorderDecorator), new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender));

    public double ArrowTipToArrowTriangleBaseDistance
    {
        get { return (double)GetValue(ArrowTipToArrowTriangleBaseDistanceProperty); }
        set { SetValue(ArrowTipToArrowTriangleBaseDistanceProperty, value); }
    }

Then you need to override the ArrangeOverride, MeasureOverride and the OnRender methods. The first two comes from the Decorator class and the third is from the UIElement class.

Here is a nice link to understand the first two. In OnRender you have a DrawingContext to draw your desired shape using the DependenyProperties.

After these you can simply use your decorator in your xaml like this:

<localdecorators:ArrowBorderDecorator ArrowBaseHalfSegment="0"
             FillColor="{DynamicResource MahApps.Brushes.Accent3}"
             StrokeColor="{DynamicResource MahApps.Brushes.ThemeForeground}"
             ArrowBorderThickness="1"
             ArrowTipToArrowTriangleBaseDistance="10">
<TextBlock Text="{Binding Path=Title}"
           Foreground="{DynamicResource MahApps.Brushes.IdealForeground}"
           Padding="10 1 10 1"
           VerticalAlignment="Center"
           FontWeight="Bold">
</TextBlock></localdecorators:ArrowBorderDecorator>

Result Result2

bazsisz
  • 394
  • 2
  • 11