环境: VS2022+.Net Framework4.8+HelixToolkit3.12
一、HelixToolkit包安装
- 通过NuGet包管理器搜索并安装 HelixToolkit 相关的包。
二、将控件添加到工具箱
- 在项目引用好库之后,就可以把它添加到当前项目的工具箱里了。
-
打开工具箱:
在 Visual Studio 菜单栏选择“视图” -> “工具箱”,或使用快捷键 `Ctrl+Alt+X。 -
添加新选项卡(可选):
在工具箱空白处右键,选择“添加选项卡”,给这个分组起个名字,比如Helix Toolkit,方便管理。 -
选择项:
选中你新建的Helix Toolkit选项卡,右键选择“选择项...”。 -
浏览添加:
在弹出的“选择工具箱项”对话框中,点击“浏览...”按钮然后找到你的项目文件夹,我的是
C:\Users\Lenovo\.nuget\packages\helixtoolkit.wpf\3.1.2\lib\net48目录下,选择HelixToolkit.Wpf.dll文件并打开。 -
确认添加:
在“WPF 组件”列表里,你会看到新出现的HelixViewport3D等控件,确保它们被勾选后,点击“确定。
-
打开工具箱:
完成这些步骤后,工具箱里就会出现 HelixViewport3D 等控件了。之后你就可以像拖拽普通按钮一样,把它们拖到 XAML 设计视图里使用。核心用法是用 HelixViewport3D 或 TrackballDecorator 包裹住你的 3D 场景,让鼠标交互变得非常简单
三、3D地球仪
-
球体通过细分经纬度网格构建:
- 顶点生成:
遍历纬度 φ (0 到 π) 和经度 θ (0 到 2π)
每个顶点坐标:(rcos(φ)cos(θ), rsin(φ), -rcos(φ)*sin(θ)) - 三角形面片:
每四个相邻顶点构成两个三角形
形成完整的球体网格
- 顶点生成:
相机位置使用球面坐标系 (r, θ, φ) 控制:
r (CameraR) = 相机到原点的距离(半径)
θ (CameraTheta) = 水平角度(绕Y轴旋转)
φ (CameraPhi) = 垂直角度(与水平面的夹角)纹理映射
使用 ImageBrush 加载地球纹理图片,通过 TextureCoordinates 将纹理映射到球体表面。

四、MainWindow.xaml
<Window x:Class="WpfA_3DGlobe.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfA_3DGlobe"
mc:Ignorable="d"
Title="3D地球仪" Height="450" Width="800"
WindowStartupLocation="CenterScreen"
Loaded="MainWindow_Loaded" KeyDown="MainWindow_KeyDown">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="250"/>
</Grid.ColumnDefinitions>
<!-- 右侧控制面板 -->
<StackPanel Grid.Column="1" Margin="10">
<TextBlock Text="相机位置:" Margin="0,0,0,5"/>
<!-- 显示相机坐标 -->
<Border BorderBrush="Gray" BorderThickness="1" CornerRadius="5" Padding="5" Margin="0,0,0,10">
<StackPanel>
<TextBlock Name="CameraXText" Text="X: 0.00" Margin="5,2"/>
<TextBlock Name="CameraYText" Text="Y: 0.00" Margin="5,2"/>
<TextBlock Name="CameraZText" Text="Z: 0.00" Margin="5,2"/>
<TextBlock Name="CameraDistanceText" Text="距离: 0.00" Margin="5,2" Foreground="Green" FontWeight="Bold"/>
<TextBlock Name="CameraAngleText" Text="角度: 0°" Margin="5,2" Foreground="Blue"/>
</StackPanel>
</Border>
<TextBlock Text="操作说明:" FontWeight="Bold" Margin="0,10,0,5"/>
<TextBlock Text="• 鼠标拖拽旋转地球" Margin="5,2" FontSize="11"/>
<TextBlock Text="• 键盘方向键控制视角" Margin="5,2" FontSize="11"/>
<TextBlock Text="• +/- 键缩放" Margin="5,2" FontSize="11"/>
<Separator Margin="0,10,0,10"/>
<TextBlock Text="控制器:" FontWeight="Bold" Margin="0,0,0,5"/>
<StackPanel VerticalAlignment="Bottom" HorizontalAlignment="Left">
<Button Name="buttonUp" Background="Transparent" BorderThickness="0" HorizontalAlignment="Center" Click="buttonUp_Click">
<Label Content="↑" Foreground="#0f0fff" FontSize="12" Padding="5,1" BorderThickness="1" BorderBrush="Blue" VerticalAlignment="Center" Width="18"></Label>
</Button>
<StackPanel Orientation="Horizontal" Margin="5,1,5,5">
<Button Name="buttonLeft" Background="Transparent" BorderThickness="0" HorizontalAlignment="Center" Click="buttonLeft_Click">
<Label Content="←" Foreground="#0f0fff" FontSize="12" Padding="2,1" BorderThickness="1" BorderBrush="Blue" VerticalAlignment="Center" Width="18"></Label>
</Button>
<Button Name="buttonDown" Background="Transparent" BorderThickness="0" HorizontalAlignment="Center" Margin="1,0" Click="buttonDown_Click">
<Label Content="↓" Foreground="#0f0fff" FontSize="12" Padding="5,1" BorderThickness="1" BorderBrush="Blue" VerticalAlignment="Center" Width="18"></Label>
</Button>
<Button Name="buttonRight" Background="Transparent" BorderThickness="0" HorizontalAlignment="Center" Click="buttonRight_Click">
<Label Content="→" Foreground="#0f0fff" FontSize="12" Padding="2,1" BorderThickness="1" BorderBrush="Blue" VerticalAlignment="Center" Width="18"></Label>
</Button>
</StackPanel>
</StackPanel>
<!-- 重置按钮 -->
<Button Content="重置视角" Click="ResetCamera_Click" Margin="0,10,0,0" Width="80" HorizontalAlignment="Left"/>
</StackPanel>
<!-- 左侧面板 -->
<Grid Background="#000000" MouseMove="MainViewport3D_MouseMove">
<Viewport3D Name="MainViewport3D" MouseDown="MainViewport3D_MouseDown" MouseMove="MainViewport3D_MouseMove" MouseUp="MainViewport3D_MouseUp" />
</Grid>
</Grid>
</Window>
五、MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Media.Media3D;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace WpfA_3DGlobe
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
// 三维 模型.
private Model3DGroup MainModel3Dgroup = new Model3DGroup();
// 照相机.
private PerspectiveCamera TheCamera;
// 照相机默认角度.
private double CameraPhi = Math.PI / 6.0; // 30 degrees (度)
private double CameraTheta = Math.PI / 6.0; // 30 degrees (度)
private double CameraR = 3.0;
// 点击上、下方向键,来改变CameraPhi
private const double CameraDPhi = 0.1;
// 点击左、右方向键,来改变CameraTheta.
private const double CameraDTheta = 0.1;
// 点击+、-键,来改变CameraR.
private const double CameraDR = 0.1;
// mouseDown标识
private bool IsMouseDown;
// 上一个mouseMove的点
private Point MouseMovePoint;
public MainWindow()
{
InitializeComponent();
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
// 初始化照相机的视野位置
TheCamera = new PerspectiveCamera();
//照相机的水平的视野,以度为单位。 默认值为 45。
TheCamera.FieldOfView = 60;
MainViewport3D.Camera = TheCamera;
PositionCamera();
// 初始化光源。
DefineLights();
// 初始化地球仪对象。
DefineModel();
// ModelVisual3D 提供了 Visual3D 呈现的 Model3D 对象
ModelVisual3D model_visual = new ModelVisual3D();
// 将MainModel3Dgroup 添加到 ModelVisual3D
model_visual.Content = MainModel3Dgroup;
// 添加到 Viewport3D 视窗显示。
MainViewport3D.Children.Add(model_visual);
}
private void MainWindow_KeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Up:
CameraPhi += CameraDPhi;
if (CameraPhi > Math.PI / 2.0) CameraPhi = Math.PI / 2.0;
break;
case Key.Down:
CameraPhi -= CameraDPhi;
if (CameraPhi < -Math.PI / 2.0) CameraPhi = -Math.PI / 2.0;
break;
case Key.Left:
CameraTheta += CameraDTheta;
break;
case Key.Right:
CameraTheta -= CameraDTheta;
break;
case Key.Add:
case Key.OemPlus:
CameraR -= CameraDR;
if (CameraR < CameraDR) CameraR = CameraDR;
break;
case Key.Subtract:
case Key.OemMinus:
CameraR += CameraDR;
break;
}
// 更新照相机位置.
PositionCamera();
}
#region main method
/// <summary>
/// 初始化光源
/// </summary>
private void DefineLights()
{
//光适用 light 于对象统一,而不考虑其形状的对象。
AmbientLight ambient_light = new AmbientLight(Colors.Gray);
//沿 Vector3D 指定的方向投射其效果的光对象。
DirectionalLight directional_light = new DirectionalLight(Colors.Gray, new Vector3D(-1.0, -3.0, -2.0));
MainModel3Dgroup.Children.Add(ambient_light);
MainModel3Dgroup.Children.Add(directional_light);
}
/// <summary>
/// 初始化3D对象
/// </summary>
private void DefineModel()
{
Model3DGroup globe_model = new Model3DGroup();
MainModel3Dgroup.Children.Add(globe_model);
ImageBrush globe_brush = new ImageBrush(new BitmapImage(new Uri("pack://application:,,,/Resources/globe.png")));
Material globe_material = new DiffuseMaterial(globe_brush);
MeshGeometry3D globe_mesh = null;
MakeSphere(globe_model, ref globe_mesh, globe_material, 1, 0, 0, 0, 20, 30);
}
/// <summary>
/// 创建一个球体
/// </summary>
/// <param name="model_group"></param>
/// <param name="sphere_mesh"></param>
/// <param name="sphere_material"></param>
/// <param name="radius"></param>
/// <param name="cx"></param>
/// <param name="cy"></param>
/// <param name="cz"></param>
/// <param name="num_phi"></param>
/// <param name="num_theta"></param>
private void MakeSphere(Model3DGroup model_group, ref MeshGeometry3D sphere_mesh, Material sphere_material,
double radius, double cx, double cy, double cz, int num_phi, int num_theta)
{
// Make the mesh if we must.
if (sphere_mesh == null)
{
sphere_mesh = new MeshGeometry3D();
GeometryModel3D new_model = new GeometryModel3D(sphere_mesh, sphere_material);
model_group.Children.Add(new_model);
}
double dphi = Math.PI / num_phi;
double dtheta = 2 * Math.PI / num_theta;
// Remember the first point.
int pt0 = sphere_mesh.Positions.Count;
// Make the points.
double phi1 = Math.PI / 2;
for (int p = 0; p <= num_phi; p++)
{
double r1 = radius * Math.Cos(phi1);
double y1 = radius * Math.Sin(phi1);
double theta = 0;
for (int t = 0; t <= num_theta; t++)
{
sphere_mesh.Positions.Add(new Point3D(
cx + r1 * Math.Cos(theta), cy + y1, cz + -r1 * Math.Sin(theta)));
sphere_mesh.TextureCoordinates.Add(new Point(
(double)t / num_theta, (double)p / num_phi));
theta += dtheta;
}
phi1 -= dphi;
}
// Make the triangles.
int i1, i2, i3, i4;
for (int p = 0; p <= num_phi - 1; p++)
{
i1 = p * (num_theta + 1);
i2 = i1 + (num_theta + 1);
for (int t = 0; t <= num_theta - 1; t++)
{
i3 = i1 + 1;
i4 = i2 + 1;
sphere_mesh.TriangleIndices.Add(pt0 + i1);
sphere_mesh.TriangleIndices.Add(pt0 + i2);
sphere_mesh.TriangleIndices.Add(pt0 + i4);
sphere_mesh.TriangleIndices.Add(pt0 + i1);
sphere_mesh.TriangleIndices.Add(pt0 + i4);
sphere_mesh.TriangleIndices.Add(pt0 + i3);
i1 += 1;
i2 += 1;
}
}
}
/// <summary>
/// 照相机位置
/// </summary>
private void PositionCamera()
{
// 笛卡尔坐标 计算照相机位置
double y = CameraR * Math.Sin(CameraPhi);
double hyp = CameraR * Math.Cos(CameraPhi);
double x = hyp * Math.Cos(CameraTheta);
double z = hyp * Math.Sin(CameraTheta);
TheCamera.Position = new Point3D(x, y, z);
// 获取或设置 Vector3D 定义在世界坐标查找照相机方向
TheCamera.LookDirection = new Vector3D(-x, -y, -z);
// 获取或设置 Vector3D 定义照相机的向上方向
TheCamera.UpDirection = new Vector3D(0, 1, 0);
// 更新显示相机坐标
UpdateCameraDisplay(x, y, z);
}
/// <summary>
/// 更新相机坐标显示
/// </summary>
private void UpdateCameraDisplay(double x, double y, double z)
{
// 使用 String.Format 替代字符串插值
CameraXText.Text = string.Format("X: {0:F2}", x);
CameraYText.Text = string.Format("Y: {0:F2}", y);
CameraZText.Text = string.Format("Z: {0:F2}", z);
double distance = Math.Sqrt(x * x + y * y + z * z);
CameraDistanceText.Text = string.Format("距离: {0:F2}", distance);
double angleDeg = CameraTheta * 180 / Math.PI;
double phiDeg = CameraPhi * 180 / Math.PI;
CameraAngleText.Text = string.Format("水平角: {0:F1}° 俯仰角: {1:F1}°", angleDeg, phiDeg);
}
/// <summary>
/// 重置视角
/// </summary>
private void ResetCamera_Click(object sender, RoutedEventArgs e)
{
CameraPhi = Math.PI / 6.0;
CameraTheta = Math.PI / 6.0;
CameraR = 3.0;
PositionCamera();
}
#endregion
#region 鼠标拖动
private void MainViewport3D_MouseDown(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
MouseMovePoint = e.GetPosition(MainViewport3D);
IsMouseDown = true;
}
}
private void MainViewport3D_MouseMove(object sender, MouseEventArgs e)
{
if (IsMouseDown && e.LeftButton == MouseButtonState.Pressed)
{
Point point = e.GetPosition(MainViewport3D);
double x = point.X - MouseMovePoint.X;
double y = point.Y - MouseMovePoint.Y;
if (Math.Abs(x) > Math.Abs(y))
{
CameraTheta += (x / MainViewport3D.ActualWidth * 3);
}
else
{
CameraPhi += (y / MainViewport3D.ActualHeight * 3);
if (y > 0)
{
if (CameraPhi > Math.PI / 2.0) CameraPhi = Math.PI / 2.0;
}
else
{
if (CameraPhi < -Math.PI / 2.0) CameraPhi = -Math.PI / 2.0;
}
}
// 更新照相机位置.
PositionCamera();
MouseMovePoint = point;
}
}
private void MainViewport3D_MouseUp(object sender, MouseButtonEventArgs e)
{
IsMouseDown = false;
}
#endregion
#region 点击事件
private void buttonUp_Click(object sender, RoutedEventArgs e)
{
CameraPhi += CameraDPhi;
if (CameraPhi > Math.PI / 2.0) CameraPhi = Math.PI / 2.0;
// 更新照相机位置.
PositionCamera();
}
private void buttonLeft_Click(object sender, RoutedEventArgs e)
{
CameraTheta += CameraDTheta;
// 更新照相机位置.
PositionCamera();
}
private void buttonDown_Click(object sender, RoutedEventArgs e)
{
CameraPhi -= CameraDPhi;
if (CameraPhi < -Math.PI / 2.0) CameraPhi = -Math.PI / 2.0;
// 更新照相机位置.
PositionCamera();
}
private void buttonRight_Click(object sender, RoutedEventArgs e)
{
CameraTheta -= CameraDTheta;
// 更新照相机位置.
PositionCamera();
}
#endregion
}
}
