Creating a four-way grid splitter in WPF - wpf

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:
We only need to implement the handler for the splitter that is on top (in my case that'll be the HorizontalSplitter)
The Change value in DragDeltaEventArgs is bugged, the _lastHorizontalSplitterHorizontalDragChange is a workaround
To actually "drag" the other splitter, we'll have to change the dimensions of our Column/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 stars
So, 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.

Related

Stop TextBox in ScrollViewer from growing with content

I have a ScrollViewer with HorizontalScrollBarVisibility set to "Auto" that contains a TextBox. The problem is that when a user enters text, the TextBox keeps growing in order to show the entire content. What do I need to change, so that the TextBox only grabs the available width (but is not smaller than a given minimal width)?
The horizontal scroll-bar should only appear if the available horizontal space is not sufficient for the given minimal width.
The TextBox should only grow if there is more horizontal space available.
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"/>
<ColumnDefinition Width="*" MinWidth="50"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" Text="test:"/>
<TextBox Grid.Column="1"/>
</Grid>
</ScrollViewer>
The horizontal scrollbar appears even though the MinWidth constrain is fulfilled:
This seems to be a common problem but I haven't found a satisfying solution on the net.
Here is my solution:
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50"/>
<ColumnDefinition Width="*" MinWidth="100"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" Text="test:"/>
<local:TextBoxDecorator Grid.Column="1">
<TextBox Text="content content content content content content"/>
</local:TextBoxDecorator>
</Grid>
</ScrollViewer>
c#
public class TextBoxDecorator : Decorator {
// properties
public override UIElement Child {
get {
return base.Child;
}
set {
var oldValue = base.Child;
if (oldValue != null) {
var binding = BindingOperations.GetBinding(oldValue, FrameworkElement.WidthProperty);
if ((binding != null) && (binding.Source == this))
BindingOperations.ClearBinding(oldValue, FrameworkElement.WidthProperty);
}
base.Child = value;
if ((value != null) &&
BindingOperations.GetBinding(value, FrameworkElement.WidthProperty) == null)
BindingOperations.SetBinding(
value,
FrameworkElement.WidthProperty,
new Binding() {
Source = this,
Path = new PropertyPath(FrameworkElement.ActualWidthProperty),
Mode = BindingMode.OneWay
});
}
}
// methods
protected override Size MeasureOverride(Size constraint) {
Size result = base.MeasureOverride(constraint);
if (double.IsInfinity(constraint.Width))
result.Width = (Child as FrameworkElement)?.MinWidth ?? 0.0;
return result;
}
}
Let me know if this was helpful or if you have any feedback.

WPF - Change the Button Size and Position at Runtime - Adjust to the Window

My issue is, that I want to adjust the button size and also the position to the size of my window in WPF. I got the event:
private void Window_SizeChanged_1(object sender, SizeChangedEventArgs e)
{
if (e.PreviousSize.Height > e.NewSize.Height )
{
newGameButton.Height--;
}
else if (e.PreviousSize.Height < e.NewSize.Height )
{
newGameButton.Height++;
}
if (e.PreviousSize.Width > e.NewSize.Width)
{
newGameButton.Width--;
}
else if (e.PreviousSize.Width < e.NewSize.Width)
{
newGameButton.Width++;
}
}
Is there a posibility to set some points where the button is fixed at and grows and shrinks, depending on the windowsize?
Here is one way to accomplish it. The button, by default, has HorizontalContentAlignment="Stretch" and VerticalContentAlignment="Stretch". The grid's rows and columns re-size with the window, so the button re-sizes with the grid.
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" />
</Grid>

Which cell was clicked in the Grid by coordinates?

I have a Grid with a lot of cells, some of them are empty.I want to determine on which cell was the mouse, when the MouseDown event happened.How is that possible?
The first thing to remember is that a control with a transparent background doesn't generate events for its transparent regions. Either set a color for the grid you want, or bind it to the background color of the Window the grid is in, or the events will not fire.
This code sample demonstrates a computational method for determining grid element location given a MouseMove event. The ButtonClick event arguments are very similar. The relevant methods from this sample are ColumnComputation and RowComputation, which take the position on the control and the column or row definitions, for a linear analysis. The sample operates on the InnerGrid UI Element.
Form Class:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void InnerGrid_PreviewMouseMove(object sender, MouseEventArgs e)
{
this.XCoordinate.Text = e.GetPosition(InnerGrid).X.ToString();
this.YCoordinate.Text = e.GetPosition(InnerGrid).Y.ToString();
this.ColumnPosition.Text = ColumnComputation(InnerGrid.ColumnDefinitions, e.GetPosition(InnerGrid).X).ToString();
this.RowPosition.Text = RowComputation(InnerGrid.RowDefinitions, e.GetPosition(InnerGrid).Y).ToString();
}
private double ColumnComputation(ColumnDefinitionCollection c, double YPosition)
{
var columnLeft = 0.0; var columnCount = 0;
foreach (ColumnDefinition cd in c)
{
double actWidth = cd.ActualWidth;
if (YPosition >= columnLeft && YPosition < (actWidth + columnLeft)) return columnCount;
columnCount++;
columnLeft += cd.ActualWidth;
}
return (c.Count + 1);
}
private double RowComputation(RowDefinitionCollection r, double XPosition)
{
var rowTop = 0.0; var rowCount = 0;
foreach (RowDefinition rd in r)
{
double actHeight = rd.ActualHeight;
if (XPosition >= rowTop && XPosition < (actHeight + rowTop)) return rowCount;
rowCount++;
rowTop += rd.ActualHeight;
}
return (r.Count + 1);
}
}
XAML Form:
<Window x:Name="window" x:Class="GridHitTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid Name="OuterBorder" >
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="20"/>
<RowDefinition Height="20"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" HorizontalAlignment="Center" >X Coordinate</TextBlock>
<TextBlock Grid.Column="1" HorizontalAlignment="Center" >Y Coordinate</TextBlock>
<TextBlock Grid.Column="2" HorizontalAlignment="Center" >Column</TextBlock>
<TextBlock Grid.Column="3" HorizontalAlignment="Center" >Row</TextBlock>
<TextBlock Grid.Row="1" Grid.Column="0" HorizontalAlignment="Center" Name="XCoordinate">kjahsd</TextBlock>
<TextBlock Grid.Row="1" Grid.Column="1" HorizontalAlignment="Center" Name="YCoordinate">___ahsdjf</TextBlock>
<TextBlock Grid.Row="1" Grid.Column="2" HorizontalAlignment="Center" Name="ColumnPosition">___ahsdjf</TextBlock>
<TextBlock Grid.Row="1" Grid.Column="3" HorizontalAlignment="Center" Name="RowPosition">___ahsdjf</TextBlock>
<Grid Name="InnerGrid" Margin="20,45,20,10" Grid.ColumnSpan="4" Grid.RowSpan="3" Background="{Binding Background, ElementName=window}" PreviewMouseMove="InnerGrid_PreviewMouseMove" >
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
</Grid>
</Grid>
</Window>
Place a button into the empty cells (or buttons in all cells, but style hidden when other items are present and make the cell not empty). Then when the user clicks on the cell, report the cell such as
private void OnButtonClick(object sender, RoutedEventArgs e)
{
var buttonClicked = sender as Button;
var gridRow = (int)buttonClicked.GetValue( MyGrid.RowProperty );
var gridColumn = (int)buttonClicked.GetValue( MyGrid.ColumnProperty );
}
Here is another option that seems much simpler than the suggested correct answer.
private void Grid_MouseDown(object sender, MouseButtonEventArgs e)
{
var element = (UIElement)e.Source;
int row = Grid.GetRow(element);
int column = Grid.GetColumn(element);
}

