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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.69/5 (33投票s)

2008 年 9 月 8 日

CPOL

10分钟阅读

viewsIcon

266500

downloadIcon

8130

在您的 .NET 应用程序中轻松创建 WPF 3D 柱状图。使用此 WPF 3D 柱状图自定义控件,您可以在几秒钟内创建交互式 3D 柱状图。您甚至可以数据绑定您的输入,以查看“3D 图表柱状图随输入实时变化”的效果。

SampleApp.jpg

引言

使用此 WPF3DChart 自定义控件,可以在几秒钟内创建交互式 3D“可旋转”(或“可转动”)柱状图。只需几行 XAML 或 C#(或任何 .NET 兼容语言代码)即可创建交互式、可旋转的 3D 柱状图。此外,您还可以使用 WPF 数据绑定来实现“在输入数据时实时查看 3D 图表变化”的效果。

今天你想做什么?

  1. 如果您想快速查看演示,请按照“查看演示”部分的简单说明进行操作。
  2. 如果您想在示例项目中测试此 WPF3DChart 控件,请按照“如何使用代码 / 使用程序集的步骤”部分的说明进行操作。
  3. 如果您想学习如何编写此类控件,请从“WPF 3D 几何图形基础”部分开始阅读。

查看演示

  • 步骤 1:将二进制 zip 文件(使用本文顶部的链接下载)复制到一个单独的文件夹中。
  • 步骤 2:这是一个压缩文件。选择一个临时文件夹并将所有文件复制(解压缩)到该文件夹中。
  • 步骤 3:运行 Test3DChart.exe 文件。
  • 步骤 4:您应该会看到一个如上图所示的应用程序。

有趣的部分

  1. 左键单击图表并按住左键。缓慢移动鼠标。您会看到图表正在旋转/转动。
  2. 将鼠标悬停在柱状图上。您会看到选定的柱状图突出显示以及该柱状图的 X、Y、Z 值。

如何使用代码 / 使用程序集的步骤

可以按照以下步骤使用此基于 WPF 的 3D 柱状图控件编写示例应用程序

  • 步骤 1:使用 Visual Studio 2008 创建一个 Windows WPF 应用程序。
  • 步骤 2:将程序集作为引用添加到项目中。
  • 步骤 3:在工具箱中,右键单击,然后选择“选择项”。选择 WPF 选项卡。然后,单击“浏览”按钮选择程序集。选择程序集后单击“确定”按钮。
  • 步骤 4:现在,您将在工具箱中看到一个名为“WPF3DControl”的条目。将其拖放到主窗口或在 XAML 中编写代码以使用该控件。
  • 步骤 5:选择控件。然后,右键单击并选择“属性”菜单选项。
  • 步骤 6:添加/修改/删除 X、Y 和 Z 属性(如果需要)以满足您的要求。
  • 步骤 7:运行您的应用程序。您应该会看到交互式 3D 柱状图。

附加细节

  1. 您可以使用下面的 XAML 代码快速设置控件并运行测试
  2. <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>
  3. 如何在设计时修改 X、Y、Z 值?
  4. 有三种方法可以做到这一点

    • 第一种选择:最简单的方法是在设计模式下在 XAML 文件中添加 X、Y 和 Z 值,如下所示。您会注意到下面的 XAML 文件中的 XValuesInputYValuesInputZValuesInput(粗体显示)
    • <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>
    • 第二种选择:从工具箱窗口拖放此 3D 图表控件。右键单击以选择“属性”。在那里找到属性 XValuesInputYValuesInputZValuesInput。将它们更新为您想要的值。
    • 第三种选择:第三种方法是使用任何 .NET 兼容语言代码。这是一个 C# 示例
    • 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
  5. 如何将文本框(或任何其他)控件数据绑定到此 WPF 3D 图表,以便在我们在文本框中输入值时图表会更新?
  6. 数据绑定非常简单,因为 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 几何坐标系将屏幕划分为如下所示

WPF3DSpace.JPG

如上图所示,正 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 对象中。然后,可以根据您的需要将其添加到 GridWindow 中。您可以参考下面提供的 Initialize 方法源代码作为示例。

控件代码描述

此解决方案中有两个类。它们是 WPF3DChartHitDetails

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 类。要绘制矩形,我们需要添加 PositionTriangleIndicesNormalBrushOpacity 是一个参数,其值范围从 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 日 - 添加了在主窗口以外的任何窗口中显示图表的支持。
© . All rights reserved.