Stretch children proportionally inside StackPanel - c#

There's a StackPanel
<StackPanel x:Name="sp1" Orientation="Horizontal" BorderBrush="Black" BorderThickness="1" />
which is dynamically populated with rectangles:
var values = new[] { 30, 50, 20 };
foreach (int val in values)
{
sp1.Children.Add(new Rectangle
{
// Width = perecntage, not supported on Rectangle
VerticalAlignment = VerticalAlignment.Stretch,
Fill = new SolidColorBrush(colors.Pop()),
});
}
Width has to be a proportion of the parent's width based on val.
I've tried to set it via the SizeChanged event, but this works only for enlarging. Shrinking the window doesn't bring the width back.
sp1.SizeChanged += (sender, args) =>
{
int i = 0;
foreach (var val in values)
{
var prop = (double)val / valuesSum;
var r = (Rectangle)sp1.Children[i++];
r.Width = (int)(prop * args.NewSize.Width);
}
}
The only objective is to have a bar that is proportionally divided and stretched. Any idea?

It isn't entirely clear why you want to use a StackPanel for this. A StackPanel grows as it's contents get larger, the individual controls are 'stacked' to fit into the space available to the StackPanel. That doesn't match your requirements for "a bar that is proportionally divided and stretched".
If you use something like a Grid, you can define the columns and rows to be proportionally spaced. For example, a Grid with two columns whose widths are defined as 2* and 3* those columns will be two-fifths of the width of the Grid, and three-fifths (fifths because 2+3=5). Needless to say, you can have as many columns as you like, and define the relative sizing as you require, possibly using 30*,50*,20* from information in the question. Or if you take the stars away, the values become literal values rather than relative values.
If you just want something to evenly space things out, try a UniformGrid. You can limit the number of columns or rows.
If you have some complex need which is not covered by anything else, you can write a custom container control.

This can be achieved by calling child.Arrange(), which lets you set X.
The child has to have Width unset, otherwise it takes precedence.
sp1.SizeChanged += (sender, e) =>
{
var childRect = new Rect(0, 0, 0, e.NewSize.Height);
for (int i = 0; i < values.Length; i++)
{
var childWidth = (double)values[i] / valuesSum * e.NewSize.Width;
childRect.Width = childWidth;
so1.Children[i].Arrange(childRect);
childRect.X += childWidth;
}
}
I tried RelativePanel and StackPanel and both worked the same.
This can also be done in a class derived from Panel. The code is the same, but it's called from overridden ArrangeOverride().

Related

Resetting a Stack Panel height to dynamic

If no height is set, a Stack Panel's height is dynamic based on its contents. My question is: if the c# code sets the height, is there a way (in c#) to wipe those values out and return the Stack Panel to sizing dynamically? Something like this...
spnTest.Height = 100; //expands height to 100, even if contents don't fill it
spnTest.HeightIsDynamic = true; //allows the height of spnTest to go back to being dynamic and expand/contract based on its contents
For some reason, setting the height to double.NaN has no effect...
stkTest.Height = 117;
Console.WriteLine($"Height = {stkTest.Height}"); //writes "Height = 117"
stkTest.Height = double.NaN;
Console.WriteLine($"Height = {stkTest.Height}"); //writes "Height = 117"
.ClearValue(HeightProperty) also does not work
A StackPanel initially measures the needed size of the element by using the sizes of the child elements contained within. This initial measurement should be stored in DesiredSize. I would try using the height that is stored there to get the original measurement for the Element. Check out MSDN entry.
MSDN StackPanel

How to add a percentage to top of Column chart in C#

