Title: Determine which pie slice is under the mouse in C#
When the program starts, the following code initializes the pie chart's parameters.
// The pie chart's center.
private Point EllipseCenter;
// The pie chart's drawing area.
private Rectangle EllipseRect;
// The ellipse's X and Y radii.
private float EllipseRx, EllipseRy;
// The slices' ending angles in degrees.
// The first angle is 0 and marks the start of the first slice.
private float[] Angles =
{ 0, 45, 80, 110, 130, 170, 220, 245, 300, 360 };
// The slices' colors.
private Brush[] ChartBrushes =
{
Brushes.Red, Brushes.LightGreen, Brushes.LightBlue,
Brushes.Yellow, Brushes.Orange, Brushes.White,
Brushes.Cyan, Brushes.Pink, Brushes.Black,
};
// Initialize the pie chart data.
private void Form1_Load(object sender, EventArgs e)
{
const int margin = 10;
int wid = picPie.ClientSize.Width - 2 * margin;
int hgt = picPie.ClientSize.Height - 2 * margin;
EllipseRect = new Rectangle(margin, margin, wid, hgt);
EllipseRx = wid / 2;
EllipseRy = hgt / 2;
EllipseCenter = new Point(
picPie.ClientSize.Width / 2,
picPie.ClientSize.Height / 2);
}
This code initializes the Angles array, which gives the angles where each of the pie slices should end. The array starts with 0 to mark the start of the first angle.
The form's Load event handler initializes the ellipse's properties to make the ellipse the same size as the picPie PictureBox minus a 10 pixel margin.
The following code shows how the program draws the pie slices.
// Draw the pie slices.
private void picPie_Paint(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.FillEllipse(Brushes.Blue, EllipseRect);
for (int i = 1; i < Angles.Length; i++)
{
e.Graphics.FillPie(ChartBrushes[i - 1],
EllipseRect, Angles[i], Angles[i - 1] - Angles[i]);
}
e.Graphics.DrawEllipse(Pens.Blue, EllipseRect);
}
This code sets the Graphics object's SmoothingMode property. It then draws a blue ellipse. This makes a nicer result if the pie slices don't completely fill the ellipse's area.
Next the code loops through the Angles array and uses its values to draw the pie slices. The code finishes by outlining the ellipse with a blue pen.
The more interesting part of the program is the code that determines which pie slice is under the mouse. When the mouse moves, the following code executes.
// Display the number of the slice under the mouse.
private void picPie_MouseMove(object sender, MouseEventArgs e)
{
// Get the slice number.
int slice_number = GetSliceNumber(EllipseRect,
Angles, e.Location);
// Display the slice number.
if (slice_number == -1) lblSliceNumber.Text = "";
else lblSliceNumber.Text = slice_number.ToString();
}
This code calls the GetSliceNumber method described shortly. It then displays a blank string if the mouse isn't over a mouse (indicated by slice number -1) or the slice number.
To determine which pie slice is at a specific point, the program must perform two tasks. First it must decide whether the point is within the ellipse. Second it must determine which slice contains the point.
To perform the first task, you need to know that the points inside an ellipse centered at the origin satisfy this equation:
Where:
- a is half of the ellipse's width
- b is half of the ellipse's height
To see whether a point lies within the ellipse, you can simply plug in the point's coordinates for x and y and see if the inequality is satisfied.
If the ellipse isn't centered at the origin, as is the case in this example, you can subtract the X and Y coordinates of the ellipse's center from the point's coordinates and then perform the test.
The following code shows how the example determines which slice is below a point.
// Return the slice number or -1 if the
// mouse isn't over a slice.
private int GetSliceNumber(Rectangle rect, float[] angles,
Point point)
{
// Get the position relative to the ellipse's center.
float rx = rect.Width / 2;
float ry = rect.Height / 2;
float cx = rect.X + rx;
float cy = rect.Y + ry;
float dx = point.X - cx;
float dy = point.Y - cy;
float value =
dx * dx / rx / rx +
dy * dy / ry / ry;
// See if the mouse is at the center.
if (value < 0.0001) return -1;
// See if the point is outside of the ellipse.
if (value > 1) return -1;
// The point is inside the ellipse.
// Get the angle.
double angle = Math.Atan2(dy, dx);
if (angle < 0) angle += 2 * Math.PI;
// Convert the angle into degrees.
angle = angle * 180 / Math.PI;
// Get the slice number.
for (int i = 0; i < Angles.Length - 1; i++)
if (angle <= Angles[i + 1]) return i;
throw new Exception("Cannot find angle " + angle);
}
This method takes as parameters a rectangle that determines the ellipse's bounds, an array of the angles used to draw the pie slices, and the point in question.
It starts by finding the center of the ellipse and subtracting the center coordinates from the point. It then calculates the inequality described above. If the result is less than 0.0001, the point is at the center of the ellipse so the method returns -1 to indicate that it isn't over a particular pie slice.
Next if the value is greater than 1, the point lies outside of the ellipse so the method again returns -1.
If the method hasn't returned by now, then the point lies inside the ellipse. The code uses Math.Atan2 to get the angle from the center of the ellipse to the point. (Actually it gets the angle from the origin to the point when the ellipse is translated to the origin, but the result is the same.)
The Math.Atan2 method returns a value between -π and π so, if the value is negative, the code adds 2π to it to make it positive.
Next the code converts the angle into degrees and loops through the pie slice angles until it finds the slice that includes the point's angle. The code then returns the slice's number.
If the angles array ends with the value 360 as it should, then the method will always find the angle. If it does not, the program throws an exception.
Download the example to experiment with it and to see additional details.
|