MouseEventArgs pass different values for mouse position to different click actions? - c#

I'm working on a connect 4 game and I've got the following part done:
I've made the board which is able to display tiles of the board in 3 colors (white, red and yellow). I've also written some code to display a box around the column on which the user is hovering such that he can see where he is placing a dish.
I've also created the code which allows a dish to be added to the board. Both of these processes use the following code:
//Calculate the size of a single cell
int cellX = this.Size.Width / this.Columns;
//calculate the cell which was clicked
int nColumn = (int)Math.Floor((Double)x / cellX);
x is the value of MouseEventArgs.X of the pannel on which this is called. For drawing a boundry box this code works perfectly but for dropping a dish it doesn't. sometimes it drops 1 left of where I want it sometimes one right.
Here is the code for both events:
//draws the bounding box around the column for a given x value.
public void drawBoundingBox(int x)
{
//Calculate the size of a single cell
int cellX = this.Size.Width / this.Columns;
//calculate the cell which was clicked
int nColumn = (int)Math.Floor((Double)x / cellX);
if (nColumn != this.lastColumn)
{
this.Refresh();
Graphics g = CreateGraphics();
Pen pRed = new Pen(Color.Red, 3);
if (nColumn < this.Columns)
{
Rectangle rect = new Rectangle(new Point(nColumn * cellX, 0),
new Size(cellX, this.Size.Height));
g.DrawRectangle(pRed, rect);
}
}
this.lastColumn = nColumn;
}
public Boolean move(int mousePosition) {
int cellX = this.Size.Width / this.Columns;
int nColumn = (int)Math.Floor((Double)mousePosition / cellX);
return board.Move(nColumn);
}
Here is the code for board.move():
public bool Move(int x)
{
bool found = false;
for (int i = Rows - 1; i >= 0 && !found; i--)
{
Console.WriteLine("X equals: " + x + " i equals: " + i);
if (States[i,x] == 0) {
found = true;
States[i, x] = (byte)(((moves++) % 2) + 1);
}
}
return found;
}
Here is a gif showing what I mean:
To me it seams like a rounding error but it's wierd to me that the bounding box works well with the mouse but the clicking action doesn't...

Related

Setting fixed major grid marks independent of data range

This is my first project in c# and I'm trying to create plots from data.
I'm struggling with drawing minor and major grid lines and labels on a logarithmic scale.
I've set the scale to logarithmic, set the base to 10 and both major and minor intervals to 1, and it works great, however, the interval starts with the minimum value on scale, so for example if data starts at 30M (I'm dealing with frequencies) the next major tick is at 300M and 3G, which is not as it should be.
Is there a way to set major grid to 1, 10, 100 etc, independent of what data is displayed? i've tried changing intervals, base and offset but have not achieved much.
area.AxisX.IsLogarithmic = true;
area.AxisX.LogarithmBase = 10;
area.AxisX.Interval = 1;
//area.AxisX.IntervalOffset = 10000;
area.AxisX.IntervalAutoMode = IntervalAutoMode.FixedCount;
area.AxisX.MajorGrid.Enabled = true;
area.AxisX.MajorTickMark.Enabled = true;
area.AxisX.MinorGrid.Enabled = true;
area.AxisX.MinorGrid.Interval = 1;
area.AxisX.MinorTickMark.Enabled = true;
area.AxisX.MinorTickMark.Interval = 1;
area.AxisX.Minimum = minMaxXY[0]; // in this example 30 M
area.AxisX.Maximum = minMaxXY[1]; // in this example 1 G
here's the link to the current grid
https://ibb.co/3WkxLfc
Thank you for your time and answers!
Thanks to TaW replay I managed to get my program working.
Here is my solution using customLabels location to draw the grid lines.
private void Chart1_PostPaint(object sender, ChartPaintEventArgs e)
{
if (e.Chart.ChartAreas.Count > 0) // I don't yet truly understand when this event occurs,
// so I got plenty of null references.
{
Graphics g = e.ChartGraphics.Graphics;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
Color minorGridColor = Color.Gainsboro;
ChartArea area = e.Chart.ChartAreas[0];
double aymin = area.AxisY.Minimum;
double aymax = area.AxisY.Maximum;
int y0 = (int)area.AxisY.ValueToPixelPosition(aymin);
int y1 = (int)area.AxisY.ValueToPixelPosition(aymax);
foreach (var label in chart1.ChartAreas[0].AxisX.CustomLabels)
{
double xposition = area.AxisX.ValueToPixelPosition(Math.Pow(10, label.FromPosition + 0.1));
if (xposition > area.AxisX.ValueToPixelPosition(minMaxXY[0]) && xposition < area.AxisX.ValueToPixelPosition(minMaxXY[1]))
//this prevents drawing of lines outside of the chart area
{
int x = (int)xposition;
using (Pen dashed_pen = new Pen(Color.FromArgb(10, 0, 0, 0), 1))
{
dashed_pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
g.DrawLine(dashed_pen, x, y0, x, y1);
}
}
}
}
}
I also found the CustomLabel.GridTicks Property, but for some reason it did not work.

