Disappearing WPF controls after 'Arrange' with Infinity size - c#

I'm facing an odd bug for around a month onwards.
I have a form that looks like this :
When I click on Save picture :
var g = new Grid();
var mv = new MyControl();
g.Children.Add(mv);
g.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
g.Arrange(new Rect(g.DesiredSize));
var rtb = new RenderTargetBitmap((int)g.ActualWidth, (int)g.ActualHeight,
96, 96, PixelFormats.Pbgra32);
rtb.Render(g);
Calling Measure() and Arrange() sets the size of the grid with proper size ( retrieved from MyControl )
The form control and the user control being rendered on the RenderTargetBitmap share no common data.
The visuals which become hidden are there and the viewmodel data is populated properly.
After executing this code and showing the dialog again it looks like this :
Both of the checkboxes have disappeared.Why?!
This problem doesn't appear if I set the size of the grid ( var g in the code )
var g = new Grid()
{
Width = 1024,
Height = 768
};

The default RowDefinition and ColumnDefinition for Grid is 1*. This means that the contents are given however much space the Grid has. In this case, the Grid will ask its parent to give it the maximum amount of space available for itself.
In your case, because your Grid has no visual parent, it has no idea how big it should render itself. You need to give it an explicit width/height. If you want to convert the contents of MyControl into Bitmap, why not just do it without the Grid?
If you insist on using Grid, and the size is dynamic, then you should manually specify the RowDefinitions and ColumnDefinitions. Change both of them to auto, which would force the Grid to size itself to the contents' sizes.

As said on MSDN info about Measure (Remarks section):
Computation of layout positioning in Windows Presentation Foundation
(WPF) is comprised of a Measure call and an Arrange call. During the
Measure call, an element determines its size requirements by using an
availableSize input. During the Arrange call, the element size is
finalized.
availableSize can be any number from zero to infinite. Elements
participating in layout should return the minimum Size they require
for a given availableSize.
When a layout is first instantiated, it always receives a Measure call
before Arrange. However, after the first layout pass, it may receive
an Arrange call without a Measure; this can happen when a property
that affects only Arrange is changed (such as alignment), or when the
parent receives an Arrange without a Measure. A Measure call will
automatically invalidate an Arrange call.
Layout updates happen asynchronously, such that the main thread is
not waiting for every possible layout change. Querying an element via
code-behind checking of property values may not immediately reflect
changes to properties that interact with the sizing or layout
characteristics (the Width property, for example).
Layout updates can be forced by using the UpdateLayout method.
However, calling this method is usually unnecessary and can cause poor
performance.
Hence call g.UpdateLayout in your save picture button click.
var g = new Grid();
var mv = new MyControl();
g.Children.Add(mv);
g.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
g.Arrange(new Rect(g.DesiredSize));
g.UpdateLayout();
var rtb = new RenderTargetBitmap((int)g.ActualWidth, (int)g.ActualHeight,
96, 96, PixelFormats.Pbgra32);
rtb.Render(g);

Related

Create Form.MinimumClientSize Property

The Windows Forms have Size and ClientSize Properties. Windows Forms also have a MinimumSize property that lets you set the smallest allowed size of the form. I am looking for a way to set the minimum ClientSize of a form. I'm new to c# and I am unsure of the best way to do this.
It occurred to me that I could use the Form.SizeChanged event to check and restrict the form size but this seemed messy and I am looking for another way of doing it.
Note: If the form border size is changed I want the Minimum Client Size to be maintained.
How to set a minimum Size of a Form's Client Area.
The Form.MinimumSize property sets the minimum size of the Form as a whole, including the current borders, the Caption and the subtle internal padding that is applied when the Form is resized and a new Layout is performed (many factors determine this activity).
It may be imperceptible, because it's fast (and, usually, nobody cares), but the relation between the Form Size and the Client Size is not constant. It can change when the Form is resized, dragging its borders. Or a System event causes this to happen.
Plus, changing the System settings, in many departments (the properties of the Windows, the Theme, Dpi, Font size etc.), can determine a change in the relations between the Form size and the size of its client area.
The good thing is that the Form is aware of these changes and, when notified of a System event that alters the aspect of Windows in some way, re-performs its layout.
When this happens (and it can happen frequently, always when a Form is resized), the OnLayout method is called.
Overriding OnLayout, allows to update values that rely on the Window/Client area measures.
➨ It can be interesting to see what happens when System settings,
affecting the aspect of the Windows, are changed while the application
is running. In this specific context, how many times the OnLayout
method is called and what the LayoutEventArgs properties are set
to.
This all considered, we can create a public MinimumClientSize Property.
We override OnLayout and reset the Form.MinimumSize to the new Form.MinimumClientSize plus the difference between the Form.Size and the Form.ClientSize.
For example, if we need to keep the Client Area size to a minimum of (500, 500):
private Size m_MinimumClientSize = new Size(500, 500);
public Size MinimumClientSize {
get => m_MinimumClientSize;
set { m_MinimumClientSize = value;
PerformLayout();
}
}
protected override void OnLayout(LayoutEventArgs e) {
base.OnLayout(e);
MinimumSize = m_MinimumClientSize + (Size - ClientSize);
}
If we add to the OnLayout method:
Console.WriteLine($"ClientSize: {ClientSize}");
Console.WriteLine($"MinimumSize: {MinimumSize}");
Console.WriteLine($"Size: {Size}");
it becomes clear that the relation between Form.Size and Form.ClientSize is not always the same.
We could also calculate the difference between Size and ClientSize this way:
var borderSize = new Size(
(SystemInformation.FrameBorderSize.Width * SystemInformation.BorderMultiplierFactor
+ (SystemInformation.Border3DSize.Width * 2)) * 2,
(SystemInformation.FrameBorderSize.Height * SystemInformation.BorderMultiplierFactor
+ (SystemInformation.Border3DSize.Height * 2)) * 2);
var captionSize = new Size(0, SystemInformation.CaptionHeight);
MinimumSize = MinimumClientSize + borderSize + captionSize;
These fixed measures are correct. In normal situations, they provides the same values.
Not always, though. Never, when a Form is resized to its MinimumSize.
Plus, we're only considering a Form with a 3d Border.
Well, we could also override WndProc...