Gridsplitter prevents grid columns to be resized

I'm trying to hide first 2 columns of a Grid when a Button is clicked. My Grid layout has 3 columns, one with a grid, the second with a grid splitter and the third one with another Grid which has the Button.
When I run my program with the below code, it collapses the first 2 columns on click of the Button properly as expected and resizes the third grid, however the moment I resize the grid using the splitter, this does not work anymore. It hides the columns, however the third column is not resized to fill the Window. I want the first 2 columns to be collapsed and the third column to fill the whole area of the window(which happens if I do not resize using the splitter).
The xaml is as below:
<Grid>
<ColumnDefinition Width="Auto" x:Name="column1"/>
<ColumnDefinition Width="Auto" x:Name="column2"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" x:Name="left" MinWidth="100">
<Border Background="Red" Margin="5"/>
<TextBlock Text="A Brown fox jumped oversomething" Width="{Binding ActualWidth, ElementName=TreeView}" Margin="5"></TextBlock>
</Grid>
<GridSplitter x:Name="splitter"
Width="5"
Grid.Column="1"
HorizontalAlignment="Left"
Margin="0,5,0,5"
Panel.ZIndex="1"
VerticalAlignment="Stretch"
ResizeDirection="Columns"/>
<Grid Grid.Column="2">
<Grid Grid.Column="0" Grid.Row="0">
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="20"></RowDefinition>
</Grid.RowDefinitions>
<Border Grid.Row="0" Background="Green" Margin="5"/>
<Button Grid.Row="1" Click="OnClick">HideAndResize</Button>
</Grid>
</Grid>
</Grid>
and the Button.click event is handled as below:
private bool clicked;
private void OnClick(object sender, RoutedEventArgs e)
{
clicked = !clicked;
left.Visibility = clicked ? Visibility.Collapsed : Visibility.Visible;
splitter.Visibility = clicked ? Visibility.Collapsed : Visibility.Visible;
}
Seems like the Column isnt autosizing correctly, so its still not 0, even if it's childs Visibility is set to Collapsed.
A quick and dirty solution would be:
private bool clicked;
private double oldLenght;
private void OnClick(object sender, RoutedEventArgs e)
{
clicked = !clicked;
splitter.Visibility = clicked ? Visibility.Collapsed : Visibility.Visible;
left.Visibility = clicked ? Visibility.Collapsed : Visibility.Visible;
oldLenght = clicked ? column1.ActualWidth : oldLenght;
column1.Width = clicked ? new GridLength(0.0) : new GridLength(oldLenght);
}

