Centering or manually positioning dynamic controls on a WPF Canvas - c#

I'm trying to create Labels that are centered around different columns on a Canvas. This code looks plausible:
string[] titles = { "Acorn", "Banana", "Chrysanthemum" };
double col = 20.0;
foreach (string s in titles)
{
var lbl = new Label() { Content = s };
lbl.SetValue(Canvas.LeftProperty, col - (lbl.Width / 2.0));
myCanvas.Children.Add(lbl);
col += 150.0;
}
But it does not work because the lbl.Width (and lbl.ActualWidth) aren't calculated until rendering, which is long after the Labels are being created.
Is there a way to get the accurate Label.Width prior to a layout operation? On a Canvas there isn't all the control layout and flow you get with Grids or StackPanels.

The trick is to ask the component what size it wants to be with the Measure method. If you specify unlimited available space with the double.PositiveInfinity value, you can then use the control's DesiredSize property to get its ideal, unclipped, unflowed size.
This code shows the working solution:
string[] titles = { "Acorn", "Banana", "Chrysanthemum" };
double col = 20.0;
foreach (string s in titles) {
var lbl = new Label() { Content = s };
lbl.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
lbl.SetValue(Canvas.LeftProperty, col - (lbl.DesiredSize.Width / 2.0));
myCanvas.Children.Add(lbl);
col += 150.0;
}

You cant get the width before a layout pass... But what you could try is setting the Label to Visibility of Hidden (not Collapsed), then attach a Loaded event, and set its position then change back the visibility?

Have you tried:
double pos = Canvas.GetLeft(lbl);
I'm not sure if this works before the layout pass, but worth a shot

Related

Dynamically add Chart to Windows Forms - shows up blank

I'm trying to add a Chart control dynamically to a Form (using C#, this should all be .NET 4.0), but it's always blank (only the background color shows). I tried the same code in a Form that already has a control and it works, so I imagine it's some initialization function I should call (I tried Invalidate() on the control and Refresh() on both control and the panel it's being placed in, made no difference). I went through the few similar posts I found, tried throwing in any other commands they used (BeginInit() is from one such post) but no luck so far. Any ideas?
BTW I want to display 6-9 charts (position, speed and acceleration in 3D space) so I'd rather add them dynamically than have 9 sets of assignments. Here's the code that adds the charts to the panel:
foreach (KeyValuePair<string, List<double>> p in b.storedValues)
{
Control c = getChartForData(p);
panel1.Controls.Add(c);
c.Invalidate();
c.Refresh();
break;
}
And the function that creates each chart:
private Chart getChartForData(KeyValuePair<string, List<double>> data)
{
Chart c = new Chart();
((System.ComponentModel.ISupportInitialize)c).BeginInit();
c.Series.Clear();
c.BackColor = Color.White;
c.Height = 300;
c.Width = 500;
c.Palette = ChartColorPalette.Bright;
Series s = new Series(data.Key);
s.ChartType = SeriesChartType.Spline;
double maxValue = 0;
//NOTE: Going logarithmic on this, too big numbers
for (int i = 0; i < data.Value.Count; i++)
{
maxValue = Math.Max(Math.Log10(data.Value[i]), maxValue);
}
for (int i = 0; i < data.Value.Count; i++)
{
s.Points.AddXY(i,Math.Log10(data.Value[i]) * c.Height / maxValue);
}
c.Series.Add(s);
return c;
}
Many thanks in advance.
When you create a Chart yourself, in code, it does not contain any ChartArea.
Therefore, nothing is displayed.
I'm guessing that the designer generates some code to initialize a default chart area when you drag and drop a chart control onto the form.
Also, you should really let the chart control handle the layout, instead of calculating the desired position of each point based on the height of the chart control.
I would go as simple as possible to get something that's working, and then you can tweak the range of the axis afterwards. You can also set an axis to be logarithmic.
Start with trying out this minimal version, and make sure that displays something, before you complicate things. This works for me.
private Chart getChartForData(string key, List<double> data)
{
Chart c = new Chart();
Series s = new Series(key);
s.ChartType = SeriesChartType.Spline;
for (int i = 0; i < data.Count; i++)
{
s.Points.AddXY(i, data[i]);
}
c.Series.Add(s);
var area = c.ChartAreas.Add(c.ChartAreas.NextUniqueName());
s.ChartArea = area.Name;
// Here you can tweak the axis of the chart area - min and max value,
// where they display "ticks", and so on.
return c;
}

Print multiple canvas WPF?

Today I'm working with WPF! My app is almost finished, except for the last feature. I have created a graphic element which represents a label like the ones you can see under the goods in a supermarket shelf. Print one of them is easy, using PrintDialog.PrintVisual(myVisualItem,"description");.
Now, I have to print a series of these labels, using PintVisual() in a loop, prints every element in a separate document! There's no overload of PrintVisual() which accepts an IEnumerable of visual item. I've seen online that i should paginate these elements, but I cannot find a way to do it What can I do? Thanks!
You could combine all canvas you got into one big canvas and then print this one.
Here is some sample code how to concat multiple canvas into one big one:
public Canvas CombineCanvases(params Canvas[] canvases)
{
// Force each canvas to update its desired size
foreach (var canvas in canvases) { canvas.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); }
var result = new Canvas
{
Height = canvases.Sum(x => x.DesiredSize.Height), // add all heights to get needed height
Width = canvases.Max(x => x.DesiredSize.Width) // find required width
};
var pos = 0d;
foreach (var canvas in canvases)
{
Canvas.SetLeft(canvas, 0);
Canvas.SetTop(canvas, pos); // position element at this pixel count
pos += canvas.DesiredSize.Height; // and increment it
result.Children.Add(canvas);
}
return result;
}
You need to pass all canvas's you want to print and print the result.
var c1 = new Canvas
{
Width = 100,
Height = 100,
Children = { new TextBlock { Text = "Canvas 1" } }
};
var c2 = new Canvas
{
Width = 100,
Height = 100,
Children = { new TextBlock { Text = "Canvas 2" } }
};
var canvas = CombineCanvases(c1, c2);
// print canvas here
Pagination would be if you check how many labels fit on one page and then not concat all canvas into one huge on, but just take as many as are needed to fill one page.
Then print the page and repeat this until all Labels were printed. This is called pagination.
If you believe they will all fit on one page, you could put them in a StackPanel with Orientation = Vertical. Then print the stack panel.
var panel = new StackPanel() { Orientation = Orientation.Vertical };
panel.Children.Add(canvas1);
panel.Children.Add(canvas2);
panel.UpdateLayout();
var dlg = new PrintDialog();
dlg.PrintVisual(panel, "description");

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.

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