自定义 IScrollInfo 面板在 ItemsControl 中托管时失去滚动能力
Custom IScrollInfo panel loses scrolling ability when hosted in an ItemsControl
我正在尝试使用 IScrollInfo
界面创建自定义面板,但收效甚微。如果我在 XAML 的自定义面板中手动声明项目,我可以让它工作,但是当我将它放入 ItemsControl
时,滚动功能停止。如果有人能告诉我哪里出了问题,我将不胜感激。这是面板的(相当冗长但简单的)代码:
using IScrollInfoExample.Extentions;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
namespace IScrollInfoExample
{
public class ExampleScrollPanel : Panel, IScrollInfo
{
private TranslateTransform _trans = new TranslateTransform();
private Size _extent = new Size(0, 0);
private Size _viewport = new Size(0, 0);
private Point _offset;
private const double _scrollAmount = 3;
public ExampleScrollPanel()
{
Loaded += ExampleScrollPanel_Loaded;
RenderTransform = (_trans = new TranslateTransform());
}
public bool CanHorizontallyScroll { get; set; } = false;
public bool CanVerticallyScroll { get; set; } = true;
public double HorizontalOffset
{
get { return _offset.X; }
}
public double VerticalOffset
{
get { return _offset.Y; }
}
public double ExtentHeight
{
get { return _extent.Height; }
}
public double ExtentWidth
{
get { return _extent.Width; }
}
public double ViewportHeight
{
get { return _viewport.Height; }
}
public double ViewportWidth
{
get { return _viewport.Width; }
}
public ScrollViewer ScrollOwner { get; set; }
public void LineUp()
{
SetVerticalOffset(VerticalOffset - _scrollAmount);
}
public void PageUp()
{
SetVerticalOffset(VerticalOffset - _viewport.Height);
}
public void MouseWheelUp()
{
SetVerticalOffset(VerticalOffset - _scrollAmount);
}
public void LineDown()
{
SetVerticalOffset(VerticalOffset + _scrollAmount);
}
public void PageDown()
{
SetVerticalOffset(VerticalOffset + _viewport.Height);
}
public void MouseWheelDown()
{
SetVerticalOffset(VerticalOffset + _scrollAmount);
}
public void LineLeft()
{
SetHorizontalOffset(HorizontalOffset - _scrollAmount);
}
public void PageLeft()
{
SetHorizontalOffset(HorizontalOffset - _viewport.Width);
}
public void MouseWheelLeft()
{
LineLeft();
}
public void LineRight()
{
SetHorizontalOffset(HorizontalOffset + _scrollAmount);
}
public void PageRight()
{
SetHorizontalOffset(HorizontalOffset + _viewport.Width);
}
public void MouseWheelRight()
{
LineRight();
}
public Rect MakeVisible(Visual visual, Rect rectangle)
{
return new Rect();
}
public void SetHorizontalOffset(double offset)
{
if (offset < 0 || _viewport.Width >= _extent.Width)
{
offset = 0;
}
else if (offset + _viewport.Width >= _extent.Width)
{
offset = _extent.Width - _viewport.Width;
}
_offset.X = offset;
if (ScrollOwner != null) ScrollOwner.InvalidateScrollInfo();
_trans.X = -offset;
}
public void SetVerticalOffset(double offset)
{
offset = CoerceVerticalOffset(offset);
_offset.Y = offset;
_trans.Y = -offset;
if (ScrollOwner != null) ScrollOwner.InvalidateScrollInfo();
}
private double CoerceVerticalOffset(double offset)
{
if (offset < 0 || _viewport.Height >= _extent.Height)
{
offset = 0;
}
else if (offset + _viewport.Height >= _extent.Height)
{
offset = _extent.Height - _viewport.Height;
}
return offset;
}
private void ExampleScrollPanel_Loaded(object sender, RoutedEventArgs e)
{
ScrollViewer scrollOwner = this.GetParentOfType<ScrollViewer>();
if (scrollOwner != null) ScrollOwner = scrollOwner;
}
protected override Size MeasureOverride(Size availableSize)
{
UpdateScrollInfo(availableSize);
Size totalSize = new Size();
foreach (UIElement child in InternalChildren)
{
child.Measure(availableSize);
totalSize.Height += child.DesiredSize.Height;
totalSize.Width = Math.Max(totalSize.Width, child.DesiredSize.Width);
}
return base.MeasureOverride(totalSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
UpdateScrollInfo(finalSize);
double verticalPosition = 0;
int childCount = InternalChildren.Count;
for (int i = 0; i < childCount; i++)
{
UIElement child = InternalChildren[i];
child.Arrange(new Rect(0, verticalPosition, finalSize.Width, finalSize.Height));
verticalPosition += child.DesiredSize.Height;
}
return base.ArrangeOverride(finalSize);
}
private void UpdateScrollInfo(Size availableSize)
{
bool viewportChanged = false, extentChanged = false;
Size extent = CalculateExtent(availableSize);
if (extent != _extent)
{
_extent = extent;
extentChanged = true;
}
if (availableSize != _viewport)
{
_viewport = availableSize;
viewportChanged = true;
}
if (extentChanged || viewportChanged) ScrollOwner?.InvalidateScrollInfo();
}
private Size CalculateExtent(Size availableSize)
{
Size totalExtentSize = new Size();
foreach (UIElement child in InternalChildren)
{
child.Measure(availableSize);
totalExtentSize.Height += child.DesiredSize.Height;
totalExtentSize.Width = Math.Max(totalExtentSize.Width, child.DesiredSize.Width);
}
return totalExtentSize;
}
}
}
现在MainWindow.xaml.cs
:
<Window x:Class="IScrollInfoExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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"
xmlns:Local="clr-namespace:IScrollInfoExample"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<ScrollViewer CanContentScroll="True">
<ItemsControl ItemsSource="{Binding Buttons, RelativeSource={RelativeSource AncestorType={x:Type Local:MainWindow}}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Local:ExampleScrollPanel IsItemsHost="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Height="100" Width="250" HorizontalAlignment="Center" Content="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!--<Local:ExampleScrollPanel>
<Button Height="100" Width="250" HorizontalAlignment="Center" Content="A" />
<Button Height="100" Width="250" HorizontalAlignment="Center" Content="B" />
<Button Height="100" Width="250" HorizontalAlignment="Center" Content="C" />
<Button Height="100" Width="250" HorizontalAlignment="Center" Content="D" />
<Button Height="100" Width="250" HorizontalAlignment="Center" Content="E" />
<Button Height="100" Width="250" HorizontalAlignment="Center" Content="F" />
<Button Height="100" Width="250" HorizontalAlignment="Center" Content="G" />
<Button Height="100" Width="250" HorizontalAlignment="Center" Content="H" />
<Button Height="100" Width="250" HorizontalAlignment="Center" Content="I" />
</Local:ExampleScrollPanel>-->
</ScrollViewer>
</Window>
后面的代码:
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
namespace IScrollInfoExample
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Buttons = new ObservableCollection<string>();
IEnumerable<int> characterCodes = Enumerable.Range(65, 26);
foreach (int characterCode in characterCodes) Buttons.Add(((char)characterCode).ToString().ToUpper());
}
public static readonly DependencyProperty ButtonsProperty = DependencyProperty.Register(nameof(Buttons), typeof(ObservableCollection<string>), typeof(MainWindow), null);
public ObservableCollection<string> Buttons
{
get { return (ObservableCollection<string>)GetValue(ButtonsProperty); }
set { SetValue(ButtonsProperty, value); }
}
}
}
先看XAML,可以看到手动添加的Button
对象。 运行 应用程序,您应该会在面板中看到一些可垂直滚动的按钮……到目前为止,还不错。如果您在 MouseWheelUp
或 MouseWheelDown
方法中放置断点并滚动,您会注意到断点会立即命中。
现在,如果您用手动创建的按钮注释掉下面的 ExampleScrollPanel
并取消注释上面的 ItemsControl
,您会发现滚动项目的功能已经消失。我的问题是“当自定义面板托管在 ItemsControl
元素中时,如何使滚动工作?”
请注意,ScrollOwner
属性 是使用自定义 GetParentOfType<T>
方法填充的,该方法成功找到了合适的 ScrollViewer
,并不是导致此问题的原因。因此,我没有包含此方法的基于 VisualTreeHelper
的代码。
此外,我注意到一旦面板位于 ItemsControl
范围内,滚动条就不再出现,但我检查了一下,面板的 extent
和 viewport
值似乎仍然存在更新成功。任何帮助将不胜感激。
ItemsControl
没有自己的 ScrollViewer
,它无法访问您为此目的提供的外部文件。您需要在 ItemsControl
可以访问它的地方添加 ScrollViewer
,使用 ItemsControl.Template
属性,如下所示:
<ItemsControl ItemsSource="{Binding Buttons, RelativeSource={RelativeSource
AncestorType={x:Type Local:MainWindow}}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Local:ExampleScrollPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Template>
<ControlTemplate TargetType="{x:Type ItemsControl}">
<!--Add the ScrollViewer here, inside the ControlTemplate-->
<ScrollViewer CanContentScroll="True">
<!--Your items will be added here-->
<ItemsPresenter/>
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Height="100" Width="250" HorizontalAlignment="Center"
Content="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
请注意,您必须将ScrollViewer
的CanContentScroll
property设置为True
,以通知它您已经在您的面板中实现了IScrollInfo
接口,并希望采取在滚动功能上。
我正在尝试使用 IScrollInfo
界面创建自定义面板,但收效甚微。如果我在 XAML 的自定义面板中手动声明项目,我可以让它工作,但是当我将它放入 ItemsControl
时,滚动功能停止。如果有人能告诉我哪里出了问题,我将不胜感激。这是面板的(相当冗长但简单的)代码:
using IScrollInfoExample.Extentions;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
namespace IScrollInfoExample
{
public class ExampleScrollPanel : Panel, IScrollInfo
{
private TranslateTransform _trans = new TranslateTransform();
private Size _extent = new Size(0, 0);
private Size _viewport = new Size(0, 0);
private Point _offset;
private const double _scrollAmount = 3;
public ExampleScrollPanel()
{
Loaded += ExampleScrollPanel_Loaded;
RenderTransform = (_trans = new TranslateTransform());
}
public bool CanHorizontallyScroll { get; set; } = false;
public bool CanVerticallyScroll { get; set; } = true;
public double HorizontalOffset
{
get { return _offset.X; }
}
public double VerticalOffset
{
get { return _offset.Y; }
}
public double ExtentHeight
{
get { return _extent.Height; }
}
public double ExtentWidth
{
get { return _extent.Width; }
}
public double ViewportHeight
{
get { return _viewport.Height; }
}
public double ViewportWidth
{
get { return _viewport.Width; }
}
public ScrollViewer ScrollOwner { get; set; }
public void LineUp()
{
SetVerticalOffset(VerticalOffset - _scrollAmount);
}
public void PageUp()
{
SetVerticalOffset(VerticalOffset - _viewport.Height);
}
public void MouseWheelUp()
{
SetVerticalOffset(VerticalOffset - _scrollAmount);
}
public void LineDown()
{
SetVerticalOffset(VerticalOffset + _scrollAmount);
}
public void PageDown()
{
SetVerticalOffset(VerticalOffset + _viewport.Height);
}
public void MouseWheelDown()
{
SetVerticalOffset(VerticalOffset + _scrollAmount);
}
public void LineLeft()
{
SetHorizontalOffset(HorizontalOffset - _scrollAmount);
}
public void PageLeft()
{
SetHorizontalOffset(HorizontalOffset - _viewport.Width);
}
public void MouseWheelLeft()
{
LineLeft();
}
public void LineRight()
{
SetHorizontalOffset(HorizontalOffset + _scrollAmount);
}
public void PageRight()
{
SetHorizontalOffset(HorizontalOffset + _viewport.Width);
}
public void MouseWheelRight()
{
LineRight();
}
public Rect MakeVisible(Visual visual, Rect rectangle)
{
return new Rect();
}
public void SetHorizontalOffset(double offset)
{
if (offset < 0 || _viewport.Width >= _extent.Width)
{
offset = 0;
}
else if (offset + _viewport.Width >= _extent.Width)
{
offset = _extent.Width - _viewport.Width;
}
_offset.X = offset;
if (ScrollOwner != null) ScrollOwner.InvalidateScrollInfo();
_trans.X = -offset;
}
public void SetVerticalOffset(double offset)
{
offset = CoerceVerticalOffset(offset);
_offset.Y = offset;
_trans.Y = -offset;
if (ScrollOwner != null) ScrollOwner.InvalidateScrollInfo();
}
private double CoerceVerticalOffset(double offset)
{
if (offset < 0 || _viewport.Height >= _extent.Height)
{
offset = 0;
}
else if (offset + _viewport.Height >= _extent.Height)
{
offset = _extent.Height - _viewport.Height;
}
return offset;
}
private void ExampleScrollPanel_Loaded(object sender, RoutedEventArgs e)
{
ScrollViewer scrollOwner = this.GetParentOfType<ScrollViewer>();
if (scrollOwner != null) ScrollOwner = scrollOwner;
}
protected override Size MeasureOverride(Size availableSize)
{
UpdateScrollInfo(availableSize);
Size totalSize = new Size();
foreach (UIElement child in InternalChildren)
{
child.Measure(availableSize);
totalSize.Height += child.DesiredSize.Height;
totalSize.Width = Math.Max(totalSize.Width, child.DesiredSize.Width);
}
return base.MeasureOverride(totalSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
UpdateScrollInfo(finalSize);
double verticalPosition = 0;
int childCount = InternalChildren.Count;
for (int i = 0; i < childCount; i++)
{
UIElement child = InternalChildren[i];
child.Arrange(new Rect(0, verticalPosition, finalSize.Width, finalSize.Height));
verticalPosition += child.DesiredSize.Height;
}
return base.ArrangeOverride(finalSize);
}
private void UpdateScrollInfo(Size availableSize)
{
bool viewportChanged = false, extentChanged = false;
Size extent = CalculateExtent(availableSize);
if (extent != _extent)
{
_extent = extent;
extentChanged = true;
}
if (availableSize != _viewport)
{
_viewport = availableSize;
viewportChanged = true;
}
if (extentChanged || viewportChanged) ScrollOwner?.InvalidateScrollInfo();
}
private Size CalculateExtent(Size availableSize)
{
Size totalExtentSize = new Size();
foreach (UIElement child in InternalChildren)
{
child.Measure(availableSize);
totalExtentSize.Height += child.DesiredSize.Height;
totalExtentSize.Width = Math.Max(totalExtentSize.Width, child.DesiredSize.Width);
}
return totalExtentSize;
}
}
}
现在MainWindow.xaml.cs
:
<Window x:Class="IScrollInfoExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
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"
xmlns:Local="clr-namespace:IScrollInfoExample"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<ScrollViewer CanContentScroll="True">
<ItemsControl ItemsSource="{Binding Buttons, RelativeSource={RelativeSource AncestorType={x:Type Local:MainWindow}}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Local:ExampleScrollPanel IsItemsHost="True" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Height="100" Width="250" HorizontalAlignment="Center" Content="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!--<Local:ExampleScrollPanel>
<Button Height="100" Width="250" HorizontalAlignment="Center" Content="A" />
<Button Height="100" Width="250" HorizontalAlignment="Center" Content="B" />
<Button Height="100" Width="250" HorizontalAlignment="Center" Content="C" />
<Button Height="100" Width="250" HorizontalAlignment="Center" Content="D" />
<Button Height="100" Width="250" HorizontalAlignment="Center" Content="E" />
<Button Height="100" Width="250" HorizontalAlignment="Center" Content="F" />
<Button Height="100" Width="250" HorizontalAlignment="Center" Content="G" />
<Button Height="100" Width="250" HorizontalAlignment="Center" Content="H" />
<Button Height="100" Width="250" HorizontalAlignment="Center" Content="I" />
</Local:ExampleScrollPanel>-->
</ScrollViewer>
</Window>
后面的代码:
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
namespace IScrollInfoExample
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Buttons = new ObservableCollection<string>();
IEnumerable<int> characterCodes = Enumerable.Range(65, 26);
foreach (int characterCode in characterCodes) Buttons.Add(((char)characterCode).ToString().ToUpper());
}
public static readonly DependencyProperty ButtonsProperty = DependencyProperty.Register(nameof(Buttons), typeof(ObservableCollection<string>), typeof(MainWindow), null);
public ObservableCollection<string> Buttons
{
get { return (ObservableCollection<string>)GetValue(ButtonsProperty); }
set { SetValue(ButtonsProperty, value); }
}
}
}
先看XAML,可以看到手动添加的Button
对象。 运行 应用程序,您应该会在面板中看到一些可垂直滚动的按钮……到目前为止,还不错。如果您在 MouseWheelUp
或 MouseWheelDown
方法中放置断点并滚动,您会注意到断点会立即命中。
现在,如果您用手动创建的按钮注释掉下面的 ExampleScrollPanel
并取消注释上面的 ItemsControl
,您会发现滚动项目的功能已经消失。我的问题是“当自定义面板托管在 ItemsControl
元素中时,如何使滚动工作?”
请注意,ScrollOwner
属性 是使用自定义 GetParentOfType<T>
方法填充的,该方法成功找到了合适的 ScrollViewer
,并不是导致此问题的原因。因此,我没有包含此方法的基于 VisualTreeHelper
的代码。
此外,我注意到一旦面板位于 ItemsControl
范围内,滚动条就不再出现,但我检查了一下,面板的 extent
和 viewport
值似乎仍然存在更新成功。任何帮助将不胜感激。
ItemsControl
没有自己的 ScrollViewer
,它无法访问您为此目的提供的外部文件。您需要在 ItemsControl
可以访问它的地方添加 ScrollViewer
,使用 ItemsControl.Template
属性,如下所示:
<ItemsControl ItemsSource="{Binding Buttons, RelativeSource={RelativeSource
AncestorType={x:Type Local:MainWindow}}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Local:ExampleScrollPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Template>
<ControlTemplate TargetType="{x:Type ItemsControl}">
<!--Add the ScrollViewer here, inside the ControlTemplate-->
<ScrollViewer CanContentScroll="True">
<!--Your items will be added here-->
<ItemsPresenter/>
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Height="100" Width="250" HorizontalAlignment="Center"
Content="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
请注意,您必须将ScrollViewer
的CanContentScroll
property设置为True
,以通知它您已经在您的面板中实现了IScrollInfo
接口,并希望采取在滚动功能上。