WPF 中简单易用的饼图控件






4.96/5 (13投票s)
本文旨在让你非常轻松地在WPF中创建饼图。
介绍
本文的目的是构建一对简单的饼图控件。我不想创建任何花哨的东西;只想制作一些易于使用且易于学习的东西。
以下是展示了所提供的饼图控件用法的示例应用程序的外观:
第一部分:使用控件
使用这些控件非常简单。提供了两个控件:PieControl
,它只显示饼图;PieChart
,它同时包含饼图和图例。
创建PieControl
(即不带图例的饼图)和PieChart
几乎是相同的。你可以通过执行以下步骤来创建其中任何一个:
xmlns:pie="clr-namespace:PieControls;assembly=PieControls"
<StackPanel>
<pie:PieControl x:Name="pie1" Width="120" Height="120"/>
<pie:PiChart x:Name="chart1" Width="260" Height="140" PieWidth="120" PieHeight="120"/>
</StackPanel>
请务必为PieControl
使用相同的Width
和Hight
值。对于PieChart
,PieWidth
和PieHeight
应相同。
using PieControls;
ObservableCollection<PieSegment> pieCollection = new ObservableCollection<PieSegment>();
pieCollection.Add(new PieSegment { Color = Colors.Green, Value = 5, Name = "Dogs" });
pieCollection.Add(new PieSegment { Color = Colors.Yellow, Value = 12, Name = "Cats" });
pieCollection.Add(new PieSegment { Color = Colors.Red, Value = 20, Name = "Mice" });
pieCollection.Add(new PieSegment { Color = Colors.DarkCyan, Value = 22, Name = "Lizards" });
pie1.Data = pieCollection;
chart1.Data = pieCollection;
- 将引用添加到下载中提供的PieControls 和Microsoft.Expression.Drawing 程序集中。
- 在XAML文件中包含
PieControls
命名空间。 - 使用类似以下的代码将
PieControl
和/或PieChart
添加到你的Window/Control中: - 在你的代码隐藏文件中包含'using'语句。
- 5. 在代码隐藏文件中填充饼图数据,并像这样将其分配给饼图控件:
- 你可以随时对
pieCollection
或其任何项进行任何更改。它们将自动反映在生成的饼图中!
1.1. 自定义选项
你可以使用以下方法来自定义控件:
PieSegment
类的属性。它们都设计为立即更改饼图视图。PieControl
和PieChart
的Width
和Height
属性。PieChart
的PieWidth
和PieHeight
属性(这些代表PieChart
中包含的PieControl
的宽度/高度)。PieControl
和PieChart
类的PopupBrush
属性。此属性用于绘制鼠标光标悬停在饼图上时显示的弹出窗口的背景。- 为了简单起见,我在示例中只使用了整数值。然而,控件在内部使用双精度浮点值;因此,你的选项不仅限于整数。
第二部分:理解源代码和背后的数学原理
附加源代码中的PieControl.cs文件包含我们饼图控件的定义。其他相关的类是PieChart
和PieSegment
。PieChart
利用PieControl
并提供额外的图例功能。PieSegment
类是用于将饼图值传达给PieChart
和PieControl
的数据载体。
一个基本的饼图只是一个圆,根据每个数据类别的百分比分成更小的扇形。每个占有0%以上份额的类别都得到一个圆扇形。0份额的什么也得不到,尽管我们会保留该类别,以防其未来获得份额。
想象一下,如果我们为家庭开支分为4个类别:(1)食物,(2)服装,(3)租金和(4)娱乐,需要创建饼图,我们会怎么做?
- 计算所有类别的总和。
- 通过将每个类别的份额除以总数,然后将结果乘以360(圆的总内角度是360度)来计算每个类别的份额(即角度)。
- 确定坐标并绘制每个扇形。
前两个步骤很简单。最后一步取决于我们如何使用WPF的原始图元来创建饼图。详细信息稍后会添加。首先,让我们将理论付诸实践,并通过一个例子来查看上述步骤是如何实际执行的。
示例
假设我在2012年7月在以下四个类别上花费了以下金额:
- 食物:200美元
- 服装:160美元
- 租金:280美元
- 娱乐:80美元
以下是我们将如何执行前面提到的三个步骤:
1. 所有类别的总和: 200 + 160 + 280 + 80 = 720美元。
2. 每个类别的份额(角度)
- 食物: (200/720)*360=100度
- 服装:(160/720)*360=80度
- 租金: (280/720)*360=140度
- 娱乐: (80/720)*360=40度
(100+80+140+40=360度,即整个圆/饼图的内角)
3. 绘制扇形:
这一部分有点复杂。如果你不熟悉WPF绘图原语,请先查看MSDN文档中的Geometry
和Path
类;否则,以下细节将很难让你理解(我在创建这些控件时也遇到了理解它们的麻烦。只有MSDN帮助了我!)。请访问此链接以获得一个好的开始。你可能还需要在继续之前查看ArcSegment
类(点击这里)。
我们将创建一个Path
对象来表示每个饼图扇形。Path
对象将绘制表示饼图扇形的圆扇形,并为我们处理命中测试(并跟踪鼠标事件)。如果一个类别获得了所有份额,Path
将包含一个单独的EllipseGeometry
。但在大多数情况下,会有一个以上的类别拥有0%以上的份额。在这种情况下,Path
将包含一个PathGeometry
,它又会包含一个PathFigure
,而该PathFigure
将包含2个LineSegment
和1个ArcSegment
,如下所示(这些都是WPF类):
LineSegment firstLineSegment = new LineSegment(startingPointOfArc, true);
ArcSegment arcSegment = new ArcSegment(endPointOfArc, pieSize, angleShare, angleShare > 180, SweepDirection.Clockwise, false);
LineSegment secondLineSegment = new LineSegment(centerPoint, true);
PathFigure pathFigure = new PathFigure(centerPoint, new PathSegment[] { firstLineSegment, arcSegment, secondLineSegment }, false);
PathGeometry pathGeometry = new PathGeometry();
pathGeometry.Figures.Add(pathFigure);
Path path = new Path();
path.Data = geometry;
myPanel.Children.Add(path);
现在我们将尝试理解代码的作用。请记住,在LineSegement
和ArcSegment
的构造函数中,指定的Point
对象代表终点。起点始终是隐式的,并由前一个段的结束位置确定。第一个LineSegment
将从包含的PathFigure
获取其起点。为了绘制每个饼图扇形,我们将始终从PieControl
的中心开始PathFigure
。为了方便起见,我们确保整个PieControl
的大小与饼图相同。此外,控件必须具有相同的Width
和Height
(我们的实现不支持椭圆饼图)。因此,饼图的半径将是Width/2,pathFigure的起点将是(radius, radius)。
现在,让我们谈谈如何计算直线段和弧线段的坐标。我们需要Math
类来执行计算。Math
类使用角度的弧度度量,而WPFGeometry
类使用度。因此,我们需要在这两种度量之间进行转换。源代码中提供了两个简单的函数来进行这种转换(在PieControl
类中)。另一个微妙之处是,从数学上讲,角度从X轴开始;而我们需要从12点钟方向(即直角)开始绘制饼图。我们是顺时针绘制图形,因此在扫过圆周时,我们将通过减去90度来调整坐标。
让我们计算代表食物(100度)的第一个扇形。为了简单起见,我们将忽略舍入误差。下面给出的所有Point
对象代表相对于包含控件的屏幕像素的X和Y坐标。
(记住数学从X轴开始,但我们希望从直角开始绘制饼图,即减去90度,顺时针思考)
centerpoint
= (100, 100) - 这是控件/饼图的中心,假设我们的饼图宽度为200像素。- 起始角度 = -90度 = -PI/2 弧度 = -1.57 弧度
- startingPointOfArc.X = Math.Cos(-1.57) * 100 + 100 = 100
- startingPointOfArc.Y = Math.Sin(-1.57) * 100 + 100 = 0
- 食物角度 = (200/720)*360= 100度 = 1.75 弧度。
- 归一化食物角度:100-90度 = 1.75-1.57 弧度=0.18 弧度(通过归一化,我们得到相对于X轴的度量。当然,扇形的实际内角将保持100度,因为我们也从起始点减去了90度)。
- endPoint.X = Math.Cos(0.18) * 100 + 100 = 198
- endPoint.Y = Math.Sin(0.18) * 100 + 100 = 117
第二个LineSegment
将endPoint
连接回centerpoint
;从而闭合图形,我们将用指定为该扇形的Brush来填充它。这就完成了创建一个饼图扇形的整个过程。其余的也不例外。它们在循环中使用相同的计算。唯一需要注意的是,下一个扇形自动从上一个扇形的末尾开始,顺时针方向。
下图将进一步阐明上述细节:
数据绑定
我们想将饼图数据绑定到PieControl
和ChartControl
对象,以便当数据更改时,饼图视图始终保持更新。为了做到这一点,我们将PieSegment
对象放入一个ObservableCollection
中。此外,我们让PieSegment
类实现INotifyPropertyChanged
接口。每当任何感兴趣的属性发生更改时,我们就会在PieChart
和/或PieControl
类中收到通知,并相应地更新视图。我们通过附加到PropertyChanged
事件,并在通知更改时重新计算和重绘图表来实现这一点。
结论
请提供您认为可以改进控件/文章的任何反馈和建议。如果您发现错误,也请告知我,以便在下一版本中将其移除。