3D 饼图
3D 饼图和三角学

引言
这是一篇关于带有透明度的 3D 饼图的文章。
背景
在一个小型应用程序中,我需要一个饼图,而不需要第三方软件的麻烦和复杂性。起初,我认为这是一项简单的任务,直到我意识到我的数学需要一些复习,这花了一段时间,尽管数学内容相当简单,只有有限的三角学,但它最初确实给我的大脑带来了压力。
2D 饼图非常直接,知道了起始角度和扫描角度,DrawPie 可以很好地完成,但一旦你为图表应用了透视,你很快就会意识到这里涉及一些数学。这是因为随着透视的引入,特定切片弧线的长度会根据透视倾斜而变化。
从图 2 可以看出,弧线的长度,尤其是在左侧和右侧,与图 1 中的长度不同,但它们都是同一个饼图。这就是我们学校里学过的东西派上用场的地方。那么我们怎么做呢?嗯,这个方法叫做笛卡尔坐标系,以一位法国人(一位数学家)笛卡尔的名字命名。不过,那是在 1637 年。想想看,在 1637 年,这相当聪明。
嗯,微软并没有让事情变得容易,我很快就会指出这一点。
首先,回到笛卡尔坐标系。
正如你在图 3 中看到的,图上的一个点可以有两种方式绘制,第一种是知道角度和长度(半径 - 极坐标),或者使用 X 和 Y 坐标(矩形坐标)。注意中心处的零参考点;这一点很重要,因为当我们使用系统绘图编写代码时,参考点是从绑定矩形的左上角开始的。为了弥补这一点(换句话说,移动零参考点),只需从 X 中减去半径(矩形宽度/2),然后将半径加到 Y 上即可。
左上角是 +Y,因为它在笛卡尔零坐标的上方,而且它是 -X,因为它在笛卡尔零坐标的左侧。你也会在图 3 中注意到笛卡尔平面中有 4 个象限,知道这一点也很重要,因为,例如,如果我们看坐标 X = -5 和 Y = 5 在第二象限,与第四象限的 X = 5 和 Y = -5 相比,尽管数字相同,但正负号的不同,使我们处于不同的象限,因此给出了不同的角度。例如,如果我们使用 +X 作为基线,并称之为零度,如图 3 所示,那么如果我们考虑逆时针旋转,那么任何位于第一个象限的角度都在 0 到 90 之间(X = +X)和(Y = +Y),任何位于第二个象限的角度都在 90(Y = +Y)到 180(X = -X)之间,第三个象限的角度将在 180(X = -X)到 270(Y = -Y)之间,依此类推。这一点的重要性在于,当使用三角学从给定角度计算 X 和 Y 坐标时,请考虑以下几点。
Trig formula X = cos(angle)
Y = sin (angle)
听起来很简单,但比如说两个角度 0 度和 180 度,求 Y
Y = Sin (0) = 0
Y = Sin (180) = 0
哈,两者都等于 0。太棒了!我们怎么区分呢??要回答这个问题,我们现在看看给定相同角度的 X 坐标。
处理角度 = 0 度
X = cos(0) = 1
现在处理角度 = 180 度
X = cos(180) = - 1
区别在于 180 度时 X = -1,0 度时 X = 1。
原因是三角公式是针对直角三角形的,或者从角度的角度来说是 0 到 90 度,因此一个圆有 4 个直角三角形,即 4 个象限。所以回到图 3,你现在可以看到,当 X 为负且 Y 为正时,我们在第二象限,如果 X 为负且 Y 为负,那么我们在第三象限。
依此类推。因此,通过测试 X 和 Y 的极性,我们可以确定角度所在的象限,并且通过了解这一点,我们可以根据需要添加 90 的倍数。
要记住的一个重要事情是,基本的三角公式需要角度以弧度为单位。例如,360 度 = 6.21318 弧度,或 2 x pi,因此 180 度 = pi,因此当你看到一个包含 Math.PI / 180) 的公式时,它实际上是将度转换为弧度。
所以 Math.Pi = 3.14159 加上一些小数点。
现在将其付诸实践。
首先,让我们看一下 Form 的 Load 事件。
Private Sub GraphForm_Load(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Me.Load
Dim Total As Double = 0
Dim StartAngle As Single, FinishAngle As Single
Me.Width = 340
Me.Height = 340
'Locate close button in centre of pie chart
btnClose.Top = Margin + (Radius - btnClose.Height / 2)
btnClose.Left = Margin + (Radius - btnClose.Width / 2)
'Temp values for test purposes only
For I = 0 To Values.Length - 1
Values(I) = CSng(40)
Next
'Temporary values added for test purposes only.
'This is the Text that appears on each slice
Dim CurrentYear As Single = Year(Now)
For I = 0 To Values.Length - 1
Strings(I) = CurrentYear 'Temp values for test purposes only
CurrentYear = CurrentYear - 1
Next
'Temp values for test purposes only. This is the Tooltip Text
For I = 0 To Values.Length - 1
StringsToolTip(i) = "Fiscal Year " & Strings(i)
Next
'Add all the values in the array to find a total
For I = 0 To Values.Length - 1
Total = Total + Values(I)
Next
'Give each slice a proportionate value in the values array.
'What happens here is we take 360 being a full circle and then a
'proportionate fraction is allocated to each angle in the Angles array
For I As Integer = 0 To Values.Length - 1
Angles(I + 1) = 360 * Values(I) / Total
Next
Angles(0) = 0 ' Set 1st Angle in array = 0
' Increment Angle(n) by the value before it
For I As Integer = 1 To Values.Length
Angles(I) = Angles(I) + Angles(I - 1)
Next
'Set the margins for 1st slice displacement
StartAngle = GetAngle(Angle:=Angles(0) + m_Rotation)
FinishAngle = GetAngle(Angle:=Angles(1) + m_Rotation)
SetDisplacement(StartAngle, FinishAngle)
Call MakePieChart()
Timer1.Enabled = True ‘Start timer of slice animation
End Sub
现在在 Load 事件中,我们将值放入一个数组,为了方便起见,它们都相等
角度,40 度。这里纯粹是为了测试目的,你可以在别处根据应用程序的值加载数组。数组的长度可以是可变的。
现在让我们检查一下饼图的实际绘制过程。
这是在 MakePieChart
例程中完成的。首先,因为我提供了半透明饼图(在声明部分设置 m_TransparencyLevel
和 m_Transparency2Level
变量。请注意 255 是完全不透明的,小于这个值会变得更透明。m_Transparency2Level
是饼图的顶部,m_TransparencyLevel
是主体。)通过更改 alpha 值可以实现不同的效果,例如,将主体设置为 255,顶部设置为 55。
切片绘制的顺序很重要,这在 MakePieChart
例程中已注释。如果顺序不正确,那么应该在后面的切片可能会出现在前面,为了演示这一点,只需交换 MakePieChart
例程中的一些代码块,并相应地设置 m_Transparency2Level
。
在 MakePieChart
例程中,在 Angles(n) 中的每个角度用于 drawPie
之前,它首先被发送到 GetAngle
例程,这里应用了三角学。请注意,当应用 3D 时,Y 半径不同于 X 半径,换句话说,椭圆的宽度半径(长轴)大于其高度半径(短轴)。因此,找到 Y 坐标需要将半径乘以纵横比。
angles 数组中包含的所有角度在绘制每个饼图切片之前都会被发送到 GetAngle
。GetAngle
例程中发生的事情是首先找到 X 和 Y 坐标,Y 具有通过纵横比调整过的半径。完成此操作后,我们便得到了一个给定角度在椭圆上的 X 和 Y 坐标(这是一个椭圆,因为我们通过纵横比调整了 Y 的半径)。现在这很棒,但 .NET 中的 drawPie
需要角度(极坐标),所以我们需要将 X 和 Y 坐标转换为角度。所以,实际上,我们正在进行矩形到极坐标的转换。
这是公式。
GetAngle = ( Math.Atan((x)/Y) * (180/Math.PI))+90 '90 is added to change the base line
现在检查下面的整个例程……
Private Function GetAngle(ByVal Angle As Single) As Single
Dim X as single, Y as single
'Now find the X and Y coordinance on the Cartesian plane
X = Radius + Math.Cos((Angle) * Math.PI / 180) * Radius 'Major Radius
Y = Radius - Math.Sin((Angle) * Math.PI / 180) * Radius _
* m_AspectRatio 'Multiply by m_AspectRatio to give us Minor Radius
'Subtract radius to bring coordinances relative to the centre of the pie radius.
'We mentioned above why this is necessary
X = X - Radius
Y = Y – Radius
‘Within the Cartesian plane coordinant we need a point we call zero degrees.
'So the +X line is select as zero degrees
GetAngle = (Math.Atan((X) / Y) * (180 / Math.PI)) + 90 'add 90 to change base line
if Y > 0 then that puts us in the top two quadrants so we must add 180
‘otherwise we zero again
If Y >= 0 Then
GetAngle = GetAngle + 180
End If
‘If we go past 360 we start again, so make it equal zero
If GetAngle = 360 Then GetAngle = 0
Return GetAngle
End Function
DrawText
需要一种类似的方法,除了这个参数需要矩形坐标 X 和 Y。所以我们取一个角度,位于起始角度和扫描角度之间的一半,并使用三角学得到 X 和 Y 坐标,请参阅 DrawString
例程。
深度很容易实现,只需将同一个饼图绘制几次(取决于深度),每次将 Y 值偏移一像素(这样就有了深度)。另一种方法是逐点绘制。请注意,深度必须随着透视的变化而改变,因此当你倾斜饼图时,图表的深度必须相应地改变。
我为饼图添加了一点动画(看起来总是很好),尽管有时它可能会做得太过火而变得无聊。
创建的饼图成为窗体的背景图像(然而,也可以使用 PictureBox)。我喜欢这种方法,因为我可以将图表显示在其他控件之上,例如 Listview
、Datagridview
等,使用透明窗体,这种方法可以减少窗体混乱(屏幕上过多的窗体),而且用户喜欢简单。我也不喜欢东西上有太多的按钮,鼠标总是能很好地充当界面,所以我添加了鼠标滚轮例程来处理透视和深度(按钮更少,讨厌太多的按钮)。
使用 Vscroll 按钮(小的,靠近关闭按钮)来控制亮度。这在 Paint 事件中完成。值在 ColorMatrix
中更改。
只需将鼠标悬停在图表上,滚轮即可控制透视;将鼠标悬停在关闭按钮上,滚轮即可控制深度;右键单击可以旋转图表,尽管我刚刚注意到这里有一个 bug,我会在有时间时修复它。
代码很直接,请阅读注释,值在 Load 事件中加载,因此请相应地删除和添加。顶部和主体的透明度可以通过分别更改 m_TransparencyLevel
和 m_Transparency2Level
来设置,以提供不同的效果(随意尝试)。
我还为每个切片提供了独立的 Tooltip。这同样需要一点三角数学才能获得鼠标指针相对于饼图切片所在的角度范围。这在 PieChartForm_MouseMove
事件中完成,所选角度位于每个切片的起始角度和结束角度之间的一半。StringsToolTip(n)
包含每个切片的文本,出于演示目的,这也将在 Form 的 Load 事件中完成,所以请在你的应用程序中相应地移动。
如果使用透明窗体方法(使 PieChartForm
透明),最好将窗体背景颜色(以及透明颜色,当然)设置为它将要显示的表面的背景颜色相同,这会产生更好的轮廓效果。我使用的方法是将一个窗体停靠在另一个窗体上,因此当它停靠的窗体移动时,图表也会随之移动,但其特点是,如果它被从停靠的窗体上拉出(使用鼠标左键单击),它就不再被锚定,我相信这会产生一种具有多功能性的良好效果。
在声明部分将 m_DepthModeTranparent
设置为 false
将使用斜纹填充效果绘制饼图主体。
我最终会更新以添加更多功能,例如鼠标悬停时高亮切片等,以及不同的标签等。
祝好,
Joe Mifsud