WPF/Silverlight: Clipping to Grid Cell Size & RenderTransform

I've a simple Grid defined this way:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="24" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
</Grid>
In every cell of the grid (except the upperleft cell) I add a Grid or Canvas. Into these containers I add several different objects. Some of these controls can change there viewing size because of zooming in or out and scrolling.
The original code is not my own, but I made a little test program to simulate the situation:
<Grid Grid.Column="1" Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="20"/>
</Grid.RowDefinitions>
<Grid x:Name="Frame" Grid.Row="0">
<Canvas Width="200" Height="300" Background="Green" >
<Canvas x:Name="Page" Width="200" Height="300" Background="Bisque" Margin="0 -20 0 0">
<Canvas.RenderTransform>
<ScaleTransform ScaleX="{Binding ElementName=Zoom, Path=Value}"
ScaleY="{Binding ElementName=Zoom, Path=Value}"
CenterX="100" CenterY="150" />
</Canvas.RenderTransform>
</Canvas>
</Canvas>
</Grid>
<Slider x:Name="Zoom" Grid.Row="1" HorizontalAlignment="Right" Width="200"
Minimum="0.1" Maximum="2" Value="1"
TickPlacement="BottomRight" TickFrequency="0.1" IsSnapToTickEnabled="True" />
The Page is too big and goes out of range, especially when I zoom in.
I try to add a Clip, but I do not know how to set the value dynamically.
<Grid x:Name="Frame" Grid.Row="0">
<Grid.Clip>
<!-- I want to bind to the actual size of the cell -->
<RectangleGeometry Rect="0 0 480 266" />
</Grid.Clip>
<Canvas Width="200" Height="300" Background="Green" >
....
Moreover, how can I get the actual size and position of the rendered canvas. I inserted Zoom_ValueChanged to read out the values after zooming, but Width & Height are still 200 or 300, ActualWidth & ActualHeight are both zero.
Thanks in advance.
Em1, make sure you are checking the ActualWidth and ActualHeight of the canvas after your content has finished loading (i.e. after the Loaded event has been raised).
Also, one way to get the size of a canvas taking into account all of the scale transformations that have been applied to it is to walk up the visual tree and apply all scale transforms to the ActualWidth and ActualHeight of a control:
public static Size GetActualSize(FrameworkElement control)
{
Size startSize = new Size(control.ActualWidth, control.ActualHeight);
// go up parent tree until reaching root
var parent = LogicalTreeHelper.GetParent(control);
while(parent != null && parent as FrameworkElement != null && parent.GetType() != typeof(Window))
{
// try to find a scale transform
FrameworkElement fp = parent as FrameworkElement;
ScaleTransform scale = FindScaleTransform(fp.RenderTransform);
if(scale != null)
{
startSize.Width *= scale.ScaleX;
startSize.Height *= scale.ScaleY;
}
parent = LogicalTreeHelper.GetParent(parent);
}
// return new size
return startSize;
}
public static ScaleTransform FindScaleTransform(Transform hayStack)
{
if(hayStack is ScaleTransform)
{
return (ScaleTransform) hayStack;
}
if(hayStack is TransformGroup)
{
TransformGroup group = hayStack as TransformGroup;
foreach (var child in group.Children)
{
if(child is ScaleTransform)
{
return (ScaleTransform) child;
}
}
}
return null;
}
To get the position of a control, you need to find its transformation relative to the containing window. Here's how:
public static Point TransformToWindow(Visual control)
{
var hwndSource = PresentationSource.FromVisual(control) as HwndSource;
if (hwndSource == null)
return new Point(-1, -1);
Visual root = hwndSource.RootVisual; // Translate the point from the visual to the root.
GeneralTransform transformToRoot = control.TransformToAncestor(root);
return transformToRoot.Transform(new Point(0, 0));
}

Resources