Redraw panel contents after ClientSizeChanged

So my application runs in fixed size window and in full screen. The problem I'm facing is how to properly scale the current contents of the panel (which depend on the application use) when the window is resized. This is my current code:
private void Form1_ClientSizeChanged(object sender, EventArgs e)
{
System.Drawing.Drawing2D.Matrix transformMatrix = new System.Drawing.Drawing2D.Matrix();
float px = panel2.Width;
float py = panel2.Height;
panel2.Width = this.Width / 2;
panel2.Height = panel2.Width;
panel2.Location = new Point(this.Width - panel2.Width - 30, 30);
transformMatrix.Scale(panel2.Width / px, panel2.Height / py);
panel2.Region.Transform(transformMatrix);
//Rest of the code
}
But the drawn content doesn't scale, and if I use Invalidate() or Refresh() the drawn content gets cleared (the panel is redrawn empty). What am I missing?
.NET doesn't remember what's drawn on the panel, as simple as that. As soon as anything invalidates the windows bitmap buffer (causing a WM_PAINT), it's going to be repainted again. So, you have to draw what you want to draw using the Paint event (or overriding OnPaint).
However, there is another way that might be easier to implement - don't paint into a Panel. Instead, paint into a PictureBox (or rather, a Bitmap assigned to the Image property of the PictureBox). The Bitmap will be reused when invalidating (and redrawing) the picture box, so nothing will be lost. By using PictureBox.ScaleMode, you can define how you want the picture box to scale the bitmap, and it will do so as well as it can.
In any case, transforming the Region property doesn't do anything useful - you're simply changing the region, not doing anything to the drawing itself. To use 2D transformation matrices, you want to apply them on a Graphics object during the drawing (in Paint handler or OnPaint override) - drawing anything on the Graphics object will then transform everything you're trying to draw, which in your case means scaling the painting.
So you have to decide: do you want to just scale a stored bitmap with the painted image, or do you want to redraw it all from scratch (which also means you can pick any level of detail you can provide)?
I think that you're mistaking what the Region property is meant for. According to the MSDN docs (empasis mine, replace 'window' with 'control' when reading):
The window region is a collection of pixels within the window where the operating system permits drawing. The operating system does not display any portion of a window that lies outside of the window region. The coordinates of a control's region are relative to the upper-left corner of the control, not the client area of the control.
All that you're doing is changing the region that the OS will allow painting, which explains why you're not seeing anything. I think that you should be resizing the control when the form is resized, either through Anchor, or through my preference of Dock with several controls, or a panel like TableLayoutPanel where it will handle scaling and relative sizing for you.
Thank you for your answers, but I wrote my own function and logic that serves the purpose for this application. Basically the function checks for the state of the application variables, and calls the appropriate function that originally drew the content, and since those functions use the panel width and height as arguments they properly scale the drawn content and retain the drawing composition.
P.S. I'll accept Luaan's answers since it offers a valid alternative and is complete.

Custom control size on InitializeComponent()

I am trying to create a custom user control in WPF. I want to be able to set the size manually when I later use the control within another window.
As a short test I have just made a control comprising a canvas within a grid, which totally fills the control. When initialised it draws a rectangle within itself showing its size. I then position this on a window, making it whatever size I want.
However I now have problems, as if I make the height of the rectangle I draw
this.ActualHeight
then when the control initialises this value is still 0, and so I get nothing. If instead I use
this.Height
then I get the height that I made it during design time, and not the size I have subsequently made it within the window.
The height and width seem to be set within the XAML designer, so I don't know what to do.
Is there an easy way around this?
I think what you're experiencing is how WPF performs layout and specifically how Canvas does not participate in Layout.
In your specific case you are setting the width of the Rectangle to Canvas.ActualWidth? Unless the width / height of the canvas have been explictly set then the ActualWidth/Actualheight will be zero, hence you can't get a point of reference within which to position children of Canvas. What I would do is bind the canvas width and height to its parent container (or set in code) to get the ActualWidth / ActualHeight correctly propagated.
As a point of interest try this example to understand how the WPF Layout engine works. The following code can force a height on a FrameworkElement to set the Width, Height as normal, then force a layout (measure, arrange pass) on the element in question. This causes the WPF layout engine to measure/arrange the element and propagate the Width, Height to ActualWidth, ActualHeight.
For example:
// Set the width, height you want
element.Width = 123;
element.Height = 456;
// Force measure/arrange
element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
element.Arrange(new Rect(0, 0, element.DesiredWidth, element.DesiredHeight));
// Subject to sufficient space, the actual width, height will have
// the values propagated from width/height after a measure and arrange call
double width = element.ActualWidth;
double height = element.ActualHeight;
Also see this related question for clarity. I have used the above method occasionally to measure the text in a textblock so as to position correctly on a canvas.

