WPF 列表框虚拟化面板

Posted

技术标签:

【中文标题】WPF 列表框虚拟化面板【英文标题】:WPF ListBox VirtualizingPanel 【发布时间】:2020-06-03 01:45:27 【问题描述】:

我需要一些建议。我正在尝试使用扩展的详细信息项创建一个 ListBox。我还创建了 VirtualizingTilePanel,但不幸的是 ListBox 中的项目没有移动,因此项目的细节不可见。我用 ElasticWrapPanel 试过,但没有虚拟化。我需要维护虚拟化。我很乐意为您提供任何建议。

源码:ExpandItemVirtualizingTilePanel

带有 ElasticWrapPanel 的 ListBox 没有虚拟化:

具有 VirtualizingTilePanel 的 ListBox 具有虚拟化但不滚动项目,您无法看到项目详细信息:

XAML 源代码

<ListBox ItemsSource="Binding MoviesCvs.View,IsAsync=True"
          ScrollViewer.HorizontalScrollBarVisibility="Disabled" 
          ScrollViewer.CanContentScroll="True"

          VirtualizingPanel.CacheLengthUnit="Pixel"
          VirtualizingPanel.CacheLength="100,100"
          VirtualizingPanel.ScrollUnit="Pixel"
          VirtualizingPanel.VirtualizationMode="Recycling">
    <ListBox.Resources>
        <DataTemplate x:Key="DetailTempalte">
            <DataTemplate.Resources>
                <DropShadowEffect x:Key="z-depth3" BlurRadius="14" ShadowDepth="4.5" Direction="270" Opacity="0.6"  Color="Black"/>
            </DataTemplate.Resources>
            <Grid Height="300" Effect="StaticResource z-depth3" Background="White" Margin="10">
                <cachedImage:Image Stretch="UniformToFill" ImageUrl="Binding BackgropPath">
                </cachedImage:Image>
            </Grid>
        </DataTemplate>

        <ControlTemplate TargetType="x:Type ListBoxItem" x:Key="withDetailTemplate">
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
                <Border Grid.Row="0" x:Name="Bd" BorderBrush="TemplateBinding BorderBrush" BorderThickness="TemplateBinding BorderThickness" Background="TemplateBinding Background" Padding="TemplateBinding Padding" SnapsToDevicePixels="true">
                    <ContentPresenter  HorizontalAlignment="TemplateBinding HorizontalContentAlignment" SnapsToDevicePixels="TemplateBinding SnapsToDevicePixels" VerticalAlignment="TemplateBinding VerticalContentAlignment"/>
                </Border>

                <!-- **************** -->
                <Canvas Grid.Row="1" x:Name="detailCanvas" 
                        Width="0"
                        Height="Binding ElementName=detailGrid,Path=ActualHeight"
                        HorizontalAlignment="Left" VerticalAlignment="Top" Visibility="Collapsed">
                    <Grid x:Name="detailGrid" Width="Binding RelativeSource=RelativeSource Mode=FindAncestor,AncestorType=x:Type ScrollContentPresenter,Path=ActualWidth"
                          Canvas.Left="Binding RelativeSource=RelativeSource Mode=FindAncestor,AncestorType=x:Type ListBoxItem,Path=(panelVirtualizing:VirtualizingTilePanel.ItemLocation).LocationN.X">
                        <ContentPresenter ContentTemplate="DynamicResource ResourceKey=DetailTempalte" />
                    </Grid>
                </Canvas>
                <!-- **************** -->
            </Grid>
            <ControlTemplate.Triggers>
                <Trigger Property="IsSelected" Value="true">
                    <Setter Property="Background" TargetName="Bd" Value="DynamicResource x:Static SystemColors.HighlightBrushKey"/>
                    <Setter Property="Foreground" Value="DynamicResource x:Static SystemColors.HighlightTextBrushKey"/>

                    <Setter TargetName="detailCanvas" Property="Visibility" Value="Visible"/>
                </Trigger>

                <MultiTrigger>
                    <MultiTrigger.Conditions>
                        <Condition Property="IsSelected" Value="true"/>
                        <Condition Property="Selector.IsSelectionActive" Value="false"/>
                    </MultiTrigger.Conditions>
                    <Setter Property="Background" TargetName="Bd" Value="DynamicResource x:Static SystemColors.InactiveSelectionHighlightBrushKey"/>
                    <Setter Property="Foreground" Value="DynamicResource x:Static SystemColors.InactiveSelectionHighlightTextBrushKey"/>
                </MultiTrigger>
                <Trigger Property="IsEnabled" Value="false">
                    <Setter Property="Foreground" Value="DynamicResource x:Static SystemColors.GrayTextBrushKey"/>
                </Trigger>
            </ControlTemplate.Triggers>
        </ControlTemplate>
    </ListBox.Resources>

    <ListBox.ItemContainerStyle>
        <Style TargetType="x:Type ListBoxItem">
            <Setter Property="Template" Value="StaticResource withDetailTemplate" />
        </Style>
    </ListBox.ItemContainerStyle>

    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>

             <!--I need to leave VirtualizingTilePanel there-->
             <panelVirtualizing:VirtualizingTilePanel 
                x:Name="VirtualizingTilePanel"  

                IsVirtualizing="True"

                VirtualizingPanel.CacheLengthUnit="Pixel"
                VirtualizingPanel.CacheLength="100,100"
                VirtualizingPanel.ScrollUnit="Pixel"
                VirtualizingPanel.VirtualizationMode="Recycling"

                ChildWidth="260" ChildHeight="455"/>

            <!--<local:WrapPaneEx Width="Binding RelativeSource=RelativeSource Mode=FindAncestor,AncestorType=x:Type ScrollContentPresenter,Path=ActualWidth"/>-->
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>

    <ListBox.ItemTemplate>
        <DataTemplate>
            <DataTemplate.Resources>
                <DropShadowEffect x:Key="z-depth3" BlurRadius="14" ShadowDepth="4.5" Direction="270" Opacity="0.6"  Color="Black"/>
            </DataTemplate.Resources>
            <Grid Opacity="1" Effect="StaticResource z-depth3"  Width="250" Height="445" Background="White" Margin="5">
                <StackPanel VerticalAlignment="Top">
                    <cachedImage:Image Stretch="Uniform" ImageUrl="Binding PosterPath" >

                    </cachedImage:Image>
                    <TextBlock Margin="5,10,5,0" Text="Binding MovieTitle" TextTrimming="CharacterEllipsis" Foreground="Black" TextWrapping="Wrap" MaxHeight="50" FontSize="17"/>
                </StackPanel>
            </Grid>
        </DataTemplate>
    </ListBox.ItemTemplate>
