WPF自定義控件(2)——圖表設計[1]


0、小敘閑言

除了儀表盤控件比較常用外,還有圖表也經常使用,同樣網上也有非常強大的圖表控件,有收費的(DEVexpress),也有免費的。但我們平時在使用時,只想簡單地繪一個圖,控件庫里面的許多功能我們都用不到,沒必要使用那么功能豐富的控件,以提高程序運行的效率和減小程序的占用空間。同時,我們自己如果能夠繪制圖表出來,對於程序的移植,也非常方便。對於大部分平台,相信設計方法是不會變的。廢話少講,直接上圖看效果,如果覺得還不錯,可以支持一下,繼續往下看。源碼下載地址:https://github.com/Endless-Coding/MyGauge/blob/master/CustomControl.zip

1、圖表整體設計

簡單來看一個圖表的組成,一般由4個部分組成,坐標軸,刻度和刻度值,繪圖區域(添加數據點和繪制曲線)。后面如果要做到數據動態顯示,還要花一點點功夫,不過也是很容易的,耐心研究,不會比高等數學難。

2、坐標軸繪制

先作出兩根垂直的直線出來,為x軸和y軸,XAML代碼如下。2,3行代碼即為兩個數軸。4~23行,是作出兩個小三角形,以形成箭頭的形狀。

 1 <Canvas Margin="5">
 2     <Line x:Name="x_axis" Stroke="Black" StrokeThickness="3" X1="40" Y1="280" X2="480" Y2="280" StrokeStartLineCap="Round"/>
 3     <Line x:Name="y_axis" Stroke="Black" StrokeThickness="3" X1="40" Y1="280" X2="40" Y2="30" StrokeStartLineCap="Round"/>
 4     <Path x:Name="x_axisArrow" Fill="Black">
 5         <Path.Data>
 6             <PathGeometry>
 7                 <PathFigure StartPoint="480,276" IsClosed="True">
 8                     <LineSegment Point="480,284"/>
 9                     <LineSegment Point="490,280"/>
10                 </PathFigure>
11             </PathGeometry>
12         </Path.Data>
13     </Path>
14     <Path x:Name="y_axisArrow" Fill="Black">
15         <Path.Data>
16             <PathGeometry>
17                 <PathFigure StartPoint="36,30" IsClosed="True">
18                     <LineSegment Point="44,30"/>
19                     <LineSegment Point="40,20"/>
20                 </PathFigure>
21             </PathGeometry>
22         </Path.Data>
23     </Path>
24 </Canvas>

C#作出兩個小箭頭的后台代碼如下: 

 1 /// <summary>
 2 /// 作出箭頭
 3 /// </summary>
 4 private void DrawArrow()
 5 {
 6     Path x_axisArrow = new Path();//x軸箭頭
 7     Path y_axisArrow = new Path();//y軸箭頭
 8 
 9     x_axisArrow.Fill = new SolidColorBrush(Color.FromRgb(0xff, 0, 0));
10     y_axisArrow.Fill = new SolidColorBrush(Color.FromRgb(0xff, 0, 0));
11 
12     PathFigure x_axisFigure = new PathFigure();
13     x_axisFigure.IsClosed = true;
14     x_axisFigure.StartPoint = new Point(480, 276);                          //路徑的起點
15     x_axisFigure.Segments.Add(new LineSegment(new Point(480, 284), false)); //第2個點
16     x_axisFigure.Segments.Add(new LineSegment(new Point(490, 280), false)); //第3個點
17 
18     PathFigure y_axisFigure = new PathFigure();
19     y_axisFigure.IsClosed = true;
20     y_axisFigure.StartPoint = new Point(36, 30);                          //路徑的起點
21     y_axisFigure.Segments.Add(new LineSegment(new Point(44, 30), false)); //第2個點
22     y_axisFigure.Segments.Add(new LineSegment(new Point(40, 20), false)); //第3個點
23 
24     PathGeometry x_axisGeometry = new PathGeometry();
25     PathGeometry y_axisGeometry = new PathGeometry();
26 
27     x_axisGeometry.Figures.Add(x_axisFigure);
28     y_axisGeometry.Figures.Add(y_axisFigure);
29 
30     x_axisArrow.Data = x_axisGeometry;
31     y_axisArrow.Data = y_axisGeometry;
32 
33     this.chartCanvas.Children.Add(x_axisArrow);
34     this.chartCanvas.Children.Add(y_axisArrow);
35 }
兩個小箭頭

