图表是对数据进行分析评估的常用工具,也是最直观的表示数据的方法。用户界面中使用适当的图表来表现数据.可提高软件的性能和数据的可读性,数据库查询和数据分析中采用图表代替数据列表,能带给用户快速直观的判断,例如股票K线均线和成交变化的走势图。许多专用数据分析软件如Excel、Origin等都具有图形报表功能,但在软件开发项目中需要动态图形表示时,集成使用这些专业分析软件或绘图软件,经常遇到一定的条件限制。从头编写GDI虽然理论上完全可以实现。但无疑是低效率的方法。从开源代码中,应用Zedgraph是在.NET平台开发图表界面的不错选择。
ZedGraph是基于.NET平台的一套实时曲线绘制控件,即支持WinForm程序设计,也支持WebForm程序设计。类库编写是面向对象的,在编程使用时有着很强的灵活性,开发者可以修改并覆盖几乎所有图表特性的默认值,包括数值范围、数值单位、步长等。由于是开源代码,必要时还可以对源代码进行修改,重新生成。类库允许在一张图表中显示多个数值范围、多种单位以及多个坐标轴。图1是Zedgraph设计时应用的控件默认显示,其中包含堆叠的柱状图、透明的覆盖图以及填充图的图表,还包含了图例和注释。
这么好用的一套类库,几乎成为了基于.NET平台的桌面或Web绘图首选工具,但遗憾的是,这套类库暂未推出支持.NET CF的版本,而如今,随着Windows Embeded品牌的大力推广及Windows Mobile手机的广泛应用,在加上WinCE在嵌入式控制领域的地位,基于.NET CF平台的实时曲线绘制需求也越来越多,许多第三方软件公司推出了.NET CF版本的绘图控件,如QCRTGraph CF - Real-Time Graphics Tools for .Net Compact Framework,下图演示了该控件所绘制的采样数据。
但这样的一套控件,其价格也相当不菲,单个开发者版本的售价是325美元,约合人民币 2000多元。因此,在参考了ZedGraph的基础上,笔者实现了一个简单的,可以绘制单条曲线的基于.NET CF的绘图控件。
-
控件的类库结构
在曲线绘制中,最多的操作其实是绘制出坐标系,并将实际的数值映射到绘图区域的像素空间中。
本控件的名称为WaysGraphCtlCF,完全使用了.NET Compact Framework中的基础类库,未使用其他第三方库。控件基于.NET CF的UserControl,用户自定义控件设计的,这样生成的dll文件,可被方便地引入toolbox设计框,实现拖拽式开发。 由于.NET类库中提供的Point类只支持int数据,而在绘图时,输入控件的多数是双精度的double数据,因此专门设计了PointPair结构体。Aixs是一个抽象类,定义了坐标绘制时需要的基本功能,其子类XAxis与YAxis分别完成X轴与Y轴的绘制。 其中XAixs与YAxis在刻度生长方向与坐标计算等方面有很大的区别,这是因为windows的坐标系是有左上角为(0,0)点所造成的,在程序设计时,要充分考虑该问题。 |
-
用户自定义控件中属性的说明
private double _xMax;
[EditorBrowsable(EditorBrowsableState.Always)]
[DefaultValue(100)]
public double XAxisMax
{
get { return _xMax; }
set { _xMax = value; }
}
如上述代码所述,[EditorBrowsable(EditorBrowsableState.Always)]可使得XAxisMax属性在toolbox属性框中出现,如下图所示:
|
-
坐标轴的生成实现
-
Axis类的定义
namespace RealTimeGraphCtlCF
{
public
abstract
class Axis
{
protected
internal Pen blackPen;
protected
internal Rectangle rectRegion;
protected
internal Graphics gHdc;
protected
internal Font axisFont;
protected
internal Font ticFont;
private
int _axisLength;
public
int AxisLength
{
get { return _axisLength; }
set { _axisLength = value; }
}
private Point _origin;
public Point Origin
{
get { return _origin; }
set { _origin = value; }
}
private
double _max;
public double Max
{
get { return _max; }
set { _max = value; }
}
private
string _title;
public
string Title
{
get { return _title; }
set { _title = value; }
}
public Axis(Graphics g, Rectangle rect, Point orgin, int length)
{
this.gHdc = g;
this.rectRegion = rect;
this._origin = orgin;
this._axisLength = length;
this.blackPen = new Pen(Color.Black);
this.axisFont = new Font(FontFamily.GenericSerif, 9, FontStyle.Bold);
this.ticFont = new Font(FontFamily.GenericSerif, 6, FontStyle.Italic);
}
public Axis(Graphics g, Rectangle rect, Point orgin, int length, double max, string title)
: this(g, rect, orgin, length)
{
this._max = max;
this._title = title;
}
public
void Draw(int ticCount, int ticLength)
{
float step = (float)_axisLength / (float)ticCount;
int length = ticLength;
DrawBaseLine();
for (int i = 1; i <= ticCount; i++)
{
length = ticLength;
if (i % 5 == 0)
{
length = ticLength * 2;
}
if (i % 10 == 0)
{
double tic = (_max / ticCount) * i;
tic = Math.Round(tic, 3);
length = ticLength * 3;
DrawTicTitle((int)(step * i), tic.ToString());
}
DrawTic((int)(step * i), length);
}
DrawTitle();
}
public
abstract
void DrawTitle();
public
abstract
void DrawTic(int pos, int length);
public
abstract
void DrawTicTitle(int pos, string text);
public
abstract
void DrawBaseLine();
}
}
DrawTitle()方法用于画出坐标名称;
DrawTic()方法用于画出坐标刻度线;
DrawTicTitle()方法用于画出坐标刻度线上的数值;
DrawBaseLine()方法用于画出坐标线;
XAxis与YAxis类中,分别重写了这几个方法。
-
XAxis类
namespace RealTimeGraphCtlCF
{
class XAxis : Axis
{
public XAxis(Graphics g, Rectangle rect, Point orgin, int length, double max, string title) : base(g, rect, orgin, length, max, title)
{
}
public
override
void DrawTic(int pos, int length)
{
int x1 = Origin.X + pos;
int y1 = rectRegion.Height - (Origin.Y + length);
int x2 = x1;
int y2 = rectRegion.Height - Origin.Y;
gHdc.DrawLine(blackPen, x1, y1, x2, y2);
}
public
override
void DrawTitle()
{
gHdc.DrawString(Title, axisFont, new SolidBrush(Color.Black), rectRegion.Width - Origin.X - (int)(Title.Length * 2.5), rectRegion.Height - (int)(Origin.Y * 0.8));
}
public
override
void DrawTicTitle(int pos, string text)
{
gHdc.DrawString(text, ticFont, new SolidBrush(Color.Red), Origin.X + pos - text.Length * 3, rectRegion.Height - Origin.Y);
}
public
override
void DrawBaseLine()
{
int x1 = Origin.X;
int y1 = rectRegion.Height - Origin.Y;
int x2 = Origin.X + AxisLength;
int y2 = y1;
gHdc.DrawLine(blackPen, x1, y1, x2, y2);
}
}
}
-
XAxis类
namespace RealTimeGraphCtlCF
{
class YAxis : Axis
{
public YAxis(Graphics g, Rectangle rect, Point orgin, int length, double max, string title) : base(g, rect, orgin, length, max, title)
{
}
public override void DrawTic(int pos, int length)
{
int x1 = Origin.X;
int y1 = rectRegion.Height - (Origin.Y + pos);
int x2 = x1 - length;
int y2 = y1;
gHdc.DrawLine(blackPen, x1, y1, x2, y2);
}
public override void DrawTitle()
{
gHdc.DrawString(Title, axisFont, new SolidBrush(Color.Black), Origin.X/4, Origin.Y/4);
}
public override void DrawTicTitle(int pos, string text)
{
gHdc.DrawString(text, ticFont, new SolidBrush(Color.Red), Origin.X/8, rectRegion.Height - Origin.Y - pos);
}
public override void DrawBaseLine()
{
int x1 = Origin.X;
int y1 = rectRegion.Height - Origin.Y;
int x2 = x1;
int y2 = y1 - AxisLength;
gHdc.DrawLine(blackPen, x1, y1, x2, y2);
}
}
}
-
实时曲线的绘制
namespace RealTimeGraphCtlCF
{
public partial class GraphPane : UserControl
{
private Graphics gHdc;
private XAxis xAxis;
private YAxis yAxis;
private double _xMax;
[EditorBrowsable(EditorBrowsableState.Always)]
[DefaultValue(100)]
public double XAxisMax
{
get { return _xMax; }
set { _xMax = value; }
}
private double _yMax;
[EditorBrowsable(EditorBrowsableState.Always)]
[DefaultValue(100)]
public double YAxisMax
{
get { return _yMax; }
set { _yMax = value; }
}
private string _xTitle = "X";
[EditorBrowsable(EditorBrowsableState.Always)]
[DefaultValue("X")]
public string XAxisTitle
{
get { return _xTitle; }
set { _xTitle = value; }
}
private string _yTitle = "Y";
[EditorBrowsable(EditorBrowsableState.Always)]
[DefaultValue("Y")]
public string YAxisTitle
{
get { return _yTitle; }
set { _yTitle = value; }
}
private int _xTicCount = 100;
[EditorBrowsable(EditorBrowsableState.Always)]
[DefaultValue(100)]
public int XAxisTicCount
{
get { return _xTicCount; }
set { _xTicCount = value; }
}
private int _yTicCount = 100;
[EditorBrowsable(EditorBrowsableState.Always)]
[DefaultValue(100)]
public int YAxisTicCount
{
get { return _yTicCount; }
set { _yTicCount = value; }
}
private Pen _linePen;
[EditorBrowsable(EditorBrowsableState.Never)]
public Pen LinePen
{
get { return _linePen; }
set { _linePen = value; }
}
public List<PointPair> PointList;
private const int ticLength = 2;
Point origin = new Point(20, 20);
public GraphPane()
{
InitializeComponent();
this.PointList = new List<PointPair>();
this._linePen = new Pen(Color.Black, 1);
}
private void GraphPane_Paint(object sender, PaintEventArgs e)
{
Bitmap b = new Bitmap(this.Width, this.Height);
gHdc = Graphics.FromImage((System.Drawing.Image)b);
gHdc.FillRectangle(new SolidBrush(Color.White), this.ClientRectangle);
int yAxisLength = ClientRectangle.Height - 2 * origin.Y;
yAxis = new YAxis(gHdc, this.ClientRectangle, origin, yAxisLength, _yMax, _yTitle);
yAxis.Draw(_yTicCount, ticLength);
int xAxisLength = ClientRectangle.Width - 2 * origin.X;
xAxis = new XAxis(gHdc, this.ClientRectangle, origin, xAxisLength, _xMax, _xTitle);
xAxis.Draw(_xTicCount, ticLength);
DrawLines(gHdc);
e.Graphics.DrawImage((System.Drawing.Image)b, 0, 0);
b.Dispose();
}
public void DrawLines(Graphics gHdc)
{
Point previous = new Point();
Point current = new Point();
if (PointList.Count >= 2)
{
previous = AxisTransfrom(PointList[0]);
for (int i = 1; i < PointList.Count; i++)
{
current = AxisTransfrom(PointList[i]);
gHdc.DrawLine(LinePen, previous.X, previous.Y, current.X, current.Y);
previous = current;
}
}
}
protected override void OnPaintBackground(PaintEventArgs e)
{
}
private Point AxisTransfrom(PointPair trueValue)
{
double tempX = ((this.ClientRectangle.Width - 2 * this.origin.X) / this.XAxisMax) * trueValue.Current + origin.X;
double tempY = this.ClientRectangle.Height - (((this.ClientRectangle.Height - 2 * this.origin.Y) / this.YAxisMax) * trueValue.Voltage) - origin.Y;
return new Point((int)tempX, (int)tempY);
}
}
}
这里需要说明的是如何利用.NET GDI+中特有的双缓冲功能,只要重写OnPaintBackground方法即可。这是一个小小的编程技巧,很简单,但很实用。
protected override void OnPaintBackground(PaintEventArgs e)
{
}
另外就是坐标系变换的问题,必须将绘图点的以左下为(0,0)的坐标系,转换到以左上为(0,0)的像素坐标系中,另外,就是绘图点的double数据映射到像素空间时,必须取整。
private Point AxisTransfrom(PointPair trueValue)
{
double tempX = ((this.ClientRectangle.Width - 2 * this.origin.X) / this.XAxisMax) * trueValue.Current + origin.X;
double tempY = this.ClientRectangle.Height - (((this.ClientRectangle.Height - 2 * this.origin.Y) / this.YAxisMax) * trueValue.Voltage) - origin.Y;
return new Point((int)tempX, (int)tempY);
}
AxisTransfrom方法输入一个双精度的trueValue,得到的是,像素坐标系中的整数点。
-
控件测试
-
设计界面
功能说明:
Open——打开一个数据点文件
Draw——以timer规定的时间动态绘图
Pause——停止绘图
测试数据:
0.04193625,0.629025
0.0075975,0.08791875
0.00720875,0.08626875
0.00739625,0.0885375
0.01359625,0.2632125
0.01842125,0.4385625
0.0244125,0.710625
0.0266025,0.7911
0.030705,0.973125
0.03622,1.244175
0.0448775,1.67385
0.04982,1.970625
0.0553875,2.3233125
0.058725,2.5066875
0.06475,2.8715625
0.0689,3.183375
0.07437,3.49275
0.079475,3.833625
0.08668,4.32075
0.093885,4.90725
0.099445,5.3145
0.10462,5.6685
0.110785,6.137625
0.112405,6.3855
0.119375,6.725625
0.1241625,7.08525
0.1271125,7.3035
0.131175,7.645125
0.1350625,7.852125
0.137375,8.2305
0.1431125,8.4675
0.1482625,8.988
0.1493875,9.096
0.1504875,9.1455
0.1532875,9.39675
0.1560875,9.67575
0.159375,9.84675
0.166,10.446
0.1686125,10.63275
0.1736375,10.99725
0.1780375,11.29125
0.181575,11.7225
0.1868,11.85225
0.1917,12.2295
0.1955625,12.59925
0.2007875,12.93525
0.2037375,13.1685
0.2105125,13.50225
0.215475,13.89525
0.2185875,14.037
0.2219125,14.28075
0.226775,14.5095
0.2312375,14.90925
0.2356625,15.06075
0.2408875,15.453
0.2472625,15.7065
0.2518,16.002
0.25915,16.3125
0.2646875,16.36875
0.2671125,16.47825
0.2695625,16.51425
0.2786625,16.96575
0.33905,17.9775
0.3391,18.25875
0.342275,18.406875
0.3503,18.70125
0.36585,19.1325
0.381475,19.456875
0.38845,19.700625
0.39345,20.075625
0.40005,20.499375
0.4075,20.49375
0.405375,20.619375
0.412425,20.9625
0.422025,21.375
0.4348,21.42
0.457025,21.98625
0.465225,22.27125
0.472025,22.24125
0.50745,22.963125
0.53305,23.175
0.5405,23.29875
0.5636,23.626875
0.56685,23.98875
0.5763,23.731875
0.58565,24.135
0.64985,24.748125
0.6653,24.901875
0.68975,25.28625
0.7361,26.056875
0.7975,26.13
0.846125,26.54625
0.906625,26.88
0.944125,27.305625
1.0595,27.39375
1.12475,27.845625
1.195375,28.08
1.214875,28.39125
运行结果: