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.
Related
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
}
I imagine it for example
public int ChildSize
{
get { return childSize; }
set
{
foreach (UIElement child in Children)
{
child.DesiredSize.Width = value;
}
}
}
I suspect you are writing a custom panel, which is quite an advanced task.
Your Panel should create overrides for the MeasureOverride and ArrangeOverride methods.
In these methods you should be calling each child's Measure and Arrange methods passing in appropriate Size and Rect according your determined childSize and calculated relative positions of each child.
I think I have understood how MeasureOverrride works, but I am trying to use it in a very simple case and It doesn't work... So now I'm not so sure... After using Measureoverride do I have to use Arrageoverride too or the system will do it for me? The situation is this one: I have a LinearLayout class inherited from Panel and it has two fields called wrapwidht and wrapheigh, if they are true the width or the height of the LinearLayout has to be as its children require. so my Measureoveride looks like:
protected override Size MeasureOverride(Size availableSize) {
Size panelDesiredSize = new Size();
if ((this.widthWrap) || (this.heightWrap))
{
foreach (UIElement elemento in this.Children)
{
System.Diagnostics.Debug.WriteLine(((FrameworkElement)elemento).DesiredSize.ToString());
if (this.Orientation.Equals(System.Windows.Controls.Orientation.Vertical))
{
if (this.widthWrap)
{
//the widest element will determine containers width
if (panelDesiredSize.Width < ((FrameworkElement)elemento).Width)
panelDesiredSize.Width = ((FrameworkElement)elemento).Width;
}
//the height of the Layout is determine by the sum of all the elment that it cointains
if (this.heightWrap)
panelDesiredSize.Height += ((FrameworkElement)elemento).Height;
}
else
{
if (this.heightWrap)
{
//The highest will determine the height of the Layout
if (panelDesiredSize.Height < ((FrameworkElement)elemento).Height)
panelDesiredSize.Height = ((FrameworkElement)elemento).Height;
}
//The width of the container is the sum of all the elements widths
if (this.widthWrap)
panelDesiredSize.Width += ((FrameworkElement)elemento).Width;
}
}
}
System.Diagnostics.Debug.WriteLine("desiredsizzeeeeeee" + panelDesiredSize);
return panelDesiredSize;
}
The children I am aading to the LinerLayout are 3 buttons, but nothing is drawn.. even if the panelDesiredSize filed is correct.. so maybe I didn't understand how it works very well. If anybody can help me would be very nice :-)
Check my answer on a previous post similar to yours: Two Pass Layout system in WPF and Silverlight
The answer is that no, you don't have to override ArrangeOverride, but what is the point of using MeasureOverride if you are not going to use ArrangeOverride?
You should call the Measure method on the child. See the example here: http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.measureoverride.aspx
You must call Measure on "elemento". It's during the Measure that Silverlight creates the UI elements declared in the elemento's template since they'll be needed to actually measure and come up with a desired size. You should then use the elemento.DesiredSize.Width and Height to come up with the desired size for your panelDesiredSize.
I'm working on an application for a client, and one of the requirements is the ability to make appointments, and display the current week's appointments in a visual format, much like in Google Calendar's or Microsoft Office. I found a great (3 part) article on codeproject, in which he builds a "RangePanel", and composes one for each "period" (for example, the work day.) You can find part 1 here:
http://www.codeproject.com/KB/WPF/OutlookWpfCalendarPart1.aspx
The code presents, but seems to choose an arbitrary height value overall (440.04), and won't resize below that without clipping. What I mean to say, is that the window/container will resize, but it just cuts off the bottom of the control instead of recalculating the height of the range panels, and the controls in the range panels representing the appointment. It will resize and recalculate for greater values, but not less.
Code-wise, what's happening is that when you resize below that value, first the MeasureOverride is called with the correct "new height". However, by the time the ArrangeOverride method is called, it's passing the same 440.04 value as the height to arrange to.
I need to find a solution/workaround, but any information that you can provide that might direct me for things to look into would also be greatly appreciated (I understand how frustrating it is to debug code when you don't have the codebase in front of you. :) )
The code for the various Arrange and Measure functions are provided below. The CalendarView control has a CalendarViewContentPresenter, which handles several periods. Then, the periods have a CalendarPeriodContentPresenter, which handles each "block" of appointments. Finally, the RangePanel has its own implementation. (To be honest, I'm still a bit hazy on how the control works, so if my explanations are a bit hazy, the article I linked probably has a more cogent explanation. :) )
CalendarViewContentPresenter:
protected override Size ArrangeOverride(Size finalSize)
{
int columnCount = this.CalendarView.Periods.Count;
Size columnSize = new Size(finalSize.Width / columnCount, finalSize.Height);
double elementX = 0;
foreach (UIElement element in this.visualChildren)
{
element.Arrange(new Rect(new Point(elementX, 0), columnSize));
elementX = elementX + columnSize.Width;
}
return finalSize;
}
protected override Size MeasureOverride(Size constraint)
{
this.GenerateVisualChildren();
this.GenerateListViewItemVisuals();
// If it's coming back infinity, just return some value.
if (constraint.Width == Double.PositiveInfinity)
constraint.Width = 10;
if (constraint.Height == Double.PositiveInfinity)
constraint.Height = 10;
return constraint;
}
CalendarViewPeriodPersenter:
protected override Size ArrangeOverride(Size finalSize)
{
foreach (UIElement element in this.visualChildren)
{
element.Arrange(new Rect(new Point(0, 0), finalSize));
}
return finalSize;
}
protected override Size MeasureOverride(Size constraint)
{
this.GenerateVisualChildren();
return constraint;
}
RangePanel:
protected override Size ArrangeOverride(Size finalSize)
{
double containerRange = (this.Maximum - this.Minimum);
foreach (UIElement element in this.Children)
{
double begin = (double)element.GetValue(RangePanel.BeginProperty);
double end = (double)element.GetValue(RangePanel.EndProperty);
double elementRange = end - begin;
Size size = new Size();
size.Width = (Orientation == Orientation.Vertical) ? finalSize.Width : elementRange / containerRange * finalSize.Width;
size.Height = (Orientation == Orientation.Vertical) ? elementRange / containerRange * finalSize.Height : finalSize.Height;
Point location = new Point();
location.X = (Orientation == Orientation.Vertical) ? 0 : (begin - this.Minimum) / containerRange * finalSize.Width;
location.Y = (Orientation == Orientation.Vertical) ? (begin - this.Minimum) / containerRange * finalSize.Height : 0;
element.Arrange(new Rect(location, size));
}
return finalSize;
}
protected override Size MeasureOverride(Size availableSize)
{
foreach (UIElement element in this.Children)
{
element.Measure(availableSize);
}
// Constrain infinities
if (availableSize.Width == double.PositiveInfinity)
availableSize.Width = 10;
if (availableSize.Height == double.PositiveInfinity)
availableSize.Height = 10;
return availableSize;
}
So, after much hunting this morning, I found a workaround. If this gives insight and someone else finds a solution, I'll still mark yours as the accepted solution, because this just seems so hacky to me.
Basically, this morning, I pulled out the proverbial bludgeon, and decided to see what would happen if I set the MaxHeight to some value (100), and see how it would render then. It turns out, it scaled the control properly, and there was no clipping in sight! So, I figured I would try databinding the MaxHeight property to the window's height in the XAML. This didn't work, so I tried doing it from the code behind, and still had no luck. So, I went back to verify the phenomenon, and set the MaxHeight to 100 again, and ran the program. It resized to the size of the window, which was odd since hadn't I just set it to 100? Then I realized it was setting it to 100, and then the codebehind was overriding that value allowing it to scale upwards. Resizing the window even had the desired no-clipping effect... right up until you got to 100 height. So, I set the MaxHeight to zero, and that apparently goes inside the control and breaks some psuedo-minimum-height property, and then the window allows it to expand up to it's full size.
It's extremely hacky, but it works for now. =/ It seems so odd to me that I enabled the shrinking of a control by setting the MaxHeight property. ><
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));
}