WPF中沒有畫帶箭頭直線的函數,這個必需要自己寫了,最好的方法當然還是在XAML中,用Path來繪制出一個三角形在線的末端。當然這種用絕對坐標繪出來的小三角形,它不方便繪制到別的畫布中,當前單純為了做出效果,后面可以用C#動態生成箭頭,在后台完成繪制。上述代碼的效果如下所示。

然后給坐標軸添加上x,y標簽, 使用TextBlock表示出,對於標簽的樣式,是可以定義一個樣式資源的,來統一風格,不必要每一個標簽進行設置。但是目前,主要是為了實現功能,暫且不做得過於復雜

1             <TextBlock x:Name="x_label" Text="x" Canvas.Left="477" Canvas.Top="279"
2                        FontFamily="Arial" FontStyle="Italic" FontSize="20"/>
3             <TextBlock x:Name="y_label" Text="y" Canvas.Left="20" Canvas.Top="20"
4                        FontFamily="Arial" FontStyle="Italic" FontSize="20"/>
5             <TextBlock x:Name="o_label" Text="o" Canvas.Left="20" Canvas.Top="272"
6                        FontFamily="Arial" FontStyle="Italic" FontSize="20"/>
添加X,Y,O標簽 

3、坐標軸刻度和標簽添加

刻度就是一系列的小直線,控制好每一條小直線的位置,就可以輕松作出標簽。同樣,先用XAML語言,靜態畫一個小線段,看一看效果,然后用C#語言,在后台動態作出所有的小線段。XAML代碼如下

1 <Line x:Name="x_scale1" Stroke="Black" StrokeThickness="1" X1="50" Y1="280" X2="50" Y2="276" StrokeEndLineCap="Triangle"/>
2 <Line x:Name="y_scale1" Stroke="Black" StrokeThickness="1" X1="40" Y1="270" X2="44" Y2="270" StrokeEndLineCap="Triangle"/>

原點坐標O=(40,280)(這是相對於窗口的坐標),第一個x_scale1刻度,離原點10px距離,即打算每10px作一個刻度,故刻度的起點為(50,280);在軸上的刻度,終點的坐標相同,故為(50,276);|Y2-Y1|=4px,即為小刻度的長度。y_scale1也是同樣的原理。作出兩個小刻度后,效果如下

①坐標軸刻度線添加

