Photoshop 风格的角度和高度选择器
具有 Photoshop 角度选择器外观和功能的 C# 自定义控件。

引言
Adobe Photoshop 有两个非常专业的控件,用于“投影”和“斜面和浮雕”等效果:一个是角度选择器控件,另一个是角度和高度选择器控件。
在本文中,我们将通过创建两个 C# 自定义控件来模仿 Photoshop 中控件的外观和行为。
基础 - 需要了解的数学知识
勾股定理
勾股定理可以帮助我们计算直角三角形的斜边(最长的那条边)。公式是 a2 + b2 = c2。因此,斜边 c 始终等于 a2 + b2 的平方根。
单位圆
由于我们将处理角度和圆,因此了解单位圆的格式将非常有帮助。单位圆就是一个以常规网格上的点 (0, 0) 为中心,半径为 1 的圆。角度 0 从点 (1, 0) [右侧] 开始,逆时针增加。因此,角度 90 位于 (0, 1),角度 180 位于 (-1, 0),角度 270 位于 (0, -1),而 360 又回到了 0 的位置。
三角测量
我们只需要三个基本的三角函数:sin、cos 和 tan。如果我们记住 *SOH CAH TOA*,我们就知道在三角形中,正弦等于对边比斜边,余弦等于邻边比斜边,正切等于对边比邻边。
还要记住,反三角函数用于计算未知角度。
常用函数
我们的两个自定义控件将共享两个重要函数
- 一个函数用于接收角度和半径,并返回围绕指定原点的圆上的相应 Point位置。(基本上,将角度转换为点。)
- 另一个函数用于做相反的操作,接收一个点 (X, Y)并找到最接近的角度。
第一个函数是两者中比较简单的
private PointF DegreesToXY(float degrees, float radius, Point origin)
{
  PointF xy = new PointF();
  double radians = degrees * Math.PI / 180.0;
  xy.X = (float)Math.Cos(radians) * radius + origin.X;
  xy.Y = (float)Math.Sin(-radians) * radius + origin.Y;
  return xy;
}
需要注意的是,我们首先需要将角度转换为弧度。(将度转换为弧度。)但是,为了获得总体概念,我们只需要可视化我们的单位圆

该函数有角度和半径,因此使用三角学,我们可以计算出 X 和 Y 值,就像它们构成一个三角形一样。为了将值居中到指定原点,我们只需要加上原点的初始坐标。
还请注意,该函数使用角度的相反值作为 Y 分量。这是为了适应计算机显示器上的网格与常规网格方向相反的事实。(在计算机上,正值向下。)
第二个函数用于让用户点击我们的控件,然后将该点转换为匹配的角度。它只是稍微复杂一些,因为我们需要考虑一些因素。为了使文章长度合理,我将只发布一部分内容
private float XYToDegrees(Point xy, Point origin)
{
  double angle = 0.0;
  if (xy.Y < origin.Y)
  {
    if (xy.X > origin.X)
    {
      angle = (double)(xy.X - origin.X) / (double)(origin.Y - xy.Y);
      angle = Math.Atan(angle);
      angle = 90.0 - angle * 180.0 / Math.PI;
    }
    else if (xy.X < origin.X)
    {
      //etc.
    }
  }
  else if (xy.Y > origin.Y)
  {
    //etc.
  }
  if (angle > 180) angle -= 360;
  return (float)angle;
}
该函数的主要思想是检查鼠标位置与控件中心的关系,以确定该点位于单位圆的四个象限中的哪一个。一旦我们知道了象限,我们就可以使用三角学(切线的反函数)来计算角度。
对于这些控件,如果角度大于 180 度,我就减去 360 度。这使得角度保持在 -180 到 180 之间,就像 Photoshop 一样。不过,这是可选的,没有这一行代码控件也能正常工作。
构建控件
绘制控件
两个控件的背景将相同
- 用 2像素宽的Pen绘制圆形的轮廓。
- 使用 40% 不透明度(大约 100值)的白色填充。
- 在控件中心绘制一个 3x3 像素的正方形。
protected override void OnPaint(PaintEventArgs e)
{
  //...
  //Draw
  g.SmoothingMode = SmoothingMode.AntiAlias;
  g.DrawEllipse(outline, drawRegion);
  g.FillEllipse(fill, drawRegion);
  //...Marker
  g.SmoothingMode = SmoothingMode.HighSpeed; 
  g.FillRectangle(Brushes.Black, originSquare);
  //...
}
需要注意的重要一点是 SmoothingMode 属性。绘制圆形时将其设置为 AntiAlias,可以使控件看起来平滑且专业。但是,如果我们使用 AntiAlias 绘制正方形,边缘会变得模糊。解决方法是将 SmoothingMode 设置回 HighSpeed,使边缘重新变得清晰。
绘制标记部分取决于控件。简单的角度选择器只需要创建一条从原点到我们 DegreesToXY 函数返回的点的线。而角度和高度选择器将在该点绘制一个 1x1 的矩形,然后绘制四个十字标记。
处理用户点击
感谢我们的 XYToDegrees 函数,处理用户点击非常简单。为了成功模仿我们控件的 Photoshop 版本的功能,我们需要设置 MouseDown 和 MouseMove 事件。这样,值就可以实时更新。这是一个辅助函数
private int findNearestAngle(Point mouseXY)
{
  int thisAngle = (int)XYToDegrees(mouseXY, origin);
  if (thisAngle != 0)
    return thisAngle;
  else
    return -1;
}
高度控件还需要额外的步骤,那就是计算自定义控件中心与鼠标点击点之间的距离
private int findAltitude(Point mouseXY)
{
  float distance = getDistance(mouseXY, origin);
  int alt = 90 - (int)(90.0f * (distance / origin.X));
  if (alt < 0) alt = 0;
  return alt;
}
在 Photoshop 中,高度测量方式是:当选定点正好在原点时为 90,当选定点在控件边缘时为 0。因此,我们通过找到半径长度与点击点与原点之间距离的比例来计算高度。然后,将其从 90 中减去,以有效地“翻转”这些值(90 在 0 距离处)。
自定义事件
为了使自定义控件更加专业,它们需要能够以编程方式在值发生更改时发出警报。这就是自定义事件的作用。
例如,要创建一个角度更改时的事件,首先定义如下
public delegate void AngleChangedDelegate();
public event AngleChangedDelegate AngleChanged;
然后,我们只需要在自定义控件的 Angle 属性发生更改时调用 AngleChanged()(并确保它不为 null)。
局限性和改进
闪烁
完全没有!.NET Framework 2.0 及更高版本提供了 DoubleBuffer 属性,在构造控件时只需将其设置为 true 即可。Framework 会负责处理,确保控件平滑重绘。
大小
由于两个控件都依赖于假设半径始终相等的数学计算,因此控件的宽度和高度尺寸必须始终相等,这样控件才能正确显示和工作。
颜色
我没有为控件的背景和轮廓颜色包含属性,因为我追求的是 Photoshop 的外观。但是,请查看代码,您会发现更改为您喜欢的颜色,甚至使其动态化,并不会太难。
结论
我建议您下载项目文件(或至少是示例应用程序),以便您可以看到这些控件是如何完美组合在一起的。
至于控件的用途和应用,那就取决于您的想象力了。




