WPF UserControl is not drawn when overriding MeasureOverride and ArrangeOverride - wpf

I have a UserControl looking like this:
<UserControl
MaxHeight="32"
MaxWidth="32"
MinHeight="25"
MinWidth="25">
<DockPanel>
<!-- some stuff -->
</DockPanel>
</UserControl>
In addition to the min/max size constraint, I want the control always being painted with Width = Height. So i override MeasureOverride and ArrangeOverride:
protected override Size MeasureOverride(Size availableSize)
{
var resultSize = new Size(0, 0);
((UIElement)Content).Measure(availableSize);
var sideLength = Math.Min(((UIElement)Content).DesiredSize.Width, ((UIElement)Content).DesiredSize.Height);
resultSize.Width = sideLength;
resultSize.Height = sideLength;
return resultSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
((UIElement)Content).Arrange(new Rect(0, 0, finalSize.Width, finalSize.Height));
return finalSize;
}
I understand that I must call Measure and Arrange on every child of the UserControl. Since the DocPanel is the only child of my UserControl and (in my understanding) is stored in the Content property of the UserControl, I simply call Measure and Arrange on this Content property. However the UserControl is not displayed. What am I doing wrong?

Depending on how you are hosting your UserControl, the value returned from the Measure phase may not be used. If you have it setup in a Grid with star rows/columns or a DockPanel, then the final size may be completely different.
You would need to apply similar logic to the arrange phase, so it will effectively ignore any extra space it's given.
The following code should work, and is a bit cleaner:
protected override Size MeasureOverride(Size availableSize) {
var desiredSize = base.MeasureOverride(availableSize);
var sideLength = Math.Min(desiredSize.Width, desiredSize.Height);
desiredSize.Width = sideLength;
desiredSize.Height = sideLength;
return desiredSize;
}
protected override Size ArrangeOverride(Size finalSize) {
var sideLength = Math.Min(this.DesiredSize.Width, this.DesiredSize.Height);
return base.ArrangeOverride(new Size(sideLength, sideLength));
}

Related

WPF: How to make two controls (list views) share whole available space between them?

