MeasureOverride of custom shape - wpf

I like to create a custom Shape in WPF. It is supposed to look like a "T", a horizontal line on top as wide as possible, a vertical line in the center of the horizontal line.
Since I need the possibility to hide the left side or right side of the horizontal line, I can't set the Shape to Stretch. But for the sake of simplicity I'm skipping this possibility in my example code.
So, I create a class that inherits from Shape and overriding its DefiningGeometry property:
Public Class MyShape
Inherits Shape
Public Sub New()
MyBase.New
End Sub
Protected Overrides ReadOnly Property DefiningGeometry As Geometry
Get
Return Geometry.Parse(String.Format("M 0,0 L {0},0 M {1},0 L{1},{2}", MyBase.ActualWidth.ToString(CultureInfo.InvariantCulture),
(MyBase.ActualWidth / 2).ToString(CultureInfo.InvariantCulture),
MyBase.ActualHeight.ToString(CultureInfo.InvariantCulture)))
End Get
End Property
End Class
If I put this custom Shape in a Grid like this:
<Grid>
<local:MyShape Stroke="Black" StrokeThickness="2" />
</Grid>
it only seems to work like expected. If I make my Window wider, my vertical line moves to the right, staying in the center of the horizontal line (like expected). But if I make my Window smaller again, the vertical line stays in place, it's not moving to the left, and so it's not staying in the center of my horizontal line.
A little bit of googling brought me to the solution of this problem, I just have to override the MeasureOverride function and return the constraint parameter.
Protected Overrides Function MeasureOverride(constraint As Size) As Size
Return constraint
End Function
Now the vertical line stays in the center of the horizontal line, no matter how I resize my Window.
As long as I use this custom Shape stand-alone (like in my test window), everything works fine.
But now I wanted to use this custom Shape in a Template of another Control and when starting my program I get the following InvalidOperationException:
Layout measurement override of element 'MyShape' should not return PositiveInfinity as its DesiredSize, even if Infinity is passed in as available size.
When I'm debugging, I see that in the constraint parameter of the MeasureOverride function Width and Height are both Infinity, so returning the constraint parameter causes this error to appear.
Since I want my Shape to be as wide and high as possible, I don't know what measures to return instad of the Infinity constraint.
You can experience this problem when you expand the Grid with a Row with Auto height.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<local:MyShape Stroke="Black" StrokeThickness="2" />
</Grid>
I assume the problem is, that there's no one to decide how high my Shape is going to be. The Row height is set to Auto, and the Shape wants to be as high as possible. But shouldn't 'as high as possible' be the height of the container control, in my case the Window?
Is there a way to get around this problem in a way that this control works 'on its own', without relying on the parent control to provide the right settings?

Related

How to use ScrollViewer.ScrollToVerticalOffset?

I hope this isn't a duplicate but I can't find any documentation or examples on how to actually use ScrollToVerticalOffset(). I'm using it in a Windows Phone 8 app, but I think it will still apply to WP7 and Silverlight (although, feel free to correct me if I'm wrong).
So here is my basic set up (pseudo-code from memory):
<phone.PivotItem>
<ScrollViewer>
<Grid Height="1500">
<Grid.RowDefinitions>
<!-- about 20 rows, all auto-height -->
</Grid.RowDefinitions>
<Border Grid.Row="0">
<TextBox x:Name="txt1" />
</Border>
<Border Grid.Row="1">
<TextBox x:Name="txt2" />
</Border>
<!-- ...... -->
<Border Grid.Row="19">
<TextBox x:Name="txt20" />
</Border>
</Grid>
</ScrollViewer>
</phone.PivotItem>
So as you can see, I've got a ScrollViewer within a PivotItem, and inside is a Grid. In the Grid there are about 20 TextBoxs, each within a Border. I am dynamically setting focus to one of these TextBoxs when this page loads, so anytime I set focus to TextBox #6-20 (roughly) - I have to manually scroll down to see it. I want to auto-scroll my ScrollViewer so that whichever TextBox has focus, it will be centered for the user to see.
The documentation for ScrollToVerticalOffset() says:
Scrolls the content that is within the ScrollViewer to the specified
vertical offset position.
And that it accepts a type of System.Double.
What I don't understand is A) the value I'm supposed to pass, and B) how I could even get that value? Is it supposed to be a number between 0 and the height of my Grid (1500)? If so, how could I determine the position of any given TextBox so I can scroll to it?
If there are any straightforward examples, please feel free to link out to them. I'm not sure if the content within the ScrollViewer matters when calling this method, but in case it does I wanted to show exactly how I'm using it.
Many thanks in advance!
You can see any UIElement's position relative to another UIElement using the UIElement.TransformToVisual call.
First, get the transform between the TextBox and ScrollViewer.
GeneralTransform transform = textBox.TransformToVisual(scrollViewer);
Then, figure out what point (0,0) of the TextBox is relative to the ScrollViewer. Meaning, the TextBox origin (0,0) is located at what ScrollViewer position.
Point textBoxPosition = transform.Transform(new Point(0, 0));
Now that you know the Y position of the TextBox relative to the ScrollViewer, scroll to that Y offset.
scrollViewer.ScrollToVerticalOffset(textBoxPosition.Y);
Good luck!
This is a very old post, but the meaning of VerticalOffset varies.
Most of the solutions I have seen assume VeritcalOffset is in pixels. This is not always the case.
From: https://learn.microsoft.com/en-us/dotnet/api/system.windows.controls.scrollviewer.extentheight
If CanContentScroll is true, the values of the ExtentHeight, ScrollableHeight,
ViewportHeight, and VerticalOffset properties are number of items. If
CanContentScroll is false, the values of these properties are Device Independent Pixels.

WPF scrolling on ScaleTransform issue, multiple UI elements

Also asked at http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/f765d4c9-1719-4757-b467-2492d87bb4ab
All,
I have a slider that performs a scale-transform in my WPF App. Now I'm running into an issue where I have an element inside of a scrollviewer inside of a row that has Height="*" and another element in a subsequent row that has Height="Auto".
The goal is that we don't know the row heights until run-time (or even how big the elements will be), but that we will have both elements displayed on the screen, with the first element taking up as much space as it can and the bottom element always being visible, taking up whatever space it needs.
I have the following problem/solution statement (which I think best describes my issue) and, as you can see, I'm stuck at what to do when I want to achieve this goal and still allow access to all UI elements should the zoom be large enough to push one of the elements off the screen (hopefully that made sense).
Problem statement:
ScaleTransform is causing top DG to disappear where RowDefinition Height="*"
Solution:
Set Minimum height on RowDefinition to prevent DG from disappearing.
Problem statement:
Setting Minimum height on RowDefinitoin causes lower rows to dissapear.
Solution:
Add scrollviewer encapsulating grid.
Problem statement:
Since top DG is in RowDefinition Height="*", if there is a lot of data in top DG, bottom DGs cannot be seen without scrolling.
Solution statement:
???
Here's my current code:
<Grid.RowDefinitions>
<RowDefinition Height="*" MinHeight="120" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<GroupBox Grid.Row="0">
<DataGrid ItemsSource="{Binding Path=SomePathThatCouldHaveLotsOfData}">
<!--Note that DataGrid implements its own scrollviewer as long as it's not surrounded by another scrollviewer-->
<!--...-->
</DataGrid>
</GroupBox>
<GroupBox Grid.Row="1">
<!--...-->
</GroupBox>
Hopefully that all made sense what my issue is and what I'm trying to do. Any ideas how I can get this done (hopefully in a clean way). I'm using MVVM if it helps.
EDIT: I should point out that I also won't know any max-height until run-time (and max-height is probably irrelevant anyway since we'll be using scale-transform).
All,
I managed to acheive my goal, but it was a bit of an ugly hack.
Basically, I had to trap the Loaded and SizeChanged event of my UserControl/Page/Window and use these events to set the ScrollViewer.ViewPortHeight value to a property in my VM. Then, it was simply a matter of subtracting a known value from this to use as my Max value. Then, it was simply a matter of multiplying the ViewPortHeight by the percentage that I wanted each UI element to take up on the screen and bind the UIs element to that.
Hopefully this will help someone else having the same business requirement.

