0、小叙闲言

除了仪表盘控件比较常用外,还有图表也经常使用,同样网上也有非常强大的图表控件,有收费的(DEVexpress),也有免费的。但我们平时在使用时,只想简单地绘一个图,控件库里面的许多功能我们都用不到,没必要使用那么功能丰富的控件,以提高程序运行的效率和减小程序的占用空间。同时,我们自己如果能够绘制图表出来,对于程序的移植,也非常方便。对于大部分平台,相信设计方法是不会变的。废话少讲,直接上图看效果,如果觉得还不错,可以支持一下,继续往下看。源码下载地址:https://github.com/Endless-Coding/MyGauge/blob/master/CustomControl.zip

1、图表整体设计

简单来看一个图表的组成,一般由4个部分组成,坐标轴,刻度和刻度值,绘图区域(添加数据点和绘制曲线)。后面如果要做到数据动态显示,还要花一点点功夫,不过也是很容易的,耐心研究,不会比高等数学难。

2、坐标轴绘制

先作出两根垂直的直线出来,为x轴和y轴,XAML代码如下。2,3行代码即为两个数轴。4~23行,是作出两个小三角形,以形成箭头的形状。

 <Canvas Margin="5">
<Line x:Name="x_axis" Stroke="Black" StrokeThickness="3" X1="40" Y1="280" X2="480" Y2="280" StrokeStartLineCap="Round"/>
<Line x:Name="y_axis" Stroke="Black" StrokeThickness="3" X1="40" Y1="280" X2="40" Y2="30" StrokeStartLineCap="Round"/>
<Path x:Name="x_axisArrow" Fill="Black">
<Path.Data>
<PathGeometry>
<PathFigure StartPoint="480,276" IsClosed="True">
<LineSegment Point="480,284"/>
<LineSegment Point="490,280"/>
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
<Path x:Name="y_axisArrow" Fill="Black">
<Path.Data>
<PathGeometry>
<PathFigure StartPoint="36,30" IsClosed="True">
<LineSegment Point="44,30"/>
<LineSegment Point="40,20"/>
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
</Canvas>

C#作出两个小箭头的后台代码如下:

 /// <summary>
/// 作出箭头
/// </summary>
private void DrawArrow()
{
Path x_axisArrow = new Path();//x轴箭头
Path y_axisArrow = new Path();//y轴箭头 x_axisArrow.Fill = new SolidColorBrush(Color.FromRgb(0xff, , ));
y_axisArrow.Fill = new SolidColorBrush(Color.FromRgb(0xff, , )); PathFigure x_axisFigure = new PathFigure();
x_axisFigure.IsClosed = true;
x_axisFigure.StartPoint = new Point(, ); //路径的起点
x_axisFigure.Segments.Add(new LineSegment(new Point(, ), false)); //第2个点
x_axisFigure.Segments.Add(new LineSegment(new Point(, ), false)); //第3个点 PathFigure y_axisFigure = new PathFigure();
y_axisFigure.IsClosed = true;
y_axisFigure.StartPoint = new Point(, ); //路径的起点
y_axisFigure.Segments.Add(new LineSegment(new Point(, ), false)); //第2个点
y_axisFigure.Segments.Add(new LineSegment(new Point(, ), false)); //第3个点 PathGeometry x_axisGeometry = new PathGeometry();
PathGeometry y_axisGeometry = new PathGeometry(); x_axisGeometry.Figures.Add(x_axisFigure);
y_axisGeometry.Figures.Add(y_axisFigure); x_axisArrow.Data = x_axisGeometry;
y_axisArrow.Data = y_axisGeometry; this.chartCanvas.Children.Add(x_axisArrow);
this.chartCanvas.Children.Add(y_axisArrow);
}

两个小箭头

WPF中没有画带箭头直线的函数,这个必需要自己写了,最好的方法当然还是在XAML中,用Path来绘制出一个三角形在线的末端。当然这种用绝对坐标绘出来的小三角形,它不方便绘制到别的画布中,当前单纯为了做出效果,后面可以用C#动态生成箭头,在后台完成绘制。上述代码的效果如下所示。

然后给坐标轴添加上x,y标签, 使用TextBlock表示出,对于标签的样式,是可以定义一个样式资源的,来统一风格,不必要每一个标签进行设置。但是目前,主要是为了实现功能,暂且不做得过于复杂。

             <TextBlock x:Name="x_label" Text="x" Canvas.Left="477" Canvas.Top="279"
FontFamily="Arial" FontStyle="Italic" FontSize="20"/>
<TextBlock x:Name="y_label" Text="y" Canvas.Left="20" Canvas.Top="20"
FontFamily="Arial" FontStyle="Italic" FontSize="20"/>
<TextBlock x:Name="o_label" Text="o" Canvas.Left="20" Canvas.Top="272"
FontFamily="Arial" FontStyle="Italic" FontSize="20"/>

添加X,Y,O标签

3、坐标轴刻度和标签添加

刻度就是一系列的小直线,控制好每一条小直线的位置,就可以轻松作出标签。同样,先用XAML语言,静态画一个小线段,看一看效果,然后用C#语言,在后台动态作出所有的小线段。XAML代码如下

 <Line x:Name="x_scale1" Stroke="Black" StrokeThickness="1" X1="50" Y1="280" X2="50" Y2="276" StrokeEndLineCap="Triangle"/>
<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行代码上相减,这样就正确绘制了所有刻度了。

 /// <summary>
/// 作出x轴和y轴的刻度线
/// </summary>
private void DrawScale()
{
for (int i = ; i < ; i += )//作480个刻度,因为当前x轴长 480px,每10px作一个小刻度,还预留了一些小空间
{
//原点 O=(40,280)
Line x_scale = new Line();
x_scale.StrokeEndLineCap = PenLineCap.Triangle;
x_scale.StrokeThickness = ;
x_scale.Stroke = new SolidColorBrush(Color.FromRgb(, , )); x_scale.X1 = + i * ; //原点x=40,每10px作1个刻度
x_scale.X2 = x_scale.X1; //在x轴上的刻度线,起点和终点相同 x_scale.Y1 = ; //与原点坐标的y=280,相同
x_scale.Y2 = x_scale.Y1 - ;//刻度线长度为4px if (i < )//由于y轴短一些,所以在此作出判断,只作25个刻度
{
//作出Y轴的刻度
Line y_scale = new Line();
y_scale.StrokeEndLineCap = PenLineCap.Triangle;
y_scale.StrokeThickness = ;
y_scale.Stroke = new SolidColorBrush(Color.FromRgb(, , )); y_scale.X1 = ; //原点x=40,在y轴上的刻度线的起点与原点相同
y_scale.X2 = y_scale.X1 + ;//刻度线长度为4px y_scale.Y1 = - i * ; //每10px作一个刻度
y_scale.Y2 = y_scale.Y1; //起点和终点y坐标相同
this.chartCanvas.Children.Add(y_scale);
}
this.chartCanvas.Children.Add(x_scale);
}
}

上述代码执行后效果如下(左图):

为了表达更加清楚,平时所用的图表都有一个大刻度,在此,我也添加一个,其实也非常容易,无非就是在for循环里面添加一个判断,到了所需要的位置的时候,将刻度线加粗,加长一些,也就是改变它的样式。添加大刻度线的C#代码如下,代码只是对上面的代码作了点小修改,其效果如上图(右图)。

 /// <summary>
