因为实习工作需要制作一个如下图所示的可交互的三维坐标轴,制作这个坐标轴,首先需要创建一些三维图形,接着需要熟悉交互模块和鼠标进行交互,最后将它们封装成一个vtkWidget

VTK中一些基础类介绍

下面是VTK中经常会使用到的类的描述。

vtkProp

渲染场景中数据的可视表达(Visible Depictions)是由vtkProp的子类负责。三维空间中渲染对象最常用的vtkProp子类是vtkActor和vtkVolume,其中vtkActor用于表示场景中的几何数据(Geometry Data),vtkVolume表示场景中的体数据(Volumetric Data)。vtkActor2D常用来表示二维空间中的数据。vtkProp的子类负责确定场景中对象的位置、大小和方向信息。控制Prop位置信息的参数依赖于对象是否在渲染场景中,比如一个三维物体或者二维注释,它们的位置信息控制方式是有所区别的。三维的Prop如vtkActor和vtkVolume(vtkActor和vtkVolume都是vtkProp3D的子类,而vtkProp3D继承自vtkProp),既可以直接控制对象的位置、方向和放缩信息,也可以通过一个4×4的变换矩阵来实现。而对于二维注释功能的Props如vtkScalarBarActor,其大小和位置有许多的定义方式,其中包括指定相对于视口的位置、宽度和高度。Prop除了提供对象的位置信息控制之外,Prop内部通常还有两个对象,一个是Mapper对象,负责存放数据和渲染信息,另一个是Property(属性)对象,负责控制颜色、不透明度等参数。

VTK中定义了大量的功能细化的Prop(超过50个),如vtkImageActor(负责图像显示)和vtkPieChartActor(用于创建数组数据的饼图可视表示)。其中的有些Props内部直接包括了控制显示的参数和待渲染数据的索引,因此并不需要额外的Property和Mapper对象。vtkActor的子类vtkFollower可以自动的更新方向信息保持自身始终面向一个特定的相机,这样无论如何旋转渲染场景中的对象,vtkFellower对象都是可见的,适用于三维场景中的广告板(Billboards)或者是文本。vtkActor的子类vtkLodActor可以自动改变自身的几何表示来实现所要求的交互帧率,vtkProp3D的子类vtkLODProp3D则是通过从许多Mapper中进行选择来实现不同的交互性(可以是Volumetric Mapper和GeometricMapper的集合)。vtkAssembly建立了Actor的等级结构以便在整个结构平移、旋转或者放缩时能够更合理的控制变换。

vtkAbstractMapper

许多Props如vtkActor和vtkVolume利用vtkAbstractMapper的子类来保存输入数据的引用以及提供真正的渲染功能。vtkPolyDataMapper是渲染多边形几何数据主要的Mapper类。而对于体数据,VTK提供了多种渲染技术。例如,vtkFixedPointVolumeRayCastMapper用来渲染vtkImageData类型的数据,vtkProjectedTetrahedraMapper则是用来渲染vtkUnstructuredGrid类型的数据。

vtkProperty和vtkVolumeProperty

某些Props采用单独的属性对象来存储控制数据外观显示的参数,这样不同的对象可以轻松的实现外观参数的共享。vtkActor利用vtkProperty对象存储外观(属性)参数,如颜色、不透明度、材质的环境光(Ambient)系数、散射光(Diffuse)系数和反射光(Specular)系数等。而vtkVolume则是采用vtkVolumeProperty对象来获取体对象的绘制参数,如将标量值映射为颜色和不透明度的传输函数(Transfer Function)【译者:也有译成“传递函数”】。另外,一些vtkMapper提供相应的函数设置裁剪面以便显示对象的内部结构。

vtkCamera

vtkCamera存储了场景中的摄像机参数,换言之,如何来“看”渲染场景里的对象,主要参数是摄像机的位置、焦点、和场景中的上方向向量。其他参数可以控制视图变换,如平行投影或者透视投影,图像的尺度或者视角,以及视景体的远近裁剪平面等。

vtkRenderer

组成场景的对象包括Prop,Camara和Light都被集中在一个vtkRenderer对象中。vtkRenderer负责管理场景的渲染过程。一个vtkRenderWindow中可以有多个vtkRenderer对象,而这些vtkRenderer可以渲染在窗口中不同的矩形区域中(视口),甚至可以是覆盖的区域。

vtkRendererWindow

vtkRendererWindow将操作系统与VTK渲染引擎连接到一起。不同平台下的vtkRendererWindow子类负责本地计算机系统中窗口创建和渲染过程的管理。当使用VTK开发应用程序时,只需要使用平台无关的vtkRendererWindow类,程序运行时,系统会自动替换为平台相关的vtkRendererWindow子类。vtkRendererWindow中包含了vtkRenderer的集合,以及控制渲染的参数,如立体显示(Stereo)、反走样、运动模糊(Motion Blur)和焦点深度(FocalDepth)

vtkRenderWindowInteractor

vtkRenderWindowInteractor负责监听鼠标、键盘和时钟消息,并通过VTK中的Command/Observer设计模式进行相应的处理。vtkInteractorStyle监听这些消息并进行处理以完成旋转、拉伸和放缩等运动控制。vtkRenderWindowInteractor自动建立一个默认的3D场景交互器样式(InteractorStyle),当然你也可以选择一个二维图像浏览的交互器样式,或者是创建自定义的交互器样式。

vtkTransform

场景中的许多对象,如Prop、光源Light、照相机Camera等都需要在场景中合理的放置,它们通过vtkTransform参数可以方便的控制对象的位置和方向。vtkTransform能够描述三维空间中的线性坐标变换,其内部表示为一个4×4的齐次变换矩阵。vtkTransform对象初始化为一个单位矩阵,你可以通过管线连接的方式将变换进行组合来完成复杂的变换。管线方式能够确保当其中任一个变换被修改时,其后续的变换都会相应的进行更新

更多基础类的内容可以参考

VTK图像图像开发进阶 第二章的内容

第03章-VTK系统概述(1)_DolingStudio的博客-CSDN博客

VTKUsersGuide 第三章的内容

可视化管线

第03章-VTK系统概述(2)_DolingStudio的博客-CSDN博客

使用预定义的Source对象创建三维图形的数据(或通过Reader对象从文件中读取数据),一个或多个数据对象传入Filter后,经过处理产生新的数据对象,使用Mapper接收数据,并将其装换为可被渲染引擎绘制的可视化表达,再将Mapper绑定到Actor对象上。

