在 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
的处理程序完成。但是,有一些注意事项:
- 我们只需要为顶部的分离器实现处理程序(在我的例子中是
HorizontalSplitter
)
-
DragDeltaEventArgs
is bugged 中的 Change
值,_lastHorizontalSplitterHorizontalDragChange
是一种解决方法
- 要真正“拖动”另一个分离器,我们必须更改
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;
我希望把光标图像更改的实现留给你,不要太大胆。
在我的 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
的处理程序完成。但是,有一些注意事项:
- 我们只需要为顶部的分离器实现处理程序(在我的例子中是
HorizontalSplitter
) -
DragDeltaEventArgs
is bugged 中的Change
值,_lastHorizontalSplitterHorizontalDragChange
是一种解决方法 - 要真正“拖动”另一个分离器,我们必须更改
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;
我希望把光标图像更改的实现留给你,不要太大胆。