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"/>
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的界面。