一个例子
#include "vtkSmartPointer.h"
#include "vtkSphereSource.h"
#include "vtkPolyDataMapper.h"
#include "vtkActor.h"
#include "vtkRenderer.h"
#include "vtkRenderWindow.h"
#include "vtkRenderWindowInteractor.h"#include "vtkAutoInit.h"
VTK_MODULE_INIT(vtkRenderingOpenGL2);
VTK_MODULE_INIT(vtkInteractionStyle);
VTK_MODULE_INIT(vtkRenderingFreeType);
VTK_MODULE_INIT(vtkRenderingVolumeOpenGL2);int main()
{// 实例化一个球体的数据源对象vtkSmartPointer<vtkSphereSource> sphereSource = vtkSmartPointer<vtkSphereSource>::New();// 实例化一个MappervtkSmartPointer<vtkPolyDataMapper> sphereMapper = vtkSmartPointer<vtkPolyDataMapper>::New();// 实例化一个ActorvtkSmartPointer<vtkActor> sphereActor = vtkSmartPointer<vtkActor>::New();sphereSource->SetRadius(0.2); // 设置球体的Source图像源半径sphereMapper->SetInputConnection(sphereSource->GetOutputPort());  // 关联Source的输出与Mapper的输入口sphereActor->SetMapper(sphereMapper);   // 将Mapper绑定到Actor上// 实例化一个Renderer对象vtkSmartPointer<vtkRenderer> renderer = vtkSmartPointer<vtkRenderer>::New();// 实例化一个窗口对象vtkSmartPointer<vtkRenderWindow> renWin = vtkSmartPointer<vtkRenderWindow>::New();// 实例化一个窗口的交互器对象vtkSmartPointer<vtkRenderWindowInteractor> iren = vtkSmartPointer<vtkRenderWindowInteractor>::New();renderer->AddActor(sphereActor);   // 一个Renderer包含多个ActorrenWin->AddRenderer(renderer); // 一个RenderWindow包含多个Renderer,可以为不同Renderer设置视口iren->SetRenderWindow(renWin);     // 关联窗口和交互器renWin->Render();             // RenderWindow 开始渲染iren->Initialize();          // vtkRenderWindowInteractor 初始化事件监听iren->Start();           // vtkRenderWindowInteractor开启事件监听循环return 0;
}

创建坐标轴三维图形

创建XYZ坐标轴

本文封装了一个Arrow类通过指定起点和终点来创建一个箭头,并可以设置箭头上椎体的数量,设置颜色,线宽参数

#include "vtkSmartPointer.h"
#include "vtkActor.h"
#include "vtkLineSource.h"
#include "vtkConeSource.h"namespace myvtk {class Arrow{public:Arrow() :m_line(nullptr), m_cone1(nullptr), m_cone2(nullptr) {}Arrow(double origin[3], double target[3], int coneCount = 1, double color[3] = nullptr, float lineWidth = AXISWIDTH) { CreateArrow(origin, target, coneCount, color, lineWidth); }/*函数名: CreateArrow* @param origin[3]  起点坐标* @param target[3]  终点坐标* @param coneCount  椎体数量* @param color      颜色* @param lineWidth  线宽*/void CreateArrow(double origin[3], double target[3], int coneCount = 1, double color[3] = nullptr, float lineWidth = AXISWIDTH){{// 直线部分m_lineSource = vtkSmartPointer<vtkLineSource>::New();vtkNew<vtkPolyDataMapper> lineMapper;m_line = vtkSmartPointer<vtkActor>::New();lineMapper->SetInputConnection(m_lineSource->GetOutputPort());m_line->SetMapper(lineMapper);m_lineSource->SetPoint1(origin);m_lineSource->SetPoint2(target);m_line->GetProperty()->SetLineWidth(lineWidth);if (color != nullptr) m_line->GetProperty()->SetColor(color);// 箭头部分double directVec[3] = { 0 };vtkMath::Subtract(target, origin, directVec);// 箭头1m_coneCount = coneCount;if (coneCount >= 1){m_coneSource1 = vtkSmartPointer<vtkConeSource>::New();vtkNew<vtkPolyDataMapper> coneMapper1;m_cone1 = vtkSmartPointer<vtkActor>::New();m_coneSource1->SetCenter(target);m_coneSource1->SetHeight(0.5);m_coneSource1->SetRadius(0.08);m_coneSource1->SetResolution(10);m_coneSource1->SetDirection(directVec);coneMapper1->SetInputConnection(m_coneSource1->GetOutputPort());m_cone1->SetMapper(coneMapper1);if (color != nullptr) m_cone1->GetProperty()->SetColor(color);}// 箭头2if (coneCount >= 2){m_coneSource2 = vtkSmartPointer<vtkConeSource>::New();vtkNew<vtkPolyDataMapper> coneMapper2;m_cone2 = vtkSmartPointer<vtkActor>::New();vtkMath::Subtract(origin, target, directVec);m_coneSource2->SetCenter(origin);m_coneSource2->SetHeight(0.5);m_coneSource2->SetRadius(0.08);m_coneSource2->SetResolution(10);m_coneSource2->SetDirection(directVec);coneMapper2->SetInputConnection(m_coneSource2->GetOutputPort());m_cone2->SetMapper(coneMapper2);if (color != nullptr) m_cone2->GetProperty()->SetColor(color);}}}int m_coneCount = 0;vtkSmartPointer<vtkActor> m_line = nullptr;vtkSmartPointer<vtkActor> m_cone1 = nullptr;vtkSmartPointer<vtkActor> m_cone2 = nullptr;vtkSmartPointer<vtkLineSource> m_lineSource = nullptr;vtkSmartPointer<vtkConeSource> m_coneSource1 = nullptr;vtkSmartPointer<vtkConeSource> m_coneSource2 = nullptr;};
}
创建弯曲箭头

本文封装了一个CurveArrow类通过指定弯曲箭头需要经过的一系列的点来绘制曲线,并可以设置箭头上椎体的数量,设置颜色,线宽参数

其中绘制曲线涉及到了 vtkParametricSplinevtkParametricFunctionSource

