Files

306 lines
13 KiB
C#
Raw Permalink Normal View History

2026-01-02 17:30:41 +08:00
namespace Melskin.Layout
2025-08-20 12:10:13 +08:00
{
/// <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,
};
}
2025-12-28 11:47:54 +08:00
//protected override Size MeasureOverride(Size availableSize)
//{
// double spaceTakenByAutoChildren = 0;
// double totalSpanForFillChildren = 0;
// double maxHeight = 0;
2025-08-20 12:10:13 +08:00
2025-12-28 11:47:54 +08:00
// // 【修改】计算所有间距占用的总宽度
// 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);
//}
2025-08-20 12:10:13 +08:00
/// <inheritdoc />
protected override Size MeasureOverride(Size availableSize)
{
2025-12-28 11:47:54 +08:00
double totalAutoWidth = 0;
double totalSpanUnits = 0;
2025-08-20 12:10:13 +08:00
double maxHeight = 0;
2025-12-28 11:47:54 +08:00
bool isWidthInfinite = double.IsInfinity(availableSize.Width);
2025-08-20 12:10:13 +08:00
2025-12-28 11:47:54 +08:00
var childrenCount = InternalChildren.Count;
var totalSpacing = Math.Max(0, childrenCount - 1) * Spacing;
2025-08-20 12:10:13 +08:00
2025-12-28 11:47:54 +08:00
// 第一遍测量:收集信息
2025-08-20 12:10:13 +08:00
foreach (UIElement child in InternalChildren)
{
2025-12-28 11:47:54 +08:00
if (child == null || child.Visibility == Visibility.Collapsed) continue;
2025-08-20 12:10:13 +08:00
var span = GetEffectiveSpan(child);
2025-12-28 11:47:54 +08:00
if (span < 0 || isWidthInfinite)
2025-08-20 12:10:13 +08:00
{
2025-12-28 11:47:54 +08:00
// 如果宽度是无穷大或者子项是Auto
// 我们给它无穷大空间,看看它到底想占多大。
2025-08-20 12:10:13 +08:00
child.Measure(new Size(double.PositiveInfinity, availableSize.Height));
2025-12-28 11:47:54 +08:00
totalAutoWidth += child.DesiredSize.Width;
2025-08-20 12:10:13 +08:00
}
2025-12-28 11:47:54 +08:00
if (span >= 0)
2025-08-20 12:10:13 +08:00
{
2025-12-28 11:47:54 +08:00
totalSpanUnits += span;
2025-08-20 12:10:13 +08:00
}
}
2025-12-28 11:47:54 +08:00
double finalWidth = 0;
2025-08-20 12:10:13 +08:00
2025-12-28 11:47:54 +08:00
if (isWidthInfinite)
2025-08-20 12:10:13 +08:00
{
2025-12-28 11:47:54 +08:00
// 情况 A: 宽度无穷大 (例如在 StackPanel 或 ScrollViewer 中)
// 此时 Span 的逻辑无法分配“剩余空间”,通常做法是让 Span 项以 DesiredSize 形式并存
// 或者你可以给 Span 项一个默认基础宽度(比如 0或者它们内容的宽度
// 这里我们把所有子项的 DesiredSize 加起来作为我们需要的总宽度
finalWidth = totalAutoWidth + totalSpacing;
// 既然是无穷大,我们需要再次测量那些还没有正确 Measure 的 Span 项(可选)
foreach (UIElement child in InternalChildren)
2025-08-20 12:10:13 +08:00
{
2025-12-28 11:47:54 +08:00
if (child == null || child.Visibility == Visibility.Collapsed) continue;
maxHeight = Math.Max(maxHeight, child.DesiredSize.Height);
2025-08-20 12:10:13 +08:00
}
2025-12-28 11:47:54 +08:00
}
else
{
// 情况 B: 宽度是有限的 (正常的 Grid 或固定宽度的容器)
double remainingWidth = Math.Max(0, availableSize.Width - totalSpacing - totalAutoWidth);
double widthPerSpanUnit = totalSpanUnits > 0 ? remainingWidth / totalSpanUnits : 0;
foreach (UIElement child in InternalChildren)
2025-08-20 12:10:13 +08:00
{
2025-12-28 11:47:54 +08:00
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);
2025-08-20 12:10:13 +08:00
}
2025-12-28 11:47:54 +08:00
finalWidth = availableSize.Width; // 在有限宽度下,通常占用全部可用宽度
2025-08-20 12:10:13 +08:00
}
2025-12-28 11:47:54 +08:00
// 重要:返回计算出的有限尺寸,绝不返回 availableSize.Width (如果是 Infinity)
return new Size(finalWidth, maxHeight);
}
2025-08-20 12:10:13 +08:00
/// <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;
}
}
}