在制作上位機的時候,很多時候需要使用到監控繪圖界面,使用來繪制曲線的組件有很多,GDI+、char、OxyPlot等等,這篇文章用來介紹OxyPlot組件的基本應用,在本文中主要是利用隨機數生成函數結合拖桿控件來模擬出繪圖所需要的數據。
首先需要先創建一個Winfrom窗體應用的工程,創建工程過程可參考以下鏈接https://www.cnblogs.com/xionglaichuangyichuang/p/13734179.html
OxyPlot組件是一個開源的組件,需要使用它就需要在工程中安裝好給組件,便可以進行后續操作,該組件的安裝過程可以參考以下鏈接完成安裝,https://www.cnblogs.com/xionglaichuangyichuang/p/13734265.html
組件安裝好之后,打開工具箱,我們可以發現工具箱上多了一組控件,如下圖所示,將該控件往右邊拖拽既可以完成初步的控件布局。
上位機的界面的控件命名如下圖:
老套路,先拋出完整的工程代碼,再對關鍵部分代碼進行解析,完整的工程代碼如下圖所示:

1 using System; 2 using System.Collections.Generic; 3 using System.ComponentModel; 4 using System.Data; 5 using System.Drawing; 6 using System.Linq; 7 using System.Text; 8 using System.Threading; 9 using System.Threading.Tasks; 10 using System.Windows.Forms; 11 using OxyPlot; 12 using OxyPlot.Annotations; 13 using OxyPlot.Axes; 14 using OxyPlot.Series; 15 16 namespace WindowsFormsApp2 17 { 18 public partial class Form1 : Form 19 { 20 private DateTimeAxis _dateAxis;//創建一個X軸類 21 private LinearAxis _valueAxis;//創建一個Y軸類 22 23 private PlotModel _myPlotModel; 24 private Random rand = new Random();//用來生成隨機數 25 26 public int speed = 0; 27 public bool flag = false; //繪圖線程開啟標志位,true表示開始,false表示停止 28 29 public Form1() 30 { 31 InitializeComponent(); 32 } 33 #region 數據模擬 34 //隨機數模擬通道數據 35 public double getSendCount() 36 { 37 double iTotal = 0; 38 //iTotal = rand.Next(100, 300) / 10.0; 39 //iTotal = 50.0; 40 iTotal = speed; 41 42 return iTotal; 43 } 44 45 public double getChannel1() 46 { 47 double iTotal = 0; 48 iTotal = rand.Next(100, 900) / 10.0; 49 50 return iTotal; 51 } 52 53 public double getChannel2() 54 { 55 double iTotal = 0; 56 iTotal = rand.Next(300, 800) / 10.0; 57 58 return iTotal; 59 } 60 61 public double getChannel3() 62 { 63 double iTotal; 64 iTotal = rand.Next(300, 700) / 10.0; 65 66 return iTotal; 67 } 68 69 public double getChannel4() 70 { 71 double iTotal; 72 iTotal = rand.Next(200, 600) / 10.0; 73 74 return iTotal; 75 } 76 #endregion 77 78 private void Form1_Load(object sender, EventArgs e) 79 { 80 // 創建畫布坐標軸 81 coordinate(); 82 } 83 84 #region 繪制坐標軸 85 private void coordinate() 86 { 87 //定義model 88 _myPlotModel = new PlotModel() 89 { 90 Title = "速度檢測", 91 //LegendTitle = "通道值", 92 LegendOrientation = LegendOrientation.Horizontal, 93 LegendPlacement = LegendPlacement.Outside, 94 LegendPosition = LegendPosition.TopCenter, 95 LegendBackground = OxyColor.FromAColor(0, OxyColors.Beige), 96 //設置文本框邊框顏色 97 LegendBorder = OxyColors.Transparent 98 }; 99 100 101 102 //X軸 103 _dateAxis = new DateTimeAxis() 104 { 105 Position = AxisPosition.Bottom, 106 Minimum = DateTimeAxis.ToDouble(DateTime.Now), 107 Maximum = DateTimeAxis.ToDouble(DateTime.Now.AddMinutes(1)), 108 MajorGridlineStyle = LineStyle.Solid, 109 MinorGridlineStyle = LineStyle.Dot, 110 IntervalLength = 80, 111 Title = "time",//顯示標題內容 112 TitlePosition = 0.95,//顯示標題位置 113 IsZoomEnabled = true, 114 IsPanEnabled = true 115 }; 116 _myPlotModel.Axes.Add(_dateAxis); 117 118 //Y軸 119 _valueAxis = new LinearAxis() 120 { 121 //Y軸刻度間距設置 122 MajorStep = 10, 123 Position = AxisPosition.Left, 124 MajorGridlineStyle = LineStyle.Solid, 125 MinorGridlineStyle = LineStyle.Dot, 126 Title = "Speed",//顯示標題內容 127 TitlePosition = 1,//顯示標題位置 128 IntervalLength = 80, 129 //設置坐標軸上的刻度標簽的方向 130 Angle = 0, 131 IsZoomEnabled = false, 132 IsPanEnabled = false, 133 Maximum = 100, 134 Minimum = 0 135 }; 136 _myPlotModel.Axes.Add(_valueAxis); 137 138 139 //添加標注線,速度上下限 140 var lineTempMaxAnnotation = new OxyPlot.Annotations.LineAnnotation() 141 { 142 Type = LineAnnotationType.Horizontal, 143 Color = OxyColors.Red, 144 LineStyle = LineStyle.Solid, 145 Y = 10, 146 Text = "Speed Min:10" 147 148 }; 149 _myPlotModel.Annotations.Add(lineTempMaxAnnotation); 150 151 var lineTempMinAnnotation = new LineAnnotation() 152 { 153 Type = LineAnnotationType.Horizontal, 154 Y = 90, 155 Text = "Speed Max:90", 156 Color = OxyColors.Red, 157 LineStyle = LineStyle.Solid 158 }; 159 _myPlotModel.Annotations.Add(lineTempMinAnnotation); 160 161 //添加一條標准曲線,四個通道速度 162 var series = new LineSeries() 163 { 164 Color = OxyColors.Green, 165 //曲線的寬度設置 166 StrokeThickness = 2, 167 //標記大小的設置 168 MarkerSize = 3, 169 //標記的顏色設置 170 MarkerStroke = OxyColors.DarkGreen, 171 //標記的類型設置 172 MarkerType = MarkerType.Diamond, 173 Title = "standard", 174 }; 175 _myPlotModel.Series.Add(series); 176 177 series = new LineSeries() 178 { 179 Color = OxyColors.Blue, 180 StrokeThickness = 2, 181 MarkerSize = 3, 182 MarkerStroke = OxyColors.BlueViolet, 183 MarkerType = MarkerType.Star, 184 Title = "channel1", 185 }; 186 _myPlotModel.Series.Add(series); 187 188 series = new LineSeries() 189 { 190 Color = OxyColors.Yellow, 191 StrokeThickness = 2, 192 MarkerSize = 3, 193 MarkerStroke = OxyColors.Yellow, 194 MarkerType = MarkerType.Star, 195 Title = "channel2", 196 }; 197 _myPlotModel.Series.Add(series); 198 199 series = new LineSeries() 200 { 201 Color = OxyColors.SaddleBrown, 202 StrokeThickness = 2, 203 MarkerSize = 3, 204 MarkerStroke = OxyColors.SaddleBrown, 205 MarkerType = MarkerType.Star, 206 Title = "channel3", 207 }; 208 _myPlotModel.Series.Add(series); 209 210 series = new LineSeries() 211 { 212 Color = OxyColors.HotPink, 213 StrokeThickness = 2, 214 MarkerSize = 3, 215 MarkerStroke = OxyColors.HotPink, 216 MarkerType = MarkerType.Star, 217 Title = "channel4", 218 }; 219 _myPlotModel.Series.Add(series); 220 221 plotView1.Model = _myPlotModel; 222 223 } 224 #endregion 225 226 #region 清除波形 227 private void clearDrawing() 228 { 229 //遍歷,清除所有之前繪制的曲線 230 foreach (var lineSer in plotView1.Model.Series) 231 { 232 ((LineSeries)lineSer).Points.Clear(); 233 } 234 //清除完曲線之后,重新刷新坐標軸 235 _dateAxis.Minimum = DateTimeAxis.ToDouble(DateTime.Now.AddSeconds(0)); 236 _dateAxis.Maximum = DateTimeAxis.ToDouble(DateTime.Now.AddSeconds(1)); 237 _myPlotModel.InvalidatePlot(true); 238 Thread.Sleep(10); 239 } 240 #endregion 241 242 #region 曲線繪制 243 //數據曲線繪制 244 private void drawing() 245 { 246 //開啟繪圖時刷新坐標軸時間 247 _dateAxis.Minimum = DateTimeAxis.ToDouble(DateTime.Now); 248 _dateAxis.Maximum = DateTimeAxis.ToDouble(DateTime.Now.AddMinutes(1)); 249 250 bool bToMove = false; 251 252 Task.Factory.StartNew(() => 253 { 254 while (flag) 255 { 256 257 var date = DateTime.Now; 258 _myPlotModel.Axes[0].Maximum = DateTimeAxis.ToDouble(date.AddSeconds(1)); 259 260 var lineSer = plotView1.Model.Series[0] as LineSeries; 261 lineSer.Points.Add(new DataPoint(DateTimeAxis.ToDouble(date), getSendCount())); 262 if (lineSer.Points.Count > 200) 263 { 264 lineSer.Points.RemoveAt(0); 265 } 266 plotView1.Model.Series[0].Title = string.Format("standard : {0:00.00}km/h", getSendCount()); 267 268 lineSer = plotView1.Model.Series[1] as LineSeries; 269 lineSer.Points.Add(new DataPoint(DateTimeAxis.ToDouble(date), getChannel1())); 270 if (lineSer.Points.Count > 200) 271 { 272 lineSer.Points.RemoveAt(0); 273 } 274 //Current channel speed refreshes 275 plotView1.Model.Series[1].Title = string.Format("channel1 : {0:00.00}km/h", getChannel1()); 276 277 lineSer = plotView1.Model.Series[2] as LineSeries; 278 lineSer.Points.Add(new DataPoint(DateTimeAxis.ToDouble(date), getChannel2())); 279 if (lineSer.Points.Count > 200) 280 { 281 lineSer.Points.RemoveAt(0); 282 } 283 plotView1.Model.Series[2].Title = string.Format("channel2 : {0:00.00}km/h", getChannel2()); 284 285 lineSer = plotView1.Model.Series[3] as LineSeries; 286 lineSer.Points.Add(new DataPoint(DateTimeAxis.ToDouble(date), getChannel3())); 287 if (lineSer.Points.Count > 200) 288 { 289 lineSer.Points.RemoveAt(0); 290 } 291 plotView1.Model.Series[3].Title = string.Format("channel3 : {0:00.00}km/h", getChannel3()); 292 293 lineSer = plotView1.Model.Series[4] as LineSeries; 294 lineSer.Points.Add(new DataPoint(DateTimeAxis.ToDouble(date), getChannel4())); 295 if (lineSer.Points.Count > 200) 296 { 297 lineSer.Points.RemoveAt(0); 298 } 299 plotView1.Model.Series[4].Title = string.Format("channel4 : {0:00.00}km/h", getChannel4()); 300 301 302 303 if (!bToMove) 304 { 305 TimeSpan timeSpan = DateTime.Now - DateTimeAxis.ToDateTime(_dateAxis.Minimum); 306 if (timeSpan.TotalSeconds >= 58) 307 { 308 bToMove = true; 309 } 310 } 311 else 312 { 313 _dateAxis.Minimum = DateTimeAxis.ToDouble(DateTime.Now.AddSeconds(-58)); 314 _dateAxis.Maximum = DateTimeAxis.ToDouble(DateTime.Now.AddSeconds(2)); 315 } 316 317 318 _myPlotModel.InvalidatePlot(true); 319 Thread.Sleep(1000); 320 } 321 }); 322 } 323 #endregion 324 325 private void plotView1_Click(object sender, EventArgs e) 326 { 327 328 } 329 330 //滑軌控件拖拉事件 331 private void trackBar1_Scroll(object sender, EventArgs e) 332 { 333 speed = trackBar1.Value; 334 } 335 336 //start/stop按鍵單擊事件 337 private void Start_Click(object sender, EventArgs e) 338 { 339 if (flag == false) 340 { 341 Start.Text = "stop"; 342 flag = true; 343 //開啟繪圖線程 344 drawing(); 345 } 346 else 347 { 348 Start.Text = "start"; 349 flag = false; 350 } 351 } 352 353 //波形清除按鍵單擊事件 354 private void clearButton_Click(object sender, EventArgs e) 355 { 356 Start.Enabled = false; 357 clearDrawing(); 358 Thread.Sleep(10); 359 flag = false; 360 Thread.Sleep(1100); 361 flag = true; 362 drawing(); 363 Start.Enabled = true; 364 } 365 366 } 367 }
工程代碼的基本設計框架如下圖所示:
使用過GDI+繪圖的朋友都知道,在進行圖像繪制之前都需要先創建一塊畫布,對一些基本的界面元素需要先完成布局。使用OxyOplot控件也一樣,繪制曲線需要在坐標軸里面進行繪制,那么我們就需要先將該部分坐標軸中的坐標軸標題、X/Y軸、刻度值、四個曲線通道等等進行繪制,該部分的設計代碼如下:
1 #region 繪制坐標軸 2 private void coordinate() 3 { 4 //定義model 5 _myPlotModel = new PlotModel() 6 { 7 Title = "速度檢測", 8 //LegendTitle = "通道值", 9 LegendOrientation = LegendOrientation.Horizontal, 10 LegendPlacement = LegendPlacement.Outside, 11 LegendPosition = LegendPosition.TopCenter, 12 LegendBackground = OxyColor.FromAColor(0, OxyColors.Beige), 13 //設置文本框邊框顏色 14 LegendBorder = OxyColors.Transparent 15 }; 16 17 18 19 //X軸 20 _dateAxis = new DateTimeAxis() 21 { 22 Position = AxisPosition.Bottom, 23 Minimum = DateTimeAxis.ToDouble(DateTime.Now), 24 Maximum = DateTimeAxis.ToDouble(DateTime.Now.AddMinutes(1)), 25 MajorGridlineStyle = LineStyle.Solid, 26 MinorGridlineStyle = LineStyle.Dot, 27 IntervalLength = 80, 28 Title = "time",//顯示標題內容 29 TitlePosition = 0.95,//顯示標題位置 30 IsZoomEnabled = true, 31 IsPanEnabled = true 32 }; 33 _myPlotModel.Axes.Add(_dateAxis); 34 35 //Y軸 36 _valueAxis = new LinearAxis() 37 { 38 //Y軸刻度間距設置 39 MajorStep = 10, 40 Position = AxisPosition.Left, 41 MajorGridlineStyle = LineStyle.Solid, 42 MinorGridlineStyle = LineStyle.Dot, 43 Title = "Speed",//顯示標題內容 44 TitlePosition = 1,//顯示標題位置 45 IntervalLength = 80, 46 //設置坐標軸上的刻度標簽的方向 47 Angle = 0, 48 IsZoomEnabled = false, 49 IsPanEnabled = false, 50 Maximum = 100, 51 Minimum = 0 52 }; 53 _myPlotModel.Axes.Add(_valueAxis); 54 55 56 //添加標注線,速度上下限 57 var lineTempMaxAnnotation = new OxyPlot.Annotations.LineAnnotation() 58 { 59 Type = LineAnnotationType.Horizontal, 60 Color = OxyColors.Red, 61 LineStyle = LineStyle.Solid, 62 Y = 10, 63 Text = "Speed Min:10" 64 65 }; 66 _myPlotModel.Annotations.Add(lineTempMaxAnnotation); 67 68 var lineTempMinAnnotation = new LineAnnotation() 69 { 70 Type = LineAnnotationType.Horizontal, 71 Y = 90, 72 Text = "Speed Max:90", 73 Color = OxyColors.Red, 74 LineStyle = LineStyle.Solid 75 }; 76 _myPlotModel.Annotations.Add(lineTempMinAnnotation); 77 78 //添加一條標准曲線,四個通道速度 79 var series = new LineSeries() 80 { 81 Color = OxyColors.Green, 82 //曲線的寬度設置 83 StrokeThickness = 2, 84 //標記大小的設置 85 MarkerSize = 3, 86 //標記的顏色設置 87 MarkerStroke = OxyColors.DarkGreen, 88 //標記的類型設置 89 MarkerType = MarkerType.Diamond, 90 Title = "standard", 91 }; 92 _myPlotModel.Series.Add(series); 93 94 series = new LineSeries() 95 { 96 Color = OxyColors.Blue, 97 StrokeThickness = 2, 98 MarkerSize = 3, 99 MarkerStroke = OxyColors.BlueViolet, 100 MarkerType = MarkerType.Star, 101 Title = "channel1", 102 }; 103 _myPlotModel.Series.Add(series); 104 105 series = new LineSeries() 106 { 107 Color = OxyColors.Yellow, 108 StrokeThickness = 2, 109 MarkerSize = 3, 110 MarkerStroke = OxyColors.Yellow, 111 MarkerType = MarkerType.Star, 112 Title = "channel2", 113 }; 114 _myPlotModel.Series.Add(series); 115 116 series = new LineSeries() 117 { 118 Color = OxyColors.SaddleBrown, 119 StrokeThickness = 2, 120 MarkerSize = 3, 121 MarkerStroke = OxyColors.SaddleBrown, 122 MarkerType = MarkerType.Star, 123 Title = "channel3", 124 }; 125 _myPlotModel.Series.Add(series); 126 127 series = new LineSeries() 128 { 129 Color = OxyColors.HotPink, 130 StrokeThickness = 2, 131 MarkerSize = 3, 132 MarkerStroke = OxyColors.HotPink, 133 MarkerType = MarkerType.Star, 134 Title = "channel4", 135 }; 136 _myPlotModel.Series.Add(series); 137 138 plotView1.Model = _myPlotModel; 139 140 } 141 #endregion
繪制曲線需要外部提供數據,在這里為了方便顯示效果,使用了Random()隨機數方法生成了三組數據,另外一組數據通過和滑桿控件相關聯,總共完成四組通道數據的模擬。
#region 數據模擬 //隨機數模擬通道數據 public double getSendCount() { double iTotal = 0; //iTotal = rand.Next(100, 300) / 10.0; //iTotal = 50.0; iTotal = speed; return iTotal; } public double getChannel1() { double iTotal = 0; iTotal = rand.Next(100, 900) / 10.0; return iTotal; } public double getChannel2() { double iTotal = 0; iTotal = rand.Next(300, 800) / 10.0; return iTotal; } public double getChannel3() { double iTotal; iTotal = rand.Next(300, 700) / 10.0; return iTotal; } public double getChannel4() { double iTotal; iTotal = rand.Next(200, 600) / 10.0; return iTotal; } #endregion //滑軌控件拖拉事件 private void trackBar1_Scroll(object sender, EventArgs e) { speed = trackBar1.Value; }
繪制曲線的時候,為了不引起和主線程的沖突,因此單獨創建了一個獨立的子線程完成繪圖的操作,當界面中積累的曲線點數達到60個點時,坐標軸就會往左邊平移,曲線繪制函數設計如下:
1 #region 曲線繪制 2 //數據曲線繪制 3 private void drawing() 4 { 5 //開啟繪圖時刷新坐標軸時間 6 _dateAxis.Minimum = DateTimeAxis.ToDouble(DateTime.Now); 7 _dateAxis.Maximum = DateTimeAxis.ToDouble(DateTime.Now.AddMinutes(1)); 8 9 bool bToMove = false; 10 11 Task.Factory.StartNew(() => 12 { 13 while (flag) 14 { 15 16 var date = DateTime.Now; 17 _myPlotModel.Axes[0].Maximum = DateTimeAxis.ToDouble(date.AddSeconds(1)); 18 19 var lineSer = plotView1.Model.Series[0] as LineSeries; 20 lineSer.Points.Add(new DataPoint(DateTimeAxis.ToDouble(date), getSendCount())); 21 if (lineSer.Points.Count > 200) 22 { 23 lineSer.Points.RemoveAt(0); 24 } 25 plotView1.Model.Series[0].Title = string.Format("standard : {0:00.00}km/h", getSendCount()); 26 27 lineSer = plotView1.Model.Series[1] as LineSeries; 28 lineSer.Points.Add(new DataPoint(DateTimeAxis.ToDouble(date), getChannel1())); 29 if (lineSer.Points.Count > 200) 30 { 31 lineSer.Points.RemoveAt(0); 32 } 33 //Current channel speed refreshes 34 plotView1.Model.Series[1].Title = string.Format("channel1 : {0:00.00}km/h", getChannel1()); 35 36 lineSer = plotView1.Model.Series[2] as LineSeries; 37 lineSer.Points.Add(new DataPoint(DateTimeAxis.ToDouble(date), getChannel2())); 38 if (lineSer.Points.Count > 200) 39 { 40 lineSer.Points.RemoveAt(0); 41 } 42 plotView1.Model.Series[2].Title = string.Format("channel2 : {0:00.00}km/h", getChannel2()); 43 44 lineSer = plotView1.Model.Series[3] as LineSeries; 45 lineSer.Points.Add(new DataPoint(DateTimeAxis.ToDouble(date), getChannel3())); 46 if (lineSer.Points.Count > 200) 47 { 48 lineSer.Points.RemoveAt(0); 49 } 50 plotView1.Model.Series[3].Title = string.Format("channel3 : {0:00.00}km/h", getChannel3()); 51 52 lineSer = plotView1.Model.Series[4] as LineSeries; 53 lineSer.Points.Add(new DataPoint(DateTimeAxis.ToDouble(date), getChannel4())); 54 if (lineSer.Points.Count > 200) 55 { 56 lineSer.Points.RemoveAt(0); 57 } 58 plotView1.Model.Series[4].Title = string.Format("channel4 : {0:00.00}km/h", getChannel4()); 59 60 61 62 if (!bToMove) 63 { 64 TimeSpan timeSpan = DateTime.Now - DateTimeAxis.ToDateTime(_dateAxis.Minimum); 65 if (timeSpan.TotalSeconds >= 58) 66 { 67 bToMove = true; 68 } 69 } 70 else 71 { 72 _dateAxis.Minimum = DateTimeAxis.ToDouble(DateTime.Now.AddSeconds(-58)); 73 _dateAxis.Maximum = DateTimeAxis.ToDouble(DateTime.Now.AddSeconds(2)); 74 } 75 76 77 _myPlotModel.InvalidatePlot(true); 78 Thread.Sleep(1000); 79 } 80 }); 81 } 82 #endregion
開始繪制曲線和和停止整合成一個按鍵,實現原理如下:
1 //start/stop按鍵單擊事件 2 private void Start_Click(object sender, EventArgs e) 3 { 4 if (flag == false) 5 { 6 Start.Text = "stop"; 7 flag = true; 8 //開啟繪圖線程 9 drawing(); 10 } 11 else 12 { 13 Start.Text = "start"; 14 flag = false; 15 } 16 }
在實際的監控界面中,為了滿足清除界面的曲線的需求,增加了清除顯示曲線的工程,防止出現線程沖突和線程多開,需要先失能開始的按鍵,因為繪制曲線是沒1000ms繪制一次,所以線程在完成一次關閉和開啟的時候需要添加一個大於1000ms的延時,界面清除完畢之后再使能開始按鈕,設計原理如下:
1 //波形清除按鍵單擊事件 2 private void clearButton_Click(object sender, EventArgs e) 3 { 4 Start.Enabled = false; 5 clearDrawing(); 6 Thread.Sleep(10); 7 flag = false; 8 Thread.Sleep(1100); 9 flag = true; 10 drawing(); 11 Start.Enabled = true; 12 } 13 14 #region 清除波形 15 private void clearDrawing() 16 { 17 //遍歷,清除所有之前繪制的曲線 18 foreach (var lineSer in plotView1.Model.Series) 19 { 20 ((LineSeries)lineSer).Points.Clear(); 21 } 22 //清除完曲線之后,重新刷新坐標軸 23 _dateAxis.Minimum = DateTimeAxis.ToDouble(DateTime.Now.AddSeconds(0)); 24 _dateAxis.Maximum = DateTimeAxis.ToDouble(DateTime.Now.AddSeconds(1)); 25 _myPlotModel.InvalidatePlot(true); 26 Thread.Sleep(10); 27 } 28 #endregion
最終效果圖如下:
OxyPlot組件自帶一些工程,在圖像顯示區域可以滾動鼠標的滾輪對曲線進行縮小放大,通過雙擊鼠標滾輪可以恢復正常曲線顯示。
如有各位大佬們有更好的完善建議和想法,歡迎在留言區一起討論,一起學習一起進步!
需要完整工程朋友,可以通過以下鏈接下載:
鏈接:https://pan.baidu.com/s/1Vu1hFEO03_gx5yeCDbgDGA
提取碼:r8na