#include "vtkSmartPointer.h"
#include "vtkPoints.h"
#include "vtkParametricFunctionSource.h"
#include "vtkParametricSpline.h"
#include "vtkPolyDataMapper.h"
#include "vtkActor.h"
#include "vtkProperty.h"
#include "vtkConeSource.h"#define AXISWIDTH 3namespace myvtk {class CurveArrow{public:CurveArrow() :m_curve(nullptr), m_cone1(nullptr), m_cone2(nullptr) {}CurveArrow(vtkSmartPointer<vtkPoints> points, int coneCount = 1, double color[3] = nullptr, float lineWidth = AXISWIDTH) { CreateArrow(points, coneCount, color, lineWidth); }/*函数名: CreateArrow* @param points     曲线经过的一组点* @param coneCount  椎体数量* @param color      颜色* @param lineWidth  线宽*/void CreateArrow(vtkSmartPointer<vtkPoints> points, int coneCount = 1, double color[3] = nullptr, float lineWidth = AXISWIDTH){if (points->GetNumberOfPoints() < 2) throw "input points must have at least two points!";// 曲线部分m_curve = vtkSmartPointer<vtkActor>::New();m_spline = vtkSmartPointer<vtkParametricSpline>::New();m_functionSource = vtkSmartPointer<vtkParametricFunctionSource>::New();m_spline->SetPoints(points);m_functionSource->SetParametricFunction(m_spline);m_functionSource->Update();vtkNew<vtkPolyDataMapper> lineMapper;lineMapper->SetInputConnection(m_functionSource->GetOutputPort());m_curve->SetMapper(lineMapper);m_curve->GetProperty()->SetLineWidth(lineWidth);if (color != nullptr) m_curve->GetProperty()->SetColor(color);// 箭头部分double directVec[3] = { 0 };int pointsNum = points->GetNumberOfPoints();//double* target = points->GetPoint(pointsNum - 1);double target[3]{ 0 }, origin[3]{ 0 };points->GetPoint(pointsNum - 1, target);points->GetPoint(pointsNum - 2, origin);//double* origin = points->GetPoint(pointsNum - 2);vtkMath::Subtract(target, origin, directVec);// 箭头1m_coneCount = coneCount;if (coneCount >= 1){m_coneSource1 = vtkSmartPointer<vtkConeSource>::New();vtkNew<vtkPolyDataMapper> coneMapper1;m_cone1 = vtkSmartPointer<vtkActor>::New();m_coneSource1->SetCenter(target);m_coneSource1->SetHeight(0.5);m_coneSource1->SetRadius(0.08);m_coneSource1->SetResolution(10);m_coneSource1->SetDirection(directVec);coneMapper1->SetInputConnection(m_coneSource1->GetOutputPort());m_cone1->SetMapper(coneMapper1);if (color != nullptr) m_cone1->GetProperty()->SetColor(color);}// 箭头2if (coneCount >= 2){points->GetPoint(0, target);points->GetPoint(0, origin);m_coneSource2 = vtkSmartPointer<vtkConeSource>::New();vtkNew<vtkPolyDataMapper> coneMapper2;m_cone2 = vtkSmartPointer<vtkActor>::New();vtkMath::Subtract(target, origin, directVec);m_coneSource2->SetCenter(origin);m_coneSource2->SetHeight(0.5);m_coneSource2->SetRadius(0.08);m_coneSource2->SetResolution(10);m_coneSource2->SetDirection(directVec);coneMapper2->SetInputConnection(m_coneSource2->GetOutputPort());m_cone2->SetMapper(coneMapper2);if (color != nullptr) m_cone2->GetProperty()->SetColor(color);}}int m_coneCount = 0;vtkSmartPointer<vtkActor> m_curve = nullptr;vtkSmartPointer<vtkActor> m_cone1 = nullptr;vtkSmartPointer<vtkActor> m_cone2 = nullptr;vtkSmartPointer<vtkParametricSpline> m_spline = nullptr;vtkSmartPointer<vtkParametricFunctionSource> m_functionSource = nullptr;vtkSmartPointer<vtkConeSource> m_coneSource1 = nullptr;vtkSmartPointer<vtkConeSource> m_coneSource2 = nullptr;};
};
创建有厚度的平面

VTK笔记-图形相关-平面-vtkPlaneSource_黑山老妖的博客的博客-CSDN博客_vtk平面图

(4)建立一个标准尺寸的平面,并对其进行着色贴图、拉伸一定的厚度_rexinx的博客-CSDN博客

本文封装了一个Plane,首先通过 vtkPlaneSource 数据源获取一个平面的点及拓扑数据,通过设置 origin, point1, point2 设置平面图形的大小

接着使用 vtkLinearExtrusionFilter 通过SetVector()及SetScaleFactor()方法设置平面的厚度

#include "vtkSmartPointer.h"
#include "vtkActor.h"
#include "vtkPlaneSource.h"
#include "vtkLinearExtrusionFilter.h"
#include "vtkTriangleFilter.h"
#include "vtkPolyDataMapper.h"
#include "vtkProperty.h"namespace myvtk {class Plane{public:Plane() :m_plane(nullptr), m_planeSource(nullptr), m_planeLinearExtrusionFilter(nullptr), m_planeTriangleFilter(nullptr) {}Plane(double origin[3], double point1[3], double point2[3], double width, double color[3]) { CreatePlane(origin, point1, point2, width, color); };/*函数名: CreatePlane* @param origin     平面矩形的一个顶点* @param point1     平面矩形的一个与origin相邻的顶点* @param point2     平面矩形的一个与origin相邻的顶点* @param width      平面厚度* @param color      颜色*/void CreatePlane(double origin[3], double point1[3], double point2[3], double width, double color[3]){m_planeSource = vtkSmartPointer<vtkPlaneSource>::New();vtkNew<vtkPolyDataMapper> PlaneMapper;m_plane = vtkSmartPointer<vtkActor>::New();m_planeSource->SetOrigin(origin);m_planeSource->SetPoint1(point1);m_planeSource->SetPoint2(point2);m_planeSource->Update();// Apply linear extrusionm_planeLinearExtrusionFilter = vtkSmartPointer<vtkLinearExtrusionFilter>::New();m_planeLinearExtrusionFilter->SetInputConnection(m_planeSource->GetOutputPort());m_planeLinearExtrusionFilter->SetExtrusionTypeToNormalExtrusion();double vec[3]{ 1,0,0 };m_planeLinearExtrusionFilter->SetVector(vec);m_planeLinearExtrusionFilter->SetScaleFactor(width);m_planeTriangleFilter = vtkSmartPointer<vtkTriangleFilter>::New();m_planeTriangleFilter->SetInputConnection(m_planeLinearExtrusionFilter->GetOutputPort());PlaneMapper->SetInputConnection(m_planeTriangleFilter->GetOutputPort());m_plane->SetMapper(PlaneMapper);m_plane->GetProperty()->SetColor(color);}public:vtkSmartPointer<vtkActor> m_plane;vtkSmartPointer<vtkPlaneSource> m_planeSource;vtkSmartPointer<vtkLinearExtrusionFilter> m_planeLinearExtrusionFilter;vtkSmartPointer<vtkTriangleFilter> m_planeTriangleFilter;};
};
创建始终跟随镜头的坐标轴文本

