using System; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using AntDesignWPF.Utils; namespace AntDesignWPF.Controls; /// /// Draws a border, background, or both around another element. /// public class AntBorder : Decorator { #region Fields private StreamGeometry backgroundGeometryCache; private StreamGeometry upperLeftCache; private StreamGeometry upperRightCache; private StreamGeometry lowerRightCache; private StreamGeometry lowerLeftCache; private bool useComplexRender; private Pen leftPenCache; private Pen topPenCache; private Pen rightPenCache; private Pen bottomPenCache; #endregion #region Properties public static readonly DependencyProperty BackgroundProperty = DependencyProperty.Register( "Background", typeof(Brush), typeof(AntBorder), new FrameworkPropertyMetadata( null, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.SubPropertiesDoNotAffectRender )); /// /// Gets or sets the brush that fills the area between the bounds of a Border. /// public Brush Background { get { return (Brush)GetValue(BackgroundProperty); } set { SetValue(BackgroundProperty, value); } } public static readonly DependencyProperty BorderBrushProperty = DependencyProperty.Register( "BorderBrush", typeof(Brush), typeof(AntBorder), new FrameworkPropertyMetadata( null, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.SubPropertiesDoNotAffectRender, OnClearPenCache )); /// /// Gets or sets the brush that draws the outer border color. /// public Brush BorderBrush { get { return (Brush)GetValue(BorderBrushProperty); } set { SetValue(BorderBrushProperty, value); } } public static readonly DependencyProperty BorderThicknessProperty = DependencyProperty.Register( "BorderThickness", typeof(Thickness), typeof(AntBorder), new FrameworkPropertyMetadata( new Thickness(), FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender, OnClearPenCache ), IsThicknessValid); /// /// Gets or sets the relative thickness of a border. /// public Thickness BorderThickness { get { return (Thickness)GetValue(BorderThicknessProperty); } set { SetValue(BorderThicknessProperty, value); } } private static void OnClearPenCache(DependencyObject d, DependencyPropertyChangedEventArgs e) { var border = (AntBorder)d; border.leftPenCache = border.topPenCache = border.rightPenCache = border.bottomPenCache = null; } private static bool IsThicknessValid(object value) { return ThicknessUtil.IsValid((Thickness)value, false, false, false, false); } public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register( "CornerRadius", typeof(CornerRadius), typeof(AntBorder), new FrameworkPropertyMetadata( new CornerRadius(), FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender ), IsCornerRadiusValid); /// /// Gets or sets a value that represents the degree to which the corners of a Border are rounded. /// public CornerRadius CornerRadius { get { return (CornerRadius)GetValue(CornerRadiusProperty); } set { SetValue(CornerRadiusProperty, value); } } private static bool IsCornerRadiusValid(object value) { return CornerRadiusUtil.IsValid((CornerRadius)value, false, false, false, false); } public static readonly DependencyProperty PaddingProperty = DependencyProperty.Register( "Padding", typeof(Thickness), typeof(AntBorder), new FrameworkPropertyMetadata( new Thickness(), FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender ), IsThicknessValid); /// /// Gets or sets a thickness value that describes the amount of space between a border and its child element. /// public Thickness Padding { get { return (Thickness)GetValue(PaddingProperty); } set { SetValue(PaddingProperty, value); } } public static readonly DependencyProperty BorderStyleProperty = DependencyProperty.Register( "BorderStyle", typeof(BorderStyle), typeof(AntBorder), new FrameworkPropertyMetadata( BorderStyle.Solid, FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.SubPropertiesDoNotAffectRender, OnClearPenCache )); /// /// Gets or sets the border style. /// public BorderStyle BorderStyle { get { return (BorderStyle)GetValue(BorderStyleProperty); } set { SetValue(BorderStyleProperty, value); } } #endregion #region Overrides /// /// Updates DesiredSize of the Border. Called by parent UIElement. This is the first pass of layout. /// /// /// Border determines its desired size it needs from the specified border the child: its sizing /// properties, margin, and requested size. /// /// Constraint size is an "upper limit" that the return value should not exceed. /// The Decorator's desired size. protected override Size MeasureOverride(Size constraint) { var child = Child; var desiredSize = new Size(); var borders = BorderThickness; if (UseLayoutRounding) { var dpi = DpiUtil.GetDpi(this); borders = new Thickness(UIElementUtil.RoundLayoutValue(borders.Left, dpi.DpiScaleX), UIElementUtil.RoundLayoutValue(borders.Top, dpi.DpiScaleY), UIElementUtil.RoundLayoutValue(borders.Right, dpi.DpiScaleX), UIElementUtil.RoundLayoutValue(borders.Bottom, dpi.DpiScaleY)); } // Compute the total size required var borderSize = ThicknessUtil.CollapseThickness(borders); var paddingSize = ThicknessUtil.CollapseThickness(Padding); // If we have a child if (child != null) { // Combine into total decorating size var combined = new Size(borderSize.Width + paddingSize.Width, borderSize.Height + paddingSize.Height); // Remove size of border only from child's reference size. var childConstraint = new Size(Math.Max(0.0, constraint.Width - combined.Width), Math.Max(0.0, constraint.Height - combined.Height)); child.Measure(childConstraint); var childSize = child.DesiredSize; // Now use the returned size to drive our size, by adding back the margins, etc. desiredSize.Width = childSize.Width + combined.Width; desiredSize.Height = childSize.Height + combined.Height; } else { // Combine into total decorating size desiredSize = new Size(borderSize.Width + paddingSize.Width, borderSize.Height + paddingSize.Height); } return desiredSize; } /// /// Border computes the position of its single child and applies its child's alignments to the child. /// /// The size reserved for this element by the parent /// The actual ink area of the element, typically the same as finalSize protected override Size ArrangeOverride(Size finalSize) { var borders = BorderThickness; if (UseLayoutRounding) { var dpi = DpiUtil.GetDpi(this); borders = new Thickness(UIElementUtil.RoundLayoutValue(borders.Left, dpi.DpiScaleX), UIElementUtil.RoundLayoutValue(borders.Top, dpi.DpiScaleY), UIElementUtil.RoundLayoutValue(borders.Right, dpi.DpiScaleX), UIElementUtil.RoundLayoutValue(borders.Bottom, dpi.DpiScaleY)); } var boundRect = new Rect(finalSize); var innerRect = RectUtil.Deflate(boundRect, borders); // arrange child var child = Child; if (child != null) { Rect childRect = RectUtil.Deflate(innerRect, Padding); child.Arrange(childRect); } var radius = CornerRadius; useComplexRender = !CornerRadiusUtil.IsUniform(radius) || !ThicknessUtil.IsUniform(borders); backgroundGeometryCache = upperLeftCache = upperRightCache = lowerRightCache = lowerLeftCache = null; if (useComplexRender) { // calculate border / background rendering geometry if (!DoubleUtil.IsZero(boundRect.Width) && !DoubleUtil.IsZero(boundRect.Height)) { var outerRadii = new Radii(boundRect, radius, borders, true); // Upper-right corner var radiusX = boundRect.TopRight.X - outerRadii.TopRight.X; var radiusY = outerRadii.RightTop.Y - boundRect.TopRight.Y; if (!DoubleUtil.IsZero(radiusX) || !DoubleUtil.IsZero(radiusY)) { upperRightCache = GenerateRoundedGeometry(outerRadii.TopRight, outerRadii.RightTop, new Size(radiusX, radiusY)); } // Lower-right corner radiusX = boundRect.BottomRight.X - outerRadii.BottomRight.X; radiusY = boundRect.BottomRight.Y - outerRadii.RightBottom.Y; if (!DoubleUtil.IsZero(radiusX) || !DoubleUtil.IsZero(radiusY)) { lowerRightCache = GenerateRoundedGeometry(outerRadii.RightBottom, outerRadii.BottomRight, new Size(radiusX, radiusY)); } // Lower-left corner radiusX = outerRadii.BottomLeft.X - boundRect.BottomLeft.X; radiusY = boundRect.BottomLeft.Y - outerRadii.LeftBottom.Y; if (!DoubleUtil.IsZero(radiusX) || !DoubleUtil.IsZero(radiusY)) { lowerLeftCache = GenerateRoundedGeometry(outerRadii.BottomLeft, outerRadii.LeftBottom, new Size(radiusX, radiusY)); } // Upper-left corner radiusX = outerRadii.TopLeft.X - boundRect.TopLeft.X; radiusY = outerRadii.LeftTop.Y - boundRect.TopLeft.Y; if (!DoubleUtil.IsZero(radiusX) || !DoubleUtil.IsZero(radiusY)) { upperLeftCache = GenerateRoundedGeometry(outerRadii.LeftTop, outerRadii.TopLeft, new Size(radiusX, radiusY)); } } if (!DoubleUtil.IsZero(innerRect.Width) && !DoubleUtil.IsZero(innerRect.Height)) { var innerRadii = new Radii(innerRect, radius, borders, false); var backgroundGeometry = new StreamGeometry(); using (StreamGeometryContext sc = backgroundGeometry.Open()) { // create the border geometry sc.BeginFigure(innerRadii.TopLeft, true /* is filled */, true /* is closed */); // Top line sc.LineTo(innerRadii.TopRight, true /* is stroked */, false /* is smooth join */); // Upper-right corners var radiusX = innerRect.TopRight.X - innerRadii.TopRight.X; var radiusY = innerRadii.RightTop.Y - innerRect.TopRight.Y; if (!DoubleUtil.IsZero(radiusX) || !DoubleUtil.IsZero(radiusY)) { sc.ArcTo(innerRadii.RightTop, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, false); } // Right line sc.LineTo(innerRadii.RightBottom, true /* is stroked */, false /* is smooth join */); // Lower-right corners radiusX = innerRect.BottomRight.X - innerRadii.BottomRight.X; radiusY = innerRect.BottomRight.Y - innerRadii.RightBottom.Y; if (!DoubleUtil.IsZero(radiusX) || !DoubleUtil.IsZero(radiusY)) { sc.ArcTo(innerRadii.BottomRight, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, false); } // Bottom line sc.LineTo(innerRadii.BottomLeft, true /* is stroked */, false /* is smooth join */); // Lower-left corners radiusX = innerRadii.BottomLeft.X - innerRect.BottomLeft.X; radiusY = innerRect.BottomLeft.Y - innerRadii.LeftBottom.Y; if (!DoubleUtil.IsZero(radiusX) || !DoubleUtil.IsZero(radiusY)) { sc.ArcTo(innerRadii.LeftBottom, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, false); } // Left line sc.LineTo(innerRadii.LeftTop, true /* is stroked */, false /* is smooth join */); // Upper-left corners radiusX = innerRadii.TopLeft.X - innerRect.TopLeft.X; radiusY = innerRadii.LeftTop.Y - innerRect.TopLeft.Y; if (!DoubleUtil.IsZero(radiusX) || !DoubleUtil.IsZero(radiusY)) { sc.ArcTo(innerRadii.TopLeft, new Size(radiusX, radiusY), 0, false, SweepDirection.Clockwise, true, false); } } backgroundGeometry.Freeze(); backgroundGeometryCache = backgroundGeometry; } } return finalSize; } protected override void OnRender(DrawingContext dc) { if (useComplexRender) { ComplexRender(dc); } else { SimpleRender(dc); } } #endregion #region Private Methods private void SimpleRender(DrawingContext dc) { var useLayoutRounding = UseLayoutRounding; var dpi = DpiUtil.GetDpi(this); Brush brush; var borderStyle = BorderStyle; var borders = BorderThickness; var cornerRadius = CornerRadius; var outerCornerRadius = cornerRadius.TopLeft; // Already validated that all corners have the same radius var roundedCorners = !DoubleUtil.IsZero(outerCornerRadius); var width = RenderSize.Width; var height = RenderSize.Height; // Draw border if (!ThicknessUtil.IsZero(borders) && (brush = BorderBrush) != null) { var pen = GetPen(brush, borderStyle, borders.Left, dpi.DpiScaleX, useLayoutRounding); var penThickness = pen.Thickness; double x = penThickness * 0.5; var rect = new Rect(x, x, width - penThickness, height - penThickness); if (roundedCorners) { dc.DrawRoundedRectangle(null, pen, rect, outerCornerRadius, outerCornerRadius); } else { dc.DrawRectangle(null, pen, rect); } } // Draw background in rectangle inside border. if ((brush = Background) != null) { // Intialize background Point ptTL, ptBR; if (useLayoutRounding) { ptTL = new Point(UIElementUtil.RoundLayoutValue(borders.Left, dpi.DpiScaleX), UIElementUtil.RoundLayoutValue(borders.Top, dpi.DpiScaleY)); ptBR = new Point(width - UIElementUtil.RoundLayoutValue(borders.Right, dpi.DpiScaleX), height - UIElementUtil.RoundLayoutValue(borders.Bottom, dpi.DpiScaleY)); } else { ptTL = new Point(borders.Left, borders.Top); ptBR = new Point(width - borders.Right, height - borders.Bottom); } // Do not draw background if the borders are so large that they overlap. if (ptBR.X > ptTL.X && ptBR.Y > ptTL.Y) { if (roundedCorners) { // Determine the inner edge radius var innerCornerRadius = Math.Max(0.0, outerCornerRadius - borders.Top * 0.5); dc.DrawRoundedRectangle(brush, null, new Rect(ptTL, ptBR), innerCornerRadius, innerCornerRadius); } else { dc.DrawRectangle(brush, null, new Rect(ptTL, ptBR)); } } } } private void ComplexRender(DrawingContext dc) { Brush brush; var width = RenderSize.Width; var height = RenderSize.Height; //Draw border if (!DoubleUtil.IsZero(width) && !DoubleUtil.IsZero(height) && (brush = BorderBrush) != null) { var useLayoutRounding = UseLayoutRounding; var dpi = DpiUtil.GetDpi(this); var borders = BorderThickness; var borderStyle = BorderStyle; var radius = CornerRadius; double x, y; // Left Line if (!DoubleUtil.IsZero(borders.Left)) { if (leftPenCache == null) { leftPenCache = GetPen(brush, borderStyle, borders.Left, dpi.DpiScaleX, useLayoutRounding); } x = leftPenCache.Thickness * 0.5; dc.DrawLine(leftPenCache, new Point(x, radius.TopLeft), new Point(x, height - radius.BottomLeft)); } // Top Line if (!DoubleUtil.IsZero(borders.Top)) { if (topPenCache == null) { topPenCache = GetPen(brush, borderStyle, borders.Top, dpi.DpiScaleY, useLayoutRounding); } y = topPenCache.Thickness * 0.5; dc.DrawLine(topPenCache, new Point(radius.TopLeft, y), new Point(width - radius.TopRight, y)); } // Right Line if (!DoubleUtil.IsZero(borders.Right)) { if (rightPenCache == null) { rightPenCache = GetPen(brush, borderStyle, borders.Right, dpi.DpiScaleX, useLayoutRounding); } x = width - rightPenCache.Thickness * 0.5; dc.DrawLine(rightPenCache, new Point(x, radius.TopRight), new Point(x, height - radius.BottomRight)); } // Bottom Line if (!DoubleUtil.IsZero(borders.Bottom)) { if (bottomPenCache == null) { bottomPenCache = GetPen(brush, borderStyle, borders.Bottom, dpi.DpiScaleY, useLayoutRounding); } y = height - bottomPenCache.Thickness * 0.5; dc.DrawLine(bottomPenCache, new Point(radius.BottomLeft, y), new Point(width - radius.BottomRight, y)); } // Draw Rounded Pen pen; if (upperLeftCache != null && (pen = GetMaxPen(leftPenCache, topPenCache)) != null) { dc.DrawGeometry(null, pen, upperLeftCache); } if (upperRightCache != null && (pen = GetMaxPen(topPenCache, rightPenCache)) != null) { dc.DrawGeometry(null, pen, upperRightCache); } if (lowerRightCache != null && (pen = GetMaxPen(rightPenCache, bottomPenCache)) != null) { dc.DrawGeometry(null, pen, lowerRightCache); } if (lowerLeftCache != null && (pen = GetMaxPen(bottomPenCache, leftPenCache)) != null) { dc.DrawGeometry(null, pen, lowerLeftCache); } } // Draw background in rectangle inside border. if (backgroundGeometryCache != null && (brush = Background) != null) { dc.DrawGeometry(brush, null, backgroundGeometryCache); } } private Pen GetMaxPen(Pen pen1, Pen pen2) { if (pen2 == null || (pen1 != null && pen2.Thickness < pen1.Thickness)) { return pen1; } return pen2; } private static StreamGeometry GenerateRoundedGeometry(Point startPoint, Point endPoint, Size size) { var streamGeometry = new StreamGeometry(); using (StreamGeometryContext sc = streamGeometry.Open()) { sc.BeginFigure(startPoint, true, false); sc.ArcTo(endPoint, size, 0, false, SweepDirection.Clockwise, true, false); } streamGeometry.Freeze(); return streamGeometry; } private static Pen GetPen(Brush brush, BorderStyle borderStyle, double thickness, double dpi, bool useLayoutRounding) { var pen = new Pen { Brush = brush, DashCap = PenLineCap.Flat, Thickness = useLayoutRounding ? UIElementUtil.RoundLayoutValue(thickness, dpi) : thickness, }; switch (borderStyle) { case BorderStyle.Dotted: pen.DashStyle = new DashStyle(new double[] { 1 }, 0d); break; case BorderStyle.Dashed: pen.DashStyle = new DashStyle(new double[] { 4, 2 }, 0d); break; default: break; } if (brush.IsFrozen) { pen.Freeze(); } return pen; } #endregion #region Private Structures Classes private struct Radii { #region Fields internal readonly Point LeftTop; internal readonly Point LeftBottom; internal readonly Point TopLeft; internal readonly Point TopRight; internal readonly Point RightTop; internal readonly Point RightBottom; internal readonly Point BottomRight; internal readonly Point BottomLeft; #endregion internal Radii(Rect rect, CornerRadius radius, Thickness borders, bool outer) { var left = borders.Left * 0.5; var top = borders.Top * 0.5; var right = borders.Right * 0.5; var bottom = borders.Bottom * 0.5; LeftTop = new Point(0d, 0d); LeftBottom = new Point(0d, rect.Height); TopLeft = new Point(0d, 0d); TopRight = new Point(rect.Width, 0d); RightTop = new Point(rect.Width, 0d); RightBottom = new Point(rect.Width, rect.Height); BottomRight = new Point(rect.Width, rect.Height); BottomLeft = new Point(0d, rect.Height); if (outer) { LeftTop.X = left; LeftBottom.X = left; TopLeft.Y = top; TopRight.Y = top; RightTop.X -= right; RightBottom.X -= right; BottomLeft.Y -= bottom; BottomRight.Y -= bottom; if (!DoubleUtil.IsZero(radius.TopLeft)) { TopLeft.X = radius.TopLeft; // + left; LeftTop.Y = radius.TopLeft;// + top; } if (!DoubleUtil.IsZero(radius.TopRight)) { RightTop.Y = radius.TopRight;// + top; TopRight.X -= radius.TopRight;// + right; } if (!DoubleUtil.IsZero(radius.BottomRight)) { BottomRight.X -= radius.BottomRight;// + right; RightBottom.Y -= radius.BottomRight;// + bottom; ; } if (!DoubleUtil.IsZero(radius.BottomLeft)) { LeftBottom.Y -= radius.BottomLeft; // + bottom; BottomLeft.X = radius.BottomLeft;// + left; } } else { TopLeft.X = Math.Max(0.0, radius.TopLeft - left); LeftTop.Y = Math.Max(0.0, radius.TopLeft - top); RightTop.Y = Math.Max(0.0, radius.TopRight - top); TopRight.X -= Math.Max(0.0, radius.TopRight - right); BottomRight.X -= Math.Max(0.0, radius.BottomRight - right); RightBottom.Y -= Math.Max(0.0, radius.BottomRight - bottom); LeftBottom.Y -= Math.Max(0.0, radius.BottomLeft - bottom); BottomLeft.X = Math.Max(0.0, radius.BottomLeft - left); } // check keypoints for overlap and resolve by partitioning corners according to // the percentage of each one. // top edge if (TopLeft.X > TopRight.X) { var v = TopLeft.X / (TopLeft.X + rect.Width - TopRight.X) * rect.Width; TopLeft.X = v; TopRight.X = v; } // right edge if (RightTop.Y > RightBottom.Y) { var v = RightTop.Y / (RightTop.Y + rect.Height - RightBottom.Y) * rect.Height; RightTop.Y = v; RightBottom.Y = v; } // bottom edge if (BottomRight.X < BottomLeft.X) { var v = BottomLeft.X / (BottomLeft.X + rect.Width - BottomRight.X) * rect.Width; BottomRight.X = v; BottomLeft.X = v; } // left edge if (LeftBottom.Y < LeftTop.Y) { var v = LeftTop.Y / (LeftTop.Y + rect.Height - LeftBottom.Y) * rect.Height; LeftBottom.Y = v; LeftTop.Y = v; } // Apply offset var offset = new Vector(rect.TopLeft.X, rect.TopLeft.Y); LeftTop += offset; LeftBottom += offset; TopRight += offset; TopLeft += offset; RightTop += offset; RightBottom += offset; BottomRight += offset; BottomLeft += offset; } } #endregion Private Structures Classes }