扩展已经更新
https://www.cnblogs.com/dalgleish/p/18972924
由于Avalonia没有内置逻辑滚动,自己实现一个。
LogicalScrollControl类
public class LogicalScrollControl : ContentControl, ILogicalScrollable
{
private Vector offset;
private Size extent;
private Size viewport;
private Size scrollSize = new Size(100, 100);
private Size pageScrollSize = new Size(300, 300);
private bool canHorizontallyScroll;
private bool canVerticallyScroll;
private EventHandler? scrollInvalidated;
public LogicalScrollControl()
{
Focusable = true;
AddHandler(PointerWheelChangedEvent, OnPointerWheelChanged, RoutingStrategies.Tunnel);
}
protected override Size MeasureOverride(Size availableSize)
{
if (Content is Control contentControl)
{
contentControl.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
extent = contentControl.DesiredSize;
// 动态获取第一个子控件尺寸作为 scrollSize
if (contentControl is Panel panel && panel.Children.Count > 0)
{
var firstChild = panel.Children[0];
firstChild.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
var firstSize = firstChild.DesiredSize;
scrollSize = new Size(firstSize.Width, firstSize.Height);
pageScrollSize = new Size(scrollSize.Width * 3, scrollSize.Height * 3);
}
else
{
// 如果不是Panel或者没有子元素,fallback
scrollSize = new Size(100, 100);
pageScrollSize = new Size(300, 300);
}
}
else
{
extent = new Size(0, 0);
}
viewport = availableSize;
RaiseScrollInvalidated(EventArgs.Empty);
canHorizontallyScroll = Extent.Width > Viewport.Width;
canVerticallyScroll = Extent.Height > Viewport.Height;
return base.MeasureOverride(availableSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
if (Content is Control contentControl)
{
var scrollX = offset.X;
var scrollY = offset.Y;
var width = Math.Max(contentControl.DesiredSize.Width, finalSize.Width);
var height = Math.Max(contentControl.DesiredSize.Height, finalSize.Height);
// 移动内容,模拟滚动视口
var rect = new Rect(-scrollX, -scrollY, width, height);
contentControl.Arrange(rect);
}
return finalSize;
}
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
switch (e.Key)
{
case Key.Down:
Offset += new Vector(0, scrollSize.Height);
e.Handled = true;
break;
case Key.Up:
Offset -= new Vector(0, scrollSize.Height);
e.Handled = true;
break;
case Key.Right:
Offset += new Vector(scrollSize.Width, 0);
e.Handled = true;
break;
case Key.Left:
Offset -= new Vector(scrollSize.Width, 0);
e.Handled = true;
break;
case Key.PageDown:
Offset += new Vector(0, pageScrollSize.Height);
e.Handled = true;
break;
case Key.PageUp:
Offset -= new Vector(0, pageScrollSize.Height);
e.Handled = true;
break;
}
}
private void OnPointerWheelChanged(object? sender, PointerWheelEventArgs e)
{
if (CanVerticallyScroll)
{
// 垂直滚动: Delta.Y 为正表示向上滚,负表示向下滚
// 这里乘以 scrollSize.Height 是滚动一个逻辑单位
Offset += new Vector(0, -e.Delta.Y * scrollSize.Height);
e.Handled = true;
}
else if (CanHorizontallyScroll)
{
// 水平滚动: Delta.Y 为正表示向左滚,负表示向右滚
Offset += new Vector(-e.Delta.Y * scrollSize.Width, 0);
e.Handled = true;
}
else
{
// 如果内容未溢出,则不滚动
e.Handled = false;
}
}
private Vector CoerceOffset(Vector value)
{
double maxX = Math.Max(Extent.Width - Viewport.Width, 0);
double maxY = Math.Max(Extent.Height - Viewport.Height, 0);
double x = Math.Max(0, Math.Min(value.X, maxX));
double y = Math.Max(0, Math.Min(value.Y, maxY));
return new Vector(x, y);
}
public void RaiseScrollInvalidated(EventArgs e)
{
scrollInvalidated?.Invoke(this, e);
}
public Size Extent => extent;
public Size Viewport => viewport;
public Vector Offset
{
get => offset;
set
{
var coerced = CoerceOffset(value);
if (!coerced.Equals(offset))
{
offset = coerced;
InvalidateArrange();
RaiseScrollInvalidated(EventArgs.Empty);
}
}
}
public bool CanHorizontallyScroll
{
get => canHorizontallyScroll;
set => canHorizontallyScroll = value;
}
public bool CanVerticallyScroll
{
get => canVerticallyScroll;
set => canVerticallyScroll = value;
}
public bool IsLogicalScrollEnabled => true;
public Size ScrollSize => scrollSize;
public Size PageScrollSize => pageScrollSize;
public event EventHandler? ScrollInvalidated
{
add => scrollInvalidated += value;
remove => scrollInvalidated -= value;
}
public bool BringIntoView(Control target, Rect targetRect = default)
{
if (target == null || !IsLogicalScrollEnabled || Content == null)
return false;
var targetBounds = AvaloniaExtensions.GetMinBoundingMatrix(target, this);
Vector newOffset = offset;
if (targetBounds.Top < 0)
newOffset = new Vector(newOffset.X, offset.Y + targetBounds.Top);
else if (targetBounds.Bottom > viewport.Height)
newOffset = new Vector(newOffset.X, offset.Y + (targetBounds.Bottom - viewport.Height));
if (targetBounds.Left < 0)
newOffset = new Vector(offset.X + targetBounds.Left, newOffset.Y);
else if (targetBounds.Right > viewport.Width)
newOffset = new Vector(offset.X + (targetBounds.Right - viewport.Width), newOffset.Y);
Offset = newOffset;
return true;
}
public Control? GetControlInDirection(NavigationDirection direction, Control? from)
{
if (from == null || !IsLogicalScrollEnabled || Content == null)
return null;
Control? root = this.Content as Control;
// 计算起点控件视觉矩形
Rect fromRect = AvaloniaExtensions.GetMinBoundingMatrix(from, this);
var fromCenter = fromRect.Center;
var candidates = root?.GetVisualDescendants()
.OfType<Control>()
.Where(c => c != from && c.Focusable && c.IsEffectivelyVisible && c is Visual)
.Select(c =>
{
var rect = AvaloniaExtensions.GetMinBoundingMatrix(c, this);
return new
{
Control = c,
Rect = rect,
Center = rect.Center
};
})
.Where(x => x != null)
.ToList()!;
Control? best = null;
double bestScore = double.MaxValue;
foreach (var candidate in candidates)
{
var targetRect = candidate!.Rect;
var targetCenter = candidate.Center;
bool isInDirection = direction switch
{
NavigationDirection.Left => targetRect.Right <= fromRect.Left,
NavigationDirection.Right => targetRect.Left >= fromRect.Right,
NavigationDirection.Up => targetRect.Bottom <= fromRect.Top,
NavigationDirection.Down => targetRect.Top >= fromRect.Bottom,
_ => false
};
if (!isInDirection)
continue;
var dx = targetCenter.X - fromCenter.X;
var dy = targetCenter.Y - fromCenter.Y;
var distance = Math.Sqrt(dx * dx + dy * dy);
if (distance < bestScore)
{
bestScore = distance;
best = candidate.Control;
}
}
return best;
}
}
LogicalScrolling.axaml代码
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Height="400" Width="400" x:Class="AvaloniaUI.LogicalScrolling" Title="LogicalScrolling"> <LogicalScrollControl Name="logical"> <StackPanel Orientation="Horizontal" Name="panel"> <Button Name="A1" Width="150">1</Button> <Button Name="A2" Width="150">2</Button> <Button Name="A3" Width="150">3</Button> <Button Name="A4" Width="150">4</Button> </StackPanel> </LogicalScrollControl> </Window>
LogicalScrolling.axaml.cs代码
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using Avalonia.VisualTree;
using System;
namespace AvaloniaUI;
public partial class LogicalScrolling : Window
{
public LogicalScrolling()
{
InitializeComponent();
Dispatcher.UIThread.Post(() =>
{
var target = panel.Children[3];
bool res = logical.BringIntoView(target);
Console.WriteLine($"{res}");
var to = logical.GetControlInDirection(NavigationDirection.Right, panel.Children[0]);
Console.WriteLine($"{to?.Name}");
}
);
}
}
运行效果

浙公网安备 33010602011771号