Positioning an element inside the Canvas by its center (instead of the top left corner) using only XAML in WPF

The common question about positioning an element inside a Canvas is "How to position the center of element (instead of the top left corner)".
WPF: Resizing a circle, keeping the center point instead of TopLeft?
WPF Center Ellipse at X, Y
WPF element positioning on a Canvas
Several solutions are presented, but they all have drawbacks.
The easiest solution is to accommodate the element size during while setting the Canvas.Left and Canvas.Top properties programmatically. This works, but only once. This solution doesn't support bindings and it will break when the element size is changed. You also cannot set the Canvas.Left or Canvas.Top using
Another set of solutions involve translate transformations utilizing either RenderTransform or Margin. These solutions require binding some property to the -0.5 * Width or -0.5 * Height. Such binding requires creating a custom ValueConverter and is impossible to create using only XAML.
So, is there a simple way to position an element inside canvas so that its Canvas.Left and Canvas.Top correspond to the element's center and both size and position properties can be bound to some other properties?
XAML and bindings seem very powerful, but sometimes there are simple problems that require very complex solutions. In my bindings library creating such binding would be as easy as writing element.Center = position or element.TopLeft = position - element.Size / 2, but don't let me get carried away.
I've found a very simple solution which uses only XAML and supports binding both size and position properties of the element. It seems that when the WPF control with alignment set too Stretch or Center is placed inside the canvas, the element "gravitates" towards centering as the (Canvas.Left, Canvas.Top) point (the state that we desire), but is stopped by the "angle plate" placed at the same (Canvas.Left, Canvas.Top) point. How do I know about this "gravitation"? It's evident when you ease the block by setting the Margin of the element to a negative value. Setting the negative margin allows the element to move towards its center goal. The element moves until the Margin reaches (-Height / 2, -Width / 2) so that the element becomes perfectly centered at the (Canvas.Left, Canvas.Top) point. Further changes don't cause any movement since the element is already perfectly positioned.
Solution: set Margin="-1000000".
So in the following code the ellipses are both centered at the (200, 200) point. The first ellipse has Left and Top properties corresponding to the ellipse center allowing to easily bind them with some other objects' properties.
<Canvas>
<Ellipse Width="100" Height="100" Canvas.Left="200" Canvas.Top="200" Opacity="0.5" Fill="Red" Margin="-100000" />
<Ellipse Width="100" Height="100" Canvas.Left="150" Canvas.Top="150" Opacity="0.5" Fill="Blue" />
</Canvas>
The bad thing is this solution only work in WPF. Silverlight and WinRT don't have the described behavior.
Even simpler, at least for Shapes, would be to use a Path with an appropriate Geometry:
<Canvas>
<Path Canvas.Left="200" Canvas.Top="200" Fill="Red" Opacity="0.5">
<Path.Data>
<EllipseGeometry RadiusX="50" RadiusY="50"/>
</Path.Data>
</Path>
</Canvas>
I solved this by putting the canvas into the bottom-right cell of a 2x2 grid. That makes the 0,0 coordinate the center of the grid, and then you can do all your drawing relative to that.
This solution worked for me Center text at a given point on a WPF Canvas. He uses a MultiBinding and a custom Converter to adjust the margin on his element. Brilliant!

How do I constrain a container's height to the height of a specific content element?

