找出鼠标光标所在的饼图区域是三角函数的一个简单应用,具体来说是反正切函数(也称为arctangent或atan)的应用。
对于那些之前遇到过这个问题的人,或者对于那些还没有遇到过的人,让我们快速地看一下正切函数。三角函数处理直角三角形的几何学,根据定义,直角三角形有两条直角边和一条斜边。斜边是三角形中与直角(90°或π/2)相对的一条边的特殊名称。另外两条边很有帮助地被称为直角边。
正切函数的值是一个角度的对边与邻边的比值。反正切是其切比值相等的角度。由于函数的对称性,我们需要计算角度,然后根据象限添加或减去一个偏移量来提取“真实”的角度。以图表形式表示如下:
![Diagram of arctangent values mapped to quadrants.](https://istack.dev59.com/ZVZnd.webp)
正切函数在几个点上有不连续性,即当相邻边的长度为0(90°和270°)时,我们必须特别处理这些点。
好了,足够的数学知识,现在进入实际应用。
对于此演示,创建一个新的C# WinForms项目,在默认的Form1
上添加一个PictureBox
。
首先,由于我没有您的颜色生成函数,因此我使用以下值列表和辅助函数:
List<int> values = Enumerable.Range(0, 360).ToList();
int Rescale(int x) => (int)(((double)x / 360.0) * 255.0);
在构造函数中,挂接一些事件并设置一些属性:
public Form1()
{
InitializeComponent();
this.pictureBox1.BorderStyle = BorderStyle.Fixed3D;
this.pictureBox1.Size = new Size(50, 50);
this.Size = new Size(450, 450);
this.DoubleBuffered = true;
this.Paint += Form1_Paint;
this.MouseMove += Form1_MouseMove;
}
为了绘制圆形,我使用了您的
OnPaint
处理程序的稍微修改版本:
private void Form1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.Clear(Color.Black);
for (int i = 0; i < values.Count; i++)
{
Brush b = new SolidBrush(Color.FromArgb(255, Rescale(values[i]), 0, 0));
e.Graphics.FillPie(b, 0, 0, 400, 400, (float)i, 1.0f);
}
}
在
MouseMove
事件中,我们进行大部分重要的工作:
private void Form1_MouseMove(object sender, MouseEventArgs e)
{
this.pictureBox1.Location = new Point(e.X + 5, e.Y - 5);
int segment = (int)GetAngle(new Rectangle(0, 0, 400, 400), e.Location);
this.pictureBox1.BackColor = Color.FromArgb(255, Rescale(segment), 0, 0);
}
你可能会注意到,由于360个楔子是以度数递增的,我只截取了角度。如果您需要更高的精度,或者决定使用大于1度的段,那么您可以使用各种舍入算法将角度舍入到最接近饼图部分的位置。
最后,我们准备实现GetAngle
函数。首先,我们计算圆的中心,因为一切都是相对于它的。
int cx = (rect.Width + rect.X) / 2;
int cy = (rect.Height + rect.Y) / 2;
接下来计算鼠标位置与矩形中心的差异。(我已经反转了y坐标以使其与“标准”笛卡尔坐标系对齐,从而使事情变得更容易,并匹配你在数学教科书中看到的坐标。)
float x = pTo.X - cx;
float y = (cy - pTo.Y);
接下来检查反正切函数的未定义点(以及我们可以采取的一些快捷方式):
if ((int)x == 0)
{
if (y > 0) return 270;
else return 90;
}
else if ((int)y == 0)
{
if (x > 0) return 0;
else return 180;
}
计算内角:
float ccwAngle = (float)Math.Atan(Math.Abs(y) / Math.Abs(x))
并将该角度映射到相应的象限:
if (x > 0 && y > 0)
{
}
else if (x < 0 && y > 0)
{
ccwAngle = (float)Math.PI - ccwAngle;
}
else if (x < 0 && y < 0)
{
ccwAngle = ccwAngle + (float)Math.PI;
}
else if (x > 0 && y < 0)
{
ccwAngle *= -1f;
}
将角度从度数转换为弧度并进行归一化处理(确保它在0°到360°之间)
ccwAngle *= (float)(180 / Math.PI)
while (ccwAngle > 360) ccwAngle -= 360
while (ccwAngle < 0) ccwAngle += 360
最后,将我们需要进行数学计算的逆时针角度转换为GDI使用的顺时针角度,并返回该值:
return 360f - ccwAngle;
所有这些组合在一起产生了最终的结果:
![Screenshot of demo implementation.](https://istack.dev59.com/nDYNO.webp)
(上面的代码也可以在这个Gist中作为完整示例获得)