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

在 WPF 中绘制基本 3D 圆柱图

starIconstarIconstarIconstarIconstarIcon

5.00/5 (16投票s)

2014年4月22日

CPOL

12分钟阅读

viewsIcon

64770

downloadIcon

1187

一个非常基本的 3D 圆柱图,绘制在 WPF 画布上

注意:此处提供的代码存在一个小缺陷:椭圆的宽度在代码中是静态设置的,因为底部不会正确地贴合 X 轴线。

引言

向应得者致敬 - 本文基于 CodeProject 用户 StewBob 撰写的文章 《在 WPF 中绘制网格覆盖的圆柱体》。好吧,我将他原来的“DrawingClass”重命名为“CylinderPainter3D”,并对其进行了修改,以便能够带有 X/Y 偏移绘制圆柱体,不带网格线,具有不同的半径值、高度和颜色 - 这就是我 3D 圆柱图所需的核心部分。

我唯一需要从头开始做的是添加另一个类来绘制图表本身 - 这个类名为 ChartPainter,它利用了前面提到的 CylinderPainter3D 类,该类基本上是 StewBob 扩展的 DrawingClass

最终,图表看起来会像这样

最终的类能够基于数据集合、最大尺寸、偏移量和标题绘制图表,从而使您的应用程序看起来更专业。
即使是图表元素(此处为圆柱体)的颜色也是可自定义的,可以设置为 System.Windows.Media.Colors 集合中任何可用的 System.Windows.Media.ColorSystem.Windows.Media.ColorSystem.Windows.Media.Colors 集合中可用

背景

关于 WPF 画布 - 它是什么?

定义了一个区域,您可以使用相对于 Canvas 区域的坐标来显式定位子元素。

这句话是从 MSDN 摘录的,基本上说明您可以在二维坐标系上绘制图形。关于 Canvas,您真正需要牢记的特殊之处在于 Y 轴是镜像的

 ------------------------------------  X [10/0]
 |[0/0]  
 |
 |
 |
 |
 |
 |
 |
 |
 | Y [0/8]  

System.Windows.Controls.Canvas 中坐标系的简图。Y 轴显示为镜像。

绘制草图

本章将向您展示图表是如何绘制的,以及应用了哪些偏移量和常量值。我们首先在 Microsoft Visio 中进行了草图绘制,以了解如何以最佳方式绘制图表。我强烈建议所有用任何编程语言绘制自定义元素的人都这样做:您可以清楚地了解如何以最佳方式完成某件事,而不会在第一次尝试失败时浪费太多时间。

