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.
Related
I've been working on a small project for some days, everything was working fine until I changed my "map" implementation to be the same as in the game (Dofus) I'm based on (it's a little helper for the community).
Basically, I've a grid layout rotated at 45° (see image below), contructed from top left to bottom right. Every cell as an xIndex and zIndex to represent where it is (xIndex ; zIndex) on the image, and I just want to get the distance between two cells, without traveling diagonally.
As I tried to explain on the picture:
GetDistanceBetweenTiles(A, B) should be 3
GetDistanceBetweenTiles(A, C) should be 5
GetDistanceBetweenTiles(B, C) should be 2
I found the "Manhattan distance" which looks like it is what I want, but it's not giving me the values above.
Here is the code:
private int GetDistanceBetweenTiles(MovableObject a, MovableObject b)
{
//int dist = Mathf.Abs(a.xIndex - b.xIndex) + Mathf.Abs(a.zIndex - b.zIndex);
int minX = a.xIndex < b.xIndex ? a.xIndex : b.xIndex;
int maxX = a.xIndex > b.xIndex ? a.xIndex : b.xIndex;
int minZ = a.zIndex < b.zIndex ? a.zIndex : b.zIndex;
int maxZ = a.zIndex > b.zIndex ? a.zIndex : b.zIndex;
int distX = (maxX - minX);
int distZ = (maxZ - minZ);
int dist = Mathf.Abs(maxX - minX) + Mathf.Abs(maxZ - minZ);
print($"Distance between {a.name} and {b.name} is {dist}");
return dist;
}
Any help would be gladly appreciated.
If it can help, here is the project working with the first map implementation I did (but not translated yet).
Let make new coordinates in inclined rows with simple formulae:
row = z/2 - x ("/" for **integer division**)
col = z - row
Now we can just calculate Manhattan distance as
abs(row2 - row1) + abs(col2 - col1)
For your example
x z r c
4, 2 => -3, 5
1, 4 => 1, 4
distance = (1-(-3)) + (5-4) = 4 + 1 = 5
To explain: your grid rotated by 45 degrees:
0 1 2 3 4 5 6 7 8 \column
40|41 row -4
30|31|42|43 row -3
20|21|32|33|44|45 row -2
10|11|22|23|34|35|46|47 row -1
00|01|12|13|24|15|36|37|48 row 0
02|03|14|15|26|27|38 row 1
04|05|16|17|28 row 2
06|07|18 row 3
The "No-Maths" solution
I maybe have a workaround solution for you. I'm kind of a lazy person and very bad in maths ... so I usually let Unity do the maths for me in situations like yours ;)
For that you would need one dedicated GameObject that is rotated in the way that it represents the grid "rotation" so 0,45,0.
Then - since your tiles move always in steps of exactly 1 just in the rotated coordinate system - you could inetad of using an index based distance rather directly compare the absolute positions using Transform.InverseTransformPoint in order to get the positions relative to that rotated object.
InverseTransformPoint retuns as said the given world position in the local space of the used transform so that if the object was originally placed at e.g. x=1, z=1 in our rotated local space it will have the position z=1.1414..., x=0.
I simply attached this component to my rotated object .. actually I totate in Awake just to be sure ;)
public class PositionsManager : MonoBehaviour
{
// I know .. singleton pattern .. buuu
// but that's the fastest way to prototype ;)
public static PositionsManager Singleton;
private void Awake()
{
// just for making sure this object is at world origin
transform.position = Vector3.zero;
// rotate the object liek you need it
// possible that in your case you rather wanted -45°
transform.eulerAngles = new Vector3(0, 45, 0);
// since InverseTransformPoint is affacted by scale
// just make sure this object has the default scale
transform.localScale = Vector3.one;
// set the singleton so we can easily access this reference
Singleton = this;
}
public Vector2Int GetDistance(Transform from, Transform to)
{
var localPosFrom = transform.InverseTransformPoint(from.position);
var localPosTo = transform.InverseTransformPoint(to.position);
// Now you can simply get the actual position distance and return
// them as vector2 so you can even still see the components
// seperately
var difference = localPosTo - localPosFrom;
// since you are using X-Z not X-Y you have to convert the vector "manually"
return new Vector2Int(Mathf.RoundToInt(difference.x), Mathf.RoundToInt(difference.z));
}
public int GetAbsoluteDistance(Transform from, Trasnform to)
{
var difference = GetDistance(from, to);
return Mathf.Abs(difference.x) + Mathf.Abs(difference.y);
}
}
Now when you need to get the absolute distance you could simply do
var difference = PositionsManager.Singleton.GetDistance(objectA.transform, objectB.transform);
var absoluteDistance = PositionsManager.Singleton.GetAbsoluteDistance(objectA.transform, objectB.transform);
Little Demo (used a chess board drawer since I had that ^^)
The maths solution
It just came to me while writing the upper explenation:
You already know your steps between the tiles: It is allways Mathf.Sqrt(2)!
So again you could simply use the absolute positions in your world and compare them like
private float Sqrt2;
private void Awake()
{
Sqrt2 = Mathf.Sqrt(2);
}
...
// devide the actual difference by Sqrt(2)
var difference = (objectA.position - objectB.position) / Mathf.Sqrt(2);
// again set the Vector2 manually since we use Z not Y
// This step is optional if you anyway aren't interrested in the Vector2
// distance .. jsut added it for completeness
// You might need the rounding part though
var fixedDifference = new Vector2Int(Mathf.RoundToInt(difference.x), Mathf.RoundToInt(difference.z));
// get the absolute difference
var absoluteDistance = Mathf.Abs(fixedDifference.x) + Mathf.Abs(fixedDifference.y);
...
still completely without having to deal with the indexes at all.
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 . . .
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.
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);
}
}
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);