I have two list views (vertically aligned) which might have various number of items. I'd like them to share space proportionally when needed (this I can achieve with regular grid and *) but when one list view doesn't have many items to show I would like other list view to fill the whole space. And vice versa.
Tried different things but could not achieve this behavior.
For instance with grid I can specify * and * (or other proportions) but it means that half of the space will be empty if one of list views does not have any items (and another has tons of them).
Is there a way to achieve this? Do I need to implement my own Panel for this or there is another (simpler) way to do it?
Thank you!
Zaki
OK, try this code:
class MyPanel : Panel
{
protected override Size MeasureOverride(Size constraint)
{
// first measuring desired size of children
var availableSize = new Size(constraint.Width, double.PositiveInfinity);
foreach (UIElement ui in InternalChildren)
ui.Measure(availableSize);
var totalHeight = InternalChildren.OfType<UIElement>().Sum(x => x.DesiredSize.Height);
// now resizing children within constraint
var factor = (totalHeight == 0 ? 1.0 : constraint.Height / totalHeight);
foreach (UIElement ui in InternalChildren)
ui.Measure(new Size(constraint.Width, ui.DesiredSize.Height * factor));
var maxWidth = InternalChildren.OfType<UIElement>().Max(x => x.DesiredSize.Width);
return new Size(Math.Min(constraint.Width, maxWidth), Math.Min(constraint.Height, totalHeight));
}
protected override Size ArrangeOverride(Size arrangeSize)
{
// aligning children vertically
var totalHeight = InternalChildren.OfType<UIElement>().Sum(ui => ui.DesiredSize.Height);
var y = 0.0;
var rect = new Rect(arrangeSize);
foreach (UIElement ui in InternalChildren)
{
rect.Y += y;
y = ui.DesiredSize.Height;
rect.Height = y;
ui.Arrange(rect);
}
return arrangeSize;
}
}
This panel would arrange children vertically and give children vertical space proportionally to their desired height, but won't allow them take more space than available.
So, if, for example, you have 200px height available, first list view wants 150px, and second wants 100px, they will be scaled down to 120px + 80px == 200px
Just would like to share the final version which does what I wanted. Thank you to torvin for providing the right direction, appreciate quick and valuable response!
=================================================================
Implemented resizing of main window in such a way that:
If there is unused space than any of list views can use it (so, no unused area with scroll bar at the same time)
If there is not enough space then bottom control takes at least 100 pixel and/or top controls takes at least Height – 100 pixels
Top control docks to top and bottom control docks to bottom
=================================================================
/// <summary>The two children effecient panel.</summary>
public class TwoChildrenEffecientPanel : Panel
{
#region Constants and Fields
/// <summary>The bottom child min size.</summary>
private const double BottomChildMinSize = 110;
#endregion
#region Methods
/// <summary>The arrange override.</summary>
/// <param name="arrangeSize">The arrange size.</param>
/// <returns>The <see cref="Size"/>.</returns>
protected override Size ArrangeOverride(Size arrangeSize)
{
Debug.Assert(this.InternalChildren.Count == 2, "This custom panel supports only two children.");
UIElement top = this.InternalChildren[0];
var topRect = new Rect(arrangeSize);
topRect.Height = top.DesiredSize.Height;
top.Arrange(topRect);
UIElement bottom = this.InternalChildren[1];
var bottomRect = new Rect(arrangeSize);
bottomRect.Height = bottom.DesiredSize.Height;
bottomRect.Y = arrangeSize.Height - bottomRect.Height;
bottom.Arrange(bottomRect);
return arrangeSize;
}
/// <summary>The measure override.</summary>
/// <param name="constraint">The constraint.</param>
/// <returns>The <see cref="Size"/>.</returns>
protected override Size MeasureOverride(Size constraint)
{
Debug.Assert(this.InternalChildren.Count == 2, "This custom panel supports only two children.");
// First measure desired size of all children.
var availableSize = new Size(constraint.Width, double.PositiveInfinity);
foreach (UIElement ui in this.InternalChildren)
{
ui.Measure(availableSize);
}
// Put constraints only if space is not enough
double totalHeight = this.InternalChildren.OfType<UIElement>().Sum(x => x.DesiredSize.Height);
if (totalHeight > constraint.Height)
{
UIElement top = this.InternalChildren[0];
UIElement bottom = this.InternalChildren[1];
if (bottom.DesiredSize.Height < BottomChildMinSize)
{
// If the second control needs less than it can get then put contraint only on the first one
top.Measure(new Size(constraint.Width, Math.Max(constraint.Height - bottom.DesiredSize.Height, 0)));
}
else if (top.DesiredSize.Height < constraint.Height - BottomChildMinSize)
{
// If the first control needs less than it can get then put contraint only on the second one
bottom.Measure(new Size(constraint.Width, Math.Max(constraint.Height - top.DesiredSize.Height, 0)));
}
else
{
top.Measure(new Size(constraint.Width, Math.Max(constraint.Height - BottomChildMinSize, 0)));
bottom.Measure(new Size(constraint.Width, BottomChildMinSize));
}
}
double maxWidth = this.InternalChildren.OfType<UIElement>().Max(x => x.DesiredSize.Width);
return new Size(Math.Min(constraint.Width, maxWidth), Math.Min(constraint.Height, totalHeight));
}
#endregion
}

Why Is InvalidateMeasure While In Arrange Pass Not Working?