Why when adding Handles.ArrowHandleCap to a line it's not drawing the ArrowHandleCap?

I'm using Editor Window maybe that's the problem ?
The idea is when connecting two nodes is also to make an arrow at the end position that will show the connecting flow direction.
In the screenshot when I'm connecting two nodes for example Window 0 to Window 1
So there should be an arrow at the end of the line near Window 1 showing indicating that Window 0 is connected to Window 1 so the flow is from Window 0 to Window 1.
But it's not drawing any ArrowHandleCap.
I don't mind to draw another simple white arrow at the end position but it's not working at all for now. Not drawing an arrow at all.
This is my Editor Window code :
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using UnityEditor.Graphs;
using UnityEngine.UI;
public class NodeEditor : EditorWindow
{
List<Rect> windows = new List<Rect>();
List<int> windowsToAttach = new List<int>();
List<int> attachedWindows = new List<int>();
int tab = 0;
float size = 10f;
[MenuItem("Window/Node editor")]
static void ShowEditor()
{
const int width = 600;
const int height = 600;
var x = (Screen.currentResolution.width - width) / 2;
var y = (Screen.currentResolution.height - height) / 2;
GetWindow<NodeEditor>().position = new Rect(x, y, width, height);
}
void OnGUI()
{
Rect graphPosition = new Rect(0f, 0f, position.width, position.height);
GraphBackground.DrawGraphBackground(graphPosition, graphPosition);
int selected = 0;
string[] options = new string[]
{
"Option1", "Option2", "Option3",
};
selected = EditorGUILayout.Popup("Label", selected, options);
if (windowsToAttach.Count == 2)
{
attachedWindows.Add(windowsToAttach[0]);
attachedWindows.Add(windowsToAttach[1]);
windowsToAttach = new List<int>();
}
if (attachedWindows.Count >= 2)
{
for (int i = 0; i < attachedWindows.Count; i += 2)
{
DrawNodeCurve(windows[attachedWindows[i]], windows[attachedWindows[i + 1]]);
}
}
BeginWindows();
if (GUILayout.Button("Create Node"))
{
windows.Add(new Rect(10, 10, 200, 40));
}
for (int i = 0; i < windows.Count; i++)
{
windows[i] = GUI.Window(i, windows[i], DrawNodeWindow, "Window " + i);
}
EndWindows();
}
void DrawNodeWindow(int id)
{
if (GUILayout.Button("Attach"))
{
windowsToAttach.Add(id);
}
GUI.DragWindow();
}
void DrawNodeCurve(Rect start, Rect end)
{
Vector3 startPos = new Vector3(start.x + start.width, start.y + start.height / 2, 0);
Vector3 endPos = new Vector3(end.x, end.y + end.height / 2, 0);
Vector3 startTan = startPos + Vector3.right * 50;
Vector3 endTan = endPos + Vector3.left * 50;
Color shadowCol = new Color(255, 255, 255);
for (int i = 0; i < 3; i++)
{// Draw a shadow
//Handles.DrawBezier(startPos, endPos, startTan, endTan, shadowCol, null, (i + 1) * 5);
}
Handles.DrawBezier(startPos, endPos, startTan, endTan, Color.white, null, 5);
Handles.color = Handles.xAxisColor;
Handles.ArrowHandleCap(0, endPos, Quaternion.LookRotation(Vector3.right), size, EventType.Repaint);
}
}
The problem is that the arrow is always behind the e.g. Window 0 since you call DrawNodeWindow always after DrawNodeCurve.
It happens because the arrow is always drawen starting from the endPos to the right direction with length = size so you always overlay it with the window later ... you have to change
// move your endpos to the left by size
var endPos = new Vector3(end.x - size, end.y + end.height / 2 , 0);
in order to have it start size pixels left before the actual end.x position.
However, as you can see it is still really small since it usually is used to display the arrow in 3D space - not using Pixel coordinates .. you might have to tweak arround or use something completely different.
How about e.g. simply using a GUI.DrawTexture instead with a given Arrow sprite?
// assign this as default reference via the Inspector for that script
[SerializeField] private Texture2D aTexture;
// ...
// since the drawTexture needs a rect which is not centered on the height anymore
// you have to use endPos.y - size / 2 for the Y start position of the texture
GUI.DrawTexture(new Rect(endPos.x, endPos.y - size / 2, size, size), aTexture, ScaleMode.StretchToFill);
like mentioned in the comment for all serialized fields in Unity you can already reference default assets for the script itself (in contrary to doing it for each instance like for MonoBehaviours) so with the NodeEditor script selected simply reference a downloaded arrow texture
If using a white arrow as texture you could then still change its color using
var color = GUI.color;
GUI.color = Handles.xAxisColor;
GUI.DrawTexture(new Rect(endPos.x, endPos.y - size / 2, size, size), aTexture, ScaleMode.StretchToFill);
GUI.color = color;
Result
P.S.: Arrow icon usedfor the example: https://iconsplace.com/red-icons/arrow-icon-14 you can already change the color directly on that page before downloading the icon ;)

