WPF Canvas.ActualWidth zero, even after loading - c#

I cannot get ActualWidth of a canvas to any nonzero value. I simplified the real scenario and devoted a new WPF Application in VS to getting a nonzero value (and understanding). Unfortunately, I'm only getting zero. I feel like I'm missing some basic WPF understanding: I'm not that familiar with WPF.
I copied the MDSN demo, modified it slightly and I now have
public partial class MainWindow : Window {
private Canvas myCanvas;
public MainWindow()
{
InitializeComponent();
CreateAndShowMainWindow();
}
private void CreateAndShowMainWindow()
{
// Create a canvas sized to fill the window
myCanvas = new Canvas();
myCanvas.Background = Brushes.LightSteelBlue;
// Add a "Hello World!" text element to the Canvas
TextBlock txt1 = new TextBlock();
txt1.FontSize = 14;
txt1.Text = "Hello World!";
Canvas.SetTop(txt1, 100);
Canvas.SetLeft(txt1, 10);
myCanvas.Children.Add(txt1);
// Add a second text element to show how absolute positioning works in a Canvas
TextBlock txt2 = new TextBlock();
txt2.FontSize = 22;
txt2.Text = "Isn't absolute positioning handy?";
Canvas.SetTop(txt2, 200);
Canvas.SetLeft(txt2, 75);
myCanvas.Children.Add(txt2);
Grid content = new Grid();
content.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
content.Children.Add(myCanvas);
this.Content = content;
this.Loaded += MainWindow_Loaded;
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
var canvasRenderWidth = myCanvas.RenderSize.Width;
var canvasActualWidth = myCanvas.ActualWidth;
} //place breakpoint here
I expect the canvas to have the same ActualWidth as the textbox after loading, but at the specified breakpoint, it's zero. Notice that the textboxes are visible when running the code above.
Can someone tell me how to make myCanvas.ActualWidth to automatically become the textbox.ActualWidth or tell me why this shouldn't be done?
In my real usage scenario I've got a Canvas in a in column of a Grid, where the columndefinition's width is set to auto, so I expected it to increase as the canvas' width increases. However, this fails, and I suspect it's due to the canvas.ActualWidth being zero.

Remove this line, and it will work:
content.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
Note: The canvas is never resizing itself to fit it's content. The reason why the Text is visible in your example, is that the canvas is not clipping it's content.
when you set
myCanvas.ClipToBounds = true;
the text will also disappear.

Related

How to change control's size and add new control while form is minimized

I have a windows form that has 2 panels inside it, one takes up left half of the form, one right. Now when a certain event occurs in my program (I receive a message from server for example), I want to add a new control (say a third panel) between the two existing, so I need to make make them smaller and move them to the sides of my form.
This can happen while the form is minimized and this is where my problem is. Panels Size returns [0,0] when the form is minimized so I cant use it for calculations.
So my first question is, how can I get "original" size of controls while the form is minimized?
And then, even if I somehow calculated the new Size (say I have 400px wide form with 2x 200px panels and I want the new 3rd panel to be 200px wide, so the old panels will become 100px wide), and applied it:
leftPanel.Size = new Size(100, 100);
then after the form is restored from minimized state to normal state, the panel will be way bigger than specified 100x100. Seems like it will restore to the forms ClientSize + the newly specified size
Therefore my question: how can I add and resize controls to form while the form is minimized?
Sample procedures to resize Panels hosted in a Form and adapt the Layout when a new Panel is inserted in the middle of the two existing.
The WindowState of the hosting Form Forms is not relevant (it can be minimized, maximized or in normal state).
► Using the first method, if the Form is maximized, the Panels will retain the initial Height.
► Using the second method, as it is now, the Panels' Height will be set to the Form's ClientSize.Height. It can of course be changed, setting the TableLayoutPanel Row(s) to an Absolute height instead of AutoSize.
Using the Docking feature alone:
Set the Form AutoSizeMode = Dpi
Add two Panels to the Form (e.g., panelLeft and panelRight)
Set the Width of both Panels to 200
Set panelLeft to Dock = DockStyle.Left and panelRight to Dock = DockStyle.Right
Right-click the Panel on the left and select SendToBack (!important)
Adjust the Form Size: it should be: (418, 138). Not important, just for a visual confirmation
In the Form constructor set this.ClientSize = new Size(400, 100);
Add a new public method to the Form:
public void AdjustPanelsWidth(int newWidth)
{
this.panelLeft.Width = newWidth;
this.panelRight.Width = newWidth;
}
When you need to add a new Panel in the middle of the two existing Panels:
(someForm represents the current instance of the minimized Form)
int newSize = 100;
someForm.AdjustPanelsWidth(newSize);
var p = new Panel() {
Size = new Size(newSize * 2, 100),
Dock = DockStyle.Fill
};
p.BringToFront();
someForm.Controls.Add(p);
Using a TableLayoutPanel:
Add a TableLayoutPanel to the Form
Set it to Dock = DockStyle.Top
Edit Columns and Rows to have 3 Columns and 1 Row
Set the Columns Styles in the TLP Designer as:
Columns:
(0) Percent 50%
(1) AutoSize
(2) Percent 50%
Row:
(0) AutoSize
Closing the TLP Designer, it should appear to have just two Columns: since the central one is auto-sized and it has no content, its Width is currently 0.
Add two Panels to the Form (not to the TableLayoutPanel directly)
Set the Size of the Panels = (200, 100)
Drag one Panel inside the left Column of the TLP and the other to the Column on the right
! Verify, in VS Property panel, that the Column property of Panel on the Left is Column 0
The same for the Panel on the Right: the Colum property must be Column 2
If the Column is wrong, edit it manually.
Select both Panels and set both to Dock = DockStyle.Fill
(now you should see the TLP completely filled by the Panels, both occupying 50% of the TLP Size)
Adjust the Form size as before (still not actually important)
In the Form constructor set this.ClientSize = new Size(400, 100); (as before)
Add a public method to the Form:
public void AddControl(Control control)
{
// Add a Control to Column 1 - Row 0
this.tableLayoutPanel1.Controls.Add(control, 1, 0);
panel.Dock = DockStyle.Fill;
}
To add a new Panel in the middle Column:
var p = new Panel() {
Size = new Size(200, 100),
BackColor = Color.Red,
Margin = new Padding(0)
};
someForm.AddControl(p);
Structure of a Form that implements the TableLAyoutPanel method described:
ClockMinimize() => Minimizes the Clock size, squeezing it between two other Panels
ClockShow() => Enlarges the Clock to overlap the other Panels, which will resize to completely fill the Form's ClientArea:
using System.Drawing;
using System.Windows.Forms;
public partial class frmClock : Form
{
public frmClock() => InitializeComponent();
private int m_ClientHeight = 0;
public void ClockShow()
{
this.panClock.Parent = this;
this.panClock.Size = new Size(360, 80);
this.panClock.Location = new Point(20, 10);
// Adjust the Clock Font Size here
this.panClock.BringToFront();
}
public void ClockMinimize()
{
this.panClock.Size = new Size(200, 40);
tableLayoutPanel1.Controls.Add(this.panClock, 1, 0);
this.panClock.Margin = new Padding(0, (m_ClientHeight - this.panClock.Height) / 2, 0, 0);
// Adjust the Clock Font Size here
AdjustPanelsWidth(panClock.Width / 2);
}
public void AdjustPanelsWidth(int newWidth)
{
this.panLeft.Width = newWidth;
this.panRight.Width = newWidth;
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
this.MinimumSize = this.Size;
m_ClientHeight = this.ClientSize.Height;
}
protected override void OnClientSizeChanged(EventArgs e)
{
base.OnClientSizeChanged(e);
if (this.ClientSize.Height > 0) {
m_ClientHeight = this.ClientSize.Height;
}
}
}
I have come up with this workaround: I'm postponing resizing/moving until the form returns from minimized state, using async/await.
Instead of my original function:
public void changeControlPositionAndSize() {
//calculate new size and location based on size and location of neighboring controls
myPanel.Location = ...
myPanel.Size = ...
}
I'm now using:
public async Task changeControlPositionAndSize()
{
while (WindowState == FormWindowState.Minimized)
{
await Task.Delay(2000);
}
//calculate new size and location based on size and location of neighboring controls
myPanel.Location = ...
myPanel.Size = ...
}

Textblock margin causes out of bounds text

I'm currently trying to create a visual component to have scrolling text (left to right and right to left) - pretty much an html marquee.
I have a grid divided in several columns & rows, and I want to place my component inside one of the grid slots.
The grid (named UIGrid) is generated like this :
for (int i = 0; i < xDivisions; i++)
{
ColumnDefinition newColumn = new ColumnDefinition();
UIGrid.ColumnDefinitions.Add(newColumn);
}
for (int i = 0; i < yDivisions; i++)
{
RowDefinition newRow = new RowDefinition();
UIGrid.RowDefinitions.Add(newRow);
}
The component I'm adding is just a border with a textblock as a child. I place the border inside the Grid like this :
border = new Border();
Grid.SetColumn(border, xPosition);
Grid.SetRow(border, yPosition);
textBlock = new TextBlock();
border.Child = textBlock;
textBlock.Text = "Scrolling text from left to right";
UIGrid.Children.Add(border);
I'm using a timer to increment the textblock margin, here's the timer callback simplified body :
textBlock.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
double textWidth = textBlock.DesiredSize.Width;
double visibleWidth = componentBase.ActualWidth;
double targetMargin = textWidth < visibleWidth ? visibleWidth : textWidth;
if (margin.Left == targetMargin)
{
margin.Left = -textWidth;
} else
{
margin.Left++;
}
When the text slides from left to right, it behaves nicely :
https://s10.postimg.org/p0nt7vl09/text_good.png
Text "leaving" the grid slot is hidden.
However, when I set the textblock's margin as negative so it may come back inside the viewable area from the left, the text is visible even though it's outside its allocated slot :
https://s10.postimg.org/pownqtjq1/text_bad.png
I've tried using padding instead, but I can't set a negative padding. I've tried a few other things, but I feel like I've encountered a roadblock.
What could I do to get a nicely scrolling text ?
If you want nicely scrolling text ListView might be a better option. It is dynamic and you can bind it to your object. It would take a lot of this guess work out.
Ed Plunkett led me in the right direction with the Clip property. The idea is to do this :
border.Clip = new RectangleGeometry
{
Rect = new Rect(0, 0, border.ActualWidth, border.ActualHeight)
};
Of course, that doesn't work if the border hasn't been rendered yet (and of course it isn't when my code is running). You can force the measurement to take place using 'Measure' as I did to measure the text length in pixels, but it behaved strangely on my border. I wouldn't get the correct size at all.
In the end, I simply subscribed to the border's SizeChanged event :
border.SizeChanged += OnSizeComputed;
When that event is fired, I create the RectangleGeometry using ActualWidth & ActualHeight.

Slide Border onto screen

I need to make a border slide from the bottom of the screen into view, however i'm having issues getting the ActualHeight of my border control. Because this is animation code i'm putting it in the code-behind as it's the Views responsibility.
My Border has got it's Loaded event tied to this:
private void NotifcationWindow_Loaded(object sender, RoutedEventArgs e)
{
SlideFromBottom(sender);
}
The Sender is the Border object, so the SlideFromBottom method SHOULD be able to use this object and get it's height as it has already been rendered.
public void SlideFromBottom(object sender)
{
//The Notification container
Border notification = (sender as Border);
//The screen size
var workingArea = SystemParameters.WorkArea;
Storyboard sb = new Storyboard();
var animation = new DoubleAnimation()
{
BeginTime = TimeSpan.FromSeconds(0),
Duration = TimeSpan.FromSeconds(5),
// Get the height and turn it to a negative value, and add screen height.
From = (notification.ActualHeight * -1),
//Slide the border into view
To = notification.ActualHeight
};
Storyboard.SetTarget(animation, notification);
Storyboard.SetTargetProperty(animation, new PropertyPath("(Margin.Bottom)"));
sb.Begin();
}
I'm not receiving any errors, but the animation isn't playing, have I got something wrong? The Border is Vertically Aligned to the bottom, so the negative margin should take it off the screen.
The reason why you're not seeing anything is that you haven't added the animation to the storyboard:
sb.Children.Add(animation);
Then there are some more problems. Such that a part of the margin cannot be animated separately. You would need a ThicknessAnimation.
But there is an easier solution. Use the RenderTransform. If you give your border the following render transform:
<Border>
<Border.RenderTransform>
<TranslateTransform/>
</Border.RenderTransform>
</Border>
, then you can animate it as follows:
// ...
var animation = new DoubleAnimation()
{
BeginTime = TimeSpan.FromSeconds(0),
Duration = TimeSpan.FromSeconds(5),
From = notification.ActualHeight,
To = 0
};
Storyboard.SetTarget(animation, notification);
Storyboard.SetTargetProperty(animation, new PropertyPath("RenderTransform.Y"));
sb.Children.Add(animation);
// ...

WinForms Button: Autosize Maximumsize

I want to add Buttons to a FlowLayoutPanel. The Buttons might contain longer texts with spaces between the words. The Buttons are Autosize=true and AutoSizeMode = AutoSizeMode.GrowAndShrink. Further more I set the MaximumSize property to (maxwidth,0). maxwidth is the width of the panel. So the button does not grow too wide.
What I see is, that the widht of the Button is limited by the MaximumSize property, but when text wrapping occurs, the Button's height doesn't autosize to the height of the wrapped text. Is there a solution to that problem?
I also tried this manually sizing the button like this:
using (Graphics cg = this.CreateGraphics()) {
SizeF size = cg.MeasureString(button.Text, button.Font, 200);
button.Width = (int)size.Width+20;
button.Height = (int)size.Height+20;
button.Text = someLongTextWithSpaces;
}
But please note that I added 20 to the calculated size. It's working, but is there a proper way to determin this additional size? Maybe 2x Padding + ?????
A few hours later...
I came to this version which seems to work quite fine.
using (Graphics cg = this.CreateGraphics()) {
var fmt = TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.WordBreak;
var prop = new Size(tableLayoutPanel1.Width - 20, 0);
var size = TextRenderer.MeasureText(button.Text, button.Font, prop, fmt);
int border = button.Height - button.Font.Height;
button.Width = (int)size.Width + border;
button.Height = (int)size.Height + border;
button.Text = someLongTextWithSpaces;
}
It seems that the initial button height is borders + the height the font. So I calculated the border subtracting button.Height-button.font.Height.
According to Hans, I now use the TextRenderer.MeasureText. I tested it without enabling VisualStyles and it worked fine. Any comments on that?
There is a proper way, but it isn't exactly very subtle. Reverse-engineering it from the ButtonRenderer class source code, the Winforms class that draws the button text, you must use the TextRenderer class to measure the text. And you must use the VisualStyleRenderer.GetBackgroundContentRectangle() method to obtain the effective drawing bounds. Note that it is smaller than the button's Size because of the border and a margin that depends on the selected visual style.
Non-trivial problems are mapping a calculated content rectangle back to the outer button size and dealing with old machines that don't have visual styles enabled. Sample code that appeared to arrive at the correct size:
private static void SetButtonSize(Graphics gr, Button button) {
VisualStyleElement ButtonElement = VisualStyleElement.Button.PushButton.Normal;
var visualStyleRenderer = new VisualStyleRenderer(ButtonElement.ClassName, ButtonElement.Part, 0);
var bounds = visualStyleRenderer.GetBackgroundContentRectangle(gr, button.Bounds);
var margin = button.Height - bounds.Height;
var fmt = TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter | TextFormatFlags.WordBreak;
var prop = new Size(bounds.Width, 0);
var size = TextRenderer.MeasureText(button.Text, button.Font, prop, fmt);
button.ClientSize = new Size(button.ClientSize.Width, size.Height - margin);
}
protected override void OnLoad(EventArgs e) {
using (var gr = this.CreateGraphics()) {
SetButtonSize(gr, this.button1);
}
base.OnLoad(e);
}
Not extensively tested for corner cases, can't say I recommend this.
It seems that the initial button height is borders + the height the font. So I calculated the border subtracting button.Height-button.font.Height. (See the last block of my original post)
This also works with VisualStyles enabled/disabled.
You should control the line breaks by adding newline characters in the text. Automatic text wrapping won't work with spaces alone:
button1.Text = "123232131232\r\nfgfdgfdgdfgdfgdf\r\nASDSADSDASD";
Or :
button1.Text = "123232131232" + Environment.NewLine +
"fgfdgfdgdfgdfgdf" + Environment.NewLine + "ASDSADSDASD";
If you'd rather get the automatic wrapping you could try to use TextMeasure to determine the height needed for the text and then set the button's height accordingly but that may need some extra attention..
But I suggest to consider using Labels instead. For a Label the wrapping works out of the box.. Huge Buttons with varying sizes are non-standard UI elements.

WPF printing/xps issue

I have written the following chunk of code that prints my ListBox perfectly when being sent to a physical printer, however when trying to send it to the XPS printer driver or using the XpsDocumentWriter class (I assume they use the same code under the hood) I receive the following exception:
System.ArgumentException was unhandled by user code
Message=Width and Height must be non-negative.
Source=ReachFramework
StackTrace:
at System.Windows.Xps.Serialization.VisualSerializer.WriteTileBrush(String element, TileBrush brush, Rect bounds)
The exception obviously points to an item not having a correct width/height however I have debugged the code when sending it to the different printers (physical and XPS driver) and I haven't been able to find any differences.
Below is how I create the visual to send to the printer:
private ScrollViewer GeneratePrintableView()
{
ScrollViewer scrollView = new ScrollViewer();
Grid grid = new Grid { Background = Brushes.White, Width = this.myListBox.ActualWidth, Height = this.myListBox.ActualHeight };
grid.RowDefinitions.Add(new RowDefinition());
grid.RowDefinitions[0].Height = new GridLength(0, GridUnitType.Auto);
grid.RowDefinitions.Add(new RowDefinition());
grid.RowDefinitions[1].Height = new GridLength(0, GridUnitType.Auto);
// Add the title and icon to the top
VisualBrush titleClone = new VisualBrush(this.TitleBar);
var titleRectangle = new Rectangle { Fill = titleClone, Width = this.TitleBar.ActualWidth, Height = this.TitleBar.ActualHeight };
grid.Children.Add(titleRectangle);
Grid.SetRow(titleRectangle, 0);
this.myListBox.Width = this.myListBox.ActualWidth;
this.myListBox.Height = this.myListBox.ActualHeight;
VisualBrush clone = new VisualBrush(this.myListBox) { Stretch = Stretch.None, AutoLayoutContent = true };
var rectangle = new Rectangle { Fill = clone, Width = this.myListBox.ActualWidth, Height = this.myListBox.ActualHeight };
Border border = new Border { Background = Brushes.White, Width = this.myListBox.ActualWidth, Height = this.myListBox.ActualHeight };
border.Child = rectangle;
grid.Children.Add(border);
Grid.SetRow(border, 1);
scrollView.Width = this.myListBox.ActualWidth;
scrollView.Height = this.myListBox.ActualHeight;
scrollView.Content = grid;
scrollView.VerticalScrollBarVisibility = ScrollBarVisibility.Hidden;
return scrollView;
}
Here is the GetPage override in my DocumentPaginator implementation:
public override DocumentPage GetPage(int pageNumber)
{
Page page = new Page();
double z = 0.0;
this.grid = new Grid();
this.grid.RowDefinitions.Add(new RowDefinition());
this.grid.RowDefinitions[0].Height = new GridLength(0, GridUnitType.Auto);
this.grid.Children.Add(this.printViewer);
Grid.SetRow(this.printViewer, 0);
//Adjusting the vertical scroll offset depending on the page number
if (pageNumber + 1 == 1) //if First Page
{
this.printViewer.ScrollToVerticalOffset(0);
this.printViewer.UpdateLayout();
}
else if (pageNumber + 1 == _verticalPageCount) //if Last Page
{
if (this.printViewer.ScrollableHeight == 0) //If printing only single page and the contents fits only on one page
{
this.printViewer.ScrollToVerticalOffset(0);
this.printViewer.UpdateLayout();
}
else if (this.printViewer.ScrollableHeight <= this.printViewer.Height) //If scrollconentheight is less or equal than scrollheight
{
this.printViewer.Height = this.printViewer.ScrollableHeight;
this.printViewer.ScrollToEnd();
this.printViewer.UpdateLayout();
}
else //if the scrollcontentheight is greater than scrollheight then set the scrollviewer height to be the remainder between scrollcontentheight and scrollheight
{
this.printViewer.Height = (this.printViewer.ScrollableHeight % this.printViewer.Height) + 5;
this.printViewer.ScrollToEnd();
this.printViewer.UpdateLayout();
}
}
else //Other Pages
{
z = z + this.printViewer.Height;
this.printViewer.ScrollToVerticalOffset(z);
this.printViewer.UpdateLayout();
}
page.Content = this.grid; //put the grid into the page
page.Arrange(new Rect(this.originalMargin.Left, this.originalMargin.Top, this.ContentSize.Width, this.ContentSize.Height));
page.UpdateLayout();
return new DocumentPage(page);
}
Interestingly if I change the Fill of rectangle to a Brush instead of clone then I do not receive the exception and the outputted file is the correct size.
I have spent over a day trying to debug why this isn't working and I am hoping that someone out there has either seen a similar issue or is able to point out any mistakes I am making.
Thanks for any responses.
I had to give up finding a solution with VisualBrush. If there is a GroupBox in the Visual for the brush I could never get it to produce a XPS file. It always fails with
System.ArgumentException was unhandled by user code Message=Width and Height must be non-negative. Source=ReachFramework StackTrace: at System.Windows.Xps.Serialization.VisualSerializer.WriteTileBrush(String element, TileBrush brush, Rect bounds)
The workaround was to clone the content that should go in the VisualBrush (Is there an easy/built-in way to get an exact copy (clone) of a XAML element?) and use that directly in a Grid instead of an VisualBrush
Have you checked the value of ActualWidth and ActualHeight of myListBox when the VisualBrush is being created? I don't know from where myListBox comes, but if it is not rendered by the time you are generating your xps document you may run into problems. You can try to manually force the control to render and see if it makes any difference.
I was unable to rectify the problem however using this link Paginated printing of WPF visuals I was able to find a suitable solution to allow printing of complicated visuals within my WPF application.
It's 2016 now and it's still not fixed. The problem is using TileBrush or any descendant type (VisualBrush in your case). If you use absolute mapping, it works, it's the relative mapping that causes the problem. Calculate the final size yourself and set Viewport to this size, ViewportUnits to Absolute. Also make sure you don't use Stretch.

Categories