每一個坐標軸的刻度線都很多,不可能一個個都用XAML語言都描述出來,還是要發揮一下C#代碼的功力。在上面已經明白如何作出x軸上和y軸上的刻度,並且已經作出來一個,多作幾個,無非是循環處理的問題,比較容易。C#代碼如下。關鍵部分在14、15行和31、32行,在窗口的坐標系統中,x軸的方向是向右,而y軸的方向是向下,因此,x軸方向與我們所作的圖表x軸方向一致,而y軸方向與圖表y軸方向相反,所以,14行代碼上相加,而31行代碼上相減,這樣就正確繪制了所有刻度了。

 1 /// <summary>
 2 /// 作出x軸和y軸的刻度線
 3 /// </summary>
 4 private void DrawScale()
 5 {
 6     for (int i = 0; i < 45; i += 1)//作480個刻度,因為當前x軸長 480px,每10px作一個小刻度,還預留了一些小空間
 7     {
 8         //原點 O=(40,280)
 9         Line x_scale = new Line();
10         x_scale.StrokeEndLineCap = PenLineCap.Triangle;
11         x_scale.StrokeThickness = 1;
12         x_scale.Stroke = new SolidColorBrush(Color.FromRgb(0, 0, 0));
13 
14         x_scale.X1 = 40 + i * 10;   //原點x=40,每10px作1個刻度
15         x_scale.X2 = x_scale.X1;    //在x軸上的刻度線,起點和終點相同
16 
17         x_scale.Y1 = 280;           //與原點坐標的y=280,相同 
18         x_scale.Y2 = x_scale.Y1 - 4;//刻度線長度為4px
19 
20         if (i < 25)//由於y軸短一些,所以在此作出判斷,只作25個刻度
21         {
22             //作出Y軸的刻度
23             Line y_scale = new Line();
24             y_scale.StrokeEndLineCap = PenLineCap.Triangle;
25             y_scale.StrokeThickness = 1;
26             y_scale.Stroke = new SolidColorBrush(Color.FromRgb(0, 0, 0));
27 
28             y_scale.X1 = 40;            //原點x=40,在y軸上的刻度線的起點與原點相同
29             y_scale.X2 = y_scale.X1 + 4;//刻度線長度為4px
30 
31             y_scale.Y1 = 280 - i * 10;  //每10px作一個刻度 
32             y_scale.Y2 = y_scale.Y1;    //起點和終點y坐標相同 
33             this.chartCanvas.Children.Add(y_scale);
34         }
35         this.chartCanvas.Children.Add(x_scale);
36     }
37 }

上述代碼執行后效果如下(左圖):

為了表達更加清楚,平時所用的圖表都有一個大刻度,在此,我也添加一個,其實也非常容易,無非就是在for循環里面添加一個判斷,到了所需要的位置的時候,將刻度線加粗,加長一些,也就是改變它的樣式。添加大刻度線的C#代碼如下,代碼只是對上面的代碼作了點小修改,其效果如上圖(右圖)。

 1 /// <summary>
 2 /// 作出x軸和y軸的標尺
 3 /// </summary>
 4 private void DrawScale()
 5 {
 6     for (int i = 0; i < 45; i += 1)//作480個刻度,因為當前x軸長 480px,每10px作一個小刻度,還預留了一些小空間
 7     {
 8         //原點 O=(40,280)
 9         Line x_scale = new Line();
10         x_scale.StrokeEndLineCap = PenLineCap.Triangle;
11         x_scale.StrokeThickness = 1;
12         x_scale.Stroke = new SolidColorBrush(Color.FromRgb(0, 0, 0));
13 
14         x_scale.X1 = 40 + i * 10;   //原點x=40,每10px作1個刻度
15         x_scale.X2 = x_scale.X1;    //在x軸上的刻度線,起點和終點相同
16 
17         x_scale.Y1 = 280;           //與原點坐標的y=280,相同 
18         if (i % 5 == 0)//每5個刻度添加一個大刻度
19         {
20             x_scale.StrokeThickness = 3;//把刻度線加粗一點
21             x_scale.Y2 = x_scale.Y1 - 8;//刻度線長度為8px 
22         }
23         else
24         {
25             x_scale.Y2 = x_scale.Y1 - 4;//刻度線長度為4px 
26         }
27 
28         if (i < 25)//由於y軸短一些,所以在此作出判斷,只作25個刻度
29         {
30             //作出Y軸的刻度
31             Line y_scale = new Line();
32             y_scale.StrokeEndLineCap = PenLineCap.Triangle;
33             y_scale.StrokeThickness = 1;
34             y_scale.Stroke = new SolidColorBrush(Color.FromRgb(0, 0, 0));
35 
36             y_scale.X1 = 40;            //原點x=40,在y軸上的刻度線的起點與原點相同
37             if (i % 5 == 0)
38             {
39                 y_scale.StrokeThickness = 3;
40                 y_scale.X2 = y_scale.X1 + 8;//刻度線長度為4px 
41             }
42             else
43             {
44                 y_scale.X2 = y_scale.X1 + 4;//刻度線長度為8px 
45             }
46 
47             y_scale.Y1 = 280 - i * 10;  //每10px作一個刻度 
48             y_scale.Y2 = y_scale.Y1;    //起點和終點y坐標相同 
49             this.chartCanvas.Children.Add(y_scale);
50         }
51         this.chartCanvas.Children.Add(x_scale);
52     }
53 }
添加大刻度