第04章-VTK基础(5)_DolingStudio的博客-CSDN博客

使用 vtkVectorText 作为数据源,vtkFollower是vtkActor的子类,通过SetCamera()方法设置后,可以始终朝向指定的镜头

auto textsSource = vtkSmartPointer<vtkVectorText>::New();
auto textMapper = vtkSmartPointer<vtkPolyDataMapper>::New();
auto texts = vtkSmartPointer<vtkFollower>::New();textsSource->SetText("x");
textMapper->SetInputConnection(textsSource->GetOutputPort());
texts->SetMapper(textMapper);
texts->SetScale(0.1, 0.1, 0.1);vtkMath::MultiplyScalar(point2, 1.05);
texts->SetPosition(point2);
texts->GetProperty()->SetColor(color);
texts->SetCamera(renderer->GetActiveCamera());
创建中心圆球
// 绘制中心圆球
m_centerSphereSource = vtkSmartPointer<vtkSphereSource>::New();
vtkNew<vtkPolyDataMapper> sphereMapper;
m_centerSphere = vtkSmartPointer<vtkActor>::New();m_centerSphereSource->SetRadius(0.2);
m_centerSphereSource->SetCenter(0, 0, 0);
sphereMapper->SetInputConnection(m_centerSphereSource->GetOutputPort());
m_centerSphere->SetMapper(sphereMapper);m_centerSphere->GetProperty()->SetColor(0.8,0.8,0.8);  // 初始设置较暗的颜色
m_centerSphere->GetProperty()->SetDiffuse(1.0);
m_centerSphere->SetPosition(0, 0, 0);

Picker通过鼠标拾取三维图形

第04章-VTK基础(4)_DolingStudio的博客-CSDN博客

拾取操作是可视化应用程序中常见的一种功能。拾取主要是用于选择数据和Actor或者获取底层的数据值。在显示位置(以像素为坐标值)中拾取时,就会调用vtkAbstractPicker的Pick()方法。

本文中使用的是 vtkPropPicker

初始化Picker

// 初始化 vtkPropPicker
this->m_picker = vtkPropPicker::New();
// 将所有的三维坐标系中的Actor使用 AddPickList() 方法添加到 picker 的列表中
TravelAllActors([this](vtkActor* actor) {this->m_picker->AddPickList(actor);
});
// 限定picker只拾取列表中的Actor
this->m_picker->PickFromListOn();

使用Picker获取鼠标位置的Actor

vtkActor* GetPickedActor(double x, double y) {m_picker->Pick(x, y, 0, this->Renderer);return m_picker->GetActor();
}

其中 x, y 对应的是鼠标在屏幕上的位置,当鼠标触发事件时,可以通过 RenderWindowInteractorGetEventPosition()方法获取鼠标当前的位置

double X = self->Interactor->GetEventPosition()[0];
double Y = self->Interactor->GetEventPosition()[1];

现在我们已经完成了拾取鼠标位置的Actor操作,但由于三维坐标系由许多的 Actor 组合而成,为了知道我们拾取的Actor是什么,当我们按下鼠标左键开始拾取的时候是点击了X轴,还是Y轴,还是Z轴,或者是点击了平面,又或者是点击了拖动旋转的箭头,我们需要对拾取的Actor在对象组中进行识别。

int GetSelectedState(vtkActor* actor)
{int offset = 0;// X Y Z 轴for (int i = 0; i < m_axesArrow.size(); i++){if ((unsigned long)(m_axesArrow[i]->m_line.GetPointer()) == (unsigned long)actor ||(m_axesArrow[i]->m_cone1 != nullptr && (unsigned long)(m_axesArrow[i]->m_cone1.GetPointer()) == (unsigned long)actor)){return SelAxis(i);}}offset += m_axesArrow.size();// 中心圆球if ((unsigned long)(m_centerSphere.GetPointer()) == (unsigned long)actor){return SelAxis(offset);}offset += 1;// planefor (int i = 0; i < m_planes.size(); i++){if ((unsigned long)(m_planes[i]->m_plane.GetPointer()) == (unsigned long)actor){return SelAxis(i + offset);}}offset += m_planes.size();// rotatefor (int i = 0; i < m_rotateArrow.size(); i++){if ((unsigned long)(m_rotateArrow[i]->m_curve.GetPointer()) == (unsigned long)actor ||(m_rotateArrow[i]->m_cone1 != nullptr && (unsigned long)(m_rotateArrow[i]->m_cone1.GetPointer()) == (unsigned long)actor) ||(m_rotateArrow[i]->m_cone2 != nullptr && (unsigned long)(m_rotateArrow[i]->m_cone2.GetPointer()) == (unsigned long)actor)){return SelAxis(i + offset);}}return noAxis;
}

依次将拾取的 Actor 地址与X Y Z 轴中心圆球平面旋转箭头对象组中各个成员进行比较,确定当前选中了哪个对象组,使用一个int类型的变量InteractionState配合SelAxis枚举类型进行指示。

坐标变换

齐次坐标

当知道了拾取的对象组之后就可以配合相关变量对操作进行响应