/// 作出x轴和y轴的标尺
/// </summary>
private void DrawScale()
{
for (int i = ; i < ; i += )//作480个刻度,因为当前x轴长 480px,每10px作一个小刻度,还预留了一些小空间
{
//原点 O=(40,280)
Line x_scale = new Line();
x_scale.StrokeEndLineCap = PenLineCap.Triangle;
x_scale.StrokeThickness = ;
x_scale.Stroke = new SolidColorBrush(Color.FromRgb(, , )); x_scale.X1 = + i * ; //原点x=40,每10px作1个刻度
x_scale.X2 = x_scale.X1; //在x轴上的刻度线,起点和终点相同 x_scale.Y1 = ; //与原点坐标的y=280,相同
if (i % == )//每5个刻度添加一个大刻度
{
x_scale.StrokeThickness = ;//把刻度线加粗一点
x_scale.Y2 = x_scale.Y1 - ;//刻度线长度为8px
}
else
{
x_scale.Y2 = x_scale.Y1 - ;//刻度线长度为4px
} if (i < )//由于y轴短一些,所以在此作出判断,只作25个刻度
{
//作出Y轴的刻度
Line y_scale = new Line();
y_scale.StrokeEndLineCap = PenLineCap.Triangle;
y_scale.StrokeThickness = ;
y_scale.Stroke = new SolidColorBrush(Color.FromRgb(, , )); y_scale.X1 = ; //原点x=40,在y轴上的刻度线的起点与原点相同
if (i % == )
{
y_scale.StrokeThickness = ;
y_scale.X2 = y_scale.X1 + ;//刻度线长度为4px
}
else
{
y_scale.X2 = y_scale.X1 + ;//刻度线长度为8px
} y_scale.Y1 = - i * ; //每10px作一个刻度
y_scale.Y2 = y_scale.Y1; //起点和终点y坐标相同
this.chartCanvas.Children.Add(y_scale);
}
this.chartCanvas.Children.Add(x_scale);
}
}

添加大刻度

②坐标轴标签添加

标签的显示还是使用文本块(TextBlock控件)来实现。为了让标签的显示位置刚刚好,对坐标值做了一些偏移,下面程序中已经解释得比较清楚了。

 /// <summary>
/// 添加刻度标签
/// </summary>
private void DrawScaleLabel()
{
for (int i = ; i < ; i++)//7 个标签,一共
{
TextBlock x_ScaleLabel = new TextBlock();
TextBlock y_ScaleLabel = new TextBlock(); x_ScaleLabel.Text = (i * ).ToString();//只给大刻度添加标签,每50px添加一个标签 Canvas.SetLeft(x_ScaleLabel, + * * i - );//40是原点的坐标,-12是为了让标签看的位置剧中一点
Canvas.SetTop(x_ScaleLabel, + );//让标签字往下移一点 y_ScaleLabel.Text = (i * ).ToString();
Canvas.SetLeft(y_ScaleLabel, - ); //-25px是字体大小的偏移
Canvas.SetTop(y_ScaleLabel, - * * i - ); //280px是原点的坐标,同样-6是为了让标签不要上坐标轴叠上 this.chartCanvas.Children.Add(x_ScaleLabel);
this.chartCanvas.Children.Add(y_ScaleLabel);
}
}

添加刻度标签

运行此代码后,效果如下:

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代码如下

 <Ellipse Fill="Blue" Height="8" Width="8" Canvas.Left="100" Canvas.Top="200"/>
<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#代码如下

 private void DrawPoint()
{
//随机生成8个点
Random rPoint = new Random();
for (int i = ; i < ; i++)
{
int x_point = i * ;
int y_point = rPoint.Next();
dataPoints.Add(new Point(x_point, y_point));
} for (int i = ; i < dataPoints.Count; i++)
{
Ellipse dataEllipse = new Ellipse();
dataEllipse.Fill = new SolidColorBrush(Color.FromRgb(, , 0xff));
dataEllipse.Width = ;
dataEllipse.Height = ; Canvas.SetLeft(dataEllipse, + dataPoints[i].X - );//-4是为了补偿圆点的大小,到精确的位置
Canvas.SetTop(dataEllipse, - dataPoints[i].Y - ); chartCanvas.Children.Add(dataEllipse);
}
}

数据点集合声明如下

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

上述代码的运行效果如下(左图):

②曲线绘制

