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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|