Files
ShrlAlgoToolkit/Melskin/Layout/FlexibleRowPanel.cs
2026-02-17 22:17:13 +08:00

306 lines
13 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace Melskin.Layout
{
/// <summary>
/// 定义 FlexibleRowPanel 子元素的默认布局行为。
/// </summary>
public enum FlexibleRowLayoutMode
{
/// <summary>
/// (默认) 子元素默认按比例分配空间 (等效于 Span = 1)。
/// </summary>
Proportional,
/// <summary>
/// 子元素默认根据其内容自动调整大小 (等效于 Span = -1)。
/// </summary>
Auto
}
/// <summary>
/// 一个全能的单行布局面板,完美支持等分、权重、自动填充以及控件间距。
/// </summary>
public class FlexibleRowPanel : Panel
{
#region 1. (Spacing) -
/// <summary>
/// 获取或设置子控件之间的间距。
/// </summary>
public static readonly DependencyProperty SpacingProperty =
DependencyProperty.Register(nameof(Spacing), typeof(double), typeof(FlexibleRowPanel),
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsMeasure));
/// <summary>
/// 获取或设置子控件之间的间距。
/// 该属性定义了 FlexibleRowPanel 中相邻子控件间的距离。通过调整此值,可以控制布局中各元素的紧凑程度。
/// </summary>
public double Spacing
{
get => (double)GetValue(SpacingProperty);
set => SetValue(SpacingProperty, value);
}
#endregion
#region 2. (LayoutMode)
/// <summary>
/// 获取或设置 FlexibleRowPanel 的布局模式,决定子控件的排列方式。
/// </summary>
public static readonly DependencyProperty LayoutModeProperty =
DependencyProperty.Register(nameof(LayoutMode), typeof(FlexibleRowLayoutMode), typeof(FlexibleRowPanel),
new FrameworkPropertyMetadata(FlexibleRowLayoutMode.Proportional, FrameworkPropertyMetadataOptions.AffectsMeasure));
/// <summary>
/// 获取或设置布局模式,决定子控件在面板中的排列方式。
/// </summary>
public FlexibleRowLayoutMode LayoutMode
{
get => (FlexibleRowLayoutMode)GetValue(LayoutModeProperty);
set => SetValue(LayoutModeProperty, value);
}
#endregion
#region 3. 便 (IsFill)
/// <summary>
/// 获取或设置是否允许控件在FlexibleRowPanel中自动填充剩余空间。
/// </summary>
public static readonly DependencyProperty IsFillProperty =
DependencyProperty.RegisterAttached("IsFill", typeof(bool), typeof(FlexibleRowPanel),
new FrameworkPropertyMetadata(false, OnIsFillChanged));
/// <summary>
/// 设置指定控件是否在FlexibleRowPanel中自动填充剩余空间。
/// </summary>
/// <param name="element">要设置属性的UI元素。</param>
/// <param name="value">布尔值,指示是否允许该控件自动填充剩余空间。</param>
public static void SetIsFill(UIElement element, bool value) => element.SetValue(IsFillProperty, value);
/// <summary>
/// 获取指定控件是否在FlexibleRowPanel中自动填充剩余空间的设置。
/// </summary>
/// <param name="element">要获取属性的UI元素。</param>
/// <returns>布尔值,指示该控件是否被允许自动填充剩余空间。</returns>
public static bool GetIsFill(UIElement element) => (bool)element.GetValue(IsFillProperty);
private static void OnIsFillChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is UIElement element && (bool)e.NewValue)
{
element.SetValue(SpanProperty, 1.0);
}
}
#endregion
#region 4. (Span)
/// <summary>
/// 获取或设置控件在FlexibleRowPanel中的跨度。
/// </summary>
public static readonly DependencyProperty SpanProperty =
DependencyProperty.RegisterAttached("Span", typeof(double), typeof(FlexibleRowPanel),
new FrameworkPropertyMetadata(1.0, FrameworkPropertyMetadataOptions.AffectsParentMeasure));
/// <summary>
/// 设置指定控件在FlexibleRowPanel中的跨度。
/// </summary>
/// <param name="element">要设置属性的UI元素。</param>
/// <param name="value">双精度浮点数表示该控件在FlexibleRowPanel中的跨度。</param>
public static void SetSpan(UIElement element, double value) => element.SetValue(SpanProperty, value);
/// <summary>
/// 获取控件在FlexibleRowPanel中的跨度。
/// </summary>
/// <param name="element">要获取跨度的UI元素。</param>
/// <returns>返回一个double值表示该控件在FlexibleRowPanel中的跨度。</returns>
public static double GetSpan(UIElement element) => (double)element.GetValue(SpanProperty);
#endregion
private double GetEffectiveSpan(UIElement child)
{
var localValue = child.ReadLocalValue(SpanProperty);
if (localValue != DependencyProperty.UnsetValue)
{
return (double)localValue;
}
return LayoutMode switch
{
FlexibleRowLayoutMode.Auto => -1.0,
_ => 1.0,
};
}
//protected override Size MeasureOverride(Size availableSize)
//{
// double spaceTakenByAutoChildren = 0;
// double totalSpanForFillChildren = 0;
// double maxHeight = 0;
// // 【修改】计算所有间距占用的总宽度
// var totalSpacing = Math.Max(0, InternalChildren.Count - 1) * Spacing;
// // 【修改】计算真正可用于子控件的宽度
// var effectiveWidth = Math.Max(0, availableSize.Width - totalSpacing);
// // 第一遍测量:自动大小的控件
// foreach (UIElement child in InternalChildren)
// {
// if (child == null) continue;
// var span = GetEffectiveSpan(child);
// if (span < 0)
// {
// child.Measure(new Size(double.PositiveInfinity, availableSize.Height));
// spaceTakenByAutoChildren += child.DesiredSize.Width;
// }
// else
// {
// totalSpanForFillChildren += span;
// }
// }
// var remainingWidth = Math.Max(0, effectiveWidth - spaceTakenByAutoChildren);
// var widthPerSpanUnit = (totalSpanForFillChildren > 0 && !double.IsInfinity(remainingWidth)) ? remainingWidth / totalSpanForFillChildren : 0;
// // 第二遍测量:比例填充的控件
// foreach (UIElement child in InternalChildren)
// {
// if (child == null) continue;
// var span = GetEffectiveSpan(child);
// if (span >= 0)
// {
// child.Measure(new Size(widthPerSpanUnit * span, availableSize.Height));
// }
// if (child.DesiredSize.Height > maxHeight)
// {
// maxHeight = child.DesiredSize.Height;
// }
// }
// // 返回的宽度应该是面板请求的总宽度
// return new Size(availableSize.Width, maxHeight);
//}
/// <inheritdoc />
protected override Size MeasureOverride(Size availableSize)
{
double totalAutoWidth = 0;
double totalSpanUnits = 0;
double maxHeight = 0;
bool isWidthInfinite = double.IsInfinity(availableSize.Width);
var childrenCount = InternalChildren.Count;
var totalSpacing = Math.Max(0, childrenCount - 1) * Spacing;
// 第一遍测量:收集信息
foreach (UIElement child in InternalChildren)
{
if (child == null || child.Visibility == Visibility.Collapsed) continue;
var span = GetEffectiveSpan(child);
if (span < 0 || isWidthInfinite)
{
// 如果宽度是无穷大或者子项是Auto
// 我们给它无穷大空间,看看它到底想占多大。
child.Measure(new Size(double.PositiveInfinity, availableSize.Height));
totalAutoWidth += child.DesiredSize.Width;
}
if (span >= 0)
{
totalSpanUnits += span;
}
}
double finalWidth = 0;
if (isWidthInfinite)
{
// 情况 A: 宽度无穷大 (例如在 StackPanel 或 ScrollViewer 中)
// 此时 Span 的逻辑无法分配“剩余空间”,通常做法是让 Span 项以 DesiredSize 形式并存
// 或者你可以给 Span 项一个默认基础宽度(比如 0或者它们内容的宽度
// 这里我们把所有子项的 DesiredSize 加起来作为我们需要的总宽度
finalWidth = totalAutoWidth + totalSpacing;
// 既然是无穷大,我们需要再次测量那些还没有正确 Measure 的 Span 项(可选)
foreach (UIElement child in InternalChildren)
{
if (child == null || child.Visibility == Visibility.Collapsed) continue;
maxHeight = Math.Max(maxHeight, child.DesiredSize.Height);
}
}
else
{
// 情况 B: 宽度是有限的 (正常的 Grid 或固定宽度的容器)
double remainingWidth = Math.Max(0, availableSize.Width - totalSpacing - totalAutoWidth);
double widthPerSpanUnit = totalSpanUnits > 0 ? remainingWidth / totalSpanUnits : 0;
foreach (UIElement child in InternalChildren)
{
if (child == null || child.Visibility == Visibility.Collapsed) continue;
var span = GetEffectiveSpan(child);
if (span >= 0)
{
// 给 Span 项分配计算后的精确宽度
child.Measure(new Size(widthPerSpanUnit * span, availableSize.Height));
}
maxHeight = Math.Max(maxHeight, child.DesiredSize.Height);
}
finalWidth = availableSize.Width; // 在有限宽度下,通常占用全部可用宽度
}
// 重要:返回计算出的有限尺寸,绝不返回 availableSize.Width (如果是 Infinity)
return new Size(finalWidth, maxHeight);
}
/// <inheritdoc />
protected override Size ArrangeOverride(Size finalSize)
{
double spaceTakenByAutoChildren = 0;
double totalSpanForFillChildren = 0;
// 【修改】计算所有间距占用的总宽度
var totalSpacing = Math.Max(0, InternalChildren.Count - 1) * Spacing;
// 【修改】计算真正可用于子控件的宽度
var effectiveWidth = Math.Max(0, finalSize.Width - totalSpacing);
// 重新计算一遍各项数值,因为 Measure 和 Arrange 的 Size 可能不同
foreach (UIElement child in InternalChildren)
{
if (child == null) continue;
var span = GetEffectiveSpan(child);
if (span < 0)
{
spaceTakenByAutoChildren += child.DesiredSize.Width;
}
else
{
totalSpanForFillChildren += span;
}
}
var remainingWidth = Math.Max(0, effectiveWidth - spaceTakenByAutoChildren);
var widthPerSpanUnit = (totalSpanForFillChildren > 0) ? remainingWidth / totalSpanForFillChildren : 0;
double currentX = 0;
// 遍历并排列每个子元素
for (var i = 0; i < InternalChildren.Count; i++)
{
var child = InternalChildren[i];
if (child == null) continue;
var span = GetEffectiveSpan(child);
var childWidth = (span < 0) ? child.DesiredSize.Width : widthPerSpanUnit * span;
child.Arrange(new Rect(currentX, 0, childWidth, finalSize.Height));
// 【修改】移动 currentX并为下一个控件加上间距
currentX += childWidth;
if (i < InternalChildren.Count - 1)
{
currentX += Spacing;
}
}
return finalSize;
}
}
}