I'm trying to do something which seems like it should be extremely simple and yet I can't see how. I have a very simple layout, a TextBox with an image next to it, similar to the way it might look adorned with an ErrorProvider in a WinForms application. The problem is, I want the image to be no higher than the TextBox it's next to. If I lay it out like this, say:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBox Grid.Row="0" Grid.Column="0" MinWidth="100"/>
<Image Grid.Row="0" Grid.Column="1" Source="error.png" />
</Grid>
the row will size to the height of the image if the image is taller than the TextBox. This also happens if I use a DockPanel or StackPanel.
The naive solution would be to bind the Height to the TextBox's ActualHeight. I'm sure this is wrong. But what's right?
Edit
Here's an example of what looks wrong to me: In both of these layouts (which are both horizontal StackPanels), the FontSize is the only variable:
You can see that the first TextBox is constrained to the height of the icon, and as a result has an unnecessary bottom padding under the text. And the icon next to the second is out of scale to the TextBox it's next to.
As it happens, I found a completely different (and much better) way to approach the problem - originally I was scaling my layout by changing the FontSize on the Window, but using a ScaleTransform is a whole lot easier and seems to work perfectly. But even so, it still seems odd to me that it's so hard to do this.
Name your TextBox, reference the TextBox from the Image as follows.
<TextBox Name="myTextBox"
Grid.Row="0" Grid.Column="0"
MinWidth="100"/>
<Image Grid.Row="0"
Grid.Column="1"
Source="error.png"
Height="{Binding ActualHeight, ElementName=myTextBox}"/>
You want a layout algorithm that measures the other elements with a height constraint equal to the desired height of a specific one. While several of the existing Panel implementations will reduce the available space for remaining elements based on the size used by previous ones, none of them will set the constraint the way you want. If you want the behavior you describe in a single layout pass, you will need to write your own layout algorithm.
For example, you can get close by overriding the behavior of StackPanel like this:
public class SizeToFirstChildStackPanel
: StackPanel
{
protected override Size MeasureOverride(Size constraint)
{
if (Children.Count > 0)
{
var firstChild = Children[0];
firstChild.Measure(constraint);
if (Orientation == Orientation.Horizontal)
{
constraint = new Size(
constraint.Width,
Math.Min(firstChild.DesiredSize.Height, constraint.Height));
}
else
{
constraint = new Size(
Math.Min(firstChild.DesiredSize.Width, constraint.Width),
constraint.Height);
}
}
return base.MeasureOverride(constraint);
}
}
This will constrain the height of all children of the StackPanel to the desired height of the first one (or width if the panel is oriented vertically).
It's still not ideal because the first child will get measured a second time by the base class MeasureOverride, and the second measure will have a constraint. This is extra work, and will cause odd behavior if the first child wants to get larger.
To do it right you would need to implement the entire MeasureOverride method method yourself. I'm not going to do that here because it would be a lot of code that isn't really relevant to the problem, and it depends on how exactly you want the layout to work. The important point is to measure the specific element first and use its DesiredSize to determine the availableSize when you call Measure on the others.

WPF Grid and DataGrid sizing