RenderTargetBitmap does not respect ColumnDefinitions width

I need to create snapshot of Grid with some hidden columns (by setting it's ColumnDefinition.Width = 0).
On screen it looks fine, but the created image has all columns visible (does not respect the ColumnDefinitions). I red somewhere that it is because the RenderTargetBitmap is looking at different layer where these changes aren't present (Visual layer vs. Layout layer). Is there any chance to get realistic snapshot of the grid with correct ColumnDefinitions? I cannot simply use Rectagnel.Fill = VisualBrush, because I need to store these images in cycle (every iteration = new image).
I tried ways like this snippet
It was needed to force UpdateLayout() before each snapshot. I changed the sizes in cycle and layout was updated too late.
Call this method before you create a snapshot of an UIElement:
public static UIElement GetMeasuredAndArrangedVisual(UIElement visual)
{
visual.Measure(new Size
{
Height = double.PositiveInfinity,
Width = double.PositiveInfinity
});
visual.Arrange(new Rect(0, 0, visual.DesiredSize.Width, visual.DesiredSize.Height));
visual.UpdateLayout();
return visual;
}

Odd behaviour of Image control in WPF

I'm trying to get started with WPF in c#.
I set a Canvas as the content of a window, then I create another Canvas and put it as a child of the first Canvas (together with other elements, such as buttons and labels). Everything runs fine until I create an Image object and add it dynamically to the inner Canvas:
Image m_Img = new Image
{
Name = "img1",
Width = cvWindow.Width,
Height = dUsefulHeight,
Stretch = Stretch.Uniform
};
m_Img.Source = new BitmapImage(
new Uri(xMap.SelectSingleNode("#image").FirstChild.Value,
UriKind.RelativeOrAbsolute));
Canvas.SetLeft(m_Img, 0);
Canvas.SetTop(m_Img, 0);
double d = m_Img.Source.Width;
cvWindow.Children.Add(m_Img);
Here m_Img is the image I create, cvWindow is the inner Canvas. The source of the image is a PNG file, extracted from an XML file (the string returned is correct).
The odd behaviour is here: if I comment out the line
double d = m_Img.Source.Width;
the Image is not displayed anymore, although other controls in the Canvas (such as labels and buttons) are correctly displayed.
I don't need the width of the source image, so the compiler tells me that variable is never used.
I updated Visual Studio 2010 to the last SP1, but the behaviour remained. Google doesn't help either. I came to think that the Width property may have a getter method that triggers some action, but cannot solve the puzzle.
Edit: Same thing happens using another property of Source (e.g. Height). If I access at least one property, the image displays ok.
I finally discovered what happens: the Image control needs that the properties DecodePixelWidth and DecodePixelHeight of the Source are set to the correct values.
Once the Source is created, those values are not set, and the Image is not drawn. Upon first access to any property of the BitmapImage that serves as source the image is actually decoded and those properties are set to the final width and height of the decoded image (so the Image can be drawn).
I can solve this by setting those values by hand (with a cast to int) like this
BitmapImage bs1 = new BitmapImage();
bs1.BeginInit();
bs1.UriSource = new Uri(
xMap.SelectSingleNode("#image").FirstChild.Value,
UriKind.RelativeOrAbsolute);
bs1.EndInit();
bs1.DecodePixelHeight = (int)bs1.Height;
bs1.DecodePixelWidth = (int)bs1.Width;
m_Img.Source = bs1;
but I think I will re-design my views with a better separation (views in XAML, model and viewmodel via code).
This bit is also mentioned in the second note in http://msdn.microsoft.com/en-us/library/ms747027.aspx
When you specify the size of an image with either Width or Height, you should also set either DecodePixelWidth or DecodePixelHeight to the same respective size.
Don't define your views via code - use XAML,
even if you are trying to creating dynamic views, using XAML is much more clean
and using a good designer app (e.g. Blend), you'll notice things that you didn't consider.
Some things that you didn't consider are that .Width is not necessary equal to .ActalWidth.
Default widths are usually double.NaN (which means auto-width).
Use bindings, bind width of A to width of B, use margin, padding or value converters to make width A binded to width of B - const.
Use layout panels (e.g. Grid, StackPanel, DockPanel) to make alignment of multiple controls simple (and binding free).
Also, prefer using standard naming conventions (e.g. no m_).

Categories