65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF 中简单易用的饼图控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (13投票s)

2012年8月19日

CPOL

7分钟阅读

viewsIcon

78862

downloadIcon

7817

本文旨在让你非常轻松地在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使用相同的WidthHight值。对于PieChartPieWidthPieHeight应相同。

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;
  1. 将引用添加到下载中提供的PieControls Microsoft.Expression.Drawing 程序集中。
  2. 在XAML文件中包含PieControls命名空间。
  3. 使用类似以下的代码将PieControl和/或PieChart添加到你的Window/Control中:
  4. 在你的代码隐藏文件中包含'using'语句。
  5. 5. 在代码隐藏文件中填充饼图数据,并像这样将其分配给饼图控件:
  6. 你可以随时对pieCollection或其任何项进行任何更改。它们将自动反映在生成的饼图中!

1.1. 自定义选项

你可以使用以下方法来自定义控件:

  1. PieSegment类的属性。它们都设计为立即更改饼图视图。
  2. PieControlPieChartWidthHeight属性。
  3. PieChartPieWidthPieHeight属性(这些代表PieChart中包含的PieControl的宽度/高度)。
  4. PieControlPieChart类的PopupBrush属性。此属性用于绘制鼠标光标悬停在饼图上时显示的弹出窗口的背景。
  5. 为了简单起见,我在示例中只使用了整数值。然而,控件在内部使用双精度浮点值;因此,你的选项不仅限于整数。

第二部分:理解源代码和背后的数学原理

附加源代码中的PieControl.cs文件包含我们饼图控件的定义。其他相关的类是PieChartPieSegmentPieChart利用PieControl并提供额外的图例功能。PieSegment类是用于将饼图值传达给PieChartPieControl的数据载体。

一个基本的饼图只是一个圆,根据每个数据类别的百分比分成更小的扇形。每个占有0%以上份额的类别都得到一个圆扇形。0份额的什么也得不到,尽管我们会保留该类别,以防其未来获得份额。

想象一下,如果我们为家庭开支分为4个类别:(1)食物,(2)服装,(3)租金和(4)娱乐,需要创建饼图,我们会怎么做?

  1. 计算所有类别的总和。
  2. 通过将每个类别的份额除以总数,然后将结果乘以360(圆的总内角度是360度)来计算每个类别的份额(即角度)。
  3. 确定坐标并绘制每个扇形。

前两个步骤很简单。最后一步取决于我们如何使用WPF的原始图元来创建饼图。详细信息稍后会添加。首先,让我们将理论付诸实践,并通过一个例子来查看上述步骤是如何实际执行的。

示例

假设我在2012年7月在以下四个类别上花费了以下金额:

  1. 食物:200美元
  2. 服装:160美元
  3. 租金:280美元
  4. 娱乐: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文档中的GeometryPath类;否则,以下细节将很难让你理解(我在创建这些控件时也遇到了理解它们的麻烦。只有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);

现在我们将尝试理解代码的作用。请记住,在LineSegementArcSegment的构造函数中,指定的Point对象代表终点起点始终是隐式的,并由前一个段的结束位置确定。第一个LineSegment将从包含的PathFigure获取其起点。为了绘制每个饼图扇形,我们将始终从PieControl的中心开始PathFigure。为了方便起见,我们确保整个PieControl的大小与饼图相同。此外,控件必须具有相同的WidthHeight(我们的实现不支持椭圆饼图)。因此,饼图的半径将是Width/2,pathFigure的起点将是(radius, radius)。

现在,让我们谈谈如何计算直线段和弧线段的坐标。我们需要Math类来执行计算。Math类使用角度的弧度度量,而WPFGeometry类使用。因此,我们需要在这两种度量之间进行转换。源代码中提供了两个简单的函数来进行这种转换(在PieControl类中)。另一个微妙之处是,从数学上讲,角度从X轴开始;而我们需要从12点钟方向(即直角)开始绘制饼图。我们是顺时针绘制图形,因此在扫过圆周时,我们将通过减去90度来调整坐标。

让我们计算代表食物(100度)的第一个扇形。为了简单起见,我们将忽略舍入误差。下面给出的所有Point对象代表相对于包含控件的屏幕像素的XY坐标。

(记住数学从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

第二个LineSegmentendPoint连接回centerpoint;从而闭合图形,我们将用指定为该扇形的Brush来填充它。这就完成了创建一个饼图扇形的整个过程。其余的也不例外。它们在循环中使用相同的计算。唯一需要注意的是,下一个扇形自动从上一个扇形的末尾开始,顺时针方向。

下图将进一步阐明上述细节:

 

数据绑定

我们想将饼图数据绑定到PieControlChartControl对象,以便当数据更改时,饼图视图始终保持更新。为了做到这一点,我们将PieSegment对象放入一个ObservableCollection中。此外,我们让PieSegment类实现INotifyPropertyChanged接口。每当任何感兴趣的属性发生更改时,我们就会在PieChart和/或PieControl类中收到通知,并相应地更新视图。我们通过附加到PropertyChanged事件,并在通知更改时重新计算和重绘图表来实现这一点。

结论 

请提供您认为可以改进控件/文章的任何反馈和建议。如果您发现错误,也请告知我,以便在下一版本中将其移除。

WPF中简单易用的饼图控件 - CodeProject - 代码之家
© . All rights reserved.