</ListBox>

VirtualizingTilePanel(无法获得扩展项目)

 public class VirtualizingTilePanel : System.Windows.Controls.VirtualizingPanel, IScrollInfo

    public VirtualizingTilePanel()
    
        // For use in the IScrollInfo implementation
        RenderTransform = _trans;
    

    // Dependency property that controls the size of the child elements
    public static readonly DependencyProperty ChildWidthProperty
        = DependencyProperty.RegisterAttached("ChildWidth", typeof(double), typeof(VirtualizingTilePanel),
            new FrameworkPropertyMetadata(220d, FrameworkPropertyMetadataOptions.AffectsMeasure |
                                                FrameworkPropertyMetadataOptions.AffectsArrange));

    public static readonly DependencyProperty ChildHeightProperty
        = DependencyProperty.RegisterAttached("ChildHeight", typeof(double), typeof(VirtualizingTilePanel),
            new FrameworkPropertyMetadata(385d, FrameworkPropertyMetadataOptions.AffectsMeasure |
                                                FrameworkPropertyMetadataOptions.AffectsArrange));

    // Accessor for the child size dependency property
    public double ChildWidth
    
        get  return (double)GetValue(ChildWidthProperty); 
        set  SetValue(ChildWidthProperty, value); 
    

    public double ChildHeight
    
        get  return (double)GetValue(ChildHeightProperty); 
        set  SetValue(ChildHeightProperty, value); 
    

    /// <summary>
    /// The panel's number of columns
    /// </summary>
    private int _columns;

    /// <summary>
    /// Measure the children
    /// </summary>
    /// <param name="availableSize">Size available</param>
    /// <returns>Size desired</returns>
    protected override Size MeasureOverride(Size availableSize)
    
        try
        

            if (availableSize.Width == double.PositiveInfinity || availableSize.Height == double.PositiveInfinity)
            
                return Size.Empty;
            

            _columns = (int)(availableSize.Width / ChildWidth);

            UpdateScrollInfo(availableSize);

            // Figure out range that's visible based on layout algorithm
            int firstVisibleItemIndex, lastVisibleItemIndex;
            GetVisibleRange(out firstVisibleItemIndex, out lastVisibleItemIndex);

            // We need to access InternalChildren before the generator to work around a bug
            var children = InternalChildren;
            var generator = ItemContainerGenerator;

            // Get the generator position of the first visible data item
            var startPos = generator.GeneratorPositionFromIndex(firstVisibleItemIndex);

            // Get index where we'd insert the child for this position. If the item is realized
            // (position.Offset == 0), it's just position.Index, otherwise we have to add one to
            // insert after the corresponding child
            var childIndex = (startPos.Offset == 0) ? startPos.Index : startPos.Index + 1;

            using (generator.StartAt(startPos, GeneratorDirection.Forward, true))
            
                for (var itemIndex = firstVisibleItemIndex;
                    itemIndex <= lastVisibleItemIndex;
                    ++itemIndex, ++childIndex)
                
                    bool newlyRealized;

                    // Get or create the child
                    var child = generator.GenerateNext(out newlyRealized) as UIElement;
                    if (child == null) continue;
                    if (newlyRealized)
                    
                        // Figure out if we need to insert the child at the end or somewhere in the middle
                        if (childIndex >= children.Count)
                        
                            AddInternalChild(child);
                        
                        else
                        
                            InsertInternalChild(childIndex, child);
                        
                        generator.PrepareItemContainer(child);
                    
                    var itemLocation = GetItemLocation(child);
                    if (itemLocation == null)
                    
                        itemLocation = new ItemLocation(this, child);
                        SetItemLocation(child, itemLocation);
                    
                    itemLocation.OnLocationPropertyChanged();
                    // Measurements will depend on layout algorithm
                    child.Measure(GetChildSize());
                
            

            // Note: this could be deferred to idle time for efficiency
            CleanUpItems(firstVisibleItemIndex, lastVisibleItemIndex);

            return availableSize;
        
        catch (Exception e)
        
            Console.WriteLine(e);
            throw;
        
    

    /// <summary>
    /// Arrange the children
    /// </summary>
    /// <param name="finalSize">Size available</param>
    /// <returns>Size used</returns>
    protected override Size ArrangeOverride(Size finalSize)
    
        try
        
            //var size = base.ArrangeOverride(finalSize);

            var generator = ItemContainerGenerator;
            UpdateScrollInfo(finalSize);
            for (var i = 0; i < Children.Count; i++)
            
                var child = Children[i];




                // Map the child offset to an item offset
                var itemIndex = generator.IndexFromGeneratorPosition(new GeneratorPosition(i, 0));

                ArrangeChild(itemIndex, child, finalSize);

                // ItemLocation expand item
                var itemLocation = GetItemLocation(child);
                if (itemLocation == null)
                
                    itemLocation = new ItemLocation(this, child);
                    SetItemLocation(child, itemLocation);
                
                itemLocation.OnLocationPropertyChanged();
            

            return finalSize;
        
        catch (Exception e)
        
            Console.WriteLine(e);
            throw;
        
    

    /// <summary>
    /// Revirtualize items that are no longer visible
    /// </summary>
    /// <param name="minDesiredGenerated">first item index that should be visible</param>
    /// <param name="maxDesiredGenerated">last item index that should be visible</param>
    private void CleanUpItems(int minDesiredGenerated, int maxDesiredGenerated)
    
        try
        
            var children = InternalChildren;
            var generator = ItemContainerGenerator;

            for (var i = children.Count - 1; i >= 0; i--)
            
                GeneratorPosition childGeneratorPos = new GeneratorPosition(i, 0);
                var itemIndex = generator.IndexFromGeneratorPosition(childGeneratorPos);
                if (itemIndex < minDesiredGenerated || itemIndex > maxDesiredGenerated)
                
                    generator.Remove(childGeneratorPos, 1);
                    RemoveInternalChildRange(i, 1);
                
            
        
        catch (Exception e)
        
            Console.WriteLine(e);
            throw;
        
    

    /// <summary>
    /// When items are removed, remove the corresponding UI if necessary
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="args"></param>
    protected override void OnItemsChanged(object sender, ItemsChangedEventArgs args)
    
        try
        
            switch (args.Action)
            
                case NotifyCollectionChangedAction.Remove:
                case NotifyCollectionChangedAction.Replace:
                case NotifyCollectionChangedAction.Move:
                    RemoveInternalChildRange(args.Position.Index, args.ItemUICount);
                    break;
            
        
        catch (Exception e)
        
            Console.WriteLine(e);
            throw;
        
    

    #region Layout specific code

    /// <summary>
    /// Calculate the extent of the view based on the available size
    /// </summary>
    /// <param name="availableSize">available size</param>
    /// <param name="itemCount">number of data items</param>
    /// <returns></returns>
    private Size CalculateExtent(Size availableSize, int itemCount)
    
        try
        
            var childrenPerRow = CalculateChildrenPerRow(availableSize);

            // See how big we are
            return new Size(childrenPerRow * ChildWidth,
                ChildHeight * Math.Ceiling((double)itemCount / childrenPerRow));
        
        catch (Exception e)
        
            Console.WriteLine(e);
            throw;
        
    

    /// <summary>
    /// Get the range of children that are visible
    /// </summary>
    /// <param name="firstVisibleItemIndex">The item index of the first visible item</param>
    /// <param name="lastVisibleItemIndex">The item index of the last visible item</param>
    private void GetVisibleRange(out int firstVisibleItemIndex, out int lastVisibleItemIndex)
    
        try
        
            var childrenPerRow = CalculateChildrenPerRow(_extent);

            firstVisibleItemIndex = (int)Math.Floor(_offset.Y / ChildHeight) * childrenPerRow;
            lastVisibleItemIndex =
                (int)Math.Ceiling((_offset.Y + _viewport.Height) / ChildHeight) * childrenPerRow - 1;

            var itemsControl = ItemsControl.GetItemsOwner(this);
            var itemCount = itemsControl.HasItems ? itemsControl.Items.Count : 0;
            if (lastVisibleItemIndex >= itemCount)
                lastVisibleItemIndex = itemCount - 1;
        
        catch (Exception e)
        
            Console.WriteLine(e);
            throw;
        

    

    /// <summary>
    /// Get the size of the children. We assume they are all the same
    /// </summary>
    /// <returns>The size</returns>
    private Size GetChildSize()
    
        return new Size(ChildWidth , ChildHeight);
    

    /// <summary>
    /// Position a child
    /// </summary>
    /// <param name="itemIndex">The data item index of the child</param>
    /// <param name="child">The element to position</param>
    /// <param name="finalSize">The size of the panel</param>
    private void ArrangeChild(int itemIndex, UIElement child, Size finalSize)
    
        try
        
            int childrenPerRow = CalculateChildrenPerRow(finalSize);

            int row = itemIndex / childrenPerRow;
            int column = itemIndex % childrenPerRow;
            var columnWidth = Math.Floor(finalSize.Width / _columns);

            child.Arrange(new Rect(columnWidth * column, row * ChildHeight, columnWidth,
                child.DesiredSize.Height));

            //var itemLocation = GetItemLocation(child);
            //if (itemLocation == null)
            //
            //    itemLocation = new ItemLocation(this, child);
            //    SetItemLocation(child, itemLocation);
            //
            //itemLocation.OnLocationPropertyChanged();

        
        catch (Exception e)
        

        
    

    /// <summary>
    /// Helper function for tiling layout
    /// </summary>
    /// <param name="availableSize">Size available</param>
    /// <returns></returns>
    private int CalculateChildrenPerRow(Size availableSize)
    
        try
        
            // Figure out how many children fit on each row
            int childrenPerRow;
            if (double.IsPositiveInfinity(availableSize.Width))
                childrenPerRow = Children.Count;
            else
                childrenPerRow = Math.Max(1, (int)Math.Floor(availableSize.Width / ChildWidth));
            return childrenPerRow;
        
        catch (Exception e)
        
            Console.WriteLine(e);
            throw;
        
    

    #endregion

    #region IScrollInfo implementation

    // See Ben Constable's series of posts at http://blogs.msdn.com/bencon/
    private void UpdateScrollInfo(Size availableSize)
    
        try
        
            // See how many items there are
            ItemsControl itemsControl = ItemsControl.GetItemsOwner(this);
            int itemCount = itemsControl.HasItems ? itemsControl.Items.Count : 0;

            Size extent = CalculateExtent(availableSize, itemCount);
            // Update extent
            if (extent != _extent)
            
                _extent = extent;
                ScrollOwner?.InvalidateScrollInfo();
            

            // Update viewport
            if (availableSize != _viewport)
            
                _viewport = availableSize;
                ScrollOwner?.InvalidateScrollInfo();
            
        
        catch (Exception e)
        

        
    

    public ScrollViewer ScrollOwner  get; set; 

    public bool CanHorizontallyScroll  get; set;  = false;

    public bool CanVerticallyScroll  get; set;  = false;

    public double HorizontalOffset => _offset.X;

    public double VerticalOffset => _offset.Y;

    public double ExtentHeight => _extent.Height;

    public double ExtentWidth => _extent.Width;

    public double ViewportHeight => _viewport.Height;

    public double ViewportWidth => _viewport.Width;

    public void LineUp()
    
        SetVerticalOffset(VerticalOffset - 10);
    

    public void LineDown()
    
        SetVerticalOffset(VerticalOffset + 10);
    

    public void PageUp()
    
        SetVerticalOffset(VerticalOffset - _viewport.Height);
    

    public void PageDown()
    
        SetVerticalOffset(VerticalOffset + _viewport.Height);
    

    public void MouseWheelUp()
    
        SetVerticalOffset(VerticalOffset - 10);
    

    public void MouseWheelDown()
    
        SetVerticalOffset(VerticalOffset + 10);
    

    public void LineLeft()
    
        throw new InvalidOperationException();
    

    public void LineRight()
    
        throw new InvalidOperationException();
    

    public Rect MakeVisible(Visual visual, Rect rectangle)
    
        return new Rect();
    

    public void MouseWheelLeft()
    
        throw new InvalidOperationException();
    

    public void MouseWheelRight()
    
        throw new InvalidOperationException();
    

    public void PageLeft()
    
        throw new InvalidOperationException();
    

    public void PageRight()
    
        throw new InvalidOperationException();
    

    public void SetHorizontalOffset(double offset)
    
        throw new InvalidOperationException();
    

    public void SetVerticalOffset(double offset)
    
        try
        
            if (offset < 0 || _viewport.Height >= _extent.Height)
            
                offset = 0;
            
            else
            
                if (offset + _viewport.Height >= _extent.Height)
                
                    offset = _extent.Height - _viewport.Height;
                
            

            _offset.Y = offset;

            ScrollOwner?.InvalidateScrollInfo();

            _trans.Y = -offset;

            // Force us to realize the correct children
            InvalidateMeasure();
        
        catch (Exception e)
        

        
    

    private readonly TranslateTransform _trans = new TranslateTransform();
    private Size _extent = new Size(0, 0);
    private Size _viewport = new Size(0, 0);
    private Point _offset;

    #endregion



    public static ItemLocation GetItemLocation(DependencyObject obj)
    
        return (ItemLocation)obj.GetValue(ItemLocationProperty);
    

    public static void SetItemLocation(DependencyObject obj, ItemLocation value)
    
        obj.SetValue(ItemLocationProperty, value);
    

    public static readonly DependencyProperty ItemLocationProperty = DependencyProperty.RegisterAttached("ItemLocation", typeof(ItemLocation), typeof(VirtualizingTilePanel), new PropertyMetadata(null));


