WPF 中交互式 3D 柱状图自定义控件,支持旋转、触控显示和数据绑定






4.69/5 (33投票s)
在您的 .NET 应用程序中轻松创建 WPF 3D 柱状图。使用此 WPF 3D 柱状图自定义控件,您可以在几秒钟内创建交互式 3D 柱状图。您甚至可以数据绑定您的输入,以查看“3D 图表柱状图随输入实时变化”的效果。
引言
使用此 WPF3DChart
自定义控件,可以在几秒钟内创建交互式 3D“可旋转”(或“可转动”)柱状图。只需几行 XAML 或 C#(或任何 .NET 兼容语言代码)即可创建交互式、可旋转的 3D 柱状图。此外,您还可以使用 WPF 数据绑定来实现“在输入数据时实时查看 3D 图表变化”的效果。
今天你想做什么?
- 如果您想快速查看演示,请按照“查看演示”部分的简单说明进行操作。
- 如果您想在示例项目中测试此
WPF3DChart
控件,请按照“如何使用代码 / 使用程序集的步骤”部分的说明进行操作。 - 如果您想学习如何编写此类控件,请从“WPF 3D 几何图形基础”部分开始阅读。
查看演示
- 步骤 1:将二进制 zip 文件(使用本文顶部的链接下载)复制到一个单独的文件夹中。
- 步骤 2:这是一个压缩文件。选择一个临时文件夹并将所有文件复制(解压缩)到该文件夹中。
- 步骤 3:运行 Test3DChart.exe 文件。
- 步骤 4:您应该会看到一个如上图所示的应用程序。
有趣的部分
- 左键单击图表并按住左键。缓慢移动鼠标。您会看到图表正在旋转/转动。
- 将鼠标悬停在柱状图上。您会看到选定的柱状图突出显示以及该柱状图的 X、Y、Z 值。
如何使用代码 / 使用程序集的步骤
可以按照以下步骤使用此基于 WPF 的 3D 柱状图控件编写示例应用程序
- 步骤 1:使用 Visual Studio 2008 创建一个 Windows WPF 应用程序。
- 步骤 2:将程序集作为引用添加到项目中。
- 步骤 3:在工具箱中,右键单击,然后选择“选择项”。选择 WPF 选项卡。然后,单击“浏览”按钮选择程序集。选择程序集后单击“确定”按钮。
- 步骤 4:现在,您将在工具箱中看到一个名为“WPF3DControl”的条目。将其拖放到主窗口或在 XAML 中编写代码以使用该控件。
- 步骤 5:选择控件。然后,右键单击并选择“属性”菜单选项。
- 步骤 6:添加/修改/删除 X、Y 和 Z 属性(如果需要)以满足您的要求。
- 步骤 7:运行您的应用程序。您应该会看到交互式 3D 柱状图。
附加细节
- 您可以使用下面的 XAML 代码快速设置控件并运行测试
- 如何在设计时修改 X、Y、Z 值?
- 第一种选择:最简单的方法是在设计模式下在 XAML 文件中添加 X、Y 和 Z 值,如下所示。您会注意到下面的 XAML 文件中的
XValuesInput
、YValuesInput
和ZValuesInput
(粗体显示) - 第二种选择:从工具箱窗口拖放此 3D 图表控件。右键单击以选择“属性”。在那里找到属性
XValuesInput
、YValuesInput
和ZValuesInput
。将它们更新为您想要的值。 - 第三种选择:第三种方法是使用任何 .NET 兼容语言代码。这是一个 C# 示例
- 如何将文本框(或任何其他)控件数据绑定到此 WPF 3D 图表,以便在我们在文本框中输入值时图表会更新?
<Window x:Class="Test3DChart.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF 3D Chart Demo" Height="594" Width="618"
xmlns:my="clr-namespace:Wpf3DChartTutorial;assembly=Wpf3DChartTutorial"
WindowState="Maximized" WindowStartupLocation="Manual"
ResizeMode="CanResizeWithGrip">
<Grid Height="563" Name="grid1" Width="602"
HorizontalAlignment="Left"
VerticalAlignment="Top" ClipToBounds="True">
<my:WPF3DChart Margin="0,5,0,136" Name="wPF3DChart1"
ClipToBounds="True" HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch" MinWidth="100" />
</Grid>
</Window>
有三种方法可以做到这一点
<Window x:Class="Test3DChart.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF 3D Chart Demo" Height="594" Width="618"
xmlns:my="clr-namespace:Wpf3DChartTutorial;assembly=Wpf3DChartTutorial"
WindowState="Maximized" WindowStartupLocation="Manual"
ResizeMode="CanResizeWithGrip">
<Grid Height="563" Name="grid1" Width="602"
HorizontalAlignment="Left" VerticalAlignment="Top"
ClipToBounds="True">
<my:WPF3DChart Margin="0,5,0,136" Name="wPF3DChart1"
ClipToBounds="True" HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch" MinWidth="100"
XValuesInput="One,Two,Three,Four,Five"
YValuesInput="2, 4, 6, 8, 4,9, 8, 10, 7, 6,11,15,12,10, 9"
ZValuesInput="Comp A,Comp B,Comp C" />
</Grid>
</Window>
wPF3DChart1.XValuesInput = "One,Two,Three,Four,Five"; // X Values Input
wPF3DChart1.YValuesInput = "2, 4, 6, 8, 4,9, 8, 10, 7, 6,11,15,12,10, 9";
// Y Values Input
wPF3DChart1.ZValuesInput = "Comp A,Comp B,Comp C"; // Z Values Input
数据绑定非常简单,因为 3D 柱状图公开的所有属性都是依赖属性。如以下示例代码所示,首先声明 Binding
类对象。然后,附加源。最后一步是使用目标类上的 SetBinding
方法调用进行绑定。如果您正在寻找详细的示例,请下载 Test3DChart 项目的源代码。
Binding XValueBinding = new Binding("Text");
XValueBinding.Source = textBox1; // Binding the Source
XValueBinding.Mode = BindingMode.TwoWay;
// Binding the target
wPF3DChart1.SetBinding(WPF3DChart.XValuesInputProperty, XValueBinding);
WPF 3D 几何图形基础
WPF 几何坐标系将屏幕划分为如下所示
如上图所示,正 X 从原点 (0,0,0) 开始到 X,正 Y 从点 (0,0,0) 开始到 Y,正 Z 从点 (0,0,0) 开始到 Z。反之亦然,相反的方向分别是负 X、负 Y 和负 Z。理解这一点对于在所需位置绘制我们的 3D 几何模型很有用。
为了让学习 WPF 更有趣,请设想您正在导演一部电影。想象一下您需要哪些基本物品:摄像机、灯光、服装和演员。这里也一样——您将需要摄像机、灯光、画笔和 3D 几何模型(即演员)。WPF 为您提供不同类型的灯光,允许您选择摄像机位置(您是导演!),提供不同种类的画笔,并提供一个库来绘制 3D 几何图形。
理解如何绘制 3D 几何图形(演员)很重要。您可以绘制的基本模型是三角形。这是因为三角形是最简单的平面。要绘制三角形,您需要在坐标系中绘制 3 个点。这些点称为三角形位置。然后,您必须添加一个法线。法线是垂直于三角形的向量。给定三角形的 3 个顶点位置,计算法线并不困难。我在此项目中提供了 CalculateNormal
方法,它将计算给定三角形的 3 个顶点位置的法线。有了这些信息,您可以创建一个 WPF MeshGeometry3D
对象。然后,您所要做的就是创建适当的画笔来绘制 MeshGeometry3D
。您可以参考下面提供的 DrawRect
方法源代码来理解绘制矩形(组合两个三角形)的过程。
接下来,您所要做的就是向场景中添加摄像机和灯光。
绘制所有几何图形后,您需要将几何图形(或几何图形组)添加到 ModelVisual3D
对象中。ViewPort3D
是将包含所有绘图的类对象。因此,ModelVisual3D
对象需要添加到 ViewPort3D
对象中。然后,可以根据您的需要将其添加到 Grid
或 Window
中。您可以参考下面提供的 Initialize
方法源代码作为示例。
控件代码描述
此解决方案中有两个类。它们是 WPF3DChart
和 HitDetails
。
HitDetails
是一个带有 Dictionary
的小类。当用户将鼠标悬停在条形图上时,Dictionary
存储几何图形和 (x,y,z) 坐标以提供命中文本。
WPF3DChart
是渲染控件的类。我将介绍 WPF3DChar
类中的重要方法。
属性 XValuesInput
:这是一个 WPF 依赖属性,用于接收 3D 柱状图的 X 值。要实现依赖属性,类需要派生自 DependencyObject
。由于这是一个自定义控件,因此该类已经如此。属性需要注册为依赖属性,如第一行所示。我们需要像往常一样为该属性定义 setter 和 getter,但有一个不同之处。这里我们不需要将值设置为类私有变量,而是需要调用基类方法 GetValue
/SetValue
。就是这样。其他一切都由框架处理。
public static DependencyProperty XValuesInputProperty =
DependencyProperty.Register("XValuesInput",
typeof(string),
typeof(WPF3DChart),
new FrameworkPropertyMetadata( "",
FrameworkPropertyMetadataOptions.AffectsMeasure,
new PropertyChangedCallback(OnXValueChanged)));
public string XValuesInput
{
get
{
return (string)this.GetValue(XValuesInputProperty);
}
set
{
this.SetValue(XValuesInputProperty, value);
}
}
方法 DrawRect
:顾名思义,此方法在 3D 空间中绘制一个矩形。要绘制矩形,我们需要绘制两个相邻的三角形。为此,我们需要使用 WPF 提供的 MeshGeometry3D
类。要绘制矩形,我们需要添加 Position
、TriangleIndices
、Normal
和 Brush
。Opacity
是一个参数,其值范围从 0.0 到 1.0。此不透明度参数决定了您的绘图将有多透明。一旦我们将上述所有内容添加到 GeometryModel3D
中,我们就完成了绘图。您可能想知道为什么我们需要四个三角形而不是两个来绘制矩形。WPF 需要您指定矩形的两个侧面。否则,在旋转/旋转绘图时,另一侧将不可见。
/// <summary>
/// This method is used to calculate a rectangle
/// taking input Points, Bar Color and Opacity.
/// </summary>
/// <param name="P0"></param>
/// <param name="P1"></param>
/// <param name="P2"></param>
/// <param name="P3"></param>
/// <param name="BarColor"></param>
/// <param name="OpacityIndex"></param>
/// <returns></returns>
private GeometryModel3D DrawRect(Point3D P0, Point3D P1, Point3D P2, Point3D P3,
Color BarColor, double OpacityIndex)
{
MeshGeometry3D side0Plane = new MeshGeometry3D();
//////////////////////////////////////////////////////
// Drawing two triangles for every rectangle that //
// is to be formed. Hence, we add positions, //
// triangle indices, and normals //
//////////////////////////////////////////////////////
// Adding Positions
side0Plane.Positions.Add(P0);
side0Plane.Positions.Add(P1);
side0Plane.Positions.Add(P2);
side0Plane.Positions.Add(P3);
// Adding triangle indices
side0Plane.TriangleIndices.Add(0);
side0Plane.TriangleIndices.Add(1);
side0Plane.TriangleIndices.Add(2);
side0Plane.TriangleIndices.Add(2);
side0Plane.TriangleIndices.Add(1);
side0Plane.TriangleIndices.Add(0);
side0Plane.TriangleIndices.Add(0);
side0Plane.TriangleIndices.Add(2);
side0Plane.TriangleIndices.Add(3);
side0Plane.TriangleIndices.Add(3);
side0Plane.TriangleIndices.Add(2);
side0Plane.TriangleIndices.Add(0);
// Adding normals
Vector3D normal = CalculateNormal(P2, P1, P0);
side0Plane.Normals.Add(normal);
side0Plane.Normals.Add(normal);
side0Plane.Normals.Add(normal);
normal = CalculateNormal(P0, P1, P2);
side0Plane.Normals.Add(normal);
side0Plane.Normals.Add(normal);
side0Plane.Normals.Add(normal);
normal = CalculateNormal(P3, P2, P0);
side0Plane.Normals.Add(normal);
side0Plane.Normals.Add(normal);
side0Plane.Normals.Add(normal);
normal = CalculateNormal(P0, P2, P3);
side0Plane.Normals.Add(normal);
side0Plane.Normals.Add(normal);
side0Plane.Normals.Add(normal);
// Atlast brush.
MaterialGroup plane0MatGroup = new MaterialGroup();
SolidColorBrush plane0Brush = new SolidColorBrush(BarColor);
plane0Brush.Opacity = DEFAULT_BRUSH_OPACITY;
plane0Brush.Opacity = OpacityIndex;
DiffuseMaterial plane0Material = new DiffuseMaterial(plane0Brush);
// Create the geometry to be added to the viewport
plane0MatGroup.Children.Add(plane0Material);
GeometryModel3D plane0Geometry =
new GeometryModel3D(side0Plane, plane0MatGroup);
return plane0Geometry;
}
方法 Draw3DBar
:这是绘制条形图的方法。此方法使用 DrawRect
来完成此任务。此方法计算 3D 条形图坐标。然后,它调用 DrawRect
来绘制其图形。此方法还做了一件重要的事情,即用条形图几何图形和 (x,y,z) 值填充 Dictionary
。我将在 OnRightMouseClick
事件处理程序中解释其用法。
/// <summary>
/// This methods draws 3D bars getting X,Y,Z values,
// Point to start, height, width and BarColor.
/// </summary>
/// <param name="XItem"></param>
/// <param name="YItem"></param>
/// <param name="ZItem"></param>
/// <param name="PointToStart"></param>
/// <param name="Height"></param>
/// <param name="Width"></param>
/// <param name="BarColor"></param>
/// <returns></returns>
private List<ModelVisual3D> Draw3DBar( string XItem,
string YItem, string ZItem, Point3D PointToStart,
double Height, double Width, Color BarColor)
{
/////////////////////////////////////////////////////////////////
// This method first calculates the 8 points P0 to P7 //
// required to draw the bar. Then it calculates //
// the points to write the Hit Text. I.e., when the bar //
// is hit by a hovering mouse, we need to //
// display the X,Y,Z values for that bar. So this point P00 //
// to P33 is calculated to draw the text //
// during hit text. We store this information //
// in a seperate class called HitDetails with the geometry //
// as the hit key. Then we draw the bars //
// using the DrawRect method. //
/////////////////////////////////////////////////////////////////
modelArray = new List<ModelVisual3D>();
// Points are calculated to draw the bar
Point3D P0 = new Point3D(PointToStart.X, PointToStart.Y, PointToStart.Z);
Point3D P1 = new Point3D(PointToStart.X + Width,
PointToStart.Y, PointToStart.Z);
Point3D P2 = new Point3D(PointToStart.X + Width,
PointToStart.Y, PointToStart.Z - Width);
Point3D P3 = new Point3D(PointToStart.X, PointToStart.Y, PointToStart.Z - Width);
Point3D P4 = new Point3D(PointToStart.X, PointToStart.Y + Height, PointToStart.Z);
Point3D P5 = new Point3D(PointToStart.X + Width,
PointToStart.Y + Height, PointToStart.Z);
Point3D P6 = new Point3D(PointToStart.X + Width,
PointToStart.Y + Height, PointToStart.Z - Width);
Point3D P7 = new Point3D(PointToStart.X, PointToStart.Y + Height,
PointToStart.Z - Width);
ModelVisual3D myModelVisual = new ModelVisual3D();
Model3DGroup myModelGroup = new Model3DGroup();
// Points are calculated to draw the hit text
Point3D P00 = new Point3D(xStartingPoint, P4.Y + 0.3, P4.Z - Width / 2.0);
Point3D P11 = new Point3D(P4.X, P4.Y + 0.3, P4.Z - Width / 2.0);
Point3D P22 = new Point3D(P4.X, P4.Y, P4.Z - Width / 2.0 - 0.3);
Point3D P33 = new Point3D(xStartingPoint, P4.Y, P4.Z - Width / 2.0 - 0.3);
Point3D PtToWrite = new Point3D(P4.X, P4.Y + LITTLE_ABOVE,
P4.Z - Width / 2.0 - 0.3);
HitDetails newHitDetails = new HitDetails(XItem, YItem,
ZItem, P00, P11, P22, P33,
PtToWrite, PointToStart, Width, Height, BarColor);
GeometryModel3D rectVisual = DrawRect(P0, P1, P5, P4,
BarColor, DEFAULT_BRUSH_OPACITY);
listOfHitPoints.Add(rectVisual, newHitDetails);
myModelGroup.Children.Add(rectVisual);
// Lets draw the rectangles that form a full bar
// convering its top and bottom
Color LightColor = BarColor;
LightColor.A -= DROP_COLOR_BRIGHTNESS;
rectVisual = DrawRect(P1, P2, P6, P5, LightColor, DEFAULT_BAR_OPACITY);
listOfHitPoints.Add(rectVisual, newHitDetails);
myModelGroup.Children.Add(rectVisual);
rectVisual = DrawRect(P3, P2, P6, P7, BarColor, DEFAULT_BAR_OPACITY);
listOfHitPoints.Add(rectVisual, newHitDetails);
myModelGroup.Children.Add(rectVisual);
rectVisual = DrawRect(P0, P3, P7, P4, LightColor, DEFAULT_BAR_OPACITY);
listOfHitPoints.Add(rectVisual, newHitDetails);
myModelGroup.Children.Add(rectVisual);
LightColor.A -= DROP_MORE_COLOR_BRIGHTNESS;
rectVisual = DrawRect(P4, P5, P6, P7, LightColor, DEFAULT_BRUSH_OPACITY);
listOfHitPoints.Add(rectVisual, newHitDetails);
myModelGroup.Children.Add(rectVisual);
rectVisual = DrawRect(P0, P1, P2, P3, BarColor, DEFAULT_BRUSH_OPACITY);
listOfHitPoints.Add(rectVisual, newHitDetails);
myModelGroup.Children.Add(rectVisual);
myModelVisual.Content = myModelGroup;
modelArray.Add(myModelVisual);
// return the model to be added to the main ViewPort3D
return modelArray;
}
方法 DrawXYZWithMarkings
:此方法绘制 X 平面、Y 平面和 Z 平面。此方法计算长度和宽度平面。它根据这些计算绘制 X、Y 和 Z 平面。三个 for
循环用于在平面上绘制标记。这些标记用于指定 X、Y 和 Z 平面中的值。
/// <summary>
/// This method is used to Draw the XZ and Y axis in the 3D bar chart
/// </summary>
/// <param name="XItems"></param>
/// <param name="NumberOfXItems"></param>
/// <param name="YIncrement"></param>
/// <param name="NumberOfYItems"></param>
/// <param name="ZItems"></param>
/// <param name="NumberOfZItems"></param>
/// <param name="BarWidth"></param>
/// <param name="PlaneXColor"></param>
/// <param name="PlaneYColor"></param>
/// <param name="XMarkingColor"></param>
/// <param name="YMarkingColor"></param>
/// <param name="ZMarkingColor"></param>
/// <returns></returns>
private List<ModelVisual3D> DrawXYZWithMarkings( string[] XItems,
int NumberOfXItems, double YIncrement,
int NumberOfYItems, string[] ZItems, int NumberOfZItems,
double BarWidth, Color PlaneXColor, Color PlaneYColor,
Color XMarkingColor, Color YMarkingColor, Color ZMarkingColor)
{
///////////////////////////////////////////////////////////
// This method draws the XZ and Y axis. Not only that, //
// this method draws the X, Y and Z markings. //
// These are important to know the values //
// of the bars to the user //
///////////////////////////////////////////////////////////
List<ModelVisual3D> listXYZ = new List<ModelVisual3D>();
Model3DGroup myModelGroup = new Model3DGroup();
ModelVisual3D myModelVisual = new ModelVisual3D();
myModelVisual.Content = myModelGroup;
listXYZ.Add(myModelVisual);
NumberOfXItems++;
NumberOfYItems++;
NumberOfZItems++;
xInGraph.Clear();
zInGraph.Clear();
yInGraph = 0;
// We calculate One X,Y and Z units.
// Also we calculate the starting points.
double Width = chartSize.Width / 10.0;
double Height = chartSize.Height / 10.0;
double StartX = -1.0 * Width / 2.0 + 0.5;
double StartY = -1.0 * Height / 2.0 + 0.5;
double EndX = Width / 2.0;
double EndY = -1.0 * Height / 2.0;
double EndYY = Height / 2.0;
xStartingPoint = StartX;
double OneUnitX = (Width - 0.5) / NumberOfXItems;
double OneUnitY = (Height - 0.5) / NumberOfYItems;
double OneUnitZ = BarWidth * SPACE_BETWEEN_BARS;
double ZEnd = (BarWidth * NumberOfZItems *
(Z_ADJUST) * (SPACE_BETWEEN_BARS));
centreX = Width / 2.0;
centreY = Height / 2.0;
centreZ = ZEnd / 2.0;
//Compute the points to draw the XZ and Y axis.
Point3D PXpoint0 = new Point3D(StartX, StartY, 0);
Point3D PXpoint1 = new Point3D(EndX, StartY, 0);
Point3D PXpoint2 = new Point3D(EndX, StartY, ZEnd);
Point3D PXpoint3 = new Point3D(StartX, StartY, ZEnd);
// Draw the Plane XZ
GeometryModel3D planeX = DrawRect(PXpoint3, PXpoint0,
PXpoint1, PXpoint2, PlaneXColor, 0.9);
Point3D PYpoint0 = new Point3D(StartX, StartY, 0);
Point3D PYpoint1 = new Point3D(StartX, StartY, ZEnd);
Point3D PYpoint2 = new Point3D(StartX, EndYY, ZEnd);
Point3D PYpoint3 = new Point3D(StartX, EndYY, 0);
// Draw the Plane Y
GeometryModel3D planeY = DrawRect(PYpoint0, PYpoint1,
PYpoint2, PYpoint3, PlaneYColor, 0.9);
myModelGroup.Children.Add(planeX);
myModelGroup.Children.Add(planeY);
double MarkingWidth = 0.07;
double MarkingHeight = 0.5;
double OneLetterWidth = 0.8;
double OneLetterHeight = 1.6;
// Draw X markings.
double StartXPosition = StartX + OneUnitX;
yInGraph = StartY;
for (int Counter = 0; Counter < NumberOfXItems - 1; Counter++)
{
xInGraph.Add(StartXPosition);
Point3D pMarkX0 = new Point3D(StartXPosition - MarkingWidth,
EndY + MarkingHeight, 0 - MarkingHeight);
Point3D pMarkX1 = new Point3D(StartXPosition + MarkingWidth,
EndY + MarkingHeight, 0 - MarkingHeight);
Point3D PMarkX2 = new Point3D(StartXPosition + MarkingWidth, EndY, 0);
Point3D pMarkX3 = new Point3D(StartXPosition - MarkingWidth, EndY, 0);
Point3D pToWrite = new Point3D(StartXPosition -
(XItems[Counter].Length * OneLetterWidth / 2.0),
EndY + MarkingHeight, 0 - MarkingHeight);
GeometryModel3D planeToMark = DrawRect(pMarkX0, pMarkX1,
PMarkX2, pMarkX3, XMarkingColor, 0.985);
myModelGroup.Children.Add(planeToMark);
double LetterLength = XItems[Counter].Length * OneLetterWidth;
planeToMark = WriteText(pToWrite, LetterLength,
OneLetterHeight, XItems[Counter]);
myModelGroup.Children.Add(planeToMark);
StartXPosition += OneUnitX;
}
// Draw Y markings
double StartYPosition = StartY + OneUnitY;
double StartYItem = YIncrement;
for (int Counter = 0; Counter < NumberOfYItems - 1; Counter++)
{
Point3D pMarkY0 = new Point3D(StartX - MarkingHeight / 2.0,
StartYPosition - MarkingWidth, 0);
Point3D pMarkY1 = new Point3D(StartX - MarkingHeight / 2.0,
StartYPosition + MarkingWidth, 0);
Point3D PMarkY2 = new Point3D(StartX + MarkingHeight / 2.0,
StartYPosition + MarkingWidth,
0 - MarkingHeight);
Point3D pMarkY3 = new Point3D(StartX + MarkingHeight / 2.0,
StartYPosition - MarkingWidth,
0 - MarkingHeight);
GeometryModel3D planeToMark = DrawRect(pMarkY0, pMarkY1,
PMarkY2, pMarkY3, YMarkingColor, 0.985);
myModelGroup.Children.Add(planeToMark);
StringBuilder YItemToWrite = new StringBuilder();
YItemToWrite.AppendFormat("{0,2:f}", StartYItem);
double LetterLength = YItemToWrite.Length * OneLetterWidth;
Point3D pToWrite = new Point3D(StartX - MarkingHeight / 2.0 - LetterLength,
StartYPosition - MarkingWidth, 0);
planeToMark = WriteText(pToWrite, LetterLength,
OneLetterHeight, YItemToWrite.ToString());
myModelGroup.Children.Add(planeToMark);
StartYPosition += OneUnitY;
StartYItem += YIncrement;
}
// Draw Z markings
double StartZPosition = 0 - OneUnitZ;
for (int Counter = 0; Counter < NumberOfZItems - 1; Counter++)
{
zInGraph.Add(StartZPosition);
Point3D pMarkX0 = new Point3D(EndX - MarkingHeight / 2.0, EndY,
StartZPosition + MarkingWidth);
Point3D pMarkX1 = new Point3D(EndX + MarkingHeight / 2.0,
EndY + MarkingHeight / 2.0,
StartZPosition + MarkingWidth);
Point3D PMarkX2 = new Point3D(EndX + MarkingHeight / 2.0,
EndY + MarkingHeight / 2.0,
StartZPosition - MarkingWidth);
Point3D pMarkX3 = new Point3D(EndX - MarkingHeight / 2.0, EndY,
StartZPosition - MarkingWidth);
Point3D pToWrite = new Point3D(EndX - MarkingHeight / 2.0 + 1.0,
EndY, StartZPosition + MarkingWidth);
GeometryModel3D planeToMark = DrawRect(pMarkX0, pMarkX1,
PMarkX2, pMarkX3, XMarkingColor, 0.985);
myModelGroup.Children.Add(planeToMark);
double LetterLength = ZItems[Counter].Length * OneLetterWidth;
planeToMark = WriteText(pToWrite, LetterLength,
OneLetterHeight, ZItems[Counter]);
myModelGroup.Children.Add(planeToMark);
StartZPosition -= OneUnitZ;
}
GeometryModel3D titleToDisplay = WriteText(new Point3D((chartSize.Width / -20.0),
(chartSize.Height / -20.0) - 7.0, 2.5),
OneLetterWidth * 2.0 * ChartTitle.Length,
OneLetterHeight * 2.0, ChartTitle);
myModelGroup.Children.Add(titleToDisplay);
return listXYZ;
}
方法 Draw3DChart
:此方法是 Initialize
调用的主方法。此方法获取 X、Y 和 Z 值的输入。然后,此方法通过调用 Draw3DBar
方法绘制条形图。最后,此方法调用 DrawXYZWithMarkings
绘制 X、Y 和 Z 平面。
#region "This method is the main method that draws the 3D bar chart "
/// <summary>
/// This method draws the 3D bar chart.
/// </summary>
/// <param name="XItems"></param>
/// <param name="YItems"></param>
/// <param name="ZItems"></param>
/// <param name="xStartingPointColor"></param>
/// <param name="YPlaneColor"></param>
/// <param name="ZPlaneColor"></param>
/// <param name="BarWidth"></param>
/// <returns></returns>
private List<ModelVisual3D> Draw3DChart(string[] XItems, double[] YItems,
string[] ZItems, Color xStartingPointColor, Color YPlaneColor,
Color[] ZPlaneColor, double BarWidth)
{
List<ModelVisual3D> modelsToAdd = new List<ModelVisual3D>();
// This If block is just for understanding sake.
// The compiler will remove this during release build.
if (XItems.Length * ZItems.Length != YItems.Length ||
ZPlaneColor.Length <= ZItems.Length)
{
// We will display less items in the graph then..
}
// If there are anything to draw then enter in this if block
if (YItems.Length > 0 && XItems.Length > 0 && ZItems.Length > 0)
{
// Calculate the max Y point
double MaxY = YItems[0];
for (int Counter = 1; Counter < YItems.Length; Counter++)
{
if (MaxY < YItems[Counter])
{
MaxY = YItems[Counter];
}
}
// Calculate the Height of the longest bar and also calculate On Y Unit
double Ht = chartSize.Height / 30.0;
double OneYUnit = ((chartSize.Height / 10.0)) / MaxY;
ComputeXZInGraph(XItems.Length, YItems.Length, BarWidth);
OneYUnit = ((chartSize.Height / 10.0) - (chartSize.Height / (10 * Ht))) / MaxY;
int CounterY = 0;
// Draw the Bars one by one based on values
// computed in the method ComputeXZInGraph above
for (int CounterZ = 0; CounterZ < ZItems.Length; CounterZ++)
{
for (int CounterX = 0; CounterX < XItems.Length; CounterX++)
{
List<ModelVisual3D> modelBars =
Draw3DBar(XItems[CounterX], YItems[CounterY].ToString(),
ZItems[CounterZ],
new Point3D(xInGraph[CounterX] - BarWidth / 2.0,
yInGraph,
zInGraph[CounterZ] + BarWidth / 2.0),
OneYUnit * YItems[CounterY], BarWidth,
ZPlaneColor[CounterZ]);
List<ModelVisual3D>.Enumerator enumModels =
modelBars.GetEnumerator();
while (enumModels.MoveNext())
{
modelsToAdd.Add(enumModels.Current);
}
CounterY++;
}
}
// Now draw the XZ plane and Y plane
Ht = chartSize.Height / 30.0;
OneYUnit = ((chartSize.Height / 10.0)) / MaxY;
List<ModelVisual3D> modelBars1 = DrawXYZWithMarkings(XItems,
XItems.Length, MaxY / (Ht - 1),
(int)Ht, ZItems, ZItems.Length,
BarWidth, XAxisColorItem,
YAxisColorItem,
Colors.Black,
Colors.Black,
Colors.Black);
List<ModelVisual3D>.Enumerator enumModels1 = modelBars1.GetEnumerator();
while (enumModels1.MoveNext())
{
modelsToAdd.Add(enumModels1.Current);
}
}
// Return the model so that it can be added to the main Viewport3D
return modelsToAdd;
}
方法 Initialize
:此方法是 OnInitialized
基类覆盖调用的第一个方法。此方法为 3D 图表的首次显示设置所有默认值。
/// <summary>
/// This method is called during initialization
/// to create the 3D Bar chart and then its called to update
/// </summary>
private void Initialize()
{
if (chartSize.Height == 0 || chartSize.Width == 0) return;
Initializing = true;
zCameraDistance = 175 + 5 * ZItems.Length;
// We clear the 3D drawings, if any previously added
mainViewPort.Children.Clear();
mainViewPort.Children.Add(lightModelVisual);
listOfHitPoints.Clear();
this.InvalidateVisual();
List<ModelVisual3D> retValue;
List<ModelVisual3D>.Enumerator enumList;
SelectedHit = null;
modelForHitText = new ModelVisual3D();
OneLetterWidthForHitText = 1.1;
OneLetterHeightForHitText = 1.7;
PointToWrite0 = new Point3D();
PointToWrite1 = new Point3D();
PointToWrite2 = new Point3D();
PointToWrite3 = new Point3D();
yPlane2DPoint0 = new Point(0, 1);
yPlane2DPoint1 = new Point(1, 1);
yPlane2DPoint2 = new Point(1, 0);
yPlane2DPoint3 = new Point(0, 0);
// We draw 3D cart here.
retValue = Draw3DChart(XItems, YItems, ZItems, Colors.Yellow,
Colors.Cyan, ZPlaneColors,
chartSize.Width/(XItems.Length * 2.2 * 10));
enumList = retValue.GetEnumerator();
// Add the resultant geometry to the main viewport.
while (enumList.MoveNext())
{
mainViewPort.Children.Add(enumList.Current);
}
retValue.Clear();
// Compute the camera position.
ComputeCameraPosition();
Initializing = false;
// Display the graph
this.InvalidateVisual();
}
鼠标移动和左键按下处理程序:这是一个重要的处理程序,负责 3D 几何图形的旋转。此方法计算移动,即在按下左键时鼠标相对于 Y 或 X 轴移动了多远。然后,它调用 ComputeCameraPosition
计算 Sphere
中的位置。嗯,更详细地解释一下:最初,我们的工作只是绘制几何图形,点亮场景,然后放置摄像机。WPF 需要这三个基本步骤才能在屏幕上显示 3D 图像。完成这些操作后,如果我们想旋转/旋转几何图形,我们所要做的就是移动摄像机。想象一下将所有这些几何图形放在您家的餐桌上。然后,想象一下在摄像机显示器中以球形平面移动来查看它。
我只是简单地使用了标准的八年级数学几何公式“球体上的任何点”来计算摄像机位置
/// <summary>
/// This method is called when the mouse is moved.
/// </summary>
/// <param name="sender"></param>
/// <param name="me"></param>
private void OnMouseMove(object sender, MouseEventArgs me)
{
if (me.LeftButton == MouseButtonState.Pressed && leftButtonDown == true)
{
// If the left mouse button is pressed, then we calculate the Camera position,
// then invalidate the current drawing for a redraw
Point retPoint = me.GetPosition(pThis);
double MouseX = retPoint.X;
double MouseY = retPoint.Y;
if (MouseX == MouseXFirstDown && MouseY == MouseYFirstDown)
{
return;
}
// To calculate how much the mouse moved X position
if (MouseXFirstDown != MouseX)
{
XAngle += (MouseX - MouseXFirstDown) / 2;
MouseXFirstDown = MouseX;
}
// To calculate how much the mouse mofed Y position
if (MouseYFirstDown != MouseY)
{
YAngle += (MouseY - MouseYFirstDown) / 2;
MouseYFirstDown = MouseY;
}
ComputeCameraPosition();
this.InvalidateVisual();
}
Point pt = me.GetPosition((UIElement)sender);
HitTestResult result = VisualTreeHelper.HitTest(mainGrid, pt);
// If there is a hit, then draw the bar slightly bigger and display the Hit text
// (as we stored thia already while drawing
if (result != null)
{
RayHitTestResult res = result as RayHitTestResult;
if (res != null)
{
GeometryModel3D geoMod = res.ModelHit as GeometryModel3D;
HitDetails myHitDetails;
if (listOfHitPoints.TryGetValue(geoMod, out myHitDetails))
{
SelectedHit = myHitDetails;
this.InvalidateVisual();
return;
}
}
}
SelectedHit = null;
this.InvalidateVisual();
}
/// <summary>
/// This methodis called when the mouse button is down.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnMouseDown(object sender, MouseEventArgs e)
{
// If the left mouse button is down, we compute the mouse first pressed point
if (e.LeftButton == MouseButtonState.Pressed)
{
leftButtonDown = true;
Point retPoint = e.GetPosition(pThis);
MouseXFirstDown = retPoint.X;
MouseYFirstDown = retPoint.Y;
}
else
{
leftButtonDown = false;
}
}
/// <summary>
/// This method is called when Left mouse button is up.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void MouseLeftButtonUpEventHandler(object sender, MouseEventArgs e)
{
// We reset the boolean so that we dont move the graph when the user is not
// pressing the left mouse down
leftButtonDown = false;
}
方法 OnRender
:最后但并非最不重要的一点是,OnRender
只是修改了摄像机位置。如果用户右键单击显示的条形图,它会绘制 HitPoint
。
/// <summary>
/// This method is called when rendering is required.
/// </summary>
/// <param name="drawingContext"></param>
protected override void OnRender(DrawingContext drawingContext)
{
if (!Initializing)
{
// Update the CAmera position
persptCamera.LookDirection = cameraLookDirection;
persptCamera.Position = cameraPosition;
// If the user's mouse is on one of the 3D bar.
//then enhance the bar and display its values.
if (null != SelectedHit && (PrevSelectedHit == null ||
0 != PrevSelectedHit.StringToDisplay.CompareTo(SelectedHit.StringToDisplay)))
{
if (geometryForHitText != null)
{
geometryForHitText = null;
modelForHitText.Content = null;
mainViewPort.Children.Remove(modelForHitText);
}
if (modelsForHitTest != null)
{
List<ModelVisual3D>.Enumerator enumModelsI =
modelsForHitTest.GetEnumerator();
while (enumModelsI.MoveNext())
{
mainViewPort.Children.Remove(enumModelsI.Current);
}
}
LetterLengthForHitText =
SelectedHit.StringToDisplay.Length * OneLetterWidthForHitText;
Point3D ptToWRite = SelectedHit.P2;
ptToWRite.Y += LITTLE_ABOVE;
geometryForHitText = WriteText(SelectedHit.PointToWrite,
LetterLengthForHitText,
OneLetterHeightForHitText,
SelectedHit.StringToDisplay);
modelsForHitTest = Draw3DBar(SelectedHit.XItem,
SelectedHit.YItem,
SelectedHit.ZItem,
SelectedHit.PointToStart,
SelectedHit.Height,
SelectedHit.Width ,
SelectedHit.BarColor);
List<ModelVisual3D>.Enumerator enumModels =
modelsForHitTest.GetEnumerator();
while (enumModels.MoveNext())
{
mainViewPort.Children.Add(enumModels.Current);
}
PrevSelectedHit = SelectedHit;
modelForHitText.Content = geometryForHitText;
mainViewPort.Children.Add(modelForHitText);
}
// Else if there is no bar selected
// remove the enhancement and Hit text.
else if (null == SelectedHit)
{
geometryForHitText = null;
modelForHitText.Content = null;
mainViewPort.Children.Remove(modelForHitText);
if (modelsForHitTest != null)
{
List<ModelVisual3D>.Enumerator enumModels =
modelsForHitTest.GetEnumerator();
while (enumModels.MoveNext())
{
mainViewPort.Children.Remove(enumModels.Current);
}
}
}
}
base.OnRender(drawingContext);
}
我认为这个控件代码为 WPF 自定义控件中的 3D 几何图形绘制提供了一些见解。
您的反馈?
反馈是改进的绝佳机制。您的反馈消息将帮助社区获得更好的文章并促进改进。所以,请告诉我您的反馈。
历史
- 2008 年 9 月 7 日 -- 初始版本。
- 2008 年 9 月 8 日 -- 上传了正确的源代码版本。添加了一个解释基础知识的部分。
- 2008 年 9 月 14 日 -- 更新了测试示例应用程序,允许用户输入 X、Y、Z 值和图表标题。
- 2008 年 9 月 19 日 -- 修改了自定义控件中的所有属性,使其成为 WPF 依赖属性。还更新了测试应用程序以绑定到依赖属性。现在,当用户输入值时,图表会动态变化。
- 2008 年 9 月 21 日 -- 修改了基于鼠标移动的 3D 条形图交互性,调整了条形图大小以适应屏幕,修复了一些错误,并添加了源代码注释。
- 2008 年 9 月 28 日 - 添加了多色 Y 轴、鼠标光标变化和鼠标灵敏度。
- 2009 年 3 月 17 日 - 添加了在主窗口以外的任何窗口中显示图表的支持。