一个重要的用例是图表可以绘制在 Canvas 元素的任何位置。我是通过指定一个偏移点来实现的 - 垂直 X 轴上的 X Offset 和垂直 Y 轴上的 Y Offset(# [1] (X Offset) 和 [2] (Y Offset))。更改 XY Offset 的值都会导致图表以更大的或更小的偏移量绘制。
Y 轴线 [4] 从 X Offset / Y Offset 开始 - 它的终点是 X Offset / Y Offset + Y 轴线 - Y 轴线等于图表的总最大高度。
绘制图表的 X 轴线 与绘制 Y 轴线 几乎相同 - 起点位于 Y 轴线的终点( X Offset / Y Offset + Y 轴线),终点与 X 轴线的起点相同,只是我必须将 X 轴线的长度添加到终点的 X 坐标 [5] 中。
定位圆柱体(在上面的图片中显示为黄色框)非常简单:我知道 X 轴线的起点,并且我知道每个圆柱体从那里开始所需的偏移量。上述偏移量是通过以下公式计算的

25 + (3r * index) 

'r' 是单个圆柱体的半径 - 它通过将 X 轴的长度除以图表需要容纳的圆柱体数量来计算。这确保了图表宽度的可变使用,并将根据它将容纳的圆柱体数量调整图表宽度的负担从开发人员身上解脱出来。
index 是一个基于零的数字,表示圆柱体距离图表的 0/0 坐标点(X Offset / Y Offset + Y 轴线)有多远。index 在绘制圆柱体时是“即时”计算的。
那么数字 25 是做什么用的?很简单:它是 X 轴上的一个默认初始偏移量,因为否则圆柱体将粘在 Y 轴线上,破坏了整个得体的外观。

现在,绘制圆柱体以使其具有正确的高度有点困难 - 由于 Canvas 的 Y 轴是镜像的,我需要使用以下公式

 (Y Axis Maximum Height – Cylinder Height) + Y Offset  

我认为这很清楚它的意思:Y 轴最大高度 - 圆柱体高度得到绘图起点和图表顶部边框之间的间隙 - 添加 Y Offset 得到传递给圆柱体绘图类的确切值。

计算圆柱体标题 [6] 的偏移量很容易:只需将 Y 轴线的长度、Y Offset 和一个额外的偏移量相加,以防止标题粘在 X 轴线上。

现在您知道了绘制图表背后的所有魔力,至少是理论上的。但还有另一件事,对吧?
没错。我们现在需要为 X 和 Y 线添加标题。

在图表的左上角放置 Y 轴的标题。它有一个左(X)偏移量 X Offset + 2 - 2 是一个常数值,可防止标题粘在 Y 轴线上。

X 轴线的标题位于距离顶部 Y Offset + Y 轴线长度 [3] 处,距离左侧 X Offset + X 轴线长度 [4] 处。

Using the Code

“使用代码”一章提供了项目中使用类的概述。每个类方法都附有解释性文本和单独解释的代码示例。属性的代码示例被省略了 - 我假设什么是属性,它的作用以及它的代码是什么样子 - 我只添加了一个解释该属性特定需求的文本。

ChartDataContainer - 数据容器

与任何数据处理的小工具一样,我们首先需要一个容器来存储数据。为此,我创建了一个名为 ChartDataContainer 的类,它提供了以下数据属性

  • List<double> Data
  • List<string> DataCaptions
  • Point MaxSize
  • Point Offset
  • string XAxisText
  • string YAxisText
  • Color ChartElementColor

属性

好了 - 如果您跳过了“背景”章节,并且/或者现在问自己“等等。我为什么需要所有这些数据?” - 这里是解释,按 ChartDataContainer 的每个属性分组。

List<double> Data

Data 属性提供了用于获取和设置 double 值列表的 getter 和 setter 方法,这些值是要在图表中显示的数据点。列表中的每个 double 值代表单个圆柱体的高度。

List<string> DataCaptions

DataCaption 属性是可选的,并且有一些限制:列表中的项目数量必须与 Data 列表中的项目数量匹配,或者为零。列表中的每个 string 代表要在图表中显示的 X 轴值(圆柱体)的标题。

Point MaxSize

MaxSize 属性用于获取或设置一个 X-Y 值对,该值定义了图表控件的最大尺寸。X-Y 值对由 System.Drawing.Point 表示。

Point Offset

Offset 属性是一个 getter 和 setter 属性,用于定义图表绘制在画布上的左侧(X)和顶部(Y)的偏移量。与 MaxSize 属性一样,它是由 System.Drawing.Point 表示的 X-Y 值对。

string XAxisText

此属性用于获取或设置图表 X 轴的标题。

string YAxisText

YAxisText 属性用于获取或设置图表 Y 轴的标题。

Color ChartElementColor

ChartElementColor 属性定义了图表元素的填充颜色,例如圆柱体。

CylinderPainter3D - 绘制圆柱体

CylinderPainter3D 类用于绘制图表中使用的 3D 圆柱体。它可以通过设置以下属性进行配置

  • Color FillingColor
  • double CylinderHeight
  • double CylinderRadius

我没有在此列出所有属性 - 实际上还有一些 - 但我遗漏的那些无法通过 ChartDataContainerClass 进行配置,因此对于解释给定示例来说没有用。

属性

Color FillingColor

FillingColor 属性定义了圆柱体的填充颜色。

double CylinderHeight

CylinderHeight 属性定义了圆柱体的高度。

double CylinderRadius

CylinderRadius 属性定义了圆柱体的半径(CylinderRadius * 2 = 圆柱体的直径)。

方法

public void DrawCylinder(System.Windows.Controls.Canvas cnv, double xOffset, double yOffset) 

System.Windows.Controls.Canvas 上绘制具有指定 X/Y 偏移量的 3D 圆柱体。其余数据由该类的 属性 提供。此方法的大部分是由 StewBob 编写的,如本文顶部所述。

整个绘图方法基于以下七个变量

 double ellipseHeight = 12;
 Point ptUpperLeft;
 Point ptUpperRight;
 Point ptLowerLeft;
 Point ptLowerRight;
 Point ptC;
 Path pth = new Path();
 
 ptUpperLeft = new Point(xOffset, ellipseHeight * 2);
 ptUpperRight = new Point(xOffset + (cylinderRadius * 2), ptUpperLeft.Y);
 ptLowerLeft = new Point(xOffset, ptUpperLeft.Y + cylinderHeight);
 ptLowerRight = new Point(ptUpperLeft.X + (cylinderRadius * 2), ptUpperLeft.Y + cylinderHeight);
 ptC = new Point(xOffset + cylinderRadius, ptUpperLeft.Y);  

每个变量都有一个非常具体的需求

  • ellipseHeight
    • 圆柱体顶部的椭圆高度被静态设置为 12。
      这给人一种从高于圆柱体的角度观察圆柱体的印象 - StewBob 的原始版本具有动态计算的椭圆高度,但我不得不将其删除,因为它会破坏偏移量计算。
  • ptUpperLeft
  • ptUpperRight
  • ptLowerLeft
  • ptLowerRight
    • 这四个变量定义了圆柱体的角作为 X/Y 坐标
  • ptC
    • ptC 变量定义了圆柱体顶部椭圆的中心坐标。
  • pth
    • 这个变量将包含 PathFigureGeometry 段。

现在我们知道了将使用哪些变量,绘制圆柱体就很容易了

//Draw cylinder body.
LineSegment ln = new LineSegment(ptLowerLeft, true);
ArcSegment arc = new ArcSegment(ptLowerRight, 
new Size(cylinderRadius, ellipseHeight), 0, false, 
System.Windows.Media.SweepDirection.Counterclockwise, true);
 
PathFigure pf = new PathFigure();
pf.StartPoint = ptUpperLeft;
//Add left side of cylinder.
pf.Segments.Add(ln);
//Add bottom arc of cylinder.
pf.Segments.Add(arc);
ln = new LineSegment(ptUpperRight, true);
//Add right side of cylinder.
pf.Segments.Add(ln);
 
PathGeometry pg = new PathGeometry();
pg.Figures.Add(pf);
 
pth.Stroke = new SolidColorBrush
(gridLineColor);//Grid Line Color is also used as Border Color
pth.StrokeThickness = 2;
pth.Fill = new SolidColorBrush(fillingColor);
pth.Data = pg;
cnv.Children.Add(pth);
 
//Add top ellipse.
pth = new Path();
pth.Stroke = new SolidColorBrush
(gridLineColor);//Grid Line Color is also used as Border Color
pth.StrokeThickness = 1;//Border is 1Px thick
pth.Fill = new SolidColorBrush(fillingColor);
pg = new PathGeometry();
pg.AddGeometry(new EllipseGeometry(ptC, cylinderRadius, ellipseHeight));
pth.Data = pg;  
cnv.Children.Add(pth);

上面的代码片段除了将 GeometryPathFigure 元素添加到路径以在着色后在画布上绘制路径 (pth) 之外,什么也不做。既然我们已经看过了绘制圆柱体的代码,只剩下一件事了:绘制图表并添加多个圆柱体以制作一个闪亮的图表,它会自动绘制在画布上。

ChartPainter - 绘制图表

CylinderChartPainter 类可以被视为一个 API,它封装了绘制图表和图表元素(圆柱体)的整个功能 - 它是 static 的,只提供一个 static 方法。

方法

void DrawChart( System.Windows.Controls.Canvas cnv, ChartDataContainer container) 

此方法将圆柱图绘制到 System.Windows.Controls.Canvas "cnv" 上 - 定义图表大小、偏移量、内容、内容标题和着色选项的数据存储在 ChartDataContainer "container" 中。

首先,图表的 X 轴线被绘制到画布上(我使用 System.Windows.Shapes 命名空间中的 Line 类来绘制线条)。

//Draw the X-Axis line of the chart
Line xLine = new Line();
xLine.Stroke = Brushes.Black; //Line color is black

//Set Start & End points of the line
xLine.X1 = container.Offset.X;
xLine.X2 = container.Offset.X + container.MaxSize.X;
xLine.Y1 = container.Offset.Y + container.MaxSize.Y;
xLine.Y2 = container.Offset.Y + container.MaxSize.Y;
 
xLine.StrokeThickness = 1; //Line's size is set to 1 pixel
xLine.SnapsToDevicePixels = true;
cnv.Children.Add(xLine);  //Add Line to canvas 

接下来绘制的是 Y 轴线(技术与 X 轴线相同,但垂直对齐)。

//Draw the Y-Axis Line of the chart.
Line yLine = new Line();
 
// Set Start & End points of the Line
yLine.Stroke = Brushes.Black;
yLine.X1 = container.Offset.X;
yLine.Y1 = container.Offset.Y;
yLine.X2 = container.Offset.X;
yLine.Y2 = xLine.Y2;
 
yLine.StrokeThickness = 1;<
span style="font-size: 9pt;">//Line's size is set to 1 pixel</span> 
<span style="font-size: 9pt;">yLine.SnapsToDevicePixels = true;
</span><span style="font-size: 9pt;">
cnv.Children.Add(yLine); </span>
<span style="font-size: 9pt;">//Add Line to canvas </span><
//span style="font-size: 9pt;"> </span>

在绘制完 X 轴线和 Y 轴线之后,我需要根据 ChartDataContainer 中提供的标题来绘制它们的标题。为了让您更好地了解哪里放置了什么,我再次展示了与上面稍早时候相同的图形

下面的代码除了根据四个偏移量 [1]、[2]、[3] 和 [4] 插入两个文本块(上图中为“Y 轴标题”和“X 轴标题”)之外,什么也不做。文本值由 container 提供。

 //Add Y-Axis description text to the chart
TextBlock yAxisDescriptionBlock = new TextBlock();
yAxisDescriptionBlock.Text = container.YAxisText;
Canvas.SetLeft(yAxisDescriptionBlock, yLine.X1 + 5);
Canvas.SetRight(yAxisDescriptionBlock, xLine.X2);
Canvas.SetTop(yAxisDescriptionBlock, container.Offset.Y);
cnv.Children.Add(yAxisDescriptionBlock);
 
//Add X-Axis description text to the chart
TextBlock xAxisDescriptionBlock = new TextBlock();
xAxisDescriptionBlock.Text = container.XAxisText;
Canvas.SetTop(xAxisDescriptionBlock, yLine.Y2 - 12);
Canvas.SetLeft(xAxisDescriptionBlock, xLine.X2 + 3);
Canvas.SetRight(xAxisDescriptionBlock, xLine.X2 + container.YAxisText.Length);
cnv.Children.Add(xAxisDescriptionBlock);  

之后,我们就可以使用 CylinderPainter3D 类来绘制图表的圆柱体元素了。

每个图表都根据 绘制草图 章中显示的草图绘制,并且单个圆柱体之间的偏移量(间隙)计算如下

 CylinderRadius * 2.5 

我在上面的草图中没有展示的另一件事是,我包含了一个缩放机制,允许图表自动调整圆柱体的高度,以便尽可能多地利用空间(因此,尽可能详细地显示数据)。例如,如果最大的数据元素是 45,而图表的最大高度是 100,则该方法会自动将每个圆柱体的高度调整为不带缩放时的两倍。此功能使图表尽可能可读。图表是为 container 中的每个数据元素绘制的。

//Add Cylinder graphics
double offsetX = container.Offset.X + 25;//Offset on the X-Axis
double scale =  container.MaxSize.Y / 
    container.Data.Max(); //Scaling value - Needed to make the chart
                                                            //more readable
double radius = (container.MaxSize.X / 
(container.Data.Count + 2.5)) / 2;   //Radius of a single chart element
                                     // = diameter/ 2
CylinderPainter3D cylinderPainter = new CylinderPainter3D();
cylinderPainter.CylinderRadius = 
    radius;                    // Set the radius of the cylinder to be drawn
cylinderPainter.FillingColor = 
    container.ChartElementColor; // Set the Cylinder's color
foreach(double dataelement in container.Data)
{
    double topY = (container.Offset.Y + 
    (container.MaxSize.Y - (dataelement*scale))) - 26;  //Calculate Y-Offset
    cylinderPainter.CylinderHeight = dataelement * scale;   //Apply scale and set height
    cylinderPainter.DrawCylinder(cnv, offsetX, topY);   //Draw the Cylinder
    offsetX += cylinderPainter.CylinderRadius * 2.5;    //Increase offset
}

在图表的圆柱体添加到画布后,我只需要为每个圆柱体添加标题元素。由于甚至可以不添加圆柱体的标题,所以我检查 container 中是否有可用的标题。

if (container.DataCaptions.Count > 0)//If available, add captions to cylinder graphics
{ 

如果存在标题元素,我将继续将它们添加到画布上(我使用文本块添加它们,并用一条水平线将标题连接到圆柱体。此外,我定义了一个名为 zipper 的变量,其值为 10,并在 foreach 循环的每次迭代中乘以 -1,导致标题具有轻微的、拉链状的偏移量,使其更易读(标题不再显得拥挤)。

offsetX = container.Offset.X + 25; //Offset on the X-Axis 
    //is reset because captioning is started from the left
short zipperOffset = 10;
foreach (string caption in container.DataCaptions)
{
    zipperOffset *= (-1);
    //Add line to associate a caption with a single chart element
    Line captionLine = new Line();
    captionLine.Stroke = Brushes.Black;
    captionLine.X1 = offsetX;
    captionLine.X2 = offsetX;
    captionLine.Y1 = yLine.Y2;
    captionLine.Y2 = yLine.Y2 + 40 + zipperOffset;
 
    cnv.Children.Add(captionLine);
 
    //Add chart element description text block
    TextBlock chartElementDescriptionTextBlock = new TextBlock();
    chartElementDescriptionTextBlock.Text = caption;
    Canvas.SetTop(chartElementDescriptionTextBlock, yLine.Y2 + 47 + zipperOffset);
    Canvas.SetLeft(chartElementDescriptionTextBlock, offsetX);
    offsetX += radius * 2.5;
    Canvas.SetRight(chartElementDescriptionTextBlock, offsetX);
    cnv.Children.Add(chartElementDescriptionTextBlock);
} 

上面的代码给出了一个连接到图表的 Cylinder 元素的标题,看起来像这样

使用 ChartPainter 类绘制圆柱图

我编造了一个简单的示例代码,用于展示 ChartPainter 类如何用于绘制图表

ChartDataContainer container = new ChartDataContainer();
container.Data = new List<double>() {20, 33, 115 , 85, 65 };//Dummy data
container.DataCaptions = new List<string>() 
{"1","2", "3", "4", "5"};//Set Captions
container.MaxSize = new Point(700, 300);//
container.Offset = new Point(10, 150);
container.ChartElementColor = Colors.SlateBlue;
CylinderChartPainter.DrawChart(container, this.Canvas1); 

执行上述代码将得到一个如下所示的图表

关注点

我学会了如何绘制 3D 圆柱体,使用绘制的圆柱体制作 3D 圆柱图控件,并学会了如何在 Canvas 上绘制文本、线条和其他炫酷的东西 - 到目前为止让我非常恼火的是 WPF Canvas 的镜像 Y 轴... 如果有人知道原因,请随时与我联系,开导我。

除此之外,我很高兴地宣布,本文介绍的图表例程已经投入生产使用,在我们 Sandro Lorek 和我一起撰写的另一篇 CodeProject 文章中。如果您有兴趣了解本文的代码有什么作用,请下载它并尝试将其包含在实际项目中。如果您有任何建议、发现的 bug/修复,或者只是想说您对我们提供的代码有多满意,请随时在下方发表评论。

历史

  • 2013 年 11 月 24 日
    • 首次发布版本
© . All rights reserved.