Search code examples
c#wpfwpf-controlsbeziergeometry-surface

Creating custom shape using BezierSegment


I want to modify an existing ControlTemplate drawing a recantgle in a custom calendar to draw the lower border as shown in the below picture:

final result

The current ControlTemplate looks like this:

<ControlTemplate x:Key="FesterBlockTemplate" TargetType="ContentControl">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <ContentControl Grid.Row="0" Style="{StaticResource ContinueFromPreviousSignStyle}" />
        <ContentControl Grid.Row="2" Style="{StaticResource ToBeContinuedSignStyle}" />

        <!--Display of the activity text-->
        <Border Opacity="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="1" BorderThickness="0">
            <Border.Background>
                <Binding RelativeSource="{RelativeSource AncestorType={x:Type controls:AktivitaetViewBase}}" Path="UpperBackground" />
            </Border.Background>
            <TextBlock Margin="3" Grid.Row="1" Text="{Binding RelativeSource={RelativeSource AncestorType={x:Type controls:AktivitaetViewBase}}, Path=UpperText}"
                          HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center" TextWrapping="WrapWithOverflow">
                <TextBlock.Foreground>
                    <Binding RelativeSource="{RelativeSource AncestorType={x:Type controls:AktivitaetViewBase}}" Path="UpperTextForeground" />
                </TextBlock.Foreground>
                <TextBlock.Background>
                    <Binding RelativeSource="{RelativeSource AncestorType={x:Type controls:AktivitaetViewBase}}" Path="UpperTextBackground" />
                </TextBlock.Background>
                <TextBlock.LayoutTransform>
                    <RotateTransform Angle="{Binding RelativeSource={RelativeSource AncestorType={x:Type controls:AktivitaetViewBase}}, Path=TextRotationAngle}" />
                </TextBlock.LayoutTransform>
                <TextBlock.Style>
                    <Style TargetType="TextBlock">
                        <Style.Triggers>
                            <DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type controls:AktivitaetViewBase}}, Path=HasCustomFontSize}" Value="True">
                                <Setter Property="FontSize" Value="{Binding RelativeSource={RelativeSource AncestorType={x:Type controls:AktivitaetViewBase}}, Path=TextFontSize}" />
                            </DataTrigger>
                        </Style.Triggers>
                    </Style>
                </TextBlock.Style>
            </TextBlock>
        </Border>

        <Path Grid.Row="1" Stretch="Fill" Stroke="Black" StrokeThickness="1" Data="M0,0 L0,1" HorizontalAlignment="Left" VerticalAlignment="Stretch"/>
    <Path Grid.Row="1" Stretch="Fill" Stroke="Black" StrokeThickness="1" Data="M0,0 L0,1" HorizontalAlignment="Right" VerticalAlignment="Stretch"/>
    </Grid>
</ControlTemplate>

I've found a way to draw the wanted shape by specifying static sizes:

<UserControl x:Class="WpfComplexShapeTest.ComplexShapeControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" 
             d:DesignHeight="300" 
             d:DesignWidth="300"
             Background="Transparent">
    <Path Stroke="Black" StrokeThickness="1" Fill="Orange">
        <Path.Data>
            <PathGeometry>
                <PathGeometry.Figures>
                    <PathFigureCollection>
                        <PathFigure StartPoint="10,10">
                            <PathFigure.Segments>
                                <LineSegment Point="10, 210"/>
                                <BezierSegment Point1="50,0" 
                                               Point2="70,350" 
                                               Point3="110,150"/>
                                <LineSegment Point="110, 10"/>
                                <LineSegment Point="10, 10"/>
                            </PathFigure.Segments>
                        </PathFigure>
                    </PathFigureCollection>
                </PathGeometry.Figures>
            </PathGeometry>
        </Path.Data>
    </Path>
</UserControl>

Which results in this shape (taken from VS designer):

wanted shape

Next step is to make it resize correctly. It has to take the available horizontal and vertical space and the amplitude of the wave form of the lower border is being specified by an int value (HourSegmentHeight) and has to remain constant. That's why I've created dependency properties and properties, which are being recalculated when the control size changes, as shown in the code below:

XAML:

<UserControl x:Class="WpfComplexShapeTest.OpenEndControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" Background="Transparent" 
             SizeChanged="OnUserControlSizeChanged"
             DataContext="{Binding RelativeSource={RelativeSource Self}}" 
             d:DesignHeight="300" 
             d:DesignWidth="500">
    <Path Stroke="Black" StrokeThickness="1" Fill="Orange" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
        <Path.Data>
            <PathGeometry>
                <PathGeometry.Figures>
                    <PathFigureCollection>
                        <PathFigure StartPoint="0,0">
                            <PathFigure.Segments>
                                <LineSegment Point="{Binding LowerLeftPoint}"/>
                                <BezierSegment Point1="{Binding BezierPoint1}" 
                                               Point2="{Binding BezierPoint2}" 
                                               Point3="{Binding BezierPoint3}"/>
                                <LineSegment Point="{Binding UpperRightPoint}"/>
                                <LineSegment Point="0, 0"/>
                            </PathFigure.Segments>
                        </PathFigure>
                    </PathFigureCollection>
                </PathGeometry.Figures>
            </PathGeometry>
        </Path.Data>
    </Path>
</UserControl>

Code behind:

using System.Diagnostics;
using System.Windows;