Flood fill missing a tile or two

edit: It works now, I moved the getting X and Y into a seperate function, and it seemed to have fixed it (I'm not sure exactly what was wrong, sorry)
I've been making a Tile Editor using WPF and C#, and I seem to have come across a rather odd problem.
When I first run the fill algorithm on a blank canvas, it fills up all tiles except one (usually around (2,7)). Depending on where I start, it jumps around slightly in that area.
What really confuses me is if I flood fill that same area, it will fix the missing tile.
I tried the recursive flood fill as well as the one using a Queue. Here's the code for the Recursion one:
private void FloodFill(Tile node, int targetID, int replaceID) {
if (targetID == replaceID) return;
if (node.TileID != targetID) return;
node.TileID = replaceID;
SetImageAtCoord(node.X, node.Y);
if (node.Y + 1 != Map.Height) FloodFill(Map.TileInformation[node.X, node.Y + 1], targetID, replaceID);
if (node.Y - 1 != -1) FloodFill(Map.TileInformation[node.X, node.Y - 1], targetID, replaceID);
if (node.X - 1 != -1) FloodFill(Map.TileInformation[node.X - 1, node.Y], targetID, replaceID);
if (node.X + 1 != Map.Width) FloodFill(Map.TileInformation[node.X + 1, node.Y], targetID, replaceID);
}
Now since I also tried a different method and got the same problem, I thought it might be in the SetImageAtCoord():
private void SetImageAtCoord(int x, int y) {
IEnumerable<Rectangle> rectangles = MapViewer.Children.OfType<Rectangle>();
Map.TileInformation[x, y].TileID = CurrentTile;
foreach (Rectangle rect in rectangles) {
int X = (int)Canvas.GetLeft(rect) / 32;
int Y = (int)Canvas.GetTop(rect) / 32;
if (X == x && Y == y) {
rect.Fill = CurrentImage;
}
}
}
My only guess is that it either misses it on the pass (yet somehow make it every other time), or it has something to do with the placement of the rectangle on the Canvas.
edit:
Here is the code for selecting what CurrentImage (and CurrentTile are):
private void Sprite_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) {
Rectangle curr = (Rectangle)e.OriginalSource;
CurrentImage = (ImageBrush)curr.Fill;
int x = (int)Canvas.GetLeft(curr) / 32;
int y = (int)Canvas.GetTop(curr) / 32;
CurrentTile = y * Map.SpriteSheet.GetLength(0) + x;
}
and the code that calls FloodFill
if (CurrentContol == MapEditControl.Fill) {
Rectangle ClickedRectangle = (Rectangle)e.OriginalSource;
ClickedRectangle.Fill = CurrentImage;
int x = (int)Canvas.GetLeft(ClickedRectangle) / 32;
int y = (int)Canvas.GetTop(ClickedRectangle) / 32;
int oldTile = Map.TileInformation[x, y].TileID;
FloodFill(Map.TileInformation[x, y], oldTile, CurrentTile);
}
Map.TileInformation is actually only really used to store the ID for exporting to JSON, and the actual rectangles aren't bound to that in anyway (probably should have)