将所有点用折线描绘出来,C#代码如下,效果如上图(右图)。由于前后两次的代码是不同时间运行的,生成的点是随机的,不一样,所以两副图点不相同。

 private void DrawCurve()
{
Polyline curvePolyline = new Polyline(); curvePolyline.Stroke = Brushes.Green;
curvePolyline.StrokeThickness = ; curvePolyline.Points = coordinatePoints;
chartCanvas.Children.Add(curvePolyline);
}

对coordinatePoints的定义和初始化如下。

//这行代码在程序的开始部分
private PointCollection coordinatePoints = new PointCollection();
----------------------------------------------------------------------------
//将数据点在画布中的位置保存下来
coordinatePoints.Add(new Point( + dataPoints[i].X, - dataPoints[i].Y));

5、让图表动起来

首先,思考要让线动起来,表中两类元素需要移,一个线,另一个是小圆点。而线是随数据点动的,这一部分WPF已经帮我们做好了;而点动,我们要不停地改变它在canvas中的位置,这样就可以看到动的效果,每次单击一下鼠标,就添加一个点,这个点的Y轴坐标还是随机生成的,C#代码如下。这个函数主要就做了2件事,一个是移除集合中的第0个点、在最后位置添加一个点,二是将所有数据都向左移50px,很容易。

 private void AddCurvePoint(Point dataPoint)
{
dataPoints.RemoveAt();
dataPoints.Add(dataPoint);
for (int i = ; i < dataPoints.Count; i++)
{
//每一个点的X数据都要向左移动50px
dataPoints[i] = new Point(dataPoints[i].X - , dataPoints[i].Y);
coordinatePoints[i] = new Point( + dataPoints[i].X, - dataPoints[i].Y); Canvas.SetLeft(pointEllipses[i], + dataPoints[i].X - );//-4是为了补偿圆点的大小,到精确的位置
Canvas.SetTop(pointEllipses[i], - dataPoints[i].Y - );
}
}

鼠标单击时的程序如下:

 private void chartCanvas_MouseDown(object sender, MouseButtonEventArgs e)
{
//随机生成Y坐标
Point dataPoint = new Point(, (new Random()).Next()); AddCurvePoint(dataPoint);
}

上面程序中用到的几个数据结构声明如下:

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

终于做出来了,效果还不错的,来欣赏一下自己的成果,花了大半天了。

心得体会

合抱之木,生于毫末;九层之台,起于垒土;千里之行,始于足下。一个图表,虽然复杂,但一步一步来,将每一步用心构思,然后慢慢实现,虽然做不到完美,但是起码可以实现基本目标。

图标还有改多可以改进的地方,功能还很弱小。可以给图表加上网格,横轴的坐标的标签可以实时变化,以形成示波器的功能。

下一个目标,给图表添加更多功能。

下下一目标,完成窗体的美化,做出类似360安全卫士11的界面。