namespace WpfComplexShapeTest {
    /// <summary>
    /// Interaction logic for OpenEndControl.xaml
    /// </summary>
    public partial class OpenEndControl {

        #region Private Fields

        private int m_hourSegmentHeight;

        #endregion

        #region Public Properties

        public static readonly DependencyProperty UpperRightPointProperty = DependencyProperty.Register("UpperRightPoint", typeof (Point), typeof (OpenEndControl));
        public static readonly DependencyProperty LowerLeftPointProperty = DependencyProperty.Register("LowerLeftPoint", typeof (Point), typeof (OpenEndControl));
        public static readonly DependencyProperty BezierPoint1Property = DependencyProperty.Register("BezierPoint1", typeof (Point), typeof (OpenEndControl));
        public static readonly DependencyProperty BezierPoint2Property = DependencyProperty.Register("BezierPoint2", typeof (Point), typeof (OpenEndControl));
        public static readonly DependencyProperty BezierPoint3Property = DependencyProperty.Register("BezierPoint3", typeof (Point), typeof (OpenEndControl));

        /// <summary>
        /// Gets or sets the upper right point.
        /// </summary>
        /// <value>
        /// The upper right point.
        /// </value>
        public Point UpperRightPoint {
            get { return (Point) GetValue(UpperRightPointProperty); }
            set { SetValue(UpperRightPointProperty, value); }
        }

        /// <summary>
        /// Gets or sets the lower left point.
        /// </summary>
        /// <value>
        /// The lower left point.
        /// </value>
        public Point LowerLeftPoint {
            get { return (Point) GetValue(LowerLeftPointProperty); }
            set { SetValue(LowerLeftPointProperty, value); }
        }

        /// <summary>
        /// Gets or sets the bezier point 1.
        /// </summary>
        /// <value>
        /// The bezier point 1.
        /// </value>
        public Point BezierPoint1 {
            get { return (Point) GetValue(BezierPoint1Property); }
            set { SetValue(BezierPoint1Property, value); }
        }

        /// <summary>
        /// Gets or sets the bezier point 2.
        /// </summary>
        /// <value>
        /// The bezier point 2.
        /// </value>
        public Point BezierPoint2 {
            get { return (Point) GetValue(BezierPoint2Property); }
            set { SetValue(BezierPoint2Property, value); }
        }

        /// <summary>
        /// Gets or sets the bezier point 3.
        /// </summary>
        /// <value>
        /// The bezier point 3.
        /// </value>
        public Point BezierPoint3 {
            get { return (Point) GetValue(BezierPoint3Property); }
            set { SetValue(BezierPoint3Property, value); }
        }

        /// <summary>
        /// Gets or sets the height of the hour segment.
        /// </summary>
        /// <value>
        /// The height of the hour segment.
        /// </value>
        public int HourSegmentHeight {
            get { return m_hourSegmentHeight; }
            set {
                if (m_hourSegmentHeight != value) {
                    m_hourSegmentHeight = value;
                    RefreshPoints();
                }
            }
        }

        #endregion

        #region Constructor

        /// <summary>
        /// Initializes a new instance of the <see cref="OpenEndControl"/> class.
        /// </summary>
        public OpenEndControl() {
            InitializeComponent();
            RefreshPoints();
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Refreshes the points.
        /// </summary>
        private void RefreshPoints() {
            UpperRightPoint = new Point(ActualWidth, 0);
            LowerLeftPoint = new Point(0, ActualHeight);
            BezierPoint1 = new Point(ActualWidth/2, HourSegmentHeight);
            BezierPoint2 = new Point(ActualWidth/2 + 10, 2*HourSegmentHeight);
            BezierPoint3 = new Point(ActualWidth, ActualHeight/2 - HourSegmentHeight);

            Debug.WriteLine("Width={0}, Height={1}", ActualWidth, ActualHeight);
        }

        /// <summary>
        /// Called when the size of the user control has changed.
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="SizeChangedEventArgs"/> instance containing the event data.</param>
        private void OnUserControlSizeChanged(object sender, SizeChangedEventArgs e) {
            RefreshPoints();
        }

        #endregion
    }
}

The RereshPoints() method doesn't compute the correct values for the bezier points 1, 2 and 3 and I can't figure out the formula to use after having read the Bézier curve article.

The result for a certain control size looks like this:

current shape

Question:
- Is this a good approach to draw the shape I want?
- If yes, can you help me out find the right formula to calculate the bezier points?


Solution

  • a) only you can decide on that. If it looks the way you want it to look, and it doesn't take forever to compute, then it's certainly good enough of one. b) cubic Bezier curves are defined by two on-curve, and two off-curve points. The start and end on-curve points are simply where the shape needs to link up with your rectangle, so you have that part down. The shape will "depart" from the starting point in the direction of the first control point, and "arrive" at the endpoint from the direction of the second control point, so you want control points C1 and C2 that are respectively (startpoint_x, ...) and (endpoint_x, ...) with the y coordinates reasonably free to choose. As long as C1 and C2 are equal highs above and below the midpoint, the curve will look decent:

    with
      start = { 0, ... }
      end = { width, ... }
      d = ...
      c1 = { 0, (start_y + end_y)/2 - d }
      c2 = { width, (start_y + end_y)/2 + d }
    form
      curve(start, c1, c2, end)
    

    Simply pick a value for d, and see what you like best - it's your visualisation, we can't tell you want you think looks best =)

    This jsfiddle is a simple demonstrator of the concept (move the mouse over graphic to vary the strength of d).