用于显示饼图(和甜甜圈图)的控件,具有高度可定制的格式






4.95/5 (12投票s)
mattsj1984 的“一个用于显示具有高度可定制格式的饼图的控件”的扩展,也渲染甜甜圈图。
运行的 .NET Framework 版本 (4)
运行的 .NET 5 版本 (窗口的图表部分)
引言
经过十多年,我更新了原始项目,将其功能扩展到甜甜圈。
此代码和本文的一部分大量取自文章:一个用于显示具有高度可定制格式的饼图的控件,作者是 mattsj1984,该文章之前取自文章:3D 饼图 - CodeProject,作者是 Julijan Sribar。
WinForms 应该死了……但它仍然活着!
为了给这个控件一个未来,我还制作了它的 .NET 5 版本。
背景
我正在开发的一个项目需要一个甜甜圈控件,我的一个同事说:“好吧,只需做一个饼图,然后在上面画一个圆柱体”……几天前,一切都开始了(不,这不是我采取的方法……)。
基本上,我不得不从“简单”的 DrawPie
指令
public void DrawPie (System.Drawing.Pen pen, float x, float y, float width,
float height, float startAngle, float sweepAngle);
改为 DrawArc
(s)
public void DrawArc (System.Drawing.Pen pen, System.Drawing.Rectangle rect,
float startAngle, float sweepAngle);
DrawPie
是一个单一的命令,因此切片(顶部或底部)一步完成,然后您必须计算边缘以连接两个切片。
使用 DrawArc
,您必须设计外部椭圆(由单个弧组成,如饼图切片)和内部椭圆,其“距离”是甜甜圈的宽度,连接在一起。
这个“距离”会根据倾斜度而变化,事实上,你必须注意透视(投影),在 90° 时是完整宽度
在 0° 时宽度为 0
这是一个如何使用 Math.Sin(Double)
函数的好例子,它完全做到这一点:90° 时值为 1,0° 时值为 0
在代码中
float bottomInternalY = ((bottomExternalY) +
(donutSize / 2) * (float)Math.Sin(Control.pieStyle.Inclination));
float topInternalY = ((topExternalY) +
(donutSize / 2) * (float)Math.Sin(Control.pieStyle.Inclination));
然后是另一个棘手的部分:您必须连接多个 Arcs
和 Lines
以获得一个 GraphicPath
。
为了实现这一点(有一个连接的路径),您必须注意绘制的角度,通常,第二个弧必须以负角度绘制。
连接 Arcs
也意味着您必须具有 StartingPoints
和 EndPoints
(参考兴趣点)。
Using the Code
该控件的结构很像标准的 Windows Forms 控件,在使用方面。PieChart
本身包含一个 Items
属性,该属性的类型为 PieChart.ItemCollection
。此集合存储 PieChartItem
类型的对象。每个 PieChartItem
都由 Text
、ToolTipText
、Color
、Offset
和 Weight
属性组成。
要创建和使用该控件,请使用 Visual Studio 中的 Windows Forms 设计器将 PieChart
添加到窗体并调整图表属性
或者您可以通过编程方式使用该控件
PieChart pieControl = new PieChart();
//set as doughtnut if you wish
pieControl.DisplayDoughnut = true;
// add an item with weight 10, color Red,
// text "Text", and tool-tip text "ToolTipText"
pieControl.Items.Add(new PieChartItem(10, Color.Red, "Text", "ToolTipText"));
// add another item with weight 5, color Blue, text "Blue",
// tool-tip text "BlueTips", and an offset
// of 25 pixels from the center of the pie
pieControl.Items.Add(new PieChartItem(5, Color.Blue, "Blue", "BlueTips"));
// set the control to automatically
// fit the pie inside the control
pieControl.AutoSizePie = true;
// set the control to only show the
// text if it fits inside its slice
pieControl.TextDisplayMode = PieChart.TextDisplayTypes.FitOnly;
// set control border
pieControl.DrawBorder = false;
// set a graph title
pieControl.GraphTitle = null;
我添加了属性 ItemTextTemplate
,其中带有关键字 #VALUE
、#PERCENTAGE
和 #ITEMTEXT
,可以通过布尔值 UseItemTextTemplate
触发显示相关信息。
(百分比的小数位数通过 PecentageDecimals
属性管理。)
这是以下结果
pieControl.ItemTextTemplate = "Val: #VALUE - Perc: #PERCENTAGE - Text: #ITEMTEXT";
pieControl.UseItemTextTemplate = true;
pieControl.PecentageDecimals = 2;
is
经典 功能(+ 增强功能)仍然存在
关注点
因为我正在通过 GDI+ 绘制弧,所以我需要找到这些弧的“终点”和“起点”,以便创建要填充或绘制(边缘)的路径。
经过搜索,我看到一个帖子 (使用 Start X/Y 和 Start+Sweep Angles 在 ArcSegment 中获取终点),其中 BlueRaja - Danny Pflughoeft 回答(终点)。
我创建了以下辅助类,您可以将其与 Graphics.DrawArc
的签名一起使用
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace ModernUI.Charting
{
public static class ChartHelper
{
public static PointF GetStartingPoint
(float x, float y, double width, double height, double startAngle, double sweepAngle)
{
return GetStartingPoint(new PointF(x, y), width, height, startAngle, sweepAngle);
}
public static PointF GetStartingPoint(PointF startPoint, double width,
double height, double startAngle, double sweepAngle)
{
Point radius = new Point((int)width / 2, (int)height / 2);
//Adjust the angles for the radius width/height
startAngle = UnstretchAngle(startAngle, radius);
//Calculate the final point
return new PointF
{
X = (float)(Math.Cos(startAngle) + 1) * radius.X + startPoint.X,
Y = (float)(Math.Sin(startAngle) + 1) * radius.Y + startPoint.Y,
};
}
public static PointF GetFinalPoint
(float x, float y, double width, double height, double startAngle, double sweepAngle)
{
return GetFinalPoint(new PointF(x, y), width, height, startAngle, sweepAngle);
}
public static PointF GetFinalPoint(PointF startPoint, double width,
double height, double startAngle, double sweepAngle)
{
Point radius = new Point((int)width / 2, (int)height / 2);
double endAngle = startAngle + sweepAngle;
double sweepDirection = (sweepAngle < 0 ? -1 : 1);
//Adjust the angles for the radius width/height
startAngle = UnstretchAngle(startAngle, radius);
endAngle = UnstretchAngle(endAngle, radius);
//Determine how many times to add the sweep-angle to the start-angle
double angleMultiplier = (double)Math.Floor(2 * sweepDirection *
(endAngle - startAngle) / Math.PI) + 1;
angleMultiplier = Math.Min(angleMultiplier, 4);
//Calculate the final resulting angle after sweeping
double calculatedEndAngle = startAngle + angleMultiplier *
Math.PI / 2 * sweepDirection;
calculatedEndAngle = sweepDirection *
Math.Min(sweepDirection * calculatedEndAngle, sweepDirection * endAngle);
//Calculate the final point
return new PointF
{
X = (float)(Math.Cos(calculatedEndAngle) + 1) * radius.X + startPoint.X,
Y = (float)(Math.Sin(calculatedEndAngle) + 1) * radius.Y + startPoint.Y,
};
}
private static double UnstretchAngle(double angle, Point radius)
{
double radians = Math.PI * angle / 180.0;
if (Math.Abs(Math.Cos(radians)) < 0.00001 ||
Math.Abs(Math.Sin(radians)) < 0.00001)
return radians;
double stretchedAngle = Math.Atan2(Math.Sin(radians) / Math.Abs(radius.Y),
Math.Cos(radians) / Math.Abs(radius.X));
double rotationOffset = (double)Math.Round(radians / (2.0 * Math.PI),
MidpointRounding.AwayFromZero) -
(double)Math.Round(stretchedAngle / (2.0 * Math.PI),
MidpointRounding.AwayFromZero);
return stretchedAngle + rotationOffset * Math.PI * 2.0;
}
}
}
历史
- 2021 年 4 月 13 日:修订版 0:原始版本
- 2021 年 4 月 14 日:修订版 1:添加了 .NET5 版本