• GDI+绘制极坐标图


    一个简单的GDI+例子。

    简单绘制极坐标系,按类似的思路,可以画直角坐标系、对数直角系、外太空银河系……

    先把最后效果贴出来,觉得不需要的请按Alt+F4。

    图中曲线是一个天线方向图,非常适合在极坐标下描绘。

    文中是直接在窗体上绘制,你完全可以自行封装到控件里,这样用起来更加方便。

    (正文开始)

    写在前面的话

    做事情,一切以目标为出发点,倒着找过去,看有哪些方法技术资源,具体的方法技术手段都是次要的,只要能达到目的。

    我不会多线程,如果你觉得这个直接在UI线程画效率低方法笨,还请自己用多线程改造一遍。(似乎还真有这样ocd的人吧,哈哈)

    欢迎把改造后的代码回传给我,我会贴在这里。(小广告)

    目标设定(例子)

    (下面是例子,不针对任何人物、事件、团体、星球)

    boss接到了一单生意,是帮某山寨厂做一个山寨手机天线的信号测试系统。其中,我分到的部分是做天线方向图的显示界面模块。其实我懂个p的天线、方向图之类的啊,于是boss告诉我,并强调:我不管你怎么做,总之要「看起来」像这样。

    ok,不管会不会,山寨是本行,拿着原版开始分析。

    分析坐标系

    说实话,数学那套玩意老早就还给老师了,现在要让我玩坐标系这样高深的东西。得亏哥们还有点印象,这样圆不拉叽的图,一般用极坐标来画是比较方便的。连上Wikipedia复习一下:极坐标是一个二维坐标系统。该坐标系统中的点由一个夹和一段相对中心——极点(相当于我们较为熟知的直角坐标系中的原点)的距离来表示。

    嗯,很好,乱七八糟的,看不太懂。把这东西先放一遍,还是用山寨的方法解决。把boss给的那张图拿来分析下,其实就是很多同心圆,和过圆心的辐条(借用自行车术语,虽然不知道正确的名字,就这么叫了吧)。

    那么我只需要画出同心圆,再画辐条,就ok了吧。画同心圆怎么画呢?嗯,我可以这样,从外面的大圆开始,用DrawEllipse()画一个圆,然后收缩下半径,再画一个,如此这般……好了,有想法就行动,管他是nb方法还是sb方法,一直坐那zb,最后被炒了那才sb。

    画出同心圆的方法。

    绘制同心圆(点击+号展开)
     1     // 画圆
     2     private void drawCircles(Graphics g, Rectangle rect)
     3     {
     4         // 圆的直径等于绘图区域最短边
     5         float diameter = Math.Min(rect.Width, rect.Height);
     6         // 半径
     7         float radius = diameter / 2;
     8         // 圆心
     9         PointF center = new PointF(
    10             rect.X + rect.Width / 2,
    11             rect.Y + rect.Height / 2
    12             );
    13 
    14         // 画几个圆,先试试5个
    15         int count = 5;
    16         float diameterStep = diameter / count;
    17         float radiusStep = radius / count;
    18 
    19         // 生成圆的范围
    20         RectangleF cirleRect = new RectangleF();
    21         cirleRect.X = center.X - radius;
    22         cirleRect.Y = center.Y - radius;
    23         cirleRect.Width = cirleRect.Height = diameter;
    24 
    25         // 画同心圆
    26         for (int i = 0; i < count; i++)
    27         {
    28             g.DrawEllipse(Pens.Gray, cirleRect);
    29 
    30             cirleRect.X += radiusStep;
    31             cirleRect.Y += radiusStep;
    32             cirleRect.Width -= diameterStep;
    33             cirleRect.Height -= diameterStep;
    34         }
    35     }

    把这段代码添加到Paint事件里,看看效果如何。

    Good,效果还凑合,好像有点锯齿哦,那我就把抗锯齿打开,顺手把文字抗锯齿也打开。

    1     e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;    // 图形抗锯齿
    2     e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; // 文字抗锯齿

    接下来就要画辐条了。那个线可不能就像图里一个十字叉就完事了的,肯定要能自己设n条。想当初就是曾经思维简单了没有考虑到这种变数,被客户和boss烦得天昏地暗。再也不会上当了。

    辐条怎么画呢,思考下,在草稿纸上画画先。

    (以下都是中学数学,本人上了大学以后数学从没及格过)

    从少到多看看辐条的规律。啊,原来是这样啊,我不一定非要把辐条看成穿过圆心的,我可以看成从圆心发出的n个射线,把圆切成了n个扇面,每个角度就是360°÷n。这样那就好办了,刚才我画圆的时候已经算出来圆心位置了,只要再算出射线终点的坐标,就可以用DrawLine()画线了。但是,射线终点又要怎么算呢,我可是要画到GDI+里哦。

    用黑色的笔画出圆,红色的画出GDI+坐标系,那么就可以算出来终点在GDI+下的坐标。圆心(x0,y0)和r半径刚才我已经算出来了,θ就是360/n。现在所有参数都确定了,只要把圆心、半径这几个我需要使用的变量从画圆的方法里拿出来大家用,我就可以开始写画辐条的方法了。

    绘制辐条(点击+展开)
    复制代码
     1     // 提出来公用
     2     float diameter, radius;
     3     PointF center;
     4     // 画圆
     5     private void drawCircles(Graphics g, Rectangle rect)
     6     {
     7         // (略)
     8     }
     9 
    10     // 辐射线
    11     private void drawSpokes(Graphics g)
    12     {
    13         int count = 8;
    14         if (count > 0)
    15         {
    16             // 计算角度
    17             float angle = 0;
    18             float angleStep = 360 / count;
    19             PointF endPoint = new PointF();
    20 
    21             for (int i = 0; i < count; i++)
    22             {
    23                 // 得到终点
    24                 endPoint = getPoint(angle);
    25                 g.DrawLine(Pens.Gray, center, endPoint);
    26 
    27                 angle += angleStep;
    28                 angle %= 360;
    29             }
    30         }
    31     }
    32 
    33     // 计算终点
    34     private PointF getPoint(double angle)
    35     {
    36         PointF pt = new PointF();
    37 
    38         pt.X = (float)(radius * Math.Cos(angle * Math.PI / 180) + center.X);
    39         pt.Y = (float)(radius * Math.Sin(angle * Math.PI / 180) + center.Y);
    40 
    41         return pt;
    42     }
    复制代码

    把代码加到Paint事件画圆的后面,看看效果。

    Yeah, baby,你太听话了。

    永不满足的客户·永不结束的工作

    没过半天,客户就找到boss,要求在辐射线边上加上角度数字。于是,我「义不容辞」的开始了新一轮改造。

    说起加上数字,先前我已经得到了每个射线终点的坐标,那我直接在那坐标上DrawString()出角度数字就行了吧?嗯,在drawSpokes()里面先加上这句试试。

    1     // 画角度值
    2     g.DrawString(angle.ToString("0") + "°", this.Font, Brushes.Gray, endPoint);

    哦,卖糕的,问题多多哦。最下面的字跑出画面了,上面的和左边的字跑到圆圈里面,右边的字也有点往里靠。改改试试看。先把画圆的区域缩小一点,以便下面的标签能显示出来。

    1     //drawDiagramCircles(e.Graphics, this.ClientRectangle);
    2     // 缩小点画圆的区域
    3     Rectangle rect = this.ClientRectangle;
    4     rect.Inflate(0, -20);
    5     drawDiagramCircles(e.Graphics, rect);

    ok,解决下一个问题。先思考下,什么情况下字会跑到圆里去:θ∈(90°, 270°)这个区间。那我就在这个区间画文字的时候,把文字往左平移出去就行了。而270°时,我把文字往上移动试试看。在drawSpokes()画文字的地方。

    复制代码
     1     // 把要画的字符串提出来便于操作
     2     string angleString = angle.ToString("0") + "°";
     3 
     4     // 画角度值,如果文字在90-270度区间内,
     5     PointF textPoint = endPoint;
     6 
     7     if (angle == 270)
     8         textPoint.Y -= TextRenderer.MeasureText(angleString, this.Font).Height; // 用TextRenderer测量字符串大小
     9     else if (angle < 270 && angle > 90)
    10         textPoint.X -= TextRenderer.MeasureText(angleString, this.Font).Width;
    11     else
    12         textPoint.X += 8; // 随便来点漂移
    13 
    14     g.DrawString(angleString, this.Font, Brushes.Gray, textPoint);
    复制代码

    看看效果。

    嗯哼,很好。(其实我觉得最好的办法是分象限,比如第一象限就增加x、增加y,第二象限就增加x、减少y,第三象限减少x、减少y,第四象限减少x、增加y’)

    加入数据点

    光画一副坐标系那肯定是什么都干不了的,所以还有最重要的添加数据。所谓一个数据,就是包含了角度、数值的这样一组数,比如天线对着某个方向(角度)的接收信号强度(数值)。角度很好理解,就是0到360°,然后转圈。数值就要费点功夫了。用户添加的数据,肯定是他们采集到的真实数据。这个数据,要映射到我这里做的坐标图里面,使其同样大小数值具有同样的映射点,最小数值映射在圆心,最大数值映射在射线终点。这样,所有的数据就都可以用这张图来记录了。下面使用最简单的线性映射来设计。所谓线性映射,其实就是这样。

    所以,在全局变量里,我加入了数据范围的上下限。

    1     float min = 0;
    2     float max = 100;

    为了便于后续操作,我决定把「角度 - 数值」这样一组数据封装在一起,然后用一个列表来存储管理。

    复制代码
     1         public class PolarValue
     2         {
     3             float ang = 0;
     4             float val = 0;
     5 
     6             // 角度
     7             public float Angle
     8             {
     9                 get { return ang; }
    10                 set { ang = value; }
    11             }
    12 
    13             // 数值
    14             public float Value
    15             {
    16                 get { return val; }
    17                 set { val = value; }
    18             }
    19 
    20             public PolarValue(float angle, float value)
    21             {
    22                 this.ang = angle;
    23                 this.val = value;
    24             }
    25         }
    26 
    27         // 数据列表
    28         public List<PolarValue> values = new List<PolarValue>();
    复制代码

    现在我有了一组数据点,我需要做的就是把数据点映射到坐标图上,如此遍历每一点并连接之,就画出了我所需要的方向图。这就是映射的方法。

    复制代码
     1     private PointF getMappedPoint(PolarValue pv)
     2     {
     3         // 计算映射在坐标图中的半径
     4         float r = radius * (pv.Value - min) / (max - min);
     5         // 计算GDI+坐标
     6         PointF pt = new PointF();
     7         pt.X = (float)(r * Math.Cos(pv.Angle * Math.PI / 180) + center.X);
     8         pt.Y = (float)(r * Math.Sin(pv.Angle * Math.PI / 180) + center.Y);
     9         return pt;
    10     }
    复制代码

    写到这里,我不由得回头看了看刚才画辐条时,为了计算辐条终点而写的getPoint()方法。这两个方法实在是太像了,唯一区别就是getMappedPoint()使用变化的数值,而getPoint()使用固定的数值(辐条终点可以认为是r=R,即value=max)。现在合并这两个方法,并修改相应调用的地方。

    复制代码
     1     // 合并后的映射方法
     2     private PointF getMappedPoint(float angle, float value)
     3     {
     4         // 计算映射在坐标图中的半径
     5         float r = radius * (value - min) / (max - min);
     6         // 计算GDI+坐标
     7         PointF pt = new PointF();
     8         pt.X = (float)(r * Math.Cos(angle * Math.PI / 180) + center.X);
     9         pt.Y = (float)(r * Math.Sin(angle * Math.PI / 180) + center.Y);
    10         return pt;
    11     }
    复制代码

    修改调用的地方

    1     // 在drawSpokes()中
    2     // (略)
    3     // 得到终点
    4     endPoint = getMappedPoint(angle, max);

    现在可以一口气把所有数据点画出来了。

    复制代码
     1     private void drawPoints(Graphics g, List<PolarValue> pointList)
     2     {
     3         // 计算下一点
     4         PointF nextPt;
     5         for (int i = 0; i < pointList.Count; i++)
     6         {
     7             if ((i + 1) < pointList.Count)
     8                 nextPt = getMappedPoint(pointList[i + 1].Angle, pointList[i + 1].Value);
     9             else
    10                 nextPt = getMappedPoint(pointList[0].Angle, pointList[0].Value);
    11 
    12             // 连接当前点和下一点
    13             g.DrawLine(
    14                 Pens.Black,
    15                 nextPt,
    16                 getMappedPoint(pointList[i].Angle, pointList[i].Value));
    17         }
    18     }
    复制代码

    随便添加几个数据,顺便设置下圆圈数和辐条数,看看效果如何。

    圆圈=3,辐条=4

    圆圈=6,辐条=8

    圆圈=9,辐条=16

    一些变化

    (以下内容为搞笑)
    好了,我们做完了这个项目,送走了天线的客户。现在又来了一个游戏的客户。他要求我们要制作一个类似FIFA或者实况的运动游戏,游戏里面要有一个运动员个人素质参数的查看界面。
    我们要怎么做?重新做?不,就着上一个客户的稍微那么改上一改,就像这样。

    如果稍微改造下,你甚至可以用它来画战斗力分析图(搞笑的)

    嗯好了,就写这么多。山寨故事到此结束。谢谢收看。

    (全文完)

    转自:http://www.cnblogs.com/conmajia/archive/2012/05/21/gdiplus-polar-diagram.html

  • 相关阅读:
    某个sql帖子的答题
    JQuery hide
    JQuery tr 循环,删除td
    JQuery Disabled
    QueryString大小设置
    Silverlight HelloWorld
    USB HID通讯流程
    动态调用c++dll(转)
    VB应用程序调用c++编写的动态库(dll)(转)
    读取图像
  • 原文地址:https://www.cnblogs.com/xiurui12345/p/2512706.html
Copyright © 2020-2023  润新知