②坐標軸標簽添加

標簽的顯示還是使用文本塊(TextBlock控件)來實現。為了讓標簽的顯示位置剛剛好,對坐標值做了一些偏移,下面程序中已經解釋得比較清楚

 1 /// <summary>
 2 /// 添加刻度標簽
 3 /// </summary>
 4 private void DrawScaleLabel()
 5 {
 6     for (int i = 1; i < 7; i++)//7 個標簽,一共
 7     {
 8         TextBlock x_ScaleLabel = new TextBlock();
 9         TextBlock y_ScaleLabel = new TextBlock();
10 
11         x_ScaleLabel.Text = (i * 50).ToString();//只給大刻度添加標簽,每50px添加一個標簽
12 
13         Canvas.SetLeft(x_ScaleLabel, 40 + 5 * 10 * i - 12);//40是原點的坐標,-12是為了讓標簽看的位置劇中一點
14         Canvas.SetTop(x_ScaleLabel, 280 + 2);//讓標簽字往下移一點
15 
16         y_ScaleLabel.Text = (i * 50).ToString();
17         Canvas.SetLeft(y_ScaleLabel, 40 - 25);              //-25px是字體大小的偏移
18         Canvas.SetTop(y_ScaleLabel, 280 - 5 * 10 * i - 6);  //280px是原點的坐標,同樣-6是為了讓標簽不要上坐標軸疊上
19 
20         this.chartCanvas.Children.Add(x_ScaleLabel);
21         this.chartCanvas.Children.Add(y_ScaleLabel);
22     }
23 }
添加刻度標簽

 運行此代碼后,效果如下:

4、數據點添加和曲線繪制

①數據點添加顯示

對於數據點的顯示,用ellipse作出一個個小圓點,畫在坐標軸中。為了解數據點顯示的方法,還是先用XAML語言靜態作出兩個數據點P1=(60,80);P2=(180,100)。由於坐標系的不同,兩個數據點在canvas畫布里面的坐標要做一個轉換。由於原點O=(40,280),且y軸是相反的方向,故x1=40+60=100;y1=280-80=200;可得P1_Canvas=(100,200);同樣的方法,P2_Canvas=(220,180)。XAML代碼如下

1 <Ellipse Fill="Blue" Height="8" Width="8" Canvas.Left="100" Canvas.Top="200"/>
2 <Ellipse Fill="Blue" Height="8" Width="8" Canvas.Left="220" Canvas.Top="180"/>

由於小圓點有一定的直徑,為了讓其中心與數據點重合,還要做一個小的調整,x軸值+半徑=x中心;y軸值-半徑=y中心。因此,兩個數據點調整后的位置為P1_Canvas=(96,196),P2_Canvas=(216,176)。

先給程序定義一個數據點集合,后面生成8個隨機點,X軸坐標是每隔50px出現一次,Y軸坐標的大小是隨機生成的。C#代碼如下

 1 private void DrawPoint()
 2 {
 3     //隨機生成8個點
 4     Random rPoint = new Random();
 5     for (int i = 0; i < 8; i++)
 6     {
 7         int x_point = i * 50;
 8         int y_point = rPoint.Next(250);
 9         dataPoints.Add(new Point(x_point, y_point));
10     }
11 
12     for (int i = 0; i < dataPoints.Count; i++)
13     {
14         Ellipse dataEllipse = new Ellipse();
15         dataEllipse.Fill = new SolidColorBrush(Color.FromRgb(0, 0, 0xff));
16         dataEllipse.Width = 8;
17         dataEllipse.Height = 8;
18 
19         Canvas.SetLeft(dataEllipse, 40 + dataPoints[i].X - 4);//-4是為了補償圓點的大小,到精確的位置
20         Canvas.SetTop(dataEllipse, 280 - dataPoints[i].Y - 4);
21 
22         chartCanvas.Children.Add(dataEllipse);
23     }
24 }

 數據點集合聲明如下