So, here's the problem.
I have a chart that displays two columns, Completed and Uncompleted, across a number of work types, using the following loop:
foreach (var workType in model.WorkTypes)
{
decimal completed = 0;
decimal uncompleted = 0;
decimal workSubmitted = 0;
decimal completionRate= 0;
foreach (var rec in model.JobList.Where(x => x.jobType== workType.Id))
{
uncompleted += model.JobList.SingleOrDefault(x => x.recID== rec.recID && x.jobType == workType.Id).Uncompleted;
completed += model.JobList.SingleOrDefault(x => x.recID == rec.recID && x.jobType == workType.Id).Completed;
}
workSubmitted = uncompleted + completed;
if (uncompleted != 0)
{
completionRate= (completed/ workSubmitted) * 100;
}
myChart.Series["Uncompleted"].Points.AddXY(workType.TypeName, uncompleted );
myChart.Series["Completed"].Points.AddXY(workType.TypeName, completed);
}
What I am trying to do is have it display a label above the two columns that displays the completionRate value as a percentage for each workType.
Any help or advice would be appreciated.
This is the current look of the chart:
By default the Labels show the y-value but you can set an arbitrary Label for each DataPoint e.g. when you add the point like this:
int p = myChart.Series["Uncompleted"].Points.AddXY(workType.TypeName, rejections);
myChart.Series["Uncompleted"].Points[p].Label = sometext;
And of course you can calculate the text for the label as needed, e.g.:
string sometext = (workSubmitted / rejections * 100).ToString("0.00") + "%";
Note that you must update the Label after changing the values in your calculation. No automatic expressions are supported!
Update
As I wrote, placing a Label centered at the x-value the columns share, is hard or even impossible; that is because a Label belongs to an individual data point. This is a unique problem with column (and bar) type charts, since here the points of the series are displayed in clusters around the common x-value. (We could workaround if and only if we had an odd number of series by adding the labels to the mid points)
So we need to use Annotations. Here is a function that will place a TextAnnotation centered at the x-value and at the height of the larger y-value of two data points..:
void setCenterAnnotation(Chart chart, ChartArea ca,
DataPoint dp1, DataPoint dp2, string lbl)
{
TextAnnotation ta = new TextAnnotation();
ta.Alignment = ContentAlignment.BottomCenter;
ta.AnchorAlignment = ContentAlignment.TopCenter;
DataPoint dp = dp1.YValues[0] > dp2.YValues[0] ? dp1 : dp2;
ta.Height = 0.36f;
ta.AxisX = ca.AxisX;
ta.AxisY = ca.AxisY;
ta.AnchorDataPoint = dp;
ta.AnchorX = dp1.XValue;
ta.Text = lbl;
chart.Annotations.Add(ta);
}
If you have more than two Series you would best determine the anchorpoint, i.e. the one with the larger value before, and pass it instead of the two points I pass here..
Placing/anchoring annotations is not really obvious, so here are a few notes:
I anchor to a DataPoint to make it show at the height of its y-value.
To use (axis-)values for anchoring one has to assign one or both axes to it.
I then (order matters!) set the AnchorX property so that it is not centered over a point but over the common x-value.
I also set some Height or else the text won't move up on top of the column; not quite sure what the rationale is here..
Here is the result:
I had added the anotations while adding the points:
int ix = s1.Points.AddXY(i, rnd.Next(i+7));
s2.Points.AddXY(i, rnd.Next(i+4)+3);
double vmax = Math.Max(s1.Points[ix].YValues[0], s2.Points[ix].YValues[0]);
string lbl = (vmax / 123f).ToString("0.0") + "%";
setCenterAnnotation(chart12, ca, s1.Points[ix], s2.Points[ix], lbl );

C# WinForms Chart Control: get Size,Region,Position of Bar