Map possible paths for amount of moves C#

I am re-creating the game of cluedo and I want to map the possible paths that the player can move after dice roll.
I have mapped the grid by drawing pictureboxes and naming them to their mapped location.
Here is my code so far for the possible paths:
int Roll;
private void RollDice()
{
ResetTiles();
JimRandom Random = new JimRandom();
//Roll DIce 1
int dice1 = Random.Next(1, 7);
//Roll DIce 2
int dice2 = Random.Next(1, 7);
Roll = dice1 + dice2;
//Set Dice images
pbDice1.BackgroundImage = Roller[dice1 - 1].Picture;
pbDice2.BackgroundImage = Roller[dice2 - 1].Picture;
btnRoll.Enabled = false;
Test(Roll);
//Show available moves
Control[] lCurrent = PnlBoard.Controls.Find("pnl" + CurrentPlauer, true);
Panel Current = null;
System.Drawing.Point CurrentLoc = new System.Drawing.Point(0, 0);
foreach (Control c in lCurrent)
{
Current = c as Panel;
CurrentLoc = new System.Drawing.Point(c.Location.X, c.Location.Y);
}
//Dynamic map
List<string> possiblities = new List<string>();
int currentRow = CurrentLoc.Y / tileWidth;
int currentCol = CurrentLoc.X / tileHeight;
//Find all possible start blocks
string down = String.Format("Col={0:00}-Row={1:00}", currentCol, currentRow + 1);
string up = String.Format("Col={0:00}-Row={1:00}", currentCol, currentRow - 1);
string left = String.Format("Col={0:00}-Row={1:00}", currentCol - 1, currentRow);
string right = String.Format("Col={0:00}-Row={1:00}", currentCol + 1, currentRow);
List<string> startBlocks = new List<string>();
//See if down is available
Control[] LPossible = PnlBoard.Controls.Find(down, true);
if (LPossible.Length > 0)
{
startBlocks.Add(down);
}
//See if Up is available
LPossible = PnlBoard.Controls.Find(up, true);
if (LPossible.Length > 0)
{
startBlocks.Add(up);
}
//See if left is available
LPossible = PnlBoard.Controls.Find(left, true);
if (LPossible.Length > 0)
{
startBlocks.Add(left);
}
//See if right is available
LPossible = PnlBoard.Controls.Find(right, true);
if (LPossible.Length > 0)
{
startBlocks.Add(right);
}
//possiblilities 1
foreach (string s in startBlocks)
{
Control[] lStarBlock = PnlBoard.Controls.Find(s, true);
PictureBox startBlock = lStarBlock[0] as PictureBox;
int sRow = startBlock.Location.Y / tileWidth;
int sCol = startBlock.Location.X / tileHeight;
//Rows
for (int row = sRow; row < sRow + Roll; row++)
{
//Columns
for (int col = sCol; col < sCol + Roll; col++)
{
possiblities.Add(String.Format("Col={0:00}-Row={1:00}", col, row));
}
}
}
//Show possible moves
foreach (string p in possiblities)
{
LPossible = PnlBoard.Controls.Find(p, true);
if (LPossible.Length > 0)
{
PictureBox active = LPossible[0] as PictureBox;
active.Image = Cluedo.Properties.Resources.TileActive;
System.Threading.Thread.Sleep(1);
Application.DoEvents();
}
//else
//{
// break;
//}
}
}
There's a lot of things I would do different here. This is more of a Code Review post, but there's a solution to your problem at the end, and perhaps the rest can help you to improve the overall state of your code.
Randomness
You're creating a new random generator instance for every method call:
JimRandom Random = new JimRandom();
This often results in the same values being generated if the method is called in rapid succession. Perhaps that's why you're using a cryptographic RNG instead of a PRNG? A PRNG should be sufficient for a game like this, as long as you reuse it.
Using the right types
You're determining the current player location with the following code:
//Show available moves
Control[] lCurrent = PnlBoard.Controls.Find("pnl" + CurrentPlauer, true);
Panel Current = null;
System.Drawing.Point CurrentLoc = new System.Drawing.Point(0, 0);
foreach (Control c in lCurrent)
{
Current = c as Panel;
CurrentLoc = new System.Drawing.Point(c.Location.X, c.Location.Y);
}
It looks like CurrentPlauer is a string. Creating a Player class that stores the name and current location of a player would make things much easier:
Point currentLocation = currentPlayer.Location;
Splitting game logic from UI code
You're checking for passable tiles by doing string lookups against controls:
string down = String.Format("Col={0:00}-Row={1:00}", currentCol, currentRow + 1);
// ...
Control[] LPossible = PnlBoard.Controls.Find(down, true);
if (LPossible.Length > 0)
{
startBlocks.Add(down);
}
Normally a 2D array is used for tile maps like these, possible encapsulated in a Tilemap or Map class. This makes working with tiles more natural, as you can work in tile coordinates directly instead of having to translate between UI and tile coordinates. It also breaks up the code more cleanly into a game-logic and a UI part (the code in your post is impossible to test without UI):
// TileMap class:
public bool IsPassable(int x, int y)
{
if (x < 0 || x >= Width || y < 0 || y >= Height)
return false;
return tiles[x][y] != Tile.Wall; // enum Tile { Wall, Ballroom, DiningRoom, Hall, ... }
}
// When called from your Board code:
if (map.IsPassable(currentLocation.X, currentLocation.Y + 1))
startBlocks.Add(new Point(currentLocation.X, currentLocation.Y + 1));
Reducing repetition
As for checking all direct neighboring tiles, there's no need to repeat the same code 4 times:
// Let's make a utility function:
public static IEnumerable<Point> GetNeighboringPositions(Point position)
{
yield return new Point(position.X - 1, position.Y);
yield return new Point(position.X, position.Y - 1);
yield return new Point(position.X + 1, position.Y);
yield return new Point(position.X, position.Y + 1);
}
// In the Board code:
foreach (Point neighboringPosition in GetNeighboringPositions(currentPosition))
{
if (map.IsPassable(neighboringPosition.X, neighboringPosition.Y))
startBlocks.Add(neighboringPosition);
}
Determining valid moves
Finally, we get to the code that determines which tiles the current player can move to:
//possiblilities 1
foreach (string s in startBlocks)
{
Control[] lStarBlock = PnlBoard.Controls.Find(s, true);
PictureBox startBlock = lStarBlock[0] as PictureBox;
int sRow = startBlock.Location.Y / tileWidth;
int sCol = startBlock.Location.X / tileHeight;
//Rows
for (int row = sRow; row < sRow + Roll; row++)
{
//Columns
for (int col = sCol; col < sCol + Roll; col++)
{
possiblities.Add(String.Format("Col={0:00}-Row={1:00}", col, row));
}
}
}
What this does is checking a rectangular area, using a starting position as its top-left corner. It's doing so for up to 4 neighboring positions, so the rectangles will partially overlap each other. That's just not going to work. If the map didn't have any obstacles, something like this, combined with a Manhattan distance check, could work (if you don't forget to look to the left and upwards too). Or better, some fancy looping that checks a diamond-shaped area.
However, you've got walls to deal with, so you'll need a different approach. The player's current position is at distance 0. Its direct neighbors are at distance 1. Their neighbors are at distance 2 - except those tiles that are at a lower distance (the tiles that have already been covered). Any neighbours of tiles at distance 2 are either at distance 3, or have already been covered. Of course, wall tiles must be skipped.
So you need to keep track of what tiles have already been covered and what neighboring tiles you still need to check, until you run out of movement points. Let's wrap that up into a reusable method:
public List<Point> GetReachableTiles(Point currentPosition, int maxDistance)
{
List<Point> coveredTiles = new List<Point> { currentPosition };
List<Point> boundaryTiles = new List<Point> { currentPosition };
for (int distance = 0; distance < maxDistance; distance++)
{
List<Point> nextBoundaryTiles = new List<Point>();
foreach (Point position in boundaryTiles)
{
foreach (Point pos in GetNeighboringPositions(position))
{
// You may also want to check against other player positions, if players can block each other:
if (!coveredTiles.Contains(pos) && !boundaryTiles.Contains(pos) && map.IsPassable(pos.X, pos.Y))
{
// We found a passable tile:
coveredTiles.Add(pos);
// And we want to check its neighbors in the next 'distance' iteration, too:
nextBoundaryTiles.Add(pos);
}
}
}
// The next 'distance' iteration should check the neighbors of the current boundary tiles:
boundaryTiles = nextBoundaryTiles;
}
return coveredTiles;
}

