You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

WinUI 3:如何在ItemsControl中编程滚动到虚拟化项(非ListView)

问题描述

我正在创建一个自定义ItemsControl子类MyControl,其样式模板如下:

<Style TargetType="local:MyControl">
    <Setter Property="ItemsPanel">
        <Setter.Value>
            <ItemsPanelTemplate>
                <ItemsStackPanel Orientation="Vertical" /> 
                <!-- or VirtualizingStackPanel; not really sure 
                     why there are two different seemingly identical 
                     virtualization panels -->
            </ItemsPanelTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="local:MyControl">
                <ScrollViewer x:Name="ScrollViewer"
                              VerticalScrollBarVisibility="Auto"
                              HorizontalScrollBarVisibility="Auto">
                    <ItemsPresenter />
                </ScrollViewer>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

我需要实现编程方式滚动到可能尚未加载的项(支持按内容或索引定位),且项的高度不统一。

遇到的困境:

  • ListView有现成的ScrollIntoView方法,但我不想继承ListView,因为它附带太多需要替换/重写的冗余功能;
  • WPF里的VirtualizingStackPanel有BringIndexIntoViewPublic方法,但WinUI的VirtualizingStackPanel或ItemsStackPanel没有等效功能;
  • 查看WinUI源码发现,ListView的ScrollIntoView依赖ItemsPresenter内部的原生功能,这些我无法直接访问。

请问有没有不继承ListView的实现方法?


可行解决方案

方案1:利用ItemContainerGenerator结合滚动触发

  1. 先在MyControl类中获取模板内的核心控件:
private ScrollViewer _scrollViewer;
private ItemsPresenter _itemsPresenter;

protected override void OnApplyTemplate()
{
    base.OnApplyTemplate();
    _scrollViewer = GetTemplateChild("ScrollViewer") as ScrollViewer;
    _itemsPresenter = GetTemplateChild("ItemsPresenter") as ItemsPresenter;
}
  1. 实现按索引滚动的方法:
public void ScrollToIndex(int index)
{
    if (index < 0 || index >= Items.Count) return;

    var generator = _itemsPresenter.ItemContainerGenerator;
    // 先尝试获取已加载的项容器
    var container = generator.ContainerFromIndex(index) as FrameworkElement;
    if (container != null)
    {
        container.StartBringIntoView();
        return;
    }

    // 容器未加载时,触发生成后再滚动
    generator.StatusChanged += OnGeneratorStatusChanged;
    generator.PrepareItemContainer(generator.GenerateItem(index));

    void OnGeneratorStatusChanged(object sender, EventArgs e)
    {
        if (generator.Status == GeneratorStatus.ContainersGenerated)
        {
            generator.StatusChanged -= OnGeneratorStatusChanged;
            var newContainer = generator.ContainerFromIndex(index) as FrameworkElement;
            newContainer?.StartBringIntoView();
        }
    }
}
  1. 按内容滚动的话,先通过Items.IndexOf(item)拿到对应索引,再调用上面的ScrollToIndex即可。

方案2:手动计算滚动偏移(适合高度可预估场景)

如果项的高度虽不统一但有明确的计算逻辑(比如数据源中存储了高度值),可以直接累加目标项之前所有项的高度,设置滚动偏移:

public void ScrollToItem(object item)
{
    int index = Items.IndexOf(item);
    if (index < 0) return;

    double totalOffset = 0;
    for (int i = 0; i < index; i++)
    {
        // 替换为你的数据模型类型及高度属性
        var dataItem = Items[i] as YourDataModel;
        totalOffset += dataItem?.ItemHeight ?? 0;
    }

    _scrollViewer?.ChangeView(null, totalOffset, null);
}

这个方法的局限性是必须能准确预估项高度,否则滚动位置会有偏差。

方案3:反射调用VirtualizingPanel内部方法

WinUI的VirtualizingStackPanel和ItemsStackPanel内部有未公开的滚动方法,可通过反射调用:

public void ScrollToIndexViaReflection(int index)
{
    if (_itemsPresenter == null) return;

    var panel = VisualTreeHelper.GetChild(_itemsPresenter, 0) as VirtualizingPanel;
    if (panel == null) return;

    var method = panel.GetType().GetMethod("BringIndexIntoView", BindingFlags.Instance | BindingFlags.NonPublic);
    method?.Invoke(panel, new object[] { index });
}

注意:反射调用内部非公开方法可能会随WinUI版本更新失效,属于非官方方案,谨慎使用。


内容的提问来源于stack exchange,提问作者Emperor Eto

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 23:11:06