很多Avalonia开发者在构建横向排列的列表时会优先想到ListBox,毕竟它自带数据绑定、选择逻辑和虚拟化支持。但很快你就会发现:无论怎么设置,ListBox里的内容依然是自上而下排列,滚动条也只在右侧出现,横向滚动压根不起作用。这个现象的根本原因在于ListBox的默认模板和默认布局面板都是为垂直方向设计的。要突破这个限制,必须同时理解ScrollViewer的滚动控制方式和ItemsPanel的布局机制。下面这张图展示了最终实现的效果方向:横向排列的卡片列表可以左右滑动浏览,并且在窄窗口下自动出现水平滚动条。

ListBox默认滚动机制与ItemsPanel
在Avalonia中,ListBox的视觉模板内部包含一个ScrollViewer,这个ScrollViewer负责处理内容的滚动。默认情况下,ScrollViewer的VerticalScrollBarVisibility被设置为Auto,所以当内容超出可视区域高度时,垂直滚动条会自动出现;而HorizontalScrollBarVisibility被设置为Disabled,横向滚动被完全禁用。即使你把ScrollViewer.HorizontalScrollBarVisibility改成Visible或Auto,如果ItemsPanel仍然使用默认的垂直StackPanel,那么所有子项只会在垂直方向上排列,横向空间的利用会受到限制,滚动条可能不会出现,因为内容宽度并没有超过视口宽度。
ItemsPanel决定了ListBox中数据项的排列方式。默认的ItemsPanel是一个垂直的StackPanel,即Orientation="Vertical"。这意味着每个Item占据一整行,新Item被添加到上一项的下方。当你把ItemsPanel切换为水平StackPanel(Orientation="Horizontal")时,Item会从左到右依次排列,如果所有Item的总宽度超过ListBox的宽度,水平方向就出现了溢出。此时如果ScrollViewer的HorizontalScrollBarVisibility为Auto或Visible,就会显示水平滚动条,从而实现水平滚动。
因此,让ListBox水平滚动的核心操作只有两步:把ItemsPanel改成水平布局,并开启ScrollViewer的水平滚动条。这两步缺一不可:只改ItemsPanel而不开滚动条,内容可能被截断且无法滚动;只开滚动条而不改ItemsPanel,水平方向根本没有溢出,滚动条也不会出现。
实现ListBox水平滚动的完整方案
最直接的方式是在XAML中配置ListBox。通过附加属性ScrollViewer.HorizontalScrollBarVisibility和ScrollViewer.VerticalScrollBarVisibility可以控制滚动条行为,而通过ListBox.ItemsPanel可以替换默认面板。下面是一个完整的ListBox水平滚动示例,它绑定了一个字符串集合,每个字符串显示在一个固定宽高的卡片中。
<ListBox ItemsSource="{Binding Images}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Disabled"
Height="100">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate>
<Border Width="120" Height="80" Margin="4" Background="LightBlue" CornerRadius="4">
<TextBlock Text="{Binding}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
上面的代码将VerticalScrollBarVisibility设为Disabled,以确保不会出现垂直滚动条;HorizontalScrollBarVisibility设为Auto,这样只有当内容宽度超出ListBox宽度时才显示水平滚动条。ItemsPanel替换为水平StackPanel,每个Item的宽度固定为120,高度固定为80,所以当Item数量超过可视区域能容纳的数量时,水平方向就会溢出并出现滚动条。
如果你希望滚动条始终可见,可以把Auto改为Visible。但在实际项目中,Auto更符合用户习惯,因为它不会在内容不溢出时显示多余的灰色滚动条。另外,如果你不想在XAML中写死附加属性,也可以在C#代码中动态设置。以下代码片段展示了如何在代码中完成同样的配置:
// 假设 listBox 已经在XAML中声明
listBox.SetValue(ScrollViewer.HorizontalScrollBarVisibilityProperty, ScrollBarVisibility.Auto);
listBox.SetValue(ScrollViewer.VerticalScrollBarVisibilityProperty, ScrollBarVisibility.Disabled);
listBox.ItemsPanel = new ItemsPanelTemplate(() => new StackPanel { Orientation = Orientation.Horizontal });
如果你还需要更细粒度的控制,比如让Item宽度自适应内容而不是固定值,或者希望Item自动换行而不是单行滚动,那么水平StackPanel可能不够灵活。这时可以考虑使用WrapPanel或者UniformGrid。WrapPanel可以让Item在宽度不足时换行,但如果你想要单行横向滚动,则应保持水平StackPanel。UniformGrid可以创建等宽的列,但它的行为更接近网格,不符合“滚动列表”的直觉。对于水平滚动场景,水平StackPanel仍然是最简单直接的选择。
常见问题与注意事项
开启水平滚动之后,很多开发者会遇到虚拟化失效的问题。Avalonia的ListBox默认启用了UI虚拟化,虚拟化依赖于ItemsPresenter在ScrollViewer中的测量逻辑。水平方向的虚拟化在官方实现中通常也能工作,但前提是每个Item必须有确定的宽度,并且ItemsPanel的排列方向与滚动方向一致。如果你的Item宽度是动态的(比如根据内容自适应),虚拟化可能无法正确计算可视区域,导致滚动时出现空白或性能下降。为了稳定启用虚拟化,建议给Item设置固定的Width和Height,或者使用ItemContainerStyle设置统一的MinWidth。
另一个常见需求是让ListBox本身宽度自适应内容,也就是说ListBox有多宽,内容就占多宽,而不是在ListBox内部滚动。这其实是另一种界面模式:外层ScrollViewer负责横向滚动,ListBox只作为内容容器。这种情况下,你需要把ListBox放在一个ScrollViewer内部,并将ListBox的ScrollViewer.HorizontalScrollBarVisibility设为Disabled,同时把外层ScrollViewer的HorizontalScrollBarVisibility设为Auto。另外别忘了把ListBox的HorizontalAlignment设为Stretch,并让ItemsPanel使用水平StackPanel。这样整个ListBox的宽度由内容决定,外层滚动条负责浏览。
触控板和触摸屏上的横向滚动体验也值得注意。在Avalonia中,ScrollViewer默认支持触摸滚动,但鼠标滚轮通常只触发垂直滚动。如果你希望鼠标滚轮横向滚动,需要捕获PointerWheelChanged事件,手动调整ScrollViewer的Offset。可以给ListBox添加一个行为或附加属性,在事件中判断Delta.Y并修改Offset.X。例如:
listBox.PointerWheelChanged += (sender, e) =>
{
var scrollViewer = listBox.FindDescendantOfType<ScrollViewer>();
if (scrollViewer != null)
{
scrollViewer.Offset = scrollViewer.Offset.WithX(scrollViewer.Offset.X - e.Delta.Y);
e.Handled = true;
}
};
最后提醒一点:不同平台(Windows、macOS、Linux)对滚动条样式的渲染有差异,水平滚动条的默认外观可能不一致,但行为是统一的。如果你需要自定义滚动条样式,可以通过重写ScrollViewer的主题来实现,但这已经超出了本文的范围。只要按照上面的步骤设置好ItemsPanel和ScrollViewer属性,ListBox的水平滚动就能在绝大多数场景下稳定工作。
AvaloniaListBox水平滚动ScrollViewer设置修改时间:2026-09-17 04:33:29