Showing Mouse Axis Coordinates on Chart Control

Is there a simple way to retrieve the X/Y coordinates of ANY point in the chart Area (relative to that chart Axis of course)?
As of now, I just managed to retrieve coordinates when the mouse is on a Series (not outside)
private void chart_GetToolTipText(object sender, ToolTipEventArgs e)
{
if (e.HitTestResult.Series != null)
{
e.Text = e.HitTestResult.Series.Points[e.HitTestResult.PointIndex].YValues[0] + " \n " + DateTime.FromOADate(e.HitTestResult.Series.Points[e.HitTestResult.PointIndex].XValue);
}
}
Anyway, as always with MS Chart Controls, there is no easy way to do things, but a funky workaround to get things done. I am sadly getting used to it...
private void chart1_MouseWhatever(object sender, MouseEventArgs e)
{
chartArea1.CursorX.SetCursorPixelPosition(new Point(e.X, e.Y), true);
chartArea1.CursorY.SetCursorPixelPosition(new Point(e.X, e.Y), true);
double pX = chartArea1.CursorX.Position; //X Axis Coordinate of your mouse cursor
double pY = chartArea1.CursorY.Position; //Y Axis Coordinate of your mouse cursor
}
This works for my purposes and doesn't side effect the cursor.
private Tuple<double,double> GetAxisValuesFromMouse(int x, int y)
{
var chartArea = _chart.ChartAreas[0];
var xValue = chartArea.AxisX.PixelPositionToValue(x);
var yValue = chartArea.AxisY.PixelPositionToValue(y);
return new Tuple<double, double>(xValue, yValue);
}
I tried your answer, but it didn't work for me. It ended up putting the cursor in one spot and never moving. I believe this is because I use decimal/double values along both axes, and the cursor is being rounded to the nearest integer.
After several attempts, I was able to work out a method for determining where the cursor is inside the chart. The hard part was figuring out that all the "positions" for the chart elements are actually percentage values (from 0 to 100).
As per
http://msdn.microsoft.com/en-us/library/system.windows.forms.datavisualization.charting.elementposition.aspx:
"Defines the position of the chart element in relative coordinates, which range from (0,0) to (100,100)."
I hope you don't mind, I am posting this answer here just for posterity, in case anyone else comes across this problem, and your method also does not work for them. Its not pretty or elegant in any way, but so far it works for me.
private struct PointD
{
public double X;
public double Y;
public PointD(double X, double Y)
{
this.X = X;
this.Y = Y;
}
}
private void chart1_MouseMove(object sender, MouseEventArgs e)
{
var pos = LocationInChart(e.X, e.Y);
lblCoords.Text = string.Format("({0}, {1}) ... ({2}, {3})", e.X, e.Y, pos.X, pos.Y);
}
private PointD LocationInChart(double xMouse, double yMouse)
{
var ca = chart1.ChartAreas[0];
//Position inside the control, from 0 to 100
var relPosInControl = new PointD
(
((double)xMouse / (double)execDetailsChart.Width) * 100,
((double)yMouse / (double)execDetailsChart.Height) * 100
);
//Verify we are inside the Chart Area
if (relPosInControl.X < ca.Position.X || relPosInControl.X > ca.Position.Right
|| relPosInControl.Y < ca.Position.Y || relPosInControl.Y > ca.Position.Bottom) return new PointD(double.NaN, double.NaN);
//Position inside the Chart Area, from 0 to 100
var relPosInChartArea = new PointD
(
((relPosInControl.X - ca.Position.X) / ca.Position.Width) * 100,
((relPosInControl.Y - ca.Position.Y) / ca.Position.Height) * 100
);
//Verify we are inside the Plot Area
if (relPosInChartArea.X < ca.InnerPlotPosition.X || relPosInChartArea.X > ca.InnerPlotPosition.Right
|| relPosInChartArea.Y < ca.InnerPlotPosition.Y || relPosInChartArea.Y > ca.InnerPlotPosition.Bottom) return new PointD(double.NaN, double.NaN);
//Position inside the Plot Area, 0 to 1
var relPosInPlotArea = new PointD
(
((relPosInChartArea.X - ca.InnerPlotPosition.X) / ca.InnerPlotPosition.Width),
((relPosInChartArea.Y - ca.InnerPlotPosition.Y) / ca.InnerPlotPosition.Height)
);
var X = relPosInPlotArea.X * (ca.AxisX.Maximum - ca.AxisX.Minimum) + ca.AxisX.Minimum;
var Y = (1 - relPosInPlotArea.Y) * (ca.AxisY.Maximum - ca.AxisY.Minimum) + ca.AxisY.Minimum;
return new PointD(X, Y);
}
private void OnChartMouseMove(object sender, MouseEventArgs e)
{
var sourceChart = sender as Chart;
HitTestResult result = sourceChart.HitTest(e.X, e.Y);
ChartArea chartAreas = sourceChart.ChartAreas[0];
if (result.ChartElementType == ChartElementType.DataPoint)
{
chartAreas.CursorX.Position = chartAreas.AxisX.PixelPositionToValue(e.X);
chartAreas.CursorY.Position = chartAreas.AxisY.PixelPositionToValue(e.Y);
}
}
This works
private void chart1_MouseWhatever(object sender, MouseEventArgs e)
{
Point chartLocationOnForm = chart1.FindForm().PointToClient(chart1.Parent.PointToScreen(chart1.Location));
double x = chart1.ChartAreas[0].AxisX.PixelPositionToValue(e.X - chartLocationOnForm.X);
double y = chart1.ChartAreas[0].AxisY.PixelPositionToValue(e.Y - chartLocationOnForm.Y);
}
And this works
private void chart1_MouseWhatever(object sender, MouseEventArgs e)
{
Point chartLocationOnForm = chart1.FindForm().PointToClient(chart1.Parent.PointToScreen(chart1.Location));
chart1.ChartAreas[0].CursorX.SetCursorPixelPosition(new PointF(e.X - chartLocationOnForm.X, e.Y - chartLocationOnForm.Y), true);
chart1.ChartAreas[0].CursorY.SetCursorPixelPosition(new PointF(e.X - chartLocationOnForm.X, e.Y - chartLocationOnForm.Y), true);
double x = chart1.ChartAreas[0].CursorX.Position;
double y = chart1.ChartAreas[0].CursorY.Position;
}
This is what I got, I think many of us are along the same lines but with different interpretations of what it is you're looking for.
This will give you the coordinates at any location in the plotting area. I found the HitTest gives a clean and simple solution, but there are a few checks to make, whether the cursor is on a data point, a gridline, or in the plotting area (which seem to take precedence in that order). I assume you'll be interested in the coordinate regardless of which of those objects the mouse is over.
private void chart_GetToolTipText(object sender, ToolTipEventArgs e)
{
// If the mouse isn't on the plotting area, a datapoint, or gridline then exit
HitTestResult htr = chart.HitTest(e.X, e.Y);
if (htr.ChartElementType != ChartElementType.PlottingArea && htr.ChartElementType != ChartElementType.DataPoint && htr.ChartElementType != ChartElementType.Gridlines)
return;
ChartArea ca = chart.ChartAreas[0]; // Assuming you only have 1 chart area on the chart
double xCoord = ca.AxisX.PixelPositionToValue(e.X);
double yCoord = ca.AxisY.PixelPositionToValue(e.Y);
e.Text = "X = " + Math.Round(xCoord, 2).ToString() + "\nY = " + Math.Round(yCoord, 2).ToString();
}
VB.net version, with zoom correction:
Private Function LocationInChart(xMouse, yMouse) As PointF
Dim ca = Chart1.ChartAreas(0)
'Position inside the control, from 0 to 100
Dim relPosInControl = New PointF((xMouse / Chart1.Width) * 100, (yMouse / Chart1.Height) * 100)
'Verify we are inside the Chart Area
If (relPosInControl.X < ca.Position.X Or relPosInControl.X > ca.Position.Right Or relPosInControl.Y < ca.Position.Y Or relPosInControl.Y > ca.Position.Bottom) Then Return New PointF(Double.NaN, Double.NaN)
'Position inside the Chart Area, from 0 to 100
Dim relPosInChartArea = New PointF(((relPosInControl.X - ca.Position.X) / ca.Position.Width) * 100, ((relPosInControl.Y - ca.Position.Y) / ca.Position.Height) * 100)
'Verify we are inside the Plot Area
If (relPosInChartArea.X < ca.InnerPlotPosition.X Or relPosInChartArea.X > ca.InnerPlotPosition.Right Or relPosInChartArea.Y < ca.InnerPlotPosition.Y Or relPosInChartArea.Y > ca.InnerPlotPosition.Bottom) Then Return New PointF(Double.NaN, Double.NaN)
'Position inside the Plot Area, 0 to 1
Dim relPosInPlotArea = New PointF(((relPosInChartArea.X - ca.InnerPlotPosition.X) / ca.InnerPlotPosition.Width), ((relPosInChartArea.Y - ca.InnerPlotPosition.Y) / ca.InnerPlotPosition.Height))
Dim X = relPosInPlotArea.X * (ca.AxisX.Maximum - ca.AxisX.Minimum) + ca.AxisX.Minimum
Dim Y = (1 - relPosInPlotArea.Y) * (ca.AxisY.Maximum - ca.AxisY.Minimum) + ca.AxisY.Minimum
' zoomo korekcija
Dim zoomx = (ca.AxisX.ScaleView.ViewMaximum - ca.AxisX.ScaleView.ViewMinimum) / (ca.AxisX.Maximum - ca.AxisX.Minimum)
Dim zoomy = (ca.AxisY.ScaleView.ViewMaximum - ca.AxisY.ScaleView.ViewMinimum) / (ca.AxisY.Maximum - ca.AxisY.Minimum)
Dim xx = ca.AxisX.ScaleView.ViewMinimum + X * zoomx
Dim yy = ca.AxisY.ScaleView.ViewMinimum + Y * zoomy
Return New PointF(xx, yy)
End Function

Categories