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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (12投票s)

2021 年 4 月 13 日

CPOL

3分钟阅读

viewsIcon

12978

downloadIcon

603

mattsj1984 的“一个用于显示具有高度可定制格式的饼图的控件”的扩展,也渲染甜甜圈图。

运行的 .NET Framework 版本 (4)

运行的 .NET 5 版本 (窗口的图表部分)

引言

经过十多年,我更新了原始项目,将其功能扩展到甜甜圈。

此代码和本文的一部分大量取自文章:一个用于显示具有高度可定制格式的饼图的控件,作者是 mattsj1984,该文章之前取自文章:3D 饼图 - CodeProject,作者是

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));
 

然后是另一个棘手的部分:您必须连接多个 ArcsLines 以获得一个 GraphicPath

为了实现这一点(有一个连接的路径),您必须注意绘制的角度,通常,第二个弧必须以负角度绘制。

连接 Arcs 也意味着您必须具有 StartingPointsEndPoints (参考兴趣点)。

Using the Code

该控件的结构很像标准的 Windows Forms 控件,在使用方面。PieChart 本身包含一个 Items 属性,该属性的类型为 PieChart.ItemCollection。此集合存储 PieChartItem 类型的对象。每个 PieChartItem 都由 TextToolTipTextColorOffsetWeight 属性组成。

要创建和使用该控件,请使用 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 版本
© . All rights reserved.