带固定水平滚动条的WPF Headered Timeline Control

时间:2017-10-07 12:54:30

标签: wpf

我想创建一个具有以下功能的多时间轴控件:

  • 每个时间轴都有一个标题区域,左边是静态的
  • 每个时间轴的其余部分可以水平滚动。
  • 如果控件不适合可用的水平空间,则应在时间轴部分下方显示滚动条,但不应显示标题。
  • 如果控件不适合可用的垂直空间,则应在右侧显示滚动条,向上和向下滚动标题和时间轴。

我最接近这个布局的是以下xaml:

<Grid>
  <HeaderedContentControl>
    <HeaderedContentControl.Template>
      <ControlTemplate TargetType="HeaderedContentControl">
        <Grid>
          <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
          </Grid.ColumnDefinitions>
          <ContentPresenter ContentSource="Header" ContentTemplate="{TemplateBinding HeaderedContentControl.HeaderTemplate}" Content="{TemplateBinding HeaderedContentControl.Header}" />
          <ScrollViewer Grid.Column="1" ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}" VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Auto" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Content="{TemplateBinding ContentControl.Content}"
          />
        </Grid>
      </ControlTemplate>
    </HeaderedContentControl.Template>
    <HeaderedContentControl.Header>
      <ItemsControl>
        <Button Height="80" Content="Fixed Header 1" />
        <Button Height="80" Content="Fixed Header 2" />
      </ItemsControl>
    </HeaderedContentControl.Header>
    <ItemsControl>
      <StackPanel Orientation="Horizontal" Height="80">
        <Button Width="100" Content="thing" />
        <Button Width="100" Content="thing" />
        <Button Width="100" Content="thing" />
        <Button Width="100" Content="thing" />
        <Button Width="100" Content="thing" />
      </StackPanel>
      <StackPanel Orientation="Horizontal" Height="80">
        <Button Width="100" Content="thing" />
        <Button Width="100" Content="thing" />
        <Button Width="100" Content="thing" />
        <Button Width="100" Content="thing" />
        <Button Width="100" Content="thing" />
      </StackPanel>
    </ItemsControl>
  </HeaderedContentControl>
</Grid>

这为我提供了正确的水平滚动,但没有垂直滚动条:

horizontal scrolling

所以为了获得垂直滚动,我只需要用ScrollViewer包装外部HeaderedContentControl元素,对吗?

<Grid>
    <ScrollViewer>
        <HeaderedContentControl>
            <HeaderedContentControl.Template>
                <ControlTemplate TargetType="HeaderedContentControl">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto" />
                            <ColumnDefinition Width="*" />
                        </Grid.ColumnDefinitions>
                        <ContentPresenter
                            ContentSource="Header"
                            ContentTemplate="{TemplateBinding HeaderedContentControl.HeaderTemplate}"
                            Content="{TemplateBinding HeaderedContentControl.Header}" />
                        <ScrollViewer Grid.Column="1"
                            ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}"
                            VerticalScrollBarVisibility="Hidden"
                            HorizontalScrollBarVisibility="Auto"
                            HorizontalAlignment="Stretch"
                            VerticalAlignment="Stretch"
                            Content="{TemplateBinding ContentControl.Content}" />
                    </Grid>
                </ControlTemplate>
            </HeaderedContentControl.Template>
            <HeaderedContentControl.Header>
                <ItemsControl>
                    <Button Height="80" Content="Fixed Header 1" />
                    <Button Height="80" Content="Fixed Header 2" />
                </ItemsControl>
            </HeaderedContentControl.Header>
            <ItemsControl>
                <StackPanel Orientation="Horizontal" Height="80">
                    <Button Width="100" Content="thing" />
                    <Button Width="100" Content="thing" />
                    <Button Width="100" Content="thing" />
                    <Button Width="100" Content="thing" />
                    <Button Width="100" Content="thing" />
                </StackPanel>
                <StackPanel Orientation="Horizontal" Height="80">
                    <Button Width="100" Content="thing" />
                    <Button Width="100" Content="thing" />
                    <Button Width="100" Content="thing" />
                    <Button Width="100" Content="thing" />
                    <Button Width="100" Content="thing" />
                </StackPanel>
            </ItemsControl>
        </HeaderedContentControl>
    </ScrollViewer>
</Grid>

结果:

enter image description here

嗯,它几乎是正确的,但是当我减小窗口高度时,我的时间轴下的水平滚动条会被修剪。我希望它像上一张图​​像一样保持在最顶层,这样我就可以一直水平滚动。

有谁知道我怎么做到这一点?

1 个答案:

答案 0 :(得分:2)

我找到了一个解决方案(虽然感觉更像是一种解决方法,但你仍然是法官)。

我在内部ScrollViewer下添加了一个外部ScrollBar,并完全隐藏了内部ScrollBars。加载UserControl时,我找到属于ScrollViewer的水平ScrollBar,并将所有可视属性绑定到我的外部ScrollBar。当移动我的外部ScrollBar时,我使用ScrollToHorizo​​ntalOffset将值传递给内部ScrollViewer。我还添加了一个网格分割器和一些共享列宽,因此可以调整标题大小。

这是xaml:

<Grid Grid.IsSharedSizeScope="True">
    <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" SharedSizeGroup="SharedHeader"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <ScrollViewer Grid.ColumnSpan="2" VerticalScrollBarVisibility="Auto">
        <HeaderedContentControl x:Name="HeaderedControl">
            <HeaderedContentControl.Template>
                <ControlTemplate TargetType="HeaderedContentControl">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto" SharedSizeGroup="SharedHeader" />
                            <ColumnDefinition Width="5" />
                            <ColumnDefinition Width="*" />
                        </Grid.ColumnDefinitions>
                        <ContentPresenter
                            ContentSource="Header"
                            ContentTemplate="{TemplateBinding HeaderedContentControl.HeaderTemplate}"
                            Content="{TemplateBinding HeaderedContentControl.Header}" />
                        <GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" />
                        <ScrollViewer Grid.Column="2"
                            ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}"
                            VerticalScrollBarVisibility="Hidden"
                            HorizontalScrollBarVisibility="Hidden"
                            HorizontalAlignment="Stretch"
                            VerticalAlignment="Stretch"
                            Content="{TemplateBinding ContentControl.Content}" />
                    </Grid>
                </ControlTemplate>
            </HeaderedContentControl.Template>
            <HeaderedContentControl.Header>
                <ItemsControl>
                    <Button Height="80" Content="Fixed Header 1" />
                    <Button Height="80" Content="Fixed Header 2" />
                </ItemsControl>
            </HeaderedContentControl.Header>
            <ItemsControl>
                <StackPanel Orientation="Horizontal" Height="80">
                    <Button Width="100" Content="thing" />
                    <Button Width="100" Content="thing" />
                    <Button Width="100" Content="thing" />
                    <Button Width="100" Content="thing" />
                    <Button Width="100" Content="thing" />
                </StackPanel>
                <StackPanel Orientation="Horizontal" Height="80">
                    <Button Width="100" Content="thing" />
                    <Button Width="100" Content="thing" />
                    <Button Width="100" Content="thing" />
                    <Button Width="100" Content="thing" />
                    <Button Width="100" Content="thing" />
                </StackPanel>
            </ItemsControl>
        </HeaderedContentControl>
    </ScrollViewer>
    <ScrollBar Grid.Row="1" Grid.Column="1" x:Name="ExternalScroller" Orientation="Horizontal"/>
</Grid>

以下是代码隐藏:

public partial class MainWindow : Window
{
    private ScrollViewer _internalScrollViewer;

    public MainWindow()
    {
        InitializeComponent();

        Loaded += MainWindow_Loaded;    
    }

    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        // bind the external scrollbar to the internal ScrollViewer horizontal scrollbar
        _internalScrollViewer = GetParentScrollViewer(HeaderedControl.Content as UIElement);
        var hsb = GetHorizontalScrollbar(_internalScrollViewer);

        BindingOperations.SetBinding(ExternalScroller, System.Windows.Controls.Primitives.RangeBase.LargeChangeProperty, new Binding("LargeChange") { Source = hsb, Mode = BindingMode.TwoWay });
        BindingOperations.SetBinding(ExternalScroller, System.Windows.Controls.Primitives.RangeBase.MaximumProperty, new Binding("Maximum") { Source = hsb, Mode = BindingMode.TwoWay });
        BindingOperations.SetBinding(ExternalScroller, System.Windows.Controls.Primitives.RangeBase.MinimumProperty, new Binding("Minimum") { Source = hsb, Mode = BindingMode.TwoWay });
        BindingOperations.SetBinding(ExternalScroller, System.Windows.Controls.Primitives.RangeBase.SmallChangeProperty, new Binding("SmallChange") { Source = hsb, Mode = BindingMode.TwoWay });
        BindingOperations.SetBinding(ExternalScroller, System.Windows.Controls.Primitives.RangeBase.ValueProperty, new Binding("Value") { Source = hsb, Mode = BindingMode.TwoWay });
        BindingOperations.SetBinding(ExternalScroller, System.Windows.Controls.Primitives.ScrollBar.ViewportSizeProperty, new Binding("ViewportSize") { Source = hsb, Mode = BindingMode.TwoWay });

        // forward change events to the internal ScrollViewer
        ExternalScroller.ValueChanged += ExternalScroller_ValueChanged;
        ExternalScroller.Value = hsb.Value;
    }

    private void ExternalScroller_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
    {
        _internalScrollViewer.ScrollToHorizontalOffset(e.NewValue);
    }

    private static System.Windows.Controls.Primitives.ScrollBar GetHorizontalScrollbar(ScrollViewer sv)
    {
        return FindChild<System.Windows.Controls.Primitives.ScrollBar>(sv, (sb => sb.Orientation == Orientation.Horizontal));
    }

    private static ScrollViewer GetParentScrollViewer(UIElement uiElement)
    {
        DependencyObject item = VisualTreeHelper.GetParent(uiElement);
        while (item != null)
        {
            string name = "";
            var ctrl = item as Control;
            if (ctrl != null)
                name = ctrl.Name;
            if (item is ScrollViewer)
            {
                return item as ScrollViewer;
            }
            item = VisualTreeHelper.GetParent(item);
        }
        return null;
    }

    private static T FindChild<T>(DependencyObject parent, Func<T, bool> additionalCheck) where T : DependencyObject
    {
        int childrenCount = VisualTreeHelper.GetChildrenCount(parent);
        T child;

        for (int index = 0; index < childrenCount; index++)
        {
            child = VisualTreeHelper.GetChild(parent, index) as T;

            if (child != null)
            {
                if (additionalCheck == null)
                {
                    return child;
                }
                else
                {
                    if (additionalCheck(child))
                    {
                        return child;
                    }
                }
            }
        }

        for (int index = 0; index < childrenCount; index++)
        {
            child = FindChild(VisualTreeHelper.GetChild(parent, index), additionalCheck);

            if (child != null)
            {
                return child;
            }
        }

        return null;
    }
}

最后,这是视觉效果:

enter image description here