简介
Adobe Photoshop 有两个非常专业的控件: 角度选择器和角度与高度选择器, 如图 1 所示.
图 1 两种控件外观
这么可爱的控件, 当然要拿来了. 仿制这两个控件很简单, 一点点基本的数学知识就足够应付.
基础知识
勾股定理
勾股定理是老祖宗传下的数学神器. 以图 2 的直角三角形为例, 它可以通过对边 \(c\) 邻边 \(b\) 计算斜边 \(a\),\(a^2=b^2+c^2\).
图 2 一个直角三角形
单位圆
如图 3 所示的单位圆是以 \((0,0)\) 为圆心, 半径为 \(1\) 的圆. 单位圆上, 角度 \(0^\circ\) 的点从 \((1,0)\) 开始, 按逆时针方向沿单位圆移动. 因此,\(90^\circ\) 是 \((0,1)\),\(180^\circ\) 是 \((-1,0)\),\(270^\circ\) 是 \((0,-1)\),\(360^\circ\) 和 \(0^\circ\) 重合.
图 3 一个单位圆
三角函数
三个基本的三角函数用于从角度计算各边比值: 正弦函数 \(\sin(x)\) 余弦函数 \(\cos(x)\) 和正切函数 \(\tan(x)\), 假设图 2 中 \(\angle BCA=\theta\), 那么 \(\sin(\theta)=\dfrac{c}{a}\),\(\cos(\theta)=\dfrac{b}{a}\),\(\tan(\theta)=\dfrac{c}{b}\). 与它们对应的反三角函数则是已知比值计算角度.
实现
点和角度的函数
下面这两个函数用于计算点和角度, 这对我要完成的两个控件来说很重要. 一个函数把角度转换为点, 另一个完成相反的功能, 把点转换为角度.
第一个函数 DegreesToXY:
- 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;
- }
注意
代码中用到的是 $-y$, 这是因为在 GDI+ 的画布上,$y$ 轴向下为正方向.
第二个函数 XYToDegrees:
- 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)
- {
- // 如此这般
- }
- }
- else if (xy.Y > origin.Y)
- {
- // 如此这般
- }
- if (angle > 180) angle -= 360; // 控制角度范围
- return (float)angle;
- }
这个函数通过检查鼠标相对中心点的位置, 确定它所在象限. 一旦知道了象限, 就可以利用反三角函数计算出角度. 如果角度大于 \(180^\circ\), 则减去 \(360^\circ\). 这样就和 Photoshop 一样, 把角度控制在 \(-180^\circ\) 和 \(180^\circ\) 之间.
绘制控件
这两个控件的背景相同:
用宽度为 2 的 Pen 绘制外圈圆
用 40% 不透明度的白色填充
控件中心是 3×3 像素的正方形
- protected override void OnPaint(PaintEventArgs e)
- {
- //...
- //Draw
- g.SmoothingMode = SmoothingMode.AntiAlias;
- g.DrawEllipse(outline, drawRegion);
- g.FillEllipse(fill, drawRegion);
- //...
- g.SmoothingMode = SmoothingMode.HighSpeed;
- g.FillRectangle(Brushes.Black, originSquare);
- //...
- }
在绘制圆圈时把 SmoothMode 属性设置为 AntiAlias(抗锯齿), 这样看起来既光滑又专业. 但是如果画正方形时也用抗锯齿, 就会显得模糊难看, 所以画正方形的时候要把 SmoothMode 改为 HighSpeed(高速), 这样画出的正方形边缘整齐犀利. 根据控件不同, 光标也有不同绘制方法, 不多说. 角度选择器比较简单, 只需要从圆心到 DegreesToXY 函数返回的点连一条直线即可. 角度与高度选择器则是在这点上绘制一个 1×1 的矩形, 然后在周围绘制一个十字型光标.
处理用户点击
有了 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 减去这个值.
自定义事件
为了让控件更加专业, 需要控件能够在数值发生变化时以编程方式进行提醒. 例如, 像这样给角度变化添加一个事件:
- public delegate void AngleChangedDelegate();
- public event AngleChangedDelegate AngleChanged;
每次变更 Angle 属性时, 调用 AngleChanged() 就可以触发这个事件了.
下载
Demo: http://files.cnblogs.com/conmajia/AngleAltitudeControls_demo.zip
源代码: http://files.cnblogs.com/conmajia/AngleAltitudeControls_src.zip
The End. \(\Box\)
来源: https://www.cnblogs.com/conmajia/p/angle-altitude-control.html