I have following XAML:
<Grid>
<StackPanel>
<local:MyControl Background="Blue" AnotherControl="{x:Reference anotherControl}"/>
<local:MyAnotherControl Background="Red" x:Name="anotherControl"/>
</StackPanel>
</Grid>
The problem is when in MeasureOverride of MyControl I return (100, 20) back. Thought when in ArrangeOverride of MyControl I return the final size I recieve. Therefore the MyControl has ActualWidth the same as Window.
So far so good but while I am inside ArrangeOverride of MyControl and before I return size I invalidate measure of MyAnotherControl.
The end result is the MyAnotherControl is being measured but while in arrange pass and therefore its not notifying StackPanel that MyAnotherControl's desired size has changed. Futhermore as result StackPanel is not displayed properly.
It seems to me I found strange behavior in wpf when invalidating a control while I am in arrange pass. MyAnotherControl is not notifing its parent which is Stackpanel about size changed but it should.
Any solution to this?
If you resize the window it will all be arranged and drawn correctly. Why?
There are two stripes. The blue one and red one. Red shall appear with another height but it doesnt. When you resize the window and force remeasuing it works. Why is this strange behavior happening?
public class MyControl : Button
{
public MyAnotherControl AnotherControl
{
get;
set;
}
protected override Size MeasureOverride(Size constraint)
{
base.MeasureOverride(constraint);
return new Size(100, 20);
}
protected override Size ArrangeOverride(Size arrangeBounds)
{
base.ArrangeOverride(arrangeBounds);
AnotherControl.MyHeight = 50;
AnotherControl.InvalidateMeasure();
return arrangeBounds;
}
}
public class MyAnotherControl : Button
{
public double MyHeight
{
get;
set;
}
protected override Size MeasureOverride(Size constraint)
{
base.MeasureOverride(constraint);
return new Size(100, this.MyHeight > 0 ? this.MyHeight : 10);
}
protected override Size ArrangeOverride(Size arrangeBounds)
{
base.ArrangeOverride(arrangeBounds);
return arrangeBounds;
}
}
This is how wpf works. The stackpanel is just enumerating its children and calling their arrange within its arrange. If you call arrange on an element whose measure is invalid, it will be remeasured with the last available size. The onchilddesiredsizechanged of the parent won't be called so the measure of the parent won't be invalidated. You should probably just get the visual parent of the sibling and invalidate its measure.

WPF - Adding ContentControl to Custom Canvas

I have a custom DrawingCanvas which is inherited from Canvas. When I add a ContentControl to DrawingCanvas with the following code nothing shows up.
GraphicsRectangle rect = new GraphicsRectangle(0, 0, 200, 200, 5, Colors.Blue);
DrawingContainer host = new DrawingContainer(rect);
ContentControl control = new ContentControl();
control.Width = 200;
control.Height = 200;
DrawingCanvas.SetLeft(control, 100);
DrawingCanvas.SetTop(control, 100);
control.Style = Application.Current.Resources["DesignerItemStyle"] as Style;
control.Content = host;
drawingCanvas.Children.Add(control);
GraphicsRectangle is a DrawingVisual and the constructor above draws a Rect with (0,0) top left point and length of 200 to the drawingContext of GraphicsRectangle. DrawingContainer is a FrameworkElement and it has one child, which is rect above, given with constructor. DrawingContainer implements GetVisualChild and VisualChildrenCount override methods. At last, Content property of ContentControl is set to the DrawingContainer to be able to show the DrawingVisual's content.
When I add the created ContentControl to a regular Canvas, control is showed correctly. I guess the reason is that DrawingCanvas doesn't implement ArrangeOverride method. It only implements MeasureOverride method. Also DrawingContainer doesn't implement Measure and
Arrange override methods. Any ideas?
As I thought the problem was missing ArrangeOverride method in DrawingCanvas. With the following ArrangeOverride method added to DrawingCanvas, ContentControls are showed correctly.
protected override Size ArrangeOverride(Size arrangeSize)
{
foreach (Visual child1 in children)
{
if (child1 is DrawingVisual)
continue;
ContentControl child = child1 as ContentControl;
GraphicsBase content = ((DrawingContainer)(child.Content)).GraphicsObject;
child.Arrange(new Rect(DrawingCanvas.GetLeft(child), DrawingCanvas.GetTop(child), content.Width, content.Height));
}
return arrangeSize;
}
where GraphicsBase is the base of the GraphicsRectangle class. In order to find the size of the GraphicsBase, I added width and height properties to GraphicsBase which are set in the constructor of GraphicsRectangle.

Problem with custom scrolling in custom panel

