简体   繁体   中英

Create a custom speech bubble tooltip that adapts/places its arrow/pointer to each situation

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

在此处输入图像描述

How can I do this? Is it possible?

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>

结果

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>

结果 结果2

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM