In my WPF app, I have four separate quadrants, each with it's own grid and data. The four grids are separated by GridSplitters. The GridSplitters allow the user to resize each box by selecting either a horizontal or vertical splitter.
I am trying to allow the user to resize the grids by selecting the center point (circled in red).
I expected to have a four-way mouse pointer that could be used to drag up, down, left, and right. But, I only have the option to move windows up and down... or left and right.
What I've tried:
<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>
Let me begin by changing your XAML a little bit, since right now we have four distinct GridSplitters
, but two is enough:
<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>
What is more important about this markup is that we now have an intersection point between two splitters:
In order to drag two splitters at a time, we need to know when should we. For that purpose, let's define a Boolean
flag:
public partial class View : Window
{
private bool _mouseIsDownOnBothSplitters;
}
We need to update the flag whenever the user clicks on either of the splitters (note that Preview
events are used - GridSplitter
implementation marks Mouse
events as Handled
):
void UpdateMouseStatusOnSplittersHandler(object sender, MouseButtonEventArgs e)
{
UpdateMouseStatusOnSplitters(e);
}
VerticalSplitter.PreviewMouseLeftButtonDown += UpdateMouseStatusOnSplittersHandler;
HorizontalSplitter.PreviewMouseLeftButtonDown += UpdateMouseStatusOnSplittersHandler;
VerticalSplitter.PreviewMouseLeftButtonUp += UpdateMouseStatusOnSplittersHandler;
HorizontalSplitter.PreviewMouseLeftButtonUp += UpdateMouseStatusOnSplittersHandler;
The UpdateMouseStatusOnSplitters
is the core method here. WPF does not provide multiple layer hit testing "out of the box", 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;
}
Now we can implement the concurrent dragging. This will be done via a handler for DragDelta
. However, there are a few caveats:
HorizontalSplitter
)Change
value in DragDeltaEventArgs
is bugged, the _lastHorizontalSplitterHorizontalDragChange
is a workaroundColumn/RowDefinitions
. In order to avoid weird clipping behavior (the splitter dragging the column/row with it), we'll have to use the size of it in pixels as the the size of it in starsSo, with that out of the way, here's the relevant handler:
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;
}
}
Now, this is almost a full solution. It, however, suffers from the fact that even if you move your mouse horizontally outside of the grid, the VerticalSplitter
still moves with it, which is inconsistent with the default behavior. In order to counteract this, let's add this check to the handler's code:
if (_mouseIsDownOnBothSplitters)
{
var mousePositionRelativeToGrid = Mouse.GetPosition(SplitGrid);
if (mousePositionRelativeToGrid.X > 0 && mousePositionRelativeToGrid.X < SplitGrid.ActualWidth)
{
//The rest of the handler's code
}
}
Finally, we need to reset our _lastHorizontalSplitterHorizontalDragChange
to zero when the dragging is over:
HorizontalSplitter.DragCompleted += (o, e) => _lastHorizontalSplitterHorizontalDragChange = 0;
I hope it is not too daring of me to leave the implementation of the cursor's image change to you.