Got a quick question about an assignment in c#, wpf. The task is to read in an XML file, containing one description for a root panel, and from then on it follows a fixed pattern where each panel has a number of child panels, each of which can have a number of child patterns. Pretty obvious. I can read it in just fine, and traversing the model is no problem.
The thing is: I have to print these panels on a wpf canvas. The relationship between parent and child panels is the following:
the root panel has X Y coordinates to determine its starting point. Other panels do not
each panel, including the root, has a width and height (not necessarily the same)
each panel (except the root panel) has a property 'attachedToSide' which has a value from 0 to 3. The value signifies the side of the parent the child should be placed against.
When printing a panel against a parent panel, we should always put the '0' side of the panel against the parents' side.
So to demonstrate: look at the draft below. The root panel is printed. The root has 4 children. Taking the panel to the right of the root. That panel would have a property attachedToSide='1' to signify it should be stuck against the 1-side of its parent panel. Now since the rule is that the 0 side is the one that should stick to the parent, we have to flip it 90°.
And the story goes on like that.
Now, printing itself is no problem. Where I'm kinda struggling is calculating the actual positions of each square. The first children of the parent are easy, but from then on, I have to do some calculations to position them correctly, based on the previous panel, and I don't want to take the route of nested if-statements. There probably exists a really simple algorithm to fix this, but since I'm not home in that field, I'm struggling a bit. Can anyone give me a nudge in the right direction?
Detail: doing it all purely mvvm too (just for the heck of it), so 0 code in the code-behind. The shapes are an itemcollection with a custom itemspaneltemplate and itemtemplate, I'm doing the rotation by binding the rotation angle to a property in my model.
user3386109's answer shoved me in the right direction, but I got some extra info about the problem that helped me solve this. Take a look at this example:
The parent is printed with the 0-side down (this is standard). It has 3 children: right, top, left. Now, the parent is the only panel for which I receive an X, Y coordinate. That (X,Y) is the center of the 0-side. Additionally I get the width and height. For all children onward, I then get the width, height, and the side of the parent it is on. Since a child should always be connected to its parent with its own 0-side, I can calculate the childs bottom side very easily using the mod-wrapping formule user3386109 already showed:
bottom side child = (bottom side parent + 6 - parents attachment side) % 4
That's the easiest part. Now, one complication is that each child can be wider or less wide than the parent, higher or less high than the parent. That could complicate matters in terms of calculating the top left (X,Y) point from where we need to draw. One thing I always know however, is that the center point of the parent side to which the child is attached, should be the same point as the child side center that is touching that parent (see the red lines on the picture, that'll tell you what I mean).
Now I used the following approach: I decided to calculate coordinates for the top left point, assuming I could draw the child "upright", so with the bottom being the 0-side. Then, I would just rotate along that point.
Using an example:
Notice the parent panel in black. I know from the XML that I need to attach the child panel on side 1 of the parent. Therefor, I calculate the center point of the parents 1 side from its own 0-side center. I know that will be the center of the childs 0-side, since that is where I need to attach them together. I then calculate the childs top left (X,Y) coordinate, which is simple. After that, I can just rotate the child along its center 0-side point. Then we get the following result, where parent and child are connected in the center, and the child is rotated the right way as well.
In short, it's always the same approach:
take the center of the 0-side of a parent (which we will store in each panel object)
relative to that point, calculate where the 0-side center of the child will be
if we have that point, calculate the childs top left point, so we know from where to draw
rotate the child along its 0-side center point (we know the rotation degrees from the side that is at the bottom)
Done. One extra complication was that each child received a certain "offset" value. In short, this is a positive or negative value indicating to push the child to a certain direction (still attached to the parent). This problem is easily solved with just adjusting the right coordinate.
Now, to calculate all the points, it's obvious that it all depends from parent rotation, own rotation and so on. When inspecting the variations, I came to the conclusion that a lot of formulas looked suspiciously similar. The total explanation would require a lot of typing, and frankly I can't be bothered. However: here is the code that creates a child rectangle based on a given parent rectangle, child width, height which side of the parent it should be on, and the offset.
private static Rectangle CreateRectangle(string name, float width, float height, int sideOfParent, float offset, Rectangle parent)
{
Rectangle rect = new Rectangle() { Name = name, Width = width, Height = height, Offset = offset };
// Calculate which side should be at the bottom, depending on the bottom side of the parent,
// and which side of the parent the new rectangle should be attached to
rect.BottomSide = (parent.BottomSide + 6 - sideOfParent) % 4;
// Calculate the bottom mid point of the rectangle
// If | bottom side parent - bottom side child | = 2, just take over the mid bottom point of the parent
if (Math.Abs(parent.BottomSide - rect.BottomSide) == 2) { rect.MidBottom = parent.MidBottom; }
else
{
// Alternative cases
// Formulas for both bottom side parent = 0 or 2 are very similar per bottom side child variation (only plus/minus changes for Y formulas)
// Formulas for both bottom side parent = 1 or 3 are vary similar per bottom side child variation (only plus/minus changes for X formulas)
// Therefor, we create a "mutator" 1 / -1 if needed, to multiply one part of the formula with, so that we either add or subtract
Point parPoint = parent.MidBottom;
if (parent.BottomSide % 2 == 0)
{
// Parent has 0 or 2 at the bottom
int mutator = (parent.BottomSide == 0) ? 1 : -1;
switch (rect.BottomSide % 2 == 0)
{
case true: rect.MidBottom = new Point(parPoint.X, parPoint.Y - (mutator * parent.Height)); break;
case false:
if (rect.BottomSide == 1) rect.MidBottom = new Point(parPoint.X + (parent.Width / 2), parPoint.Y - (mutator * (parent.Height / 2)));
else rect.MidBottom = new Point(parPoint.X - (parent.Width / 2), parPoint.Y - (mutator * (parent.Height / 2)));
break;
}
}
else
{
// Parent has 1 or 3 at the bottom
int mutator = (parent.BottomSide == 1) ? 1 : -1;
switch (rect.BottomSide % 2 == 1)
{
case true: rect.MidBottom = new Point(parPoint.X + (mutator * parent.Height), parPoint.Y); break;
case false:
if (rect.BottomSide == 0) rect.MidBottom = new Point(parPoint.X + (mutator * (parent.Height / 2)), parPoint.Y - (parent.Width / 2));
else rect.MidBottom = new Point(parPoint.X + (mutator * (parent.Height / 2)), parPoint.Y + (parent.Width / 2));
break;
}
}
}
return rect;
}
An example of a real life result of all that:
As I already mentioned, the actual drawing just happens by putting an ItemCollection on a standard grid, binding to the collection of rectangles and setting an appropriate ItemsPanel and ItemTemplate, standard WPF there.
The model for each panel consists of
X,Y coordinates
W,H dimensions
R rotation value (one of four choices)
C a list of up to four children
A attached to side
The rotation value can be encoded as: an angle in degrees, an angle in radians, or just a number between 0 and 3. I would choose the 0 to 3 encoding, where the number represents the side at the bottom. So the root panel has a rotation value of 0.
You are given a complete set of parameters (ignoring A) for the root panel. For all the other panels, you have parameters W,H,C,A but you're missing X,Y,R. So your task is to compute X,Y,R for each panel to complete the model.
Computing the rotation value for the child
Consider the following cases which show the four possible children for each orientation of the parent:
The sequences below the drawings are the child R values, ordered by the child's A value. For example, if the parentR is 0, and the childA is 0, the childR is 2. If parentR is 0 and childA is 1, childR is 1, etc.
First thing to note is that the first number in each sequence is the number at the top of the parent. Second thing to note is that the numbers decrease by 1 (as the childA increases), wrapping to 3 after 0.
So if you take the parent's R value, add 6, and subtract the child's A value, and then apply modulo 4, you get the child's rotation value:
childR = (parentR + 6 - childA) % 4;
Computing the Y value for the child
Note that the location of the child depends primarily on the child's rotation value. If the childR is 0, the child is above the parent. If childR is 1, the child is to the right, etc.
So if the childR is odd, the child has the same Y value as the parent. If the childR is 0, then the childY is the parentY adjusted by the child's height. When the childR is 2, then the childY is the parentY adjusted by either the parent width (parentR odd), or the parent's height (parentR even).
Which results in an if-else chain that looks like this:
if ( childR % 2 ) // odd values, child left or right
childY = parentY
else if ( childR == 0 ) // child above
childY = parentY - childH
else if ( parentR % 2 ) // odd values, adjust by parent width
childY = parentY + parentW
else // even values, adjust by parent height
childY = parentY + parentH
(I'm assuming here that the X,Y coordinate represents the location of the upper-left corner of the panel, and positive Y is down.)
The X calculations are similar to the Y calculations.
So you start at the root, compute X,Y,R for the children of the root, and recursively compute those parameters for each child's children.
That completes your model. Displaying the panels on the view is easy enough, since you have X,Y,W,H,R for each panel.
You could go with a recursive function that print all the children of a panel and you pass said panel as parameters so you have an easy access to the position, the transform etc... Something in the lines of :
public void PrintSelfAndChildren(Panel parent)
{
ApplyTransform();
PrintPanel();
foreach(var child in parent.children)
{
PrintSelfAndChildren(child);
}
}
Related
I have a list of coordinates (circle objects) and I'm trying to find the object on the bottom left.
(top left is 0,0)
I'm using the logic "bottom left circle is when X is minimum and Y is maximum" and it works in most cases;
But it fails in this case for example because the bottom left coordinate (circle) is not the one with smallest X value;
I use something like this and it mostly works;
private static int FindBottomLeftCircle(List<Circle> circles)
{
return circles.IndexOf(circles.OrderBy(c => c.Center.Y).Reverse().Take(13).OrderBy(c => c.Center.X).First());
}
It works best if the bottom row has roughly 13 circles. But as you can see this approach has problems.
How can I reliably find the bottom left circle object?
Just an idea. Will it be correct to say that one circle is bottom-leftier than another, when vector drawn from that circle to another makes angle with X in range (-45, 135) degrees. Then we can determine if angle is correct by calculating scalar product with vector (1, -1).
So we will need to find circle that is bottom-leftier than any other:
circles.First(x => circles.All(y => y.Center.X - x.Center.X - (y.Center.Y - x.Center.Y) => 0))
EDIT: I forgot to describe my actual probem :D After orientation changes, portrait -> landscape or vice versa, the thumbs do not layout correctly. I want a thumb thats in the middle (width / 2) of the layout to remain at the middle of the layout after orientation change.
I'm trying to create a "range slider".
What I have is 2 simple views, that represent the two thumbs and a frame layout that represents the slider.
I'm laying out my two thumbs in the overridden OnLayout method, based on the width of the slider multiplied by the value of a thumb (between 0 and 1).
public class RangeSliderView : ViewGroup
{
public RangeSliderView(Context context) : base(context)
{
var thumbL = new Thumb(context);
var thumbR = new Thumb(context);
AddView(thumbL);
AddView(thumbR);
}
protected override void OnLayout(bool changed, int l, int t, int r, int b)
{
if (changed)
{
//Get the width and height if not set yet
if (ThumbLeft.MeasuredWidth == 0 || ThumbLeft.MeasuredHeight == 0)
ThumbLeft.Measure(MeasureSpec.MakeMeasureSpec(0, MeasureSpecMode.Unspecified), MeasureSpec.MakeMeasureSpec(b, MeasureSpecMode.AtMost));
if (ThumbRight.MeasuredWidth == 0 || ThumbRight.MeasuredHeight == 0)
ThumbRight.Measure(MeasureSpec.MakeMeasureSpec(0, MeasureSpecMode.Unspecified), MeasureSpec.MakeMeasureSpec(b, MeasureSpecMode.AtMost));
//calculate width and the relative position of the thumbs
int width = r - l;
int thumbLeftX = (int)(width * lastProgressLeft); //last progresses are a value between 0 and 1
int thumbRightX = (int)(width * lastProgressRight);
//position the thumbs
ThumbLeft.Layout(l: l + thumbLeftX,
t: t,
r: l + thumbLeftX + ThumbLeft.MeasuredWidth,
b: ThumbLeft.MeasuredHeight);
ThumbRight.Layout(l: l + thumbRightX - ThumbRight.MeasuredWidth,
t: t,
r: l + thumbRightX,
b: ThumbLeft.MeasuredHeight);
}
}
}
The values really seem to be correct, in the landscape, the "r" value is bigger than in portrait. "l" is always 0 since the control is aligned to the left of my screen. The calculation seems correct, I tested moving one of the thumbs to the middle, so I can exactly see if thumbLeftX or thumbRightX are 50% of the width. It seemed correct.
I think the Layout(...) calls on the thumbs do not layout my thumbs reliably.
Is there a layout call that I'm missing?
Do I need to call other methods to re-layout my thumbs correctly?
Initially, the lastProgressLeft is 0 and the lastProgressRight is 1 (meaning thumb left should be at the left end of the slider, the right thumb at the right end of the slider. This works great, also on orientation changes, it looks correct.
I use the C# Chart in WinForms to plot a variety of variables in real time using the "line" chart type. That works well for analog values, but it's less than ideal for on/off flags.
I'd like to plot multiple flags as horizontal bars that are filled when the value is '1" and clear when the value is '0'.
Before I start coding a solution from scratch, do you have any suggestion on how I could take advantage of any features of the "chart" object to implement this more effectively?
EDIT: I am playing with the Area type, and it seems to be promising.
EDIT 2: That didn't work, because the area in the Area type always starts at the bottom of the chart, hiding the other rows. I am now trying the Range Column type
There are several ways to tackle this.: StackedBars, AreaChart, Annotations but I think by far the simplest is using a LineChartType.
The first issue is: How to create the gaps? The simplest way is to draw them as lines but with Color.Transparent. So instead of using the flag value as our y-value we use it to set the color..
So we could use a function like this:
void AddFlagLine(Chart chart, int series, int flag, int x)
{
Series s = chart.Series[series];
int px = s.Points.AddXY(x, series);
s.Points[px].Color = s.Color;
if (px > 0) s.Points[px - 1].Color = flag == 1 ? s.Color : Color.Transparent;
}
It takes the index of your Series and uses the flag to determine the color; note that the color of a line segment is controlled by the color of the end point.
So if you want to have the line going out from the new point to have its flag color, you need to set it when adding the next one..
This is simple enough and for lines as thick as 1-10 it works fine. But if you want larger widths things get a bit ugly..:
The rounded caps start to get bigger and bigger until they actually touch, flling the gaps more or less.
Unfortunately there seems to be no way to controls the caps-style of the lines. There are many CustomAttributes including DashStyles but not this one. So we have to resort to owner-drawing. This is rather simple for line charts. Here is an example:
The xxxPaint event looks like this:
private void chart_PostPaint(object sender, ChartPaintEventArgs e)
{
Graphics g = e.ChartGraphics.Graphics;
Axis ax = chart.ChartAreas[0].AxisX;
Axis ay = chart.ChartAreas[0].AxisY;
for (int si = 0; si < chart.Series.Count; si++ )
{
Series s = chart.Series[si];
for (int pi = 1; pi < s.Points.Count - 1; pi++)
{
DataPoint dp = s.Points[pi];
int y = (int) ay.ValueToPixelPosition(dp.YValues[0]+1); ///*1*
int x0 = (int)ax.ValueToPixelPosition(ax.Minimum);
int x1 = (int)ax.ValueToPixelPosition(s.Points[pi-1].XValue); ///*2*
int x2 = (int)ax.ValueToPixelPosition(dp.XValue);
x1 = Math.Max(x1, x0);
x2 = Math.Max(x2, x0);
using (Pen pen = new Pen(dp.Color, 40) ///*3*
{ StartCap = System.Drawing.Drawing2D.LineCap.Flat,
EndCap = System.Drawing.Drawing2D.LineCap.Flat })
{
g.DrawLine(pen, x1, y, x2, y);
}
}
}
A few notes:
1 : I have decided to move the the series up by one; this is up to you just as using or turning off the y-axis labels or replacing them by custom labels..
2 : Here we use the previous point's x-position!
3 : Note that instead of hard coding a width of 40 pixels you really should decide on a calculated width. This is an example that almost fills up the area:
int width = (int)( ( ay.ValueToPixelPosition(ay.Minimum) -
ay.ValueToPixelPosition(ay.Maximum)) / (chart7.Series.Count + 2));
You can twist is to fill more or less by adding less or more than 2.
I have turned all BorderWidths to 0 so only the drawn lines show.
I got it:
It turned out to actually be pretty easy; I used the Range Column type.
A) Set-up (done once):
plotChart.Series[chanNo].ChartType = SeriesChartType.RangeColumn;
plotChart.Series[chanNo].CustomProperties = "PointWidth=" + noOfFlags;
PointWidth is required to set the relative width of each rectangle so that it fills the entire width of one data point (if too small, there are gaps in the horizontal bar; if too large, there is overlap). noOfFlags is the number of flags shown (in the example shown above, noOfFlags = 4). (By the way the MSDN documentation is wrong: PointWidth is not limited to 2.)
B) Plotting (done for each new data point):
baseLine--;
int barHeight = flagHigh ? 1 : 0;
plotChart.Series[chanNo].Points.AddXY(pointX, baseLine, baseLine + barHeight);
flagHigh is a bool that is equal to the flag being monitored.
baseLine is decremented for each trace. In the example above, baseLine starts at 4, and is decremented down to 0.
Note that for each data point, RangeColumn requires 2 "Y" values: one for the bottom of the rectangle, one for the top; in the code, I set the bottom Y to the bottom of the row that I use for that particular flag, and the top to 1 above the bottom, to give me a height of 1.
I am creating a Rect in C# with two points. These points are actually Geographical bounds. The problem I am having is what when I create the rectangle the y axis is flipped.
For example say my data is west="5.42194487004" south="46.407494" east="17.166386" north="55.056664"
I pass that into Rect geoBounds = new Rect(new Point(west, north),new Point(east, south));
The Rectangle that is created has the following properties
Bottom 55.056664 double
Height 7.781945 double
IsEmpty false bool
Left 5.864166 double
Right 15.038887000000003 double
Top 47.274719 double
Width 9.1747210000000017 double
X 5.864166 double
Y 47.274719 double
The Y axis is flipped. I have triple checked that the data being fed into the call is correct. What is wrong? Also I know that I did not supply much code but did not feel any more was needed. Will provide more if needed.
The coordinate system has 0,0 at the top left of the screen, with Y increasing in the downward direction. You can see this at the example page for the Rect.Bottom property: http://msdn.microsoft.com/en-us/library/system.windows.rect.bottom.aspx
Note on that page this comment:
// Bottom property gets the y-axis value of the bottom of the rectangle.
// For this rectangle the value is 55.
rectInfo = rectInfo + "Bottom: " + myRectangle.Bottom;
and this one:
// Top property gets the y-axis position of the top of the rectangle which is
// equivalent to getting the rectangle's Y property.
// For this rectangle the value is 5.
rectInfo = rectInfo + "| Top: " + myRectangle.Top;
This is further supported by the explicit constructor for Rect: http://msdn.microsoft.com/en-us/library/ms587929%28v=vs.95%29.aspx
Note that x and y describe the top left corner, where width extends that in the rightward direction and height extends downwards.
Rect geoBounds = new Rect(west, north, (east - west), (north - south));
In my C# (.NET 2) app I'd like to determine which control is closet to the mouse.
I can think of a few ways to do this that won't quite work right. I could use the Control.Location property, but that just gives me top/left, and the mouse might be on the other side of the control. I could calculate the center point of a control, but large controls would skew this (being near the edge of a control counts as being close to the control).
So basically I have a bunch of rectangles on a canvas and a point. I need to find the rectangle nearest to the point.
(Ideally I'd like to actually know the distance between the point and rectangle, too).
Any ideas?
You need to find the following:
- Distance to the closest corner
- Distance to the closest edge
- (optionally) distance to the center
Basically, you want the smaller of these three values. Take the min of that for two controls to determine which is closer.
Begin when you load the form by iterating all the controls on the form and creating a collection of the class below.
To find the closest control to a point, iterate the collection (see code at bottom). Keep track of the control with the minimum distance you've found so far. You can test for ContainsPoint() if you want... if you find a control where the point falls within the control bounds, you've got your control (so long as you don't have overlapping controls). Else, when you get to the end of the collection, the control you found with the shortest distance to the center/edge is your control.
public class HitControl {
public Control ThisControl;
private Rectangle ControlBounds;
private Point Center;
public HitControl (Control FormControl) {
ControlBounds = FormControl.Bounds;
Center = new Point(ControlBounds.X + (ControlBounds.Width/2), ControlBounds.Y + (ControlBounds.Height/2));
}
// Calculate the minimum distance from the left, right, and center
public double DistanceFrom(Point TestPoint) {
// Note: You don't need to consider control center points unless
// you plan to allow for controls placed over other controls...
// Then you need to test the distance to the centers, as well,
// and pick the shortest distance of to-edge, to-side, to-corner
bool withinWidth = TestPoint.X > ControlBounds.X && TestPoint.X < ControlBounds.X + ControlBounds.Width;
bool withinHeight = TestPoint.Y > ControlBounds.Y && TestPoint.Y < ControlBounds.Y + ControlBounds.Height;
int EdgeLeftXDistance = Math.Abs(ControlBounds.X - TestPoint.X);
int EdgeRightXDistance = Math.Abs(ControlBounds.X + ControlBounds.Width - TestPoint.X);
int EdgeTopYDistance = Math.Abs(ControlBounds.Y - TestPoint.Y);
int EdgeBottomYDistance = Math.Abs(ControlBounds.Y + ControlBounds.Height - TestPoint.Y);
int EdgeXDistance = Math.Min(EdgeLeftXDistance, EdgeRightXDistance);
int EdgeYDistance = Math.Min(EdgeTopYDistance, EdgeBottomYDistance);
// Some points to consider for rectangle (100, 100, 100, 100):
// - (140, 90): Distance to top edge
// - (105, 10): Distance to top edge
// - (50, 50): Distance to upper left corner
// - (250, 50): Distance to upper right corner
// - (10, 105): Distance to left edge
// - (140, 105): Distance to top edge
// - (105, 140): Distance to left edge
// - (290, 105): Distance to right edge
// - (205, 150): Distance to right edge
// ... and so forth
// You're within the control
if (withinWidth && withinHeight) {
return Math.Min(EdgeXDistance, EdgeYDistance);
}
// You're above or below the control
if (withinWidth) {
return EdgeYDistance;
}
// You're to the left or right of the control
if (withinHeight) {
return EdgeXDistance;
}
// You're in one of the four outside corners around the control.
// Find the distance to the closest corner
return Math.Sqrt(EdgeXDistance ^ 2 + EdgeYDistance ^ 2);
}
public bool ContainsPoint (Point TestPoint) {
return ControlBounds.Contains(TestPoint);
}
}
// Initialize and use this collection
List<HitControl> hitControls = (from Control control in Controls
select new HitControl(control)).ToList();
Point testPoint = new Point(175, 619);
double distance;
double shortestDistance = 0;
HitControl closestControl = null;
foreach (HitControl hitControl in hitControls) {
// Optional... works so long as you don't have overlapping controls
// If you do, comment this block out
if (hitControl.ContainsPoint(testPoint)) {
closestControl = hitControl;
break;
}
distance = hitControl.DistanceFrom(testPoint);
if (shortestDistance == 0 || distance < shortestDistance) {
shortestDistance = distance;
closestControl = hitControl;
}
}
if (closestControl != null) {
Control foundControl = closestControl.ThisControl;
}
First check the point is in any rectangle. If not, you can find the distance between your point and each line segment with the algorithm in this.
You can also find the 4 segments of your control, so you have a list (initiated first time) of four segments (determining the control sides) and now you can find the nearest segment, its nearest rectangle.
You have to think in terms of rectangles :)
Test: Is mouse within control?
If not: How far away from any single edge?
Then you have to know which controls you are interested in, the form is, for example, a control..
For starters, create method that will calculate distance from the rectangle edge to some arbitrary point. Signature for this method should be:
double DistanceFrom(Rect r, Point p);
Then, for the simplest try, iterate all controls, calculate distance, remeber the minimum distance and control that provided it.
For rectangle distance, check this out.
EDIT:
In fact, you can maintain a sorted list of controls so you can always have first one that is closer on top, and maintain that list as the mouse moves - it may prove to be more efficient in the terms of speed. Interesting issue though :)
I agree with Daniel that we need:
double DistanceFrom(Rect r, Point p);
But before that, we need:
double DistanceFrom(Line r, Point p);
and
double AngleBetweenPoints(Point p1, Point p2);