I'm coding a custom panel representing the hand of cards. It's a panel that will stack the cards horizontally. If there isn't enough space, each card will overlap part of the card left of it. Minimum part should be always visible. I accomplished this and this is the code:
using System;
using System.Windows;
using System.Windows.Controls;
namespace Hand
{
public class Hand : Panel
{
//TODO Should be dependancy property
private const double MIN_PART = 0.5;
protected override Size MeasureOverride(Size availableSize)
{
Size desiredSize = new Size();
foreach (UIElement element in this.Children)
{
element.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
desiredSize.Width += element.DesiredSize.Width;
desiredSize.Height = Math.Max(desiredSize.Height, element.DesiredSize.Height);
}
return desiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
//percentage of the visible part of the child.
double part = 1;
Double desiredWidth = 0;
//TODO Check how to get desired size because without looping
//this.DesiredSize is minimum of available size and size returned from MeasureOverride
foreach (UIElement element in this.Children)
{
desiredWidth += element.DesiredSize.Width;
}
if (desiredWidth > this.DesiredSize.Width)
{
//Every, but the last child should be overlapped
double lastChildWidth = this.Children[this.Children.Count - 1].DesiredSize.Width;
part = (this.DesiredSize.Width - lastChildWidth) / (desiredWidth - lastChildWidth);
part = Math.Max(part, MIN_PART);
}
double x = 0;
foreach (UIElement element in this.Children)
{
Rect rect = new Rect(x, 0, element.DesiredSize.Width, element.DesiredSize.Height);
element.Arrange(rect);
finalSize.Width = x + element.DesiredSize.Width;
x += element.DesiredSize.Width * part;
}
return finalSize;
}
}
}
I would like to add scrollbar when minimum part is reached, so that the user could still be able to view all the cards. I cannot accomplish this. I tried with the ScrollViewer like this:
<Window x:Class="TestScrollPanel.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:h="clr-namespace:Hand;assembly=Hand"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ScrollViewer HorizontalScrollBarVisibility="Auto">
<h:Hand>
<Button Width="100">One</Button>
<Button Width="150">Two</Button>
<Button Width="200">Three</Button>
</h:Hand>
</ScrollViewer>
</Grid>
</Window>
But this doesn't work because once horizontal scrollbar is visible, MeasureOveride and ArrangeOverride of Hand panel is never called and even if it would be called, Hand would get desired size to arrange all children without overlapping.
Could this be made with ScrollViewer at all and if not, another ideas would be appreciated.
Thank you all for ypur help.
Jurica
Firstly, change your panel's logic to just the opposite: let MeasureOverride pack the cards as tightly as possible, and then let ArrangeOverride spread them evenly over whatever width is given.
Secondly, use the MinWidth property. Bind it to ScrollViewer.ActualWidth.
This way, if the cards can be tightly packed into width less than that of the ScrollViewer, then your Hand will be stretched to all available space. And if they can't, then the Hand's width will be just whatever you calculate it to.

Custom Right Aligned StackPanel control layout in Silverlight

I'm trying to create a custom layout container, with the same characteristics of StackPanel, with the exception that it lays out the items starting at the right edge of the screen. Needless to say it does not work correctly.
I have identified a flaw inside ArrangeOverride() where the line
Point elementOrigin = new Point(this.DesiredSize.Width, 0);
simply creates a point # 0, 0. In other words this.DesiredSize.Width = 0. I understand that the measuring step happens before the arranging step, so I would expect this control will have the DesiredSize property set. How could I start rendering from the right side of the screen otherwise? Is it even possible?
Secondly the finalSize argument that is passed in to the function is much much larger than the area required by the three buttons I have defined in the test xaml. Something to the tune of 1676 by 909 vs a required 250 by 60 or so.
Thank you.
Here's my code:
protected override Size MeasureOverride(Size availableSize)
{
Size availableSpace = new Size(double.PositiveInfinity, double.PositiveInfinity);
Size desiredSize = new Size(0, 0);
foreach (UIElement child in this.Children)
{
child.Measure(availableSpace);
desiredSize.Width += child.DesiredSize.Width;
desiredSize.Height = Math.Max(desiredSize.Height, child.DesiredSize.Height);
}
return base.MeasureOverride(desiredSize);
}
protected override Size ArrangeOverride(Size finalSize)
{
Point elementOrigin = new Point(this.DesiredSize.Width, 0);
foreach (UIElement child in this.Children)
{
Rect childBounds = new Rect(elementOrigin, child.DesiredSize);
elementOrigin.X -= child.DesiredSize.Width;
child.Arrange(childBounds);
}
return base.ArrangeOverride(finalSize);
}
You need to simply return your desiredSize from your MeasureOverride implementation, you don't want to be calling the base version of this method, you are replacing the default implementation with yours.
Similarly with ArrangeOverride you are providing the implementation, you are replacing the default implementation so don't call the base version of this method. Simply return finalSize.

Resources