is there a way to get the rectangles of the stackcolumn chart bar?
this code snippet is how it can be works but it's very ugly:
var points = new List<Point>();
for (int x = 0; x < chart.Size.Width; x++)
{
for (int y = 0; y < chart.Size.Height; y++)
{
var hp = chart.HitTest(x, y, false, ChartElementType.DataPoint);
var result = hp.Where(h => h.Series?.Name == "Cats");
if (result.Count() > 0)
{
points.Add(new Point(x, y));
}
}
}
var bottomright = points.First();
var topleft = points.Last();
I will try to describe my purpose:
I would like to create a chart from various testresults and make this available as a HTML file. This generated Chart is inserted as an image file in the HTML document. Now, I would like to link each part of a Bar area from the Chart to an external document. Since the graphics is static, I have only the possibility to use the "MAP Area" element to make any area as a link from HTML. The "map" element requires a "rectangle", or these coordinates. That's the reason why I need the coordinator of each part of a Bar.
I have to mention that I am not really familiar with the Chart control yet.
The graphics is generated testweise.
[SOLVED]
i got the solution:
var stackedColumns = new List<Tuple<string,string,Rectangle>>();
for (int p = 0; p < chart.Series.Select(sm => sm.Points.Count).Max(); p++)
{
var totalPoints = 0;
foreach (var series in chart.Series)
{
var width = int.Parse(series.GetCustomProperty("PixelPointWidth"));
var x = (int)area.AxisX.ValueToPixelPosition(p + 1) - (width / 2);
int y = (int)area.AxisY.ValueToPixelPosition(totalPoints);
totalPoints += series.Points.Count > p ? (int)series.Points[p].YValues[0] : 0;
int y_total = (int)area.AxisY.ValueToPixelPosition(totalPoints);
var rect = new Rectangle(x, y_total, width, Math.Abs(y - y_total));
stackedColumns.Add(new Tuple<string, string, Rectangle>(series.Name, series.Points.ElementAtOrDefault(p)?.AxisLabel, rect));
}
}
this workaround works for stackedcolumn and points starts at x-axis=0.
just the PixelPointWidth property has to be set manualy to get the right width. i have not yet found a way to get the default bar width..
This is extremely tricky and I really wish I knew how to get the bounds from some chart functionionality!
You code snippet is actulally a good start for a workaround. I agree though that it has issues:
It is ugly
It doesn't always work
It has terrible performance
Let's tackle these issues one by one:
Yes it is ugly, but then that's the way of workarounds. My solution is even uglier ;-)
There are two things I found don't work:
You can't call a HitTest during a Pre/PostPaint event or terrible things will happen, like some Series go missing, SO exceptions or other crashes..
The result for the widths of the last Series are off by 1-2 pixels.
The performance of testing each pixel in the chart will be terrible even for small charts, but gets worse and worse when you enlarge the chart. This is relatively easy to prevent, though..:
What we are searching are bounding rectangles for each DataPoint of each Series.
A rectangle is defined by left and right or width plus top and bottom or height.
We can get precise values for top and bottom by using the axis function ValueToPixelPosition feeding in the y-value and 0 for each point. This is simple and cheap.
With that out of the way we still need to find the left and right edges of the points. To do so all we need to do it test along the zero-line. (All points will either start or end there!)
This greatly reduces the number of tests.
I have decided to do the testing for each series separately, restaring at 0 each time. For even better performance one could do it all in one go.
Here is a function that returns a List<Rectangle> for a given Series:
List<Rectangle> GetColumnSeriesRectangles(Series s, Chart chart, ChartArea ca)
{
ca.RecalculateAxesScale();
List<Rectangle> rex = new List<Rectangle>();
int loff = s == chart.Series.Last() ? 2 : 0; ;
int y0 = (int)ca.AxisY.ValueToPixelPosition(0);
int left = -1;
int right = -1;
foreach (var dp in s.Points)
{
left = -1;
int delta = 0;
int off = dp.YValues[0] > 0 ? delta : -delta;
for (int x = 0; x < chart.Width; x++)
{
var hitt = chart.HitTest(x, y0 +off );
if (hitt.ChartElementType == ChartElementType.DataPoint &&
((DataPoint)hitt.Object) == dp)
{
if (left < 0) left = x;
right = x;
}
else if (left > 0 && right > left) break;
}
int y = (int)ca.AxisY.ValueToPixelPosition(dp.YValues[0]);
rex.Add(new Rectangle(left, Math.Min(y0, y),
right - left + 1 - loff, Math.Abs(y - y0)));
left = -1;
}
return rex;
}
A few notes:
I start by doing a RecalculateAxesScale because we can't Hittest before the current layout has been calculated.
I use a helper variable loff to hold the offset for the width in the last Series.
I start searching at the last x coordinate as the points should all lie in sequence. If they don't because you have used funny x-values or inserted points you may need to start at 0 instead..
I use y0 as the baseline of the zero values for both the hittesting y and also the points' base.
I use a little Math to get the bounds right for both positive and negative y-values.
Here is a structure to hold those rectangles for all Series and code to collect them:
Dictionary<string, List<Rectangle>> ChartColumnRectangles = null;
Dictionary<string, List<Rectangle>> GetChartColumnRectangles(Chart chart, ChartArea ca)
{
Dictionary<string, List<Rectangle>> allrex = new Dictionary<string, List<Rectangle>>();
foreach (var s in chart.Series)
{
allrex.Add(s.Name, GetColumnSeriesRectangles(s, chart, ca));
}
return allrex;
}
We need to re-calculate the rectangles whenever we add points or resize the chart; also whenever the axis view changes. The common code for AxisViewChanged, ClientSizeChanged, Resize and any spot you add or remove points could look like this:
Chart chart= sender as Chart;
GetChartColumnRectangles(chart, chart.ChartAreas[0]);
Let's test the result with a Paint event:
private void chart1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
chart1.ApplyPaletteColors();
foreach (var kv in ChartColumnRectangles)
{
{
foreach (var r in kv.Value)
g.DrawRectangle(Pens.Black, r);
}
}
}
Here it is in action:
Well, I've been down this path and the BIG issue for me is that the custom property of 'PixelPointWidth' is just that - it is custom. You cannot retrieve it unless you've set it. I needed the width of the item - had to scwag/calculate it myself. Keep in mind that many charts can be panned/zoomed, so once you go down this path, then you need to recalculate it and set it for the chart prepaint events.
Here is a crude little function I made (is more verbose than needed - for educational purposes and has no error handling :)):
private int CalculateChartPixelPointWidth(Chart chart, ChartArea chartArea, Series series)
{
// Get right side - takes some goofy stuff - as the pixel location isn't available
var areaRightX = Math.Round(GetChartAreaRightPositionX(chart, chartArea));
var xValue = series.Points[0].XValue;
var xPixelValue = chartArea.AxisX.ValueToPixelPosition(xValue);
var seriesLeftX = chart.Location.X + xPixelValue;
var viewPointWidth = Math.Round((areaRightX - seriesLeftX - (series.Points.Count * 2)) / series.Points.Count, 0);
return Convert.ToInt32(viewPointWidth);
}
And this as well:
private double GetChartAreaRightPositionX(Chart chart, ChartArea area)
{
var xLoc = chart.Location.X;
return xLoc + (area.Position.Width + area.Position.X) / 100 * chart.Size.Width;
}
The reason I'm calculating this is because I need to draw some graphical overlays on top of the normal chart item objects (my own rendering for my own purposes).
In the 'prepaint' event for the chart, I need to calculate the 'PixelPointWidth' that matches the current chart view (might be panned/zoomed). I then use that value to SET the chart custom property to match . . . such that the normal chart entities and MINE are correctly aligned/scaled (ensures we're in exactly the right 'x' axis position):
In my prepaint event, I do the following - just prior to drawing my graphical entities:
// Pretty close scwag . . .
var viewPointWidth = CalculateChartPixelPointWidth(e.Chart, e.Chart.ChartAreas[0], e.Chart.Series[0]);
// Set the custom property and use the same point width for my own entities . .
chart1.Series[0].SetCustomProperty("PixelPointWidth", viewPointWidth.ToString("D"));
// . . . now draw my entities below . . .

How to find how much text fits in a textbox without scrolling

I have a relatively large text. I need to add a certain amount of this text to a textbox so that it can be visible without scrolling , then add the rest of the text to another textbox and then another -.-.-.> looping through the text generating as many textboxes as necessary.
My problem is i don't know how to find out how much of the text fits in each textbox. So far the only thing i was able to do is assign a fixed number of characters that fit in a page. But this would not do for different screen resolutions. Is there a way, a trick or a workaround i can use to calculate how much of a text can fit into a textbox with fixed font and fontsize but relative width and height?
int TextLength = 1000, PageStart = 0;
List<TextBox> Pages = new List<TextBox>();
while (PageStart < TextLength)
{
TextBox p = new TextBox();
if (PageStart + PageLength < TextLength)
{
p.PageText = Text.Substring(PageStart, PageLength);
PageStart += PageLength;
Pages.Add(p);
}
else
{
PageLength = TextLength - PageStart;
p.PageText = Text.Substring(PageStart, PageLength);
Pages.Add(p);
break;
}
}
You would probably be better of using a TextBlock. Other than that the TextBlock measuring technique should work for TextBoxes too - how to calculate the textbock height and width in on load if i create textblock from code?
You would need to measure ActualHeight while increasing the amount of text until you go over your limit.

How to AutoSize the height of a Label but not the width

I have a Panel that I'm creating programmatically; additionally I'm adding several components to it.
One of these components is a Label which will contain user-generated content.
I don't know how tall the label should be, but it does have a fixed width.
How can I set the height so that it displays all the text, without changing the width?
Just use the AutoSize property, set it back to True.
Set the MaximumSize property to, say, (60, 0) so it can't grow horizontally, only vertically.
Use Graphics.MeasureString:
public SizeF MeasureString(
string text,
Font font,
int width
)
The width parameter specifies the
maximum value of the width component
of the returned SizeF structure
(Width). If the width parameter is
less than the actual width of the
string, the returned Width component
is truncated to a value representing
the maximum number of characters that
will fit within the specified width.
To accommodate the entire string, the
returned Height component is adjusted
to a value that allows displaying the
string with character wrap.
In other words, this function can calculate the height of your string based on its width.
If you have a label and you want have control over the the vertical fit, you can do the following:
MyLabel.MaximumSize = new Size(MyLabel.Width, 0)
MyLabel.Height = MyLabel.PreferredHeight
MyLabel.MaximumSize = new Size(0, 0)
This is useful for example if you have a label in a container that can be resized. In that case, you can set the Anchor property so that the label is resized horizontally but not vertically, and in the resize event, you can fit the height using the method above.
To avoid the vertical fitting to be interpreted as a new resize event, you can use a boolean:
bool _inVerticalFit = false;
And in the resize event:
if (_inVerticalFit) return;
_inVerticalFit = true;
MyLabel.MaximumSize = new Size(MyLabel.Width, 0)
MyLabel.Height = MyLabel.PreferredHeight
MyLabel.MaximumSize = new Size(0, 0)
_inVerticalFit = false;

Categories

Resources