Files
ShrlAlgoToolkit/AntdWpf/Controls/Border.cs
ShrlAlgo 4d35cadb56 更新
2025-07-11 09:20:23 +08:00

751 lines
28 KiB
C#

namespace AntdWpf.Controls
{
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using AntdWpf.Utils;
/// <summary>
/// Draws a border, background, or both around another element.
/// </summary>
public class Border : 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(Border),
new FrameworkPropertyMetadata(
null,
FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.SubPropertiesDoNotAffectRender
));
/// <summary>
/// Gets or sets the brush that fills the area between the bounds of a Border.
/// </summary>
public Brush Background
{
get { return (Brush)GetValue(BackgroundProperty); }
set { SetValue(BackgroundProperty, value); }
}
public static readonly DependencyProperty BorderBrushProperty = DependencyProperty.Register(
"BorderBrush",
typeof(Brush),
typeof(Border),
new FrameworkPropertyMetadata(
null,
FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.SubPropertiesDoNotAffectRender,
OnClearPenCache
));
/// <summary>
/// Gets or sets the brush that draws the outer border color.
/// </summary>
public Brush BorderBrush
{
get { return (Brush)GetValue(BorderBrushProperty); }
set { SetValue(BorderBrushProperty, value); }
}
public static readonly DependencyProperty BorderThicknessProperty = DependencyProperty.Register(
"BorderThickness",
typeof(Thickness),
typeof(Border),
new FrameworkPropertyMetadata(
new Thickness(),
FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender,
OnClearPenCache
),
IsThicknessValid);
/// <summary>
/// Gets or sets the relative thickness of a border.
/// </summary>
public Thickness BorderThickness
{
get { return (Thickness)GetValue(BorderThicknessProperty); }
set { SetValue(BorderThicknessProperty, value); }
}
private static void OnClearPenCache(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var border = (Border)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(Border),
new FrameworkPropertyMetadata(
new CornerRadius(),
FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender
),
IsCornerRadiusValid);
/// <summary>
/// Gets or sets a value that represents the degree to which the corners of a Border are rounded.
/// </summary>
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(Border),
new FrameworkPropertyMetadata(
new Thickness(),
FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsRender
),
IsThicknessValid);
/// <summary>
/// Gets or sets a thickness value that describes the amount of space between a border and its child element.
/// </summary>
public Thickness Padding
{
get { return (Thickness)GetValue(PaddingProperty); }
set { SetValue(PaddingProperty, value); }
}
public static readonly DependencyProperty BorderStyleProperty = DependencyProperty.Register(
"BorderStyle",
typeof(BorderStyle),
typeof(Border),
new FrameworkPropertyMetadata(
BorderStyle.Solid,
FrameworkPropertyMetadataOptions.AffectsRender | FrameworkPropertyMetadataOptions.SubPropertiesDoNotAffectRender,
OnClearPenCache
));
/// <summary>
/// Gets or sets the border style.
/// </summary>
public BorderStyle BorderStyle
{
get { return (BorderStyle)GetValue(BorderStyleProperty); }
set { SetValue(BorderStyleProperty, value); }
}
#endregion
#region Overrides
/// <summary>
/// Updates DesiredSize of the Border. Called by parent UIElement. This is the first pass of layout.
/// </summary>
/// <remarks>
/// Border determines its desired size it needs from the specified border the child: its sizing
/// properties, margin, and requested size.
/// </remarks>
/// <param name="constraint">Constraint size is an "upper limit" that the return value should not exceed.</param>
/// <returns>The Decorator's desired size.</returns>
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;
}
/// <summary>
/// Border computes the position of its single child and applies its child's alignments to the child.
/// </summary>
/// <param name="finalSize">The size reserved for this element by the parent</param>
/// <returns>The actual ink area of the element, typically the same as finalSize</returns>
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
}
}