public class ItemLocation : System.ComponentModel.INotifyPropertyChanged

    public ItemLocation(VirtualizingPanel panel, UIElement itemContainer)
    
        this._Panel = panel;
        this._ItemContainer = itemContainer;
    

    private UIElement _ItemContainer;
    private VirtualizingPanel _Panel;

    public Point? Location
    
        get
        
            if (_Location == null && _Panel != null && _ItemContainer != null)
            
                _Location = _ItemContainer.TranslatePoint(default(Point), _Panel);
            
            return _Location;
        
    
    private Point? _Location;

    public Point? LocationN
    
        get
        
            if (_LocationN == null && _Location == null && _Panel != null && _ItemContainer != null)
            
                Point? np = Location;
                if (np != null)
                
                    _LocationN = new Point(-np.Value.X, -np.Value.Y);
                
            
            return _LocationN;
        
    
    private Point? _LocationN;

    public event PropertyChangedEventHandler PropertyChanged;

    internal void OnLocationPropertyChanged()
    
        _Location = null;
        _LocationN = null;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Location)));
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(LocationN)));
    

【问题讨论】:

【参考方案1】:

对于那些有同样问题的人,我发现了这个 VirtualizingWrapPanelWithItemExpansion https://i.imgur.com/0DVsZBv.gif

【讨论】:

以上是关于WPF 列表框虚拟化面板的主要内容,如果未能解决你的问题,请参考以下文章

像wpf中的Powerpoint缩略图一样创建缩略图预览

WPF ListBox WPF XAML 内的顶部对齐

WPF 从列表框中拖动项目并放入文本框中

如何使用 wpf 代码隐藏在列表框中隐藏单个列表框项?

WPF 列表框动态填充 - 如何让它刷新?

选择时如何让wpf listboxitem拉伸列表框的整个高度