对坐标轴的平移,旋转,缩放操作,都可以通过操作Actor对象的齐次变换矩阵(UserMatrix)来完成,假设当前的坐标点 A(x1, y1, z1) 与移动后的坐标B(x2, y2, z2)
{x2=k1x1+b1y2=k2y1+b2z2=k3z1+b3\begin{cases} x_2 = k_{1}x_1 + b_{1} \\ y_2 = k_{2}y_1 + b_{2} \\ z_2 = k_{3}z_1 + b_{3} \end{cases} ⎩⎨⎧​x2​=k1​x1​+b1​y2​=k2​y1​+b2​z2​=k3​z1​+b3​​
转换为矩阵形式
(x2y2z21)=[k100b10k20b200k3b30001](x1y1z11)\left( \begin{matrix} x_2 \\ y_2 \\ z_2 \\ 1 \end{matrix} \right) = \left[ \begin{matrix} k_{1}&0&0&b_{1} \\ 0&k_{2}&0&b_{2} \\ 0&0&k_{3}&b_{3} \\ 0&0&0&1 \end{matrix} \right] \left( \begin{matrix} x_1 \\ y_1 \\ z_1 \\ 1 \end{matrix} \right) ⎝⎛​x2​y2​z2​1​⎠⎞​=⎣⎡​k1​000​0k2​00​00k3​0​b1​b2​b3​1​⎦⎤​⎝⎛​x1​y1​z1​1​⎠⎞​
A点对应的齐次变换矩阵为
[1000010000100001]\left[ \begin{matrix} 1&0&0&0 \\ 0&1&0&0 \\ 0&0&1&0 \\ 0&0&0&1 \end{matrix} \right] ⎣⎡​1000​0100​0010​0001​⎦⎤​
移动到B点对应的齐次变换矩阵为
[k100b10k20b200k3b30001]\left[ \begin{matrix} k_{1}&0&0&b_{1} \\ 0&k_{2}&0&b_{2} \\ 0&0&k_{3}&b_{3} \\ 0&0&0&1 \end{matrix} \right] ⎣⎡​k1​000​0k2​00​00k3​0​b1​b2​b3​1​⎦⎤​

vtk中对矩阵的变换可以通过vtkTransform类方便的进行

【VTK学习】空间几何变换_JinSu_的博客-CSDN博客_vtk 矩阵乘法

vtkNew<vtkTransform> transform;
transform->PostMultiply(); //M=A*M
transform->RotateZ(30);      // 绕Z轴旋转30度
transform->Translate(1, 0, 0);   // 平移一个 (1,0,0) 向量transform->GetMatrix();    // 获取一系列操作合成出的齐次变换矩阵
沿指定轴(X,Y,Z)移动

motion_vector 为坐标轴移动到鼠标位置的向量,m_moveByVec为指定移动的轴方向,m_userMatrix4x4为预定义的vtkMatrix4x4矩阵

double length = vtkMath::Dot(motion_vector, m_moveByVec);double vec[3] = { 0 };
vec[InteractionState] = length;vtkNew<vtkTransform> transform;
transform->SetMatrix(m_userMatrix4x4);
transform->Translate(vec);
transform->GetMatrix(m_userMatrix4x4);actor->GetUserMatrix()->DeepCopy(m_userMatrix4x4);
沿xyz所有方向移动
double vec[3] = { 0 };
vec[0] = motion_vector[0];
vec[1] = motion_vector[1];
vec[2] = motion_vector[2];
printf("move vec (%f, %f, %f)\n", vec[0], vec[1], vec[2]);vtkNew<vtkTransform> transform;
transform->SetMatrix(m_userMatrix4x4);
transform->Translate(vec);
transform->GetMatrix(m_userMatrix4x4);actor->GetUserMatrix()->DeepCopy(m_userMatrix4x4);
沿指定平面移动
double vec[3];
vec[0] = m_moveByVec[0] * motion_vector[0];
vec[1] = m_moveByVec[1] * motion_vector[1];
vec[2] = m_moveByVec[2] * motion_vector[2];
printf("move vec (%f, %f, %f)\n", vec[0], vec[1], vec[2]);vtkNew<vtkTransform> transform;
transform->SetMatrix(m_userMatrix4x4);
transform->Translate(vec);
transform->GetMatrix(m_userMatrix4x4);actor->GetUserMatrix()->DeepCopy(m_userMatrix4x4);
绕指定轴旋转

oldPoint为鼠标上次触发事件时的位置,newPoint为鼠标当前触发事件时的位置

// 将鼠标位置移动到自身坐标系下,求两次鼠标位置向量在投影平面的夹角
vtkNew<vtkTransform> trans;
trans->SetMatrix(m_userMatrix4x4);double pos_t1[4]{ oldPoint[0], oldPoint[1], oldPoint[2], 1 };
double pos_t2[4]{ newPoint[0], newPoint[1], newPoint[2], 1 };
vtkNew<vtkMatrix4x4> posture_inv;
vtkMatrix4x4::Invert(m_userMatrix4x4, posture_inv);
auto pos_t = posture_inv->MultiplyDoublePoint(pos_t1);
double v1[3] = { pos_t[0], pos_t[1], pos_t[2] };
pos_t = posture_inv->MultiplyDoublePoint(pos_t2);
double v2[3] = { pos_t[0], pos_t[1], pos_t[2] };double projection1[3], projection2[3];
GetPlaneProjection(m_moveByVec, v1, projection1);
GetPlaneProjection(m_moveByVec, v2, projection2);
vtkMath::Normalize(projection1);
vtkMath::Normalize(projection2);
double axis[3];
vtkMath::Cross(projection1, projection2, axis);
double radians = acos(vtkMath::Dot(projection1, projection2));
double degrees = vtkMath::DegreesFromRadians(radians);
trans->RotateWXYZ(degrees, axis);
trans->Update();m_userMatrix4x4->DeepCopy(trans->GetMatrix());

vtkWidget类

参考VTK图形图像开发进阶 8.3 VTK Widget

vtkInteractorObserver.h

vtkInteractorObserver.cxx

vtkAbstractWidget.h

vtkAbstractWidget.cxx

vtkWidgetRepresentation.h

vtkWidgetRepresentation.cxx

VTK中Widget的设计是从VTK 5.0版本开始引入的,最初的Widget是从vtk3DWidget派生出来的,从VTK5.1版本开始,VTK Widget从新进行设计,主要的设计理念是将Widget的消息处理与几何表达实体分离,但还是保留了 vtk3DWidget及其子类。vtkAbstractWidget作为基类,只定义一些公共的API以及实现了“交互/表达实体”分离的设计机制,其中,把从vtkRenderWindowInteractor路由过来的消息(事件)交给vtkAbstractWidget的“交互”部分处理,而Widget的“表达实体”则对应一个vtkProp对象(或者是vtkWidgetRepresentation的子类)。这样做的好处是:事件的处理与Widget的表达实体互不干扰,而且可以实现同类Widget使用不同的表达形式,每个VTKAbstractWidget子类内部包含一个vtkWidgetEventTranslate对象和一个vtkWidgetCallbackMapper对象,vtkWidgetEventTranslate的作用是将外部的VTK事件映射为Widget事件(定义于 vtkWidgetEvenet.h文件中), vtkWidgetCallbackMapper将相应的Widget事件与各个受保护的静态操作函数关联起来。

可以使用vtkWidgetCallbackMapper的SetCallbackMethod函数设置 VTK Event, Widget Event, Method Invocation之间的关联

void vtkWidgetCallbackMapper::SetCallbackMethod(unsigned long VTKEvent, unsigned long widgetEvent, vtkAbstractWidget* w, CallbackType f)
{this->EventTranslator->SetTranslation(VTKEvent, widgetEvent);this->SetCallbackMethod(widgetEvent, w, f);
}

示例:

this->CallbackMapper->SetCallbackMethod(vtkCommand::LeftButtonReleaseEvent,vtkWidgetEvent::EndSelect,this, vtkDistanceWidget::EndSelectAction);

本文将 可拖动的三维坐标轴封装为 vtkAbstractWidget 的子类,作为交互与表达实体相分离的部件,还需要一个继承vtkWidgetRepresentation的子类对象,

继承vtkAbstractWidget类

继承vtkAbstractWidget类只需实现 vtkAbstractWidget 的纯虚函数 virtual void CreateDefaultRepresentation() = 0;即可

void vtkMoveableAxesWidget::CreateDefaultRepresentation()
{if (!this->WidgetRep){auto rep = vtkMoveableAxesRepresentation::New();this->SetWidgetRepresentation(rep);}
}

此外 vtkWidget 类还应该实现事件到方法的映射,通过在构造函数执行时调用 vtkWidgetCallbackMapper 对象的 SetCallbackMethod 方法关联 VTK Event, Widget Event, Method Invocation

vtkMoveableAxesWidget::vtkMoveableAxesWidget(): m_cursorActor(nullptr),WidgetState(vtkMoveableAxesWidget::Start)
{this->CallbackMapper->SetCallbackMethod(vtkCommand::LeftButtonPressEvent, vtkWidgetEvent::Select, this, vtkMoveableAxesWidget::SelectAction);this->CallbackMapper->SetCallbackMethod(vtkCommand::MouseMoveEvent, vtkWidgetEvent::Move, this, vtkMoveableAxesWidget::MoveAction);this->CallbackMapper->SetCallbackMethod(vtkCommand::LeftButtonReleaseEvent, vtkWidgetEvent::EndSelect, this, vtkMoveableAxesWidget::EndSelectAction);}

接着定义响应事件的回调函数 SelectAction, MoveAction, EndSelectAction 这些回调函数必须定义为静态成员,并且设置一个形参vtkAbstractWidget *w

void vtkMoveableAxesWidget::SelectAction(vtkAbstractWidget *w)
{vtkMoveableAxesWidget* self = reinterpret_cast<vtkMoveableAxesWidget*>(w);// Get the event positionint X = self->Interactor->GetEventPosition()[0];int Y = self->Interactor->GetEventPosition()[1];if (!self->CurrentRenderer || !self->CurrentRenderer->IsInViewport(X, Y)){self->WidgetState = vtkMoveableAxesWidget::Start;return;}double e[2];e[0] = static_cast<double>(X);e[1] = static_cast<double>(Y);// 调用 vtkWidgetRepresentation 子类中定义的交互接口函数self->WidgetRep->StartWidgetInteraction(e);int interactionState = self->WidgetRep->GetInteractionState();if (interactionState < 0){return;}// 更新鼠标指针self->UpdateCursorShape(interactionState);// 设置组件状态为 激活self->WidgetState = vtkMoveableAxesWidget::Active;// 阻断事件传递,发送StartInteractionEvent事件,渲染self->GrabFocus(self->EventCallbackCommand);self->StartInteraction();self->EventCallbackCommand->SetAbortFlag(1);self->InvokeEvent(vtkCommand::StartInteractionEvent, nullptr);self->Render();
}void vtkMoveableAxesWidget::MoveAction(vtkAbstractWidget* w)
{vtkMoveableAxesWidget* self = reinterpret_cast<vtkMoveableAxesWidget*>(w);double X = self->Interactor->GetEventPosition()[0];double Y = self->Interactor->GetEventPosition()[1];// 鼠标移动时高亮功能self->MoveHighLight(X, Y);// 判断组件是否激活if (self->WidgetState == vtkMoveableAxesWidget::Start){return;}// 开始交互,调用 vtkWidgetRepresentation 子类中定义的交互接口函数double e[2];e[0] = static_cast<double>(X);e[1] = static_cast<double>(Y);self->WidgetRep->WidgetInteraction(e);// 阻断事件传递,发送InteractionEvent事件,渲染self->EventCallbackCommand->SetAbortFlag(1);self->InvokeEvent(vtkCommand::InteractionEvent, nullptr);self->Render();
}void vtkMoveableAxesWidget::EndSelectAction(vtkAbstractWidget* w)
{vtkMoveableAxesWidget* self = reinterpret_cast<vtkMoveableAxesWidget*>(w);self->WidgetState = vtkMoveableAxesWidget::Start;// 调用 vtkWidgetRepresentation 子类中定义的交互接口函数self->WidgetRep->EndWidgetInteraction(nullptr);// 更新鼠标指针self->UpdateCursorShape(-1);// 阻断事件传递,发送InteractionEvent事件,渲染self->ReleaseFocus();self->EventCallbackCommand->SetAbortFlag(1);self->EndInteraction();self->InvokeEvent(vtkCommand::EndInteractionEvent, nullptr);self->Render();
}
继承vtkWidgetRepresentation类

继承vtkWidgetRepresentation首先要实现它的纯虚函数virtual void BuildRepresentation() = 0;

void vtkMoveableAxesRepresentation::BuildRepresentation()
{// 刷新Repif (!this->Renderer || !this->Renderer->GetRenderWindow()) {return;}// actor、source重新计算if (this->GetMTime() > this->BuildTime|| this->Renderer->GetRenderWindow()->GetMTime() > this->BuildTime) {}// 重建和renderwindow更改时调整控制柄的大小if (this->GetMTime() > this->BuildTime|| this->Renderer->GetRenderWindow()->GetMTime() > this->BuildTime) {this->SizeHandles();this->BuildTime.Modified();}
}

根据 vtkWidgetRepresentation.h中所述
为了让 vtkWidgetRepresentation 子类表现得像个 vtkProp对象子类需要实现以下方法

  /*** Methods to make this class behave as a vtkProp. They are repeated here (from the* vtkProp superclass) as a reminder to the widget implementor. Failure to implement* these methods properly may result in the representation not appearing in the scene* (i.e., not implementing the Render() methods properly) or leaking graphics resources* (i.e., not implementing ReleaseGraphicsResources() properly).*/double *GetBounds() override {return nullptr;}void ShallowCopy(vtkProp *prop) override;void GetActors(vtkPropCollection *) override {}void GetActors2D(vtkPropCollection *) override {}void GetVolumes(vtkPropCollection *) override {}void ReleaseGraphicsResources(vtkWindow *) override {}int RenderOverlay(vtkViewport *vtkNotUsed(viewport)) override {return 0;}int RenderOpaqueGeometry(vtkViewport *vtkNotUsed(viewport)) override {return 0;}int RenderTranslucentPolygonalGeometry(vtkViewport *vtkNotUsed(viewport)) override {return 0;}int RenderVolumetricGeometry(vtkViewport *vtkNotUsed(viewport)) override {return 0;}int HasTranslucentPolygonalGeometry() override { return 0; }

本文由于只使用到了Actor对象,根据需要实现了如下方法

    double* GetBounds() VTK_SIZEHINT(6) override;void GetActors(vtkPropCollection* pc) override;void ReleaseGraphicsResources(vtkWindow*) override;int RenderOpaqueGeometry(vtkViewport*) override;int RenderTranslucentPolygonalGeometry(vtkViewport*) override;vtkTypeBool HasTranslucentPolygonalGeometry() override;

具体实现可以查阅VTK提供的完整的源码,部分示例如下

void vtkMoveableAxesRepresentation::GetActors(vtkPropCollection* pc)
{pc = vtkPropCollection::New();TravelAllActors([&pc](vtkActor* actor) {pc->AddItem(actor);});
}

可以使用TravelAllActors传入一个匿名函数的方式遍历所有Actor对象

#include <functional>
class vtkActor;using travelActorsCallback = std::function<void(vtkActor*)>;
void TravelAllActors(travelActorsCallback);void vtkMoveableAxesRepresentation::TravelAllActors(travelActorsCallback callback)
{vtkActor* actor;// x,y,z轴for (int i = 0; i < m_axesArrow.size(); i++){actor = m_axesArrow[i]->m_line.GetPointer();callback(actor);actor = m_axesArrow[i]->m_cone1.GetPointer();callback(actor);}// 坐标轴文本for (int i = 0; i < this->m_texts.size(); i++){actor = this->m_texts[i];callback(actor);}// 拓展部分m_extendActors->InitTraversal();while ((actor = m_extendActors->GetNextActor()) != nullptr){callback(actor);}
}

以下是推荐的vtkWidgetRepresentation 子类与 vtkWidget交互的接口函数

  /*** The following is a suggested API for widget representations. These methods* define the communication between the widget and its representation. These* methods are only suggestions because widgets take on so many different* forms that a universal API is not deemed practical. However, these methods* should be implemented when possible to insure that the VTK widget hierarchy* remains self-consistent.* <pre>* PlaceWidget() - given a bounding box (xmin,xmax,ymin,ymax,zmin,zmax), place* the widget inside of it. The current orientation of the widget* is preserved, only scaling and translation is performed.* StartWidgetInteraction() - generally corresponds to a initial event (e.g.,* mouse down) that starts the interaction process* with the widget.* WidgetInteraction() - invoked when an event causes the widget to change* appearance.* EndWidgetInteraction() - generally corresponds to a final event (e.g., mouse up)* and completes the interaction sequence.* ComputeInteractionState() - given (X,Y) display coordinates in a renderer, with a* possible flag that modifies the computation,* what is the state of the widget?* GetInteractionState() - return the current state of the widget. Note that the* value of "0" typically refers to "outside". The* interaction state is strictly a function of the* representation, and the widget/represent must agree* on what they mean.* Highlight() - turn on or off any highlights associated with the widget.* Highlights are generally turned on when the widget is selected.* </pre>* Note that subclasses may ignore some of these methods and implement their own* depending on the specifics of the widget.*/virtual void PlaceWidget(double* vtkNotUsed(bounds[6])) {}virtual void StartWidgetInteraction(double eventPos[2]) { (void)eventPos; }virtual void WidgetInteraction(double newEventPos[2]) { (void)newEventPos; }virtual void EndWidgetInteraction(double newEventPos[2]) { (void)newEventPos; }virtual int ComputeInteractionState(int X, int Y, int modify=0);virtual int GetInteractionState(){return this->InteractionState;}virtual void Highlight(int vtkNotUsed(highlightOn)) {}

与picker相关的接口函数

  /*** Register internal Pickers in the Picking Manager.* Must be reimplemented by concrete widget representations to register* their pickers.*/virtual void RegisterPickers();/*** Unregister internal pickers from the Picking Manager.*/virtual void UnRegisterPickers();/*** Update the pickers registered in the Picking Manager when pickers are* modified.*/virtual void PickersModified();

明确了满足 vtkAbstractWidget 与 vtkWidgetRepresentation 类需要实现的接口,以及它们之间交互需要的接口,完整代码见源码。

使用

#include "vtkRenderer.h"
#include "vtkRenderWindow.h"
#include "vtkRenderWindowInteractor.h"
#include "vtkAutoInit.h"
VTK_MODULE_INIT(vtkRenderingOpenGL2);
VTK_MODULE_INIT(vtkInteractionStyle);
VTK_MODULE_INIT(vtkRenderingFreeType);
VTK_MODULE_INIT(vtkRenderingVolumeOpenGL2);#include "vtkMoveableAxesWidget.h"int main()
{vtkNew<vtkRenderer> renderer;vtkNew<vtkRenderWindow> renWin;vtkNew<vtkRenderWindowInteractor> renWinI;renderer->GetActiveCamera()->SetPosition(8, 8, 8);renWin->AddRenderer(renderer);renWinI->SetRenderWindow(renWin);renWinI->Initialize();// Widget的创建需要在 renWinI->SetRenderWindow(renWin) 和 renWinI->Initialize() 之后// 创建可移动坐标轴vtkMoveableAxesWidget* axes = vtkMoveableAxesWidget::New();axes->SetInteractor(renWinI);axes->On();renWin->Render();renWinI->Start();return EXIT_SUCCESS;
}

其他

Qt VS Tools插件安装

https://blog.51cto.com/u_15707179/5447267

VS配置Qt开发环境

https://www.cnblogs.com/szitcast/p/15733691.html

解决:Qt项目中出现红色波浪线错误提示

解决:vs打开.ui文件出现Qt Designer后闪退

VTK源码编译

https://www.bilibili.com/video/BV1S5411D7uj?spm_id_from=333.1007.top_right_bar_window_history.content.click&vd_source=c9263c84459a6eabff37b3b1b85328e6

Visual Studio C++项目使用第三方库

https://blog.csdn.net/u012611644/article/details/81105539

https://blog.csdn.net/yangfchn/article/details/85162934

控制台显示调试信息

工程属性->链接器->系统->子系统 设置为 控制台

参考

VTK官方网址
VTK官方文档
VTK源码
VTK可移动三维坐标轴 vtkMovableAxesWidget
【VTK】可拖动的坐标轴MovableAxesWidget
第03章-VTK系统概述(1)_DolingStudio的博客-CSDN博客
VTK笔记-图形相关-平面-vtkPlaneSource_黑山老妖的博客的博客-CSDN博客_vtk平面图
(4)建立一个标准尺寸的平面,并对其进行着色贴图、拉伸一定的厚度_rexinx的博客-CSDN博客
第04章-VTK基础(4)_DolingStudio的博客-CSDN博客
【VTK学习】空间几何变换_JinSu_的博客-CSDN博客_vtk 矩阵乘法
第04章-VTK基础(5)_DolingStudio的博客-CSDN博客
《VTK图形图像开发进阶》
《VTKUsersGuide》

VTK可交互三维坐标轴相关推荐

  1. VTK可移动三维坐标轴 vtkMovableAxesWidget

    一.需求 实现一个可移动,可旋转的三位坐标轴. 二,步骤 1.继承 vtkAbstractWidget.将鼠标事件传递给 vtkWidgetRepresentation. 2.继承 vtkWidget ...

  2. 用几何画板怎么画三维坐标轴

    通过课本上的介绍,想必大家都知道了,三维笛卡儿坐标系是在二维笛卡儿坐标系的基础上根据右手定则增加第三维坐标(即Z轴)而形成的.那么要怎么画出三维坐标轴呢? 几何画板作为专业的绘图软件,可以用来画各种数 ...

  3. 智能全景汽车3D建模VR模型虚拟交互三维在线展示

    智能全景汽车3D建模VR模型虚拟交互三维在线展示是以3D建模技术.三维模型技术.3D虚拟现实技术.三维仿真技术.VR虚拟交互技术.VR全景技术.线上三维智能化等为基础,虚实交互技术.计算机信息化处理和 ...

  4. VTK:交互与拾取——点拾取

    1.拾取 拾取最经典的例子就是,在玩3D游戏时,场景中可能会存在多个角色,有时候需要用鼠标来选择所要控制的角色,这时候要用到拾取功能. 另外,在某些三维图形图形的编译软件中,经常需要编辑其中的一个点, ...

  5. Matlab中如何创建空的三维坐标轴

    想必很多小伙伴经常会使用axes函数来创建二维的空坐标轴吧,可是对三维的空坐标轴却一筹莫展.创建二维的空坐标轴的代码如下所示: % Matlabfigure ax = axes('Position', ...

  6. python的三维坐标轴设置_使用PyOpenGL绘制三维坐标系实例

    我就废话不多说了,直接上代码吧! def drawCoordinate(): ''' 绘制三维的坐标系,并绘制由坐标轴构成的平面的网格,各个坐标轴的颜色以及由坐标轴所引出的网格线的颜色为: x: (1 ...

  7. 三维坐标轴html实现,CSS3三维变形,其实很简单!

    原标题:CSS3三维变形,其实很简单! 本文主要内容一.前言二.坐标轴系统三.透视与变形风格四.3D变形函数五.实例展示六.总结一.前言 所谓的三维变形,无外乎就是在二维平面的基础上进而实现三维立体空 ...

  8. 利用VRML设计简单的交互三维室内漫游场景

    利用VRML设计简单的三维室内漫游场景 利用3dmaxs建模 VRMLPad里编辑代码 利用3dmaxs建模 首先,利用3dmaxs或其他的建模工具建模.这里建的模很简单,因为小文件方便调试,示例中的 ...

  9. python三维图形渲染-基于VTK/numpy的三维图像渲染与可视化

    我试图用numpy/vtk显示CT扫描获得的图像.为此,我遵循了这个sample code和{a2}的答案,但是我没有得到好的结果,也不知道原因.在 我检查了一下,我加载的数据是正确的,所以看起来我在 ...

最新文章

  1. LeetCode: 109. Convert Sorted List to Binary Search Tree
  2. 如何设计 QQ、微信、微博、Github 等第三方账号登陆 ?(附表设计)
  3. HLG2040二叉树遍历已知前中,求后
  4. re正则表达式的使用
  5. Python中使用高德API实现经纬度转地名
  6. 使用Spring访问Mongodb的方法大全——Spring Data MongoDB查询指南
  7. Unity5 Sprite 图集打包 AssetBundle 更新探索
  8. 安装eclipse版本oxygen,及maven导入spring mvc项目并运行
  9. linux-如何限制普通用户的磁盘使用空间-磁盘配额quota,Linux-如何限制普通用户的磁盘使用空间-磁盘配额quota...
  10. 最大隶属度原则_【模糊数学课程笔记】六、模糊模型识别I(最大隶属度原则)...
  11. 点击按钮复制指定代码
  12. linux C 实现HTTP get 及post 请求
  13. 【前端】【HTML+CSS+JavaScript(JS)】简易登陆界面的实现
  14. 关于Android手机拍照预览、剪裁界面出现照片九十度旋转的问题
  15. 在线靶场-墨者-安全意识1星-WEB页面分析
  16. 实现一个直播视频app源码的邀请码功能
  17. [家里蹲大学数学杂志]第409期与正弦对数有关的一个积分不等式
  18. The annotation of C++ primer {藤原豆腐坊自家用}
  19. 可以有效改进项目管理技能的十个过程
  20. 四川省部分地区经济发展水平的统计分析

热门文章

  1. windows开始菜单打不开,搜索打不开
  2. Nordic错误笔记
  3. git 使用总结(三)repo sync -m
  4. Android 语音遥控器的整体分析
  5. java word模版填充_Java 数据填充到word模板中
  6. 带隙基准(BG)的基本原理与结构
  7. 与传统媒体相比新媒体传播所具备的特点与优势!
  8. linux环境中英文切换配置以及乱码问题
  9. eclipse安装html编辑器插件
  10. 舌尖上的区块链——校园食品区块链溯源