I have a Panel which I want to extend and override MeassureOverride and Arrange to have my custom layout.
Basically, the panel will contain some labels. As the label has some text content, it should have a specific size. However when I use label.ActualHeight or actualwidth, desiredSize ... in the MeassureOverride or ArrangeOverride, all result to NaN. Is there any way I can get the desired Size of the label so that the text content is fit?
Do you call base.MeasureOverride(abailableSize) and base.ArrangeOverride(finalSize) at the end of each method?
Here is an example of creating a custom panel
A custom implementation of MeasureOverride might look like this (from the post):
protected override Size MeasureOverride(Size availableSize)
{
Size sizeSoFar = new Size(0, 0);
double maxWidth = 0.0;
foreach (UIElement child in Children)
{
child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
if (sizeSoFar.Width + child.DesiredSize.Width > availableSize.Width)
{
sizeSoFar.Height += child.DesiredSize.Height;
sizeSoFar.Width = 0;
}
else
{
sizeSoFar.Width += child.DesiredSize.Width;
maxWidth = Math.Max(sizeSoFar.Width, maxWidth);
}
}
return new Size(maxWidth, sizeSoFar.Height);
}
A custom implementation of ArrangeOverride might look like this (from the post):
protected override Size ArrangeOverride(Size finalSize)
{
Size sizeSoFar = new Size(0, 0);
foreach (UIElement child in Children)
{
child.Arrange(new Rect(sizeSoFar.Width, sizeSoFar.Height,
child.DesiredSize.Width, child.DesiredSize.Height));
if (sizeSoFar.Width + child.DesiredSize.Width >= finalSize.Width)
{
sizeSoFar.Height += child.DesiredSize.Height;
sizeSoFar.Width = 0;
}
else
{
sizeSoFar.Width += child.DesiredSize.Width;
}
}
return finalSize;
}
If you want to force the panel rendering (call the MeasureOverride function), use the InvalidateMeasure function
You could also check out Custom Panel Elements on msdn.
The DesiredSize for each child is only set after you have measured it. In your MeasureOverride you must call child.Measure() for every of your panel's children. The same goes with child.Arrange() in ArrangeOverride.
See http://msdn.microsoft.com/en-us/library/ms745058.aspx#LayoutSystem_Measure_Arrange
Edit in response to your comment: just pass the maximum size your label could have (the available size), or a constrained size if you need to. The label once measured will use its minimum size as the DesiredSize if the alignments are different from stretch.
Related
I instantiate new Ui elements onto a canvas like so:
public class MainForm :Canvas
{
List<BannerImage> bannerList;
AddImages()
{
bannerImage = new BannerImage("title", "content");
//accompanied with animation
Children.Add(bannerImage);
bannerList.Add(bannerImage);
}
I need to call the bannerImages to get their current position, the following works:
foreach(bannerItem in bannerList)
{
double rightPosition = Canvas.GetRight(bannerItem);
}
But I can't do the following:
bannerItem.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)
Size s = bannerItem.DesiredSize;
Which always ends up to be
{0,0}
Why is it that I can get the position of the item on the canvas but not the size?
I am just going to take a guess that you didn't override MeasureOverride. I will provide a basic implementation assuming that each element is stacked, but you would need to modify it to take into consideration your child controls and what ever custom layout you may have created (I don't know if they are in a grid, horizontally stacked, in a some kind of scrolled container, etc).
protected override Size MeasureOverride(Size availableSize)
{
var height = 0.0;
var width = 0.0;
foreach (UIElement child in InternalChildren)
{
child.Measure(availableSize);
if (child.DesiredSize.Width > width) width = child.DesiredSize.Width;
height += child.DesiredSize.Height;
}
width = double.IsPositiveInfinity(availableSize.Width) ? width : Math.Min(width, availableSize.Width);
height = double.IsPositiveInfinity(availableSize.Height) ? height : Math.Min(height, availableSize.Height);
return new Size(width, height);
}
Edit
I realized that I explained the issue in a comment, but didn't add it into my answer. The reason you can't get the size is because you have to provide an override in your derived class to compute it. By default, Canvas returns a DesiredSize of 0 since it will adapt to whatever size is assigned to it. In the case of your derived control, you have a Canvas as the base class but you have added additional controls to it. If you don't provide an override of the MeasureOverride method, then the base one (the one implemented by Canvas) is the only one that is called. The base Canvas knows nothing of your controls size requirements. You probably also will need to override ArrangeOverride. This article provides a pretty good explanation about the two methods, what they do and why you need to override them. It also provides and example of both methods.
I am making my own custom panel, which is supposed to scroll vertically when the content does not fit the available space, so i put it in a ScrollViewer.
Right now i can't get the ScrollViewer to activate the scrollbar when the panel inside is bigger then the ScrollViewer itself.
The permille functions get attached properties telling how big the childs have to be compared to the available size (without scrolling), aka the ViewPort.
As the size passed in MeasureOverride passes infinite, i don't think i can use the permille functions there.
That is why i measure my children in ArrangeOverride (not best practice, i guess) but that way the scrollviewer doesn't scroll.
How do i get this to work?
My XAML code:
<ScrollViewer>
<controls:TilePanel x:Name="TilePanel" PreviewMouseLeftButtonDown="TilePanel_PreviewMouseLeftButtonDown" PreviewMouseLeftButtonUp="TilePanel_PreviewMouseLeftButtonUp"
PreviewMouseMove="TilePanel_PreviewMouseMove" DragEnter="TilePanel_DragEnter" Drop="TilePanel_Drop" AllowDrop="True" />
</ScrollViewer>
My Custom Panel Class:
/// <summary>
/// A Panel Showing Tiles
/// </summary>
public class TilePanel : PermillePanel
{
public TilePanel()
{
}
protected override Size MeasureOverride(Size constraint)
{
//here constraint width or height can be infinite.
//as tiles are a permille of that height, they too can be infinite after measuring
//this is unwanted behavior, so we measure in the ArrangeOverride method
if (constraint.Width == double.PositiveInfinity)
{
return new Size(0, constraint.Height);
}
else if (constraint.Height == double.PositiveInfinity)
{
return new Size(constraint.Width, 0);
}
else
{
return constraint;
}
}
protected override Size ArrangeOverride(Size arrangeSize)
{
//return base.ArrangeOverride(arrangeSize);
foreach (FrameworkElement child in InternalChildren)
{
Size availableSize = new Size();
//set the width and height for the child
availableSize.Width = arrangeSize.Width * TilePanel.GetHorizontalPermille(child) / 1000;
availableSize.Height = arrangeSize.Height * TilePanel.GetVerticalPermille(child) / 1000;
child.Measure(availableSize);
}
// arrange the children on the panel
// fill lines horizontally, when we reach the end of the current line, continue to the next line
Size newSize = new Size(arrangeSize.Width, arrangeSize.Height);
double xlocation = 0;
double ylocation = 0;
double ystep = 0;
double maxYvalue = 0;
foreach (FrameworkElement child in InternalChildren)
{
double endxlocation = xlocation + child.DesiredSize.Width;
double constrainedWidth = arrangeSize.Width * TilePanel.GetHorizontalPermille(child) / 1000;
double constrainedHeight = arrangeSize.Height * TilePanel.GetVerticalPermille(child) / 1000;
if (TilePanel.GetVerticalPermille(child) != 0 && TilePanel.GetHorizontalPermille(child) != 0)
{
//horizontal overflow -> next line
if (endxlocation >= this.DesiredSize.Width *1.01)
{
ylocation += ystep;
xlocation = 0;
}
}
Rect rect = new Rect(xlocation, ylocation, constrainedWidth, constrainedHeight);
child.Arrange(rect);
xlocation += constrainedWidth;
ystep = Math.Max(ystep, constrainedHeight);
maxYvalue = Math.Max(maxYvalue, ystep + constrainedHeight);
}
if (maxYvalue > newSize.Height)
{
newSize.Height = maxYvalue;
}
return newSize;
}
}
Calling Measure() from within ArrangeOverride() will cause problems. The framework detects this and forces a remeasure. Set a tracepoint in MeasureOverride(), and I'll bet you'll see that it keeps getting called over and over again, even though the layout hasn't changed1.
If you absolutely have to call Measure() from ArrangeOverride(), you will need to do so conditionally such that it only forces a remeasure when the available size actually changes since the last call to Measure(). Then, you'll effectively end up with two measure + arrange passes any time the layout is invalidated, as opposed to just one. However, such an approach is hacky, and I would advise sticking to the best practice of only measuring within MeasureOverride().
1Interestingly, your UI may still respond to input, despite this apparent "infinite loop" in the layout.
If you want to use a custom Panel inside a ScrollViewer then you must add the code that does the actual scrolling. You can do that by implementing the IScrollInfo Interface in your custom Panel.
You can find a tutorial that explains this interface and provides an example code imeplementation in the WPF Tutorial - Implementing IScrollInfo page on the Tech Pro website. It's a fairly simple procedure and looks a tiny bit like this:
public void LineDown() { SetVerticalOffset(VerticalOffset + LineSize); }
public void LineUp() { SetVerticalOffset(VerticalOffset - LineSize); }
public void MouseWheelDown() { SetVerticalOffset(VerticalOffset + WheelSize); }
public void MouseWheelUp() { SetVerticalOffset(VerticalOffset - WheelSize); }
public void PageDown() { SetVerticalOffset(VerticalOffset + ViewportHeight); }
public void PageUp() { SetVerticalOffset(VerticalOffset - ViewportHeight); }
...
I have a TextBlock with a fixed size thats wrapping text. sometimes short sometimes long.
If the text is getting to long it isnt displayed entirely like this
How can i make the Fontsize flexible to make the text fit the TextBox with static size?
My solution is the following:
Set the fontsize to a value, than which you don't want any bigger.
The ActualHeight of the TextBlock changes, when you change the font size or when the content is changed. I built the solution based upon this.
You should create an event handler for the SizeChanged event and write the following code to it.
private void MyTextBlock_SizeChanged(object sender, SizeChangedEventArgs e)
{
double desiredHeight = 80; // Here you'll write the height you want the text to use
if (this.MyTextBlock.ActualHeight > desiredHeight)
{
// You want to know, how many times bigger the actual height is, than what you want to have.
// The reason for Math.Sqrt() is explained below in the text.
double fontsizeMultiplier = Math.Sqrt(desiredHeight / this.MyTextBlock.ActualHeight);
// Math.Floor() can be omitted in the next line if you don't want a very tall and narrow TextBox.
this.MyTextBlock.FontSize = Math.Floor(this.MyTextBlock.FontSize * fontsizeMultiplier);
}
this.MyTextBlock.Height = desiredHeight; // ActualHeight will be changed if the text is too big, after the text was resized, but in the end you want the box to be as big as the desiredHeight.
}
The reason why I used the Math.Sqrt() is that if you set the font size to half as big as before, then the area that the font will use, will be one quarter the size, then before (because it became half as wide and half as tall as before). And you obviously want to keep the width of the TextBox and only change the height of it.
If you were lucky, the font size will be appropriate after this method gets executed once. However, depending on the text that gets re-wrapped after the font size change, you might be so "unlucky", that the text will be one line longer than you would want it to be.
Luckily the event handler will be called again (because you changed the font size) and the resizing will be done again if it is still too big.
I tried it, it was fast and the results looked good.
However, I can imagine, that in a really unlucky choice of text and height, the correct font size would be reached after several iterations. This is why I used Math.Floor(). All in all, it doesn't matter much if the font size is in the end 12.34 or 12 and this way I wouldn't be concerned about an "unlucky" text, which will take too long to render.
But I think Math.Floor() can be omitted if you don't want to have a really tall text box (like 2000 pixels) with a lot of text.
Here's a full solution including an option to set maxheight / maxwidth and and it is calculated straight on render:
public class TextBlockAutoShrink : TextBlock
{
private double _defaultMargin = 6;
private Typeface _typeface;
static TextBlockAutoShrink()
{
TextBlock.TextProperty.OverrideMetadata(typeof(TextBlockAutoShrink), new FrameworkPropertyMetadata(new PropertyChangedCallback(TextPropertyChanged)));
}
public TextBlockAutoShrink() : base()
{
_typeface = new Typeface(this.FontFamily, this.FontStyle, this.FontWeight, this.FontStretch, this.FontFamily);
base.DataContextChanged += new DependencyPropertyChangedEventHandler(TextBlockAutoShrink_DataContextChanged);
}
private static void TextPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
var t = sender as TextBlockAutoShrink;
if (t != null)
{
t.FitSize();
}
}
void TextBlockAutoShrink_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
FitSize();
}
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
FitSize();
base.OnRenderSizeChanged(sizeInfo);
}
private void FitSize()
{
FrameworkElement parent = this.Parent as FrameworkElement;
if (parent != null)
{
var targetWidthSize = this.FontSize;
var targetHeightSize = this.FontSize;
var maxWidth = double.IsInfinity(this.MaxWidth) ? parent.ActualWidth : this.MaxWidth;
var maxHeight = double.IsInfinity(this.MaxHeight) ? parent.ActualHeight : this.MaxHeight;
if (this.ActualWidth > maxWidth)
{
targetWidthSize = (double)(this.FontSize * (maxWidth / (this.ActualWidth + _defaultMargin)));
}
if (this.ActualHeight > maxHeight)
{
var ratio = maxHeight / (this.ActualHeight);
// Normalize due to Height miscalculation. We do it step by step repeatedly until the requested height is reached. Once the fontsize is changed, this event is re-raised
// And the ActualHeight is lowered a bit more until it doesnt enter the enclosing If block.
ratio = (1 - ratio > 0.04) ? Math.Sqrt(ratio) : ratio;
targetHeightSize = (double)(this.FontSize * ratio);
}
this.FontSize = Math.Min(targetWidthSize, targetHeightSize);
}
}
}
Writing a WinRT app in XAML/C# where I'd like a simple grid of square shaped buttons. The number of buttons is fixed currently, however in future there will be more added as I create more content.
Having to handle all UI resizes (snapped, filled, portrait, etc) and resolutions I ran into problems with the UIContainer (I was using a Grid then switched the WrapGrid) simply resizing the buttons automatically because I do not know of any way to constrain the aspect ratio and having square buttons is important to my UI.
Is there a way to constrain the aspect ratio / proportions of the Width and Height of a button control? If so, I'm assuming it would be to create a custom control, but other than creating styles and data templates I'm really just out of my depth.
Any suggestions on the best way to attack this problem?
You can create a simple decorator control that would override ArrangeOverride and always arrange itself into a square, like this:
public class SquareDecorator : ContentControl
{
public SquareDecorator()
{
VerticalAlignment = VerticalAlignment.Stretch;
HorizontalAlignment = HorizontalAlignment.Stretch;
VerticalContentAlignment = VerticalAlignment.Stretch;
HorizontalContentAlignment = HorizontalAlignment.Stretch;
}
protected override Size MeasureOverride(Size availableSize)
{
var baseSize = base.MeasureOverride(availableSize);
double sideLength = Math.Max(baseSize.Width, baseSize.Height);
return new Size(sideLength, sideLength);
}
protected override Size ArrangeOverride(Size finalSize)
{
double sideLength = Math.Min(finalSize.Width, finalSize.Height);
var result = base.ArrangeOverride(new Size(sideLength, sideLength));
return result;
}
}
Now you can wrap your buttons with this decorator:
<z:SquareDecorator>
<Button Content="I'm Square"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch" />
</z:SquareDecorator>
I'm assuming you can't just set height and width to a fixed size (on the button itself or in a style for the button).
I tried doing this in Silverlight:
<Button Height={Binding ActualWidth, RelativeSource={RelativeSource Self}}/>
But it doesn't want to work. Don't know why. It might work in WinRT.
Alternatively, you can create a custom Panel to arrange and size your buttons. Should not be difficult given your simple requirements. It involves implementing just two functions and knowledge of basic arithmetic. Here is an example that creates a UniformGrid.
I don't think a UserControl or deriving from Button would be better choices.
Here's my version of Pavlo's answer. It is more efficient and elegant than deriving from ContentControl (which must use a ControlTemplate, adds other elements to the visual tree, and has tons of other unneeded functionality). I also believe it is more correct, because MeasureOverride returns the correct desired size.
public class SquareDecorator : Panel
{
protected override Size MeasureOverride(Size availableSize)
{
if( Children.Count == 0 ) return base.MeasureOverride(availableSize);
if( Children.Count > 1 ) throw new ArgumentOutOfRangeException("SquareDecorator should have one child");
Children[0].Measure(availableSize);
var sideLength = Math.Max(Children[0].DesiredSize.Width, Children[0].DesiredSize.Height);
return new Size(sideLength, sideLength);
}
protected override Size ArrangeOverride(Size finalSize)
{
if( Children.Count == 0 ) return base.ArrangeOverride(finalSize);
if( Children.Count > 1 ) throw new ArgumentOutOfRangeException("SquareDecorator should have one child");
double sideLength = Math.Min(finalSize.Width, finalSize.Height);
Children[0].Arrange(new Rect(0, 0, sideLength, sideLength));
return new Size(sideLength, sideLength);
}
}
Use it the same way (Button's Horizontal/VerticalAlignment must be stretch, but this is the default. Also note you can get useful effects if you set SquareDecorator's Horizontal/VerticalAlignment to non-stretch.):
<z:SquareDecorator>
<Button Content="I'm Square"/>
</z:SquareDecorator>
I would have derived from FrameworkElement, but it looks like neither Silverlight nor WinRT allow you to do that. (FrameworkElement is not sealed, but has no AddVisualChild method which makes it useless to derive from. Sigh, I hate .NET and/or Microsoft)
I think I'm missing something trivial here. I derived simple control directly from Control. I'm overriding OnPaint and painting the rectangle (e.Graphics.DrawRectangle)and a text inside it (e.Graphics.DrawString). I did not override any other members.
It paints itself well when the control is resized to the smaller size, but when it gets resized to the larger size, new area is not repainted properly. As soon as I resize it to the smaller size again, even if by one pixel, everything repaints correctly.
OnPaint gets called properly (with appropriate PaintEventArgs.ClipRectangle set correctly to new area), but the new area is not painted (artifacts appear) anyway.
What am I missing?
EDIT:
Code:
protected override void OnPaint(PaintEventArgs e)
{
// Adjust control's height based on current width, to fit current text:
base.Height = _GetFittingHeight(e.Graphics, base.Width);
// Draw frame (if available):
if (FrameThickness != 0)
{
e.Graphics.DrawRectangle(new Pen(FrameColor, FrameThickness),
FrameThickness / 2, FrameThickness / 2, base.Width - FrameThickness, base.Height - FrameThickness);
}
// Draw string:
e.Graphics.DrawString(base.Text, base.Font, new SolidBrush(base.ForeColor), new RectangleF(0, 0, base.Width, base.Height));
}
private int _GetFittingHeight(Graphics graphics, int width)
{
return (int)Math.Ceiling(graphics.MeasureString(base.Text, base.Font, width).Height);
}
Try adding this in your constructor:
public MyControl() {
this.ResizeRedraw = true;
this.DoubleBuffered = true;
}
and in your paint event, clear the previous drawing:
protected override void OnPaint(PaintEventArgs e) {
e.Graphics.Clear(SystemColors.Control);
// yada-yada-yada
}
While ResizeRedraw will work, it forces the entire control to repaint for every resize event, rather than only painting the area that was revealed by the resize. This may or may not be desirable.
The problem the OP was having is caused by the fact that the old rectangle does not get invalidated; only the revealed area gets repainted, and old graphics stay where they were. To correct this, detect whether the size of your rectangle has increased vertically or horizontally, and invalidate the appropriate edge of the rectangle.
How you would specifically go about this would depend on your implementation. You would need to have something that erases the old rectangle edge and you would have to call Invalidate passing an area containing the old rectangle edge. It may be somewhat complicated to get it to work properly, depending what you're doing, and using ResizeRedraw after all may be much simpler if the performance difference is negligible.
Just for example, here is something you can do for this problem when drawing a border.
// member variable; should set to initial size in constructor
// (side note: should try to remember to give your controls a default non-zero size)
Size mLastSize;
int borderSize = 1; // some border size
...
// then put something like this in the resize event of your control
var diff = Size - mLastSize;
var wider = diff.Width > 0;
var taller = diff.Height > 0;
if (wider)
Invalidate(new Rectangle(
mLastSize.Width - borderSize, // x; some distance into the old area (here border)
0, // y; whole height since wider
borderSize, // width; size of the area (here border)
Height // height; all of it since wider
));
if (taller)
Invalidate(new Rectangle(
0, // x; whole width since taller
mLastSize.Height - borderSize, // y; some distance into the old area
Width, // width; all of it since taller
borderSize // height; size of the area (here border)
));
mLastSize = Size;