WPF自定义控件(2)——图表设计[1]的更多相关文章

  1. WPF自定义控件(1)——仪表盘设计[1]

    0.小叙闲言 又接手一个新的项目了,再来一次上位机开发.网上有很多控件库,做仪表盘(gauge)的也不少,功能也很强大,但是个人觉得库很臃肿,自己就计划动手来写一个控件库,一是为学习,二是为了项目.下 ...

  2. WPF自定义控件与样式(15)-终结篇 & 系列文章索引 & 源码共享

    系列文章目录  WPF自定义控件与样式(1)-矢量字体图标(iconfont) WPF自定义控件与样式(2)-自定义按钮FButton WPF自定义控件与样式(3)-TextBox & Ric ...

  3. WPF自定义控件与样式(12)-缩略图ThumbnailImage /gif动画图/图片列表

    一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要针对WPF项目 ...

  4. WPF自定义控件与样式(15)-终结篇

    原文:WPF自定义控件与样式(15)-终结篇 系列文章目录  WPF自定义控件与样式(1)-矢量字体图标(iconfont) WPF自定义控件与样式(2)-自定义按钮FButton WPF自定义控件与 ...

  5. WPF自定义控件(四)の自定义控件

    在实际工作中,WPF提供的控件并不能完全满足不同的设计需求.这时,需要我们设计自定义控件. 这里LZ总结一些自己的思路,特性如下: Coupling UITemplate Behaviour Func ...

  6. WPF自定义控件(一)の控件分类

    一.什么是控件(Controls) 控件是指对数据和方法的封装.控件可以有自己的属性和方法,其中属性是控件数据的简单访问者,方法则是控件的一些简单而可见的功能.控件创建过程包括设计.开发.调试(就是所 ...

  7. 【转】WPF自定义控件与样式(12)-缩略图ThumbnailImage /gif动画图/图片列表

    一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要针对WPF项目 ...

  8. WPF 自定义的图表(适用大量数据绘制)下

    原文:WPF 自定义的图表(适用大量数据绘制)下 上一篇文章中讲了WPF中自定义绘制大量数据的图标,思路是先将其绘制在内存,然后一次性加载到界面,在后续的调试过程中,发现当数据量到达10W时,移动鼠标 ...

  9. WPF 自定义的图表(适用大量数据绘制)

    原文:WPF 自定义的图表(适用大量数据绘制) 在WPF中绘制图表比较简单,有很多的第三方控件,但是在绘制大量数据的时候,就显得有些吃力,即便是自己用StreamGeometry画也达不到理想的效果, ...

随机推荐

  1. Linux实战教学笔记17:精简shell基础

    第十七节 精简shell基础 标签(空格分隔): Linux实战教学笔记 1,前言 1.1 为什么学习shell编程 Shell脚本语言是实现Linux/UNIX系统管理及自动化运维所必备的重要工具, ...

  2. 每天一个linux命令(56)--crontab命令

    上一节学习了 at  命令是针对仅运行一次的任务,循环运行的例行性计划任务,Linux 系统则是由 cron(crond)这个系统服务来控制的.Linux 系统上面原本就有非常多的计划性工作,因此这个 ...

  3. 知识管理(KM) - 数据流

    快速链接: 人力资源知识体系索引 本章主要列出知识管理(KM)中涉及到的所有表. 步骤 操作 相关表 说明 1 知识管理资料   基础资料,见附表1 2 知识主题(107301) KMBlg:主题 K ...

  4. jQuery的基本操作

    jQuery就是一个js的库· 主要分为两部分:            1·寻找元素         (选择器,筛选器)            2·操作元素          (CSS的操作,属性的操 ...

  5. 【翻译】LPeg编程指南

    原文:http://www.inf.puc-rio.br/~roberto/lpeg/lpeg.html   译者序: 这个是官方的LPeg的文档.这段时间学习LPeg的时候发现国内关于LPeg的文章 ...

  6. 11g R2 RAC启动关闭步骤

    1.关闭监听 /u01/app/11.2.0/grid/bin/srvctl stop listener -n redhat-rac01 /u01/app/11.2.0/grid/bin/srvctl ...

  7. CoreGraphics--画线/圆/矩形

    - (void)drawRect:(CGRect)rect { // Drawing code NSLog(@"drawRect自动调用"); //画图步骤 //获取上下文(/画笔 ...

  8. 字符编码的种类:ASCII、GB2312、GBK、GB18030、Unicode、UTF-8、UTF-16、Base64

    ASCII码ASCII:https://zh.wikipedia.org/wiki/ASCIIASCII(American Standard Code for Information Intercha ...

  9. 详解JDBC连接数据库

    一.概念 1. 为了能让程序操作数据库,对数据库中的表进行操作,每一种数据库都会提供一套连接和操作该数据库的驱动,而且每种数据库的驱动都各不相同,例如mysql数据库使用mysql驱动,oracle数 ...

  10. android开发之多线程实现方法概述

    一.单线程模型 当一个程序第一次启动时,Android会同时启动一个对应的主线程(Main Thread),主线程主要负责处理与UI相关的事件,如:用户的按键事件,用户接触屏幕的事件以及屏幕绘图事件, ...