我想拥有一个带有页脚行的ListView,该页脚行不会与其余项目垂直滚动,它应该一直可见。我已经使用以下模板完成了此操作:
<Style x:Key="FrozenRowListView" TargetType="ListView">
<Setter Property="SnapsToDevicePixels" Value="true" />
<Setter Property="OverridesDefaultStyle" Value="true" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="ScrollViewer.CanContentScroll" Value="true" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListView">
<Border Name="Border" BorderThickness="1">
<ScrollViewer Style="{StaticResource FrozenRowScrollViewer}">
<ItemsPresenter />
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="FrozenRowScrollViewer" TargetType="ScrollViewer">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ScrollViewer">
<Grid Background="{TemplateBinding Background}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<DockPanel Margin="{TemplateBinding Padding}">
<ScrollViewer DockPanel.Dock="Bottom"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden"
Focusable="false">
<GridViewRowPresenter
Margin="2,0,2,0"
Content="{Binding Path=TemplatedParent.ItemsSource, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource summaryConverter}}"
Columns="{Binding Path=TemplatedParent.View.Columns, RelativeSource={RelativeSource TemplatedParent}}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</ScrollViewer>
<ScrollViewer DockPanel.Dock="Top"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden"
Focusable="false">
<GridViewHeaderRowPresenter DockPanel.Dock="Top"
Margin="2,0,2,0"
Columns="{Binding Path=TemplatedParent.View.Columns, RelativeSource={RelativeSource TemplatedParent}}"
ColumnHeaderContainerStyle="{Binding Path=TemplatedParent.View.ColumnHeaderContainerStyle, RelativeSource={RelativeSource TemplatedParent}}"
ColumnHeaderTemplate="{Binding Path=TemplatedParent.View.ColumnHeaderTemplate, RelativeSource={RelativeSource TemplatedParent}}"
ColumnHeaderTemplateSelector="{Binding Path=TemplatedParent.View.ColumnHeaderTemplateSelector, RelativeSource={RelativeSource TemplatedParent}}"
AllowsColumnReorder="{Binding Path=TemplatedParent.View.AllowsColumnReorder, RelativeSource={RelativeSource TemplatedParent}}"
ColumnHeaderContextMenu="{Binding Path=TemplatedParent.View.ColumnHeaderContextMenu, RelativeSource={RelativeSource TemplatedParent}}"
ColumnHeaderToolTip="{Binding Path=TemplatedParent.View.ColumnHeaderToolTip, RelativeSource={RelativeSource TemplatedParent}}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</ScrollViewer>
<ScrollContentPresenter Name="PART_ScrollContentPresenter"
KeyboardNavigation.DirectionalNavigation="Local"
CanContentScroll="True"
CanHorizontallyScroll="False"
CanVerticallyScroll="False" />
</DockPanel>
<ScrollBar Name="PART_HorizontalScrollBar"
Orientation="Horizontal"
Grid.Row="1"
Maximum="{TemplateBinding ScrollableWidth}"
ViewportSize="{TemplateBinding ViewportWidth}"
Value="{TemplateBinding HorizontalOffset}"
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" />
<ScrollBar Name="PART_VerticalScrollBar"
Grid.Column="1"
Maximum="{TemplateBinding ScrollableHeight}"
ViewportSize="{TemplateBinding ViewportHeight}"
Value="{TemplateBinding VerticalOffset}"
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
这可以正常工作,但是有一个问题。页脚不会像列标题和其他项目一样水平滚动。我已经将额外的GridViewRowPresenter放入ScrollViewer中,就像GridViewHeaderRowPresenter一样。似乎标题行必须具有某种挂钩才能跟随滚动偏移量。
如何使这项工作? GridViewHeaderRowPresenter如何处理滚动?
最佳答案
您必须将页脚行ScrollViewer.HorizontalOffset
与PART_HorizontalScrollBar.Value
同步。但是这里的this之类的解决方案将行不通。
我建议使用附加属性。看起来很复杂,但事实并非如此。
public static class ScrollViewerBinding
{
public static double GetHorizontalOffset(DependencyObject depObj)
{
return (double)depObj.GetValue(HorizontalOffsetProperty);
}
public static void SetHorizontalOffset(DependencyObject depObj, double value)
{
depObj.SetValue(HorizontalOffsetProperty, value);
}
public static readonly DependencyProperty HorizontalOffsetProperty =
DependencyProperty.RegisterAttached("HorizontalOffset",
typeof(double),
typeof(ScrollViewerBinding),
new PropertyMetadata(OnHorizontalOffsetPropertyChanged));
private static void OnHorizontalOffsetPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ScrollViewer sv = d as ScrollViewer;
if (sv != null)
{
sv.ScrollToHorizontalOffset((double)e.NewValue);
}
}
}
然后你可以像这样绑定
<ScrollViewer DockPanel.Dock="Bottom"
local:ScrollViewerBinding.HorizontalOffset="{Binding Value, ElementName=PART_HorizontalScrollBar}"
现在,您要回答第二个问题:
GridViewHeaderRowPresenter
是如何执行的?您可以在GridViewHeaderRowPresenter.cs的
RenewEvents()
方法中找到魔术(从946行开始)。// find needed elements and hook up events
private void RenewEvents()
{
ScrollViewer oldHeaderSV = _headerSV;
_headerSV = Parent as ScrollViewer;
if (oldHeaderSV != _headerSV)
{
if (oldHeaderSV != null)
{
oldHeaderSV.ScrollChanged -= new ScrollChangedEventHandler(OnHeaderScrollChanged);
}
if (_headerSV != null)
{
_headerSV.ScrollChanged += new ScrollChangedEventHandler(OnHeaderScrollChanged);
}
}
ScrollViewer oldSV = _mainSV; // backup the old value
_mainSV = TemplatedParent as ScrollViewer;
if (oldSV != _mainSV)
{
if (oldSV != null)
{
oldSV.ScrollChanged -= new ScrollChangedEventHandler(OnMasterScrollChanged);
}
if (_mainSV != null)
{
_mainSV.ScrollChanged += new ScrollChangedEventHandler(OnMasterScrollChanged);
}
}
...
如您所见,
GridViewHeaderRowPresenter
关联ScrollChanged
(HeaderScrollViewer)和Parent
(MainScrollViewer)的TemplatedParent
事件,然后也使用ScrollToHorizontalOffset
方法处理该事件(从第1034行开始)。// The following two scroll changed methods will not be called recursively and lead to dead loop.
// When scrolling _masterSV, OnMasterScrollChanged will be called, so _headerSV also scrolled
// to the same offset. Then, OnHeaderScrollChanged be called, and try to scroll _masterSV, but
// it's already scrolled to that offset, so OnMasterScrollChanged will not be called.
// When master scroll viewer changed its offset, change header scroll viewer accordingly
private void OnMasterScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (_headerSV != null && _mainSV == e.OriginalSource)
{
_headerSV.ScrollToHorizontalOffset(e.HorizontalOffset);
}
}
// When header scroll viewer changed its offset, change master scroll viewer accordingly
private void OnHeaderScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (_mainSV != null && _headerSV == e.OriginalSource)
{
_mainSV.ScrollToHorizontalOffset(e.HorizontalOffset);
}
}