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






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 版本


