GDI+繪制極坐標圖、雷達圖

来源:互联网 发布:淘宝添加不了购物车 编辑:程序博客网 时间:2024/06/07 02:03

http://blog.csdn.net/conmajia/article/details/7596720

作者:野比 (conmajia@gmail.com)

時間:May, 2012



一個簡單的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()出角度數字就行了吧?嗯,在void 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(Pens.Black, nextPt, getMappedPoint(pointList[i].Angle, pointList[i].Value));  
  14.     }  
  15. }  

隨便添加幾個數據,順便設置下圓圈數和輻條數,看看效果如何。

圓圈=3,輻條=4


圓圈=6,輻條=8


圓圈=9,輻條=16



一些變化

(以下內容為搞笑)
好了,我們做完了這個項目,送走了天線的客戶。現在又來了一個游戲的客戶。他要求我們要制作一個類似FIFA或者實況的運動游戲,游戲裡面要有一個運動員個人素質參數的查看界面。
我們要怎麼做?重新做?不,就著上一個客戶的稍微那麼改上一改,就像這樣。

如果稍微改造下,你甚至可以用它來畫戰斗力分析圖(搞笑的)



嗯好了,就寫這麼多。山寨故事到此結束。謝謝收看。



(全文完)


後記


按照本文的思路,還可以有很多變種,比如這個。




希望你能多思考,搞點有創意的東西。