private List<Point> dataPoints = new List<Point>();

 上述代碼的運行效果如下(左圖): 

②曲線繪制

 將所有點用折線描繪出來,C#代碼如下,效果如上圖(右圖)。由於前后兩次的代碼是不同時間運行的,生成的點是隨機的,不一樣,所以兩副圖點不相同。

 1 private void DrawCurve()
 2 {
 3     Polyline curvePolyline = new Polyline();
 4 
 5     curvePolyline.Stroke = Brushes.Green;
 6     curvePolyline.StrokeThickness = 2;
 7 
 8     curvePolyline.Points = coordinatePoints;
 9     chartCanvas.Children.Add(curvePolyline);  
10 }

 對coordinatePoints的定義和初始化如下。

//這行代碼在程序的開始部分
private
PointCollection coordinatePoints = new PointCollection();
----------------------------------------------------------------------------
//將數據點在畫布中的位置保存下來 coordinatePoints.Add(new Point(40 + dataPoints[i].X, 280 - dataPoints[i].Y));

5、讓圖表動起來 

首先,思考要讓線動起來,表中兩類元素需要移,一個線,另一個是小圓點。而線是隨數據點動的,這一部分WPF已經幫我們做好了;而點動,我們要不停地改變它在canvas中的位置,這樣就可以看到動的效果,每次單擊一下鼠標,就添加一個點,這個點的Y軸坐標還是隨機生成的,C#代碼如下。這個函數主要就做了2件事,一個是移除集合中的第0個點、在最后位置添加一個點,二是將所有數據都向左移50px,很容易。

 1 private void AddCurvePoint(Point dataPoint)
 2 {
 3     dataPoints.RemoveAt(0);
 4     dataPoints.Add(dataPoint);
 5     for (int i = 0; i < dataPoints.Count; i++)
 6     {
 7         //每一個點的X數據都要向左移動50px
 8         dataPoints[i] = new Point(dataPoints[i].X - 50, dataPoints[i].Y);
 9         coordinatePoints[i] = new Point(40 + dataPoints[i].X, 280 - dataPoints[i].Y);
10         
11         Canvas.SetLeft(pointEllipses[i], 40 + dataPoints[i].X - 4);//-4是為了補償圓點的大小,到精確的位置
12         Canvas.SetTop(pointEllipses[i], 280 - dataPoints[i].Y - 4);
13     }
14 }

鼠標單擊時的程序如下:

1 private void chartCanvas_MouseDown(object sender, MouseButtonEventArgs e)
2 {
3     //隨機生成Y坐標
4     Point dataPoint = new Point(400, (new Random()).Next(250));
5 
6     AddCurvePoint(dataPoint);
7 }

上面程序中用到的幾個數據結構聲明如下:

private List<Point> dataPoints = new List<Point>();
private PointCollection coordinatePoints = new PointCollection();
private List<Ellipse> pointEllipses = new List<Ellipse>();

終於做出來了,效果還不錯的,來欣賞一下自己的成果,花了大半天了。

心得體會

合抱之木,生於毫末;九層之台,起於壘土;千里之行,始於足下。一個圖表,雖然復雜,但一步一步來,將每一步用心構思,然后慢慢實現,雖然做不到完美,但是起碼可以實現基本目標。

圖標還有改多可以改進的地方,功能還很弱小。可以給圖表加上網格,橫軸的坐標的標簽可以實時變化,以形成示波器的功能。

下一個目標,給圖表添加更多功能。

下下一目標,完成窗體的美化,做出類似360安全衛士11的界面。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM