How does WPF DataGrid get frozen rows/columns working?

前端 未结 2 695
再見小時候
再見小時候 2020-12-11 17:16

I created a user control based on Grid (not DataGrid), which is wrapped in a ScrollViewer. Now I would like to have frozen rows/columns capability just like in DataGrid, but

相关标签:
2条回答
  • 2020-12-11 18:01

    After having this problem by myself I want to share what I've found out so far.

    DataGrid uses two different methods for that.


    First: The RowHeader


    This is the simplified Template for DataGridRow:

    <Border x:Name="DGR_Border" ... >
        <SelectiveScrollingGrid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
    
            <Grid.RowDefinitions>
                <RowDefinition Height="*"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
    
            <DataGridRowHeader Grid.RowSpan="2"
                SelectiveScrollingGrid.SelectiveScrollingOrientation="Vertical" ... />
    
            <DataGridCellsPresenter Grid.Column="1" ... />
    
            <DataGridDetailsPresenter Grid.Column="1" Grid.Row="1"
                SelectiveScrollingGrid.SelectiveScrollingOrientation="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGrid}},
                                                                               Path=AreRowDetailsFrozen, Converter={x:Static DataGrid.RowDetailsScrollingConverter},
                                                                               ConverterParameter={x:Static SelectiveScrollingOrientation.Vertical}}" ... />
        </SelectiveScrollingGrid>
    </Border>
    

    As you can see DataGrid uses the SelectiveScrollingOrientation attached property to hold the RowHeader in position. If this property is set (or changing) it creates an adapted TranslateTransform bound to the parental ScrollViewer Offset for the element. See the details in source code.


    Second: The FrozenColumns


    This stuff takes place in DataGridCellsPanel ArrangeOverride(). It uses a private ArrangeState class "to maintain state between arrange of multiple children".

    private class ArrangeState
    {
        public ArrangeState()
        {
            FrozenColumnCount = 0;
            ChildHeight = 0.0;
            NextFrozenCellStart = 0.0;
            NextNonFrozenCellStart = 0.0;
            ViewportStartX = 0.0;
            DataGridHorizontalScrollStartX = 0.0;
            OldClippedChild = null;
            NewClippedChild = null;
        }
    
        public int FrozenColumnCount { get; set; }
        public double ChildHeight { get; set; }
        public double NextFrozenCellStart { get; set; }
        public double NextNonFrozenCellStart { get; set; }
        public double ViewportStartX { get; set; } 
        public double DataGridHorizontalScrollStartX { get; set; }
        public UIElement OldClippedChild { get; set; }
        public UIElement NewClippedChild { get; set; }
    } 
    

    After initializing the state with

    private void InitializeArrangeState(ArrangeState arrangeState)
    {
        DataGrid parentDataGrid = ParentDataGrid;
        double horizontalOffset = parentDataGrid.HorizontalScrollOffset;
        double cellsPanelOffset = parentDataGrid.CellsPanelHorizontalOffset;
        arrangeState.NextFrozenCellStart = horizontalOffset;
        arrangeState.NextNonFrozenCellStart -= cellsPanelOffset;
        arrangeState.ViewportStartX = horizontalOffset - cellsPanelOffset;
        arrangeState.FrozenColumnCount = parentDataGrid.FrozenColumnCount;
    }
    

    it calls

    ArrangeChild(children[childIndex] as UIElement, i, arrangeState);
    

    for all realized childs and calculates the estimated width for non realized childs/columns.

    double childSize = GetColumnEstimatedMeasureWidth(column, averageColumnWidth);
    arrangeState.NextNonFrozenCellStart += childSize;
    

    At the end the values will be set in the appropriate fields in DataGrid.

    private void FinishArrange(ArrangeState arrangeState)
    {
        DataGrid parentDataGrid = ParentDataGrid;
    
        // Update the NonFrozenColumnsViewportHorizontalOffset property of datagrid
        if (parentDataGrid != null)
        {
            parentDataGrid.NonFrozenColumnsViewportHorizontalOffset = arrangeState.DataGridHorizontalScrollStartX;
        }
    
        // Remove the clip on previous clipped child
        if (arrangeState.OldClippedChild != null)
        {
            arrangeState.OldClippedChild.CoerceValue(ClipProperty);
        }
    
        // Add the clip on new child to be clipped for the sake of frozen columns.
        _clippedChildForFrozenBehaviour = arrangeState.NewClippedChild;
        if (_clippedChildForFrozenBehaviour != null)
        {
            _clippedChildForFrozenBehaviour.CoerceValue(ClipProperty);
        }
    }
    

    The details for ArrangeChild(UIElement child, int displayIndex, ArrangeState arrangeState) you can find from line 1470 in source code.


    Conclusion


    It's not as simple making columns are frozen. Even though this will work (apart from clipping and scrollbar over whole width)

    <ListView ItemsSource="some rows">
        <ListView.ItemTemplate>
            <DataTemplate>
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition />
                        <ColumnDefinition />
                    </Grid.ColumnDefinitions>
                    <TextBlock Grid.Column="0" Text="Fixed"
                               Background="LightBlue" Width="300"
                               SelectiveScrollingGrid.SelectiveScrollingOrientation="Vertical" />
                    <TextBlock Grid.Column="1" Text="Scrolled"
                               Background="LightGreen" Width="300" />
                </Grid>
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
    

    this will not:

    <ScrollViewer HorizontalScrollBarVisibility="Auto">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <TextBlock Grid.Column="0" Text="Fixed"
                       Background="LightBlue" Width="300"
                       SelectiveScrollingGrid.SelectiveScrollingOrientation="Vertical" />
            <TextBlock Grid.Column="1" Text="Scrolled"
                       Background="LightGreen" Width="300" />                    
        </Grid>
    </ScrollViewer>
    

    The reason is that DataGridHelper.FindVisualParent<ScrollViewer>(element) (see from line 149 in souce code) in SelectiveScrollingOrientation attached property fails. Maybe you find workarounds e.g. create your own attached property with a copy of the original code but get the ScrollViewer by name. Otherwise I think you have to do many things from scratch.

    0 讨论(0)
  • 2020-12-11 18:12

    Datagrid Column and Row has a property called "Frozen"

    if you want to freeze a column i recommend you do the following

    either you want it on selected Row or Column Event and then on the Event Get the Column/Row and mark it as Frozen = true

    or create another button or a context menu on mouse right click on which you Freeze/Unfreeze the currently marked

    column/row

    hope this helps

    0 讨论(0)
提交回复
热议问题