The UserControl I'm trying to work with is essentially laid out like so:
<Grid>
<Grid.RowDefinitions>
<Row Height="*"/>
<Row Height="*"/>
</Grid.RowDefinitions>
<wpftoolkit:DataGrid x:Name="PrimaryGrid"/> <!-- ~10 rows -->
<Border>
<Grid>
<Grid.RowDefinitions>
<Row Height="*"/>
<Row Height="*"/>
</Grid.RowDefinitions>
<wpftoolkit:DataGrid x:Name="SecondaryGrid"/> <!-- ~2 rows -->
<wpftoolkit:DataGrid x:Name="OtherGrid"/> <!-- ~50 rows -->
</Grid>
</Border>
</Grid>
This UserControl is then placed in my Window as the last member of a Grid, the only one with Height="*". I'm having problems with the way the DataGrids are sized.
If I set VerticalAlignment of the UserControl to Stretch in the Window, then PrimaryGrid gets 1/2 height of the UserControl, and each of the two inside the Border get 1/4. They are sized like this regardless of the number of rows each have, leaving OtherGrid with too little vertical space and the others with non-row whitespace inside the scrollview.
If I set VerticalAlignment to Top, the grids seem to size pretty well to their contents, except there is an inexplicable whitespace being left at the bottom of the UserControl. I used Snoop and the RowDefinition for the UserControl has the proper ActualHeight, but the UserControl only uses a portion of it - 80% or so.
I don't really mind whether I fix the Stretch case (How do I make the DataGrid not stretch larger than its number of rows?) or the Top case (How do I make the UserControl use all the space it has available to it?)
Summary: Use Stretch for the UserControl, but Auto (instead of *) for the row heights inside your UserControl.
Explanation: "Auto" means: as much space as needed (which is what you want), whereas "*" means: a proportional share of all available space (resulting in the 1/2, 1/4, 1/4-distribution).
Since you want the UserControl to use all available space, Stretch is the correct option (it means exactly that). Set one of the row heights inside the UserControl back to "*", if you want this row to take up the remaining available space.
This is a common problem where what you really want is 2 completely different layout behaviors: Auto sizing when there's room for all three, * sizing when there isn't. Some quick fixes you can try out with limitations:
Auto sizing (as already mentioned)
DockPanel with each set to Dock=Top - this will have a similar effect to VerticalAlignment=Top but the last DataGrid (only 1) will stretch out to fill the remaining space. Also bad if the first or second take up more space than exists because they'll push the others out.
Set MinHeight/MaxHeight in combination with one of the other 2 changes on your DataGrids to keep them from getting out of control. This gives up some of the auto-layout flexibility in exchange for making sure everything shows up.
Beyond those you can try something more complex like creating a custom Panel (or find one that someone else made already), or creating a MultiValueConverter that can calculate appropriate Height (or MinHeight, MaxHeight) settings for each DG or Row based on the height of the UC and each of the DGs.
Well, here's what I ended up with. It does what I want from a layout point of view, mostly. A bit more code behind than I'd like, but oh well.
In my DataContextChanged event handler:
//for each grid
_reportObserver = new PropertyObserver<ItemCollection>(PrimaryGrid.Items)
.RegisterHandler(c => c.Count, c => UpdateMaxHeight(PrimaryGrid));
UpdateMaxHeight(PrimaryGrid);
PropertyObserver is from http://joshsmithonwpf.wordpress.com/2009/07/11/one-way-to-avoid-messy-propertychanged-event-handling/
//Lots of ugly hard-coding
private void UpdateMaxHeight(DataGrid grid)
{
double header_height = grid.ColumnHeaderHeight;
if (double.IsNaN(header_height))
header_height = 22;
double margin_height = grid.Margin.Bottom + grid.Margin.Top;
grid.MaxHeight = header_height + margin_height + grid.Items.Count * (grid.RowHeight+2);
UpdateLayout(); //this is key for changes to number of items at runtime
}
Even after setting the DataGrid's MaxHeight, things were still ugly, so I had to set the max height on the RowDefinition's too. But that still wasn't right, causing the margin_height addition above.
<RowDefinition Height="*" MaxHeight="{Binding ElementName=PrimaryGrid, Path=MaxHeight}"/>
At some point, I'll take into account my optionally visible row details in my ugly max height code.
As far as Top vs Stretch, I ended up for other reasons having the usercontrol in a ListView. Everything sizes nicely now.
Thanks again for looking at my problem.
Just wanted to follow up on this problem. Over time, the solution I provided above just did not meet users expectations. I've now changed to a scheme like this:
<ScrollViewer VerticalScrollBarVisibility="Auto">
<Grid>
<!-- row definitions -->
<KentBoogart's Resizer ResizeDirection="South">
<DataGrid/>
</kb:Resizer>
<kb:Resizer ResizeDirection="South">
<DataGrid/>
</kb:Resizer>
</Grid>
</ScrollViewer>
In another place where I've used this idiom, I've set the Resizer to have a MaxHeight bound to the ScrollViewer's ActualHeight to keep it from going out of control. This design can be a little confusing with the overall scrollbar, plus scrollbars in the DataGrid, but with good borders and margins, it's not too bad.

Resources