在 WPF 中创建四向网格拆分器

Creating a four-way grid splitter in WPF

在我的 WPF 应用程序中,我有四个独立的象限,每个象限都有自己的网格和数据。四个网格由 GridSplitter 分隔。 GridSplitter 允许用户通过选择水平或垂直拆分器来调整每个框的大小。

我试图让用户通过选择中心点红色圆圈)来调整网格大小。

我希望有一个四向鼠标指针,可以用来上下左右拖动。但是,我只能选择上下移动 windows...或左右移动。


我尝试过的:

<Grid> <!-- Main Grid that holds A, B, C, and D -->
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="5"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="5"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

        <Grid x:Name="gridA" Grid.Column="0" Grid.Row="0"/>
        <GridSplitter Grid.Column="0" Grid.Row="1" Height="5" HorizontalAlignment="Stretch"/>

        <Grid x:Name="gridC" Grid.Column="2" Grid.Row="0"/>
        <GridSplitter Grid.Column="3" Grid.Row="1" Height="5" HorizontalAlignment="Stretch"/>

        <Grid x:Name="gridB" Grid.Column="0" Grid.Row="2"/>
        <GridSplitter Grid.Column="1" Grid.Row="0" Width="5" HorizontalAlignment="Stretch"/>

        <Grid x:Name="gridD" Grid.Column="2" Grid.Row="2"/>
        <GridSplitter Grid.Column="1" Grid.Row="2" Width="5" HorizontalAlignment="Stretch"/>
</Grid>

首先让我稍微更改一下您的 XAML,因为现在我们有四个不同的 GridSplitters,但两个就足够了:

<Grid Name="SplitGrid">    
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*"/>
        <ColumnDefinition Width="5"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    
    <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="5"/>
        <RowDefinition Height="*"/>
    </Grid.RowDefinitions>

    <Grid x:Name="GridA" Grid.Column="0" Grid.Row="0" Background="Red" />
    <Grid x:Name="GridC" Grid.Column="2" Grid.Row="0" Background="Orange" />
    <Grid x:Name="GridB" Grid.Column="0" Grid.Row="2" Background="Green" />
    <Grid x:Name="GridD" Grid.Column="2" Grid.Row="2" Background="Yellow" />

    <GridSplitter x:Name="VerticalSplitter" 
                  Grid.Column="1" 
                  Grid.Row="0" 
                  Grid.RowSpan="3"     
                  HorizontalAlignment="Stretch"
                  VerticalAlignment="Stretch" 
                  Width="5" 
                  Background="Black" />

    <GridSplitter x:Name="HorizontalSplitter" 
                  Grid.Column="0" 
                  Grid.Row="1" 
                  Grid.ColumnSpan="3" 
                  Height="5" 
                  HorizontalAlignment="Stretch" 
                  Background="Black" />
</Grid>

这个标记更重要的是我们现在在两个拆分器之间有一个交点:

为了一次拖动两个分离器,我们需要知道什么时候应该。为此,让我们定义一个 Boolean 标志:

public partial class View : Window
{
    private bool _mouseIsDownOnBothSplitters;
}

每当用户点击其中一个拆分器时,我们都需要更新标志(请注意,使用了 Preview 事件 - GridSplitter 实现将 Mouse 事件标记为 Handled):

void UpdateMouseStatusOnSplittersHandler(object sender, MouseButtonEventArgs e)
{
    UpdateMouseStatusOnSplitters(e);
}

VerticalSplitter.PreviewMouseLeftButtonDown += UpdateMouseStatusOnSplittersHandler;
HorizontalSplitter.PreviewMouseLeftButtonDown += UpdateMouseStatusOnSplittersHandler;

VerticalSplitter.PreviewMouseLeftButtonUp += UpdateMouseStatusOnSplittersHandler;
HorizontalSplitter.PreviewMouseLeftButtonUp += UpdateMouseStatusOnSplittersHandler;

UpdateMouseStatusOnSplitters是这里的核心方法。 WPF 不提供“开箱即用”的多层命中测试,so we'll have to do a custom one:

private void UpdateMouseStatusOnSplitters(MouseButtonEventArgs e)
{    
    bool horizontalSplitterWasHit = false;
    bool verticalSplitterWasHit = false;

    HitTestResultBehavior HitTestAllElements(HitTestResult hitTestResult)
    {
        return HitTestResultBehavior.Continue;
    }

    //We determine whether we hit our splitters in a filter function because only it tests the visual tree 
    //HitTestAllElements apparently only tests the logical tree
    HitTestFilterBehavior IgnoreNonGridSplitters(DependencyObject hitObject)
    {
        if (hitObject == SplitGrid)
        {
            return HitTestFilterBehavior.Continue;
        }

        if (hitObject is GridSplitter)
        {
            if (hitObject == HorizontalSplitter)
            {
                horizontalSplitterWasHit = true;

                return HitTestFilterBehavior.ContinueSkipChildren;
            }
            if (hitObject == VerticalSplitter)
            {
                verticalSplitterWasHit = true;

                return HitTestFilterBehavior.ContinueSkipChildren;
            }
        }

        return HitTestFilterBehavior.ContinueSkipSelfAndChildren;
    }

    VisualTreeHelper.HitTest(SplitGrid, IgnoreNonGridSplitters, HitTestAllElements, new PointHitTestParameters(e.GetPosition(SplitGrid)));

    _mouseIsDownOnBothSplitters = horizontalSplitterWasHit && verticalSplitterWasHit;
}

现在我们可以实现并发拖动了。这将通过 DragDelta 的处理程序完成。但是,有一些注意事项:

  1. 我们只需要为顶部的分离器实现处理程序(在我的例子中是 HorizontalSplitter
  2. DragDeltaEventArgs is bugged 中的 Change 值,_lastHorizontalSplitterHorizontalDragChange 是一种解决方法
  3. 要真正“拖动”另一个分离器,我们必须更改 Column/RowDefinitions 的尺寸。为了避免奇怪的剪辑行为(拆分器拖动 column/row),we'll have to use the size of it in pixels as the the size of it in stars

所以,除了这个,这里是相关的处理程序:

private void HorizontalSplitter_DragDelta(object sender, DragDeltaEventArgs e)
{
    if (_mouseIsDownOnBothSplitters)
    {
        var firstColumn = SplitGrid.ColumnDefinitions[0];
        var thirdColumn = SplitGrid.ColumnDefinitions[2];

        var horizontalOffset = e.HorizontalChange - _lastHorizontalSplitterHorizontalDragChange;

        var maximumColumnWidth = firstColumn.ActualWidth + thirdColumn.ActualWidth;

        var newProposedFirstColumnWidth = firstColumn.ActualWidth + horizontalOffset;
        var newProposedThirdColumnWidth = thirdColumn.ActualWidth - horizontalOffset;

        var newActualFirstColumnWidth = newProposedFirstColumnWidth < 0 ? 0 : newProposedFirstColumnWidth;

        var newActualThirdColumnWidth = newProposedThirdColumnWidth < 0 ? 0 : newProposedThirdColumnWidth;

        firstColumn.Width = new GridLength(newActualFirstColumnWidth, GridUnitType.Star);
        thirdColumn.Width = new GridLength(newActualThirdColumnWidth, GridUnitType.Star);

        _lastHorizontalSplitterHorizontalDragChange = e.HorizontalChange;
    }
}

现在,这是几乎一个完整的解决方案。然而,它的缺点是即使您将鼠标水平移动到网格之外,VerticalSplitter 仍会随之移动,这与默认行为不一致。为了抵消这一点,让我们将此检查添加到处理程序的代码中:

if (_mouseIsDownOnBothSplitters)
{
   var mousePositionRelativeToGrid = Mouse.GetPosition(SplitGrid);
   if (mousePositionRelativeToGrid.X > 0 && mousePositionRelativeToGrid.X < SplitGrid.ActualWidth)
   {
       //The rest of the handler's code
   }
}

最后,当拖动结束时,我们需要将我们的_lastHorizontalSplitterHorizontalDragChange重置为零:

HorizontalSplitter.DragCompleted += (o, e) => _lastHorizontalSplitterHorizontalDragChange = 0;

我希望把光标图像更改的实现留给你,不要太大胆。