Android OpenGLES2.0开发(四):矩阵变换和相机投影

事物的本质是事物本身所固有的、深藏于‌现象背后并决定或支配现象的方面‌。

请添加图片描述
还记得我们上一篇绘制的三角形吗,我们确实能够顺利用OpenGL ES绘制出图形了,这是一个好的开始,但这还远远不够。我们定义的坐标是正三角形,但是绘制出来三角形却拉升了(横屏显示会压缩)。

请添加图片描述
为了方便大家,我们将OpenGL ES坐标系图再次贴出。OpenGL ES的坐标系是一个正方形,他的四个顶点分别对应GLSurfaceView的四个顶点,这个是定死的我们无法改变,那要怎么才能让我的三角形变成等边三角形呢?既然坐标系为正方形,那么我们让GLSurfaceView也为正方形是否可行呢?

1. 方式一:设置GLSurfaceView宽高相等

注意这里有个误区就是OpenGL ES坐标顶点对应的是GLSurfaceView的四个顶点,而不是屏幕的四个顶点。所以好多文章说变形拉升什么的,甚至是官方文档都和手机屏幕扯上关系。我现在可以明确的告诉大家的是,这和手机的屏幕一点关系也没有

那为什么又要说和屏幕有关系,其本质是将GLSurfaceView的宽高使用了match_parent导致GLSurfaceView大小和屏幕相同而已,但是变形拉升只和GLSurfaceView的大小有关,GLSurfaceView如果不是一个正方形,那么画出的图形就会变形。

既然我们知道了上述缘由后,最简单的方式就有了,就是设置GLSurfaceView的宽高相等即可:

<com.android.xz.opengldemo.view.TriangleGLSurfaceViewandroid:layout_width="400dp"android:layout_height="400dp"/>

运行看看效果是否可行:
请添加图片描述
不出我们所料,果然是行得通的!!!

但是世界的运行往往不是我们人为能控制的,GLSurfaceView的宽高往往不是正方形,他要和应用相结合,他可能是游戏全屏界面,也可能是某个显示视频的预览界面,亦或是嵌到某个犄角旮旯充当不重要的视图,这个时候我们就引入了下面的方式。

2. 方式二:修改顶点坐标数据

GLSurfaceView的宽高不一致的时,我们该如何是好???就比如我们现在GLSurfaceView是全屏的。

我们来分析下,GLSurfaceView目前全屏后,视图高被拉升了,原本三角形的top顶点到底边的垂直距离是0.866,也就是说我们按照GLSurfaceView拉升比缩放这个距离是不是也是可行的?

    // 三角形三个点的坐标,逆时针绘制static float triangleCoords[] = {   // 坐标逆时针顺序0.0f, 0.616f, 0.0f, // top-0.5f, -0.25f, 0.0f, // bottom left0.5f, -0.25f, 0.0f  // bottom right};

好了开始动手干,我们在Triangle类中surfaceChanged方法中重新计算缩放后Y的坐标点,如下:

public void surfaceChanged(int width, int height) {// 设置OpenGL ES画布大小GLES20.glViewport(0, 0, width, height);float radio = (float) width / height;triangleCoords = new float[]{   // 坐标逆时针顺序0.0f, 0.616f * radio, 0.0f, // top-0.5f, -0.25f * radio, 0.0f, // bottom left0.5f, -0.25f * radio, 0.0f  // bottom right};// 初始化形状坐标的顶点字节缓冲区ByteBuffer bb = ByteBuffer.allocateDirect(// (number of coordinate values * 4 bytes per float)triangleCoords.length * 4);// use the device hardware's native byte orderbb.order(ByteOrder.nativeOrder());// create a floating point buffer from the ByteBuffervertexBuffer = bb.asFloatBuffer();// add the coordinates to the FloatBuffervertexBuffer.put(triangleCoords);// set the buffer to read the first coordinatevertexBuffer.position(0);
}

查看效果:
请添加图片描述
其实在没有看到效果时,我已经猜到最后肯定会绘制出正三角形,当然看了效果我们也就踏实了。

OpenGL ES绘图变形,其本质无非就是要解决View的宽高比和OpenGL ES正方形坐标系的一个变换;如果View是正方形无需变换,长方形该缩放缩放。

目前我们是绘制了一个三角形,顶点只有三个,修改顶点坐标倒还不是那么复杂。如果我们要绘制更加复杂的图像,顶点有几十个上百个,我们又该如何一个一个修改顶点的缩放比呢?聪明的你可能又想到了,我用for循环遍历修改啊那样就会方便很多,那我只能说你这是小聪明,OpenGL ES为我们提供了一个大聪明的方式:矩阵变换

3. 方式三:矩阵变换

我们知道OpenGL ES的世界里是三维空间,我们要对三维空间中的点进行缩放、平移、旋转实际在数学中有一个好的方式就是用矩阵来计算。而矩阵的知识是大学线性代数中的,这个基础需要读者自己去补。

空间中点缩放变换,建议看下这篇文章:【深度好文】3D坐标系下的点的转换矩阵(平移、缩放、旋转、错切)

3.1 自定义矩阵缩放方法

接下来我们使用矩阵相乘变换点的坐标,定义缩放方法如下:

public static void scale(float[] coords, int stride, float sx, float sy, float sz) {float[] scaleM = {sx, 0, 0,0, sy, 0,0, 0, sz};for (int i = 0; i < coords.length; i += stride) {float x = coords[i];float y = coords[i + 1];float z = coords[i + 2];coords[i] = scaleM[0] * x;coords[i + 1] = scaleM[4] * y;coords[i + 2] = scaleM[8] * z;}
}

修改surfaceChanged缩放坐标代码

public void surfaceChanged(int width, int height) {// 设置OpenGL ES画布大小GLES20.glViewport(0, 0, width, height);scale(triangleCoords, 3, 1, (float) width / height, 1);// 初始化形状坐标的顶点字节缓冲区ByteBuffer bb = ByteBuffer.allocateDirect(// (number of coordinate values * 4 bytes per float)triangleCoords.length * 4);// use the device hardware's native byte orderbb.order(ByteOrder.nativeOrder());// create a floating point buffer from the ByteBuffervertexBuffer = bb.asFloatBuffer();// add the coordinates to the FloatBuffervertexBuffer.put(triangleCoords);// set the buffer to read the first coordinatevertexBuffer.position(0);
}

运行查看效果,也可得到正三角形。

3.2 使用GLSL缩放

上面方式固然可行,但是大量顶点计算都在CPU端了,如何使用GPU程序去并行计算?

OpenGL ES也提供了矩阵相乘的方式,在三维图形学中,一般使用的是4阶矩阵。在DirectX中使用的是行向量,如[xyzw],所以与矩阵相乘时,向量在前矩阵在后。OpenGL中使用的是列向量,如[xyzx]T,所以与矩阵相乘时,矩阵在前,向量在后,我们最终通过“变换矩阵”来得到我们想要的向量

修改顶点着色器代码并定义变换矩阵如下:

// 顶点着色器代码
private final String vertexShaderCode =// 传入变换矩阵"uniform mat4 uMVPMatrix;" +"attribute vec4 vPosition;" +"void main() {" +// 变换矩阵与顶点坐标相乘等到新的坐标"  gl_Position = uMVPMatrix * vPosition;" +"}";/*** Shader程序中矩阵属性的句柄*/
private int vPMatrixHandle;// 最终变化矩阵
private final float[] mMVPMatrix = new float[16];

记得在surfaceCreated中获取矩阵属性句柄

public void surfaceCreated() {...// 获取绘制矩阵句柄vPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
}

surfaceChanged中设置缩放矩阵

    public void surfaceChanged(int width, int height) {// 设置OpenGL ES画布大小GLES20.glViewport(0, 0, width, height);float radio = (float) width / height;float[] scaleMatrix = new float[]{1, 0, 0, 0,0, radio, 0, 0,0, 0, 1, 0,0, 0, 0, 1};// 将缩放矩阵拷贝到变换矩阵中System.arraycopy(scaleMatrix, 0, mMVPMatrix, 0, scaleMatrix.length);}

draw方法中将缩放矩阵传给OpenGL ES程序

public void draw() {...// 将缩放矩阵传递给着色器程序GLES20.glUniformMatrix4fv(vPMatrixHandle, 1, false, mMVPMatrix, 0);// 画三角形GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);// 禁用顶点阵列GLES20.glDisableVertexAttribArray(positionHandle);
}

运行查看效果,也可得到正三角形。

本文到现在已经讲了很多种方式可以正确绘制正三角形,但是到现在还没有到正真要讲的内容。我们发现将缩放矩阵传入着色器程序貌似已经完成了最终的目的,如果我们仅仅只是为了把三角形画正那么到这里应该就结束了。世界的运行往往出乎我们的意料,现在可能要求你缩放,但是未来可能还会平移二维旋转三维旋转镜像等等操作,如果要我们去定义各种矩阵,那简直是灾难,于是下面的方式就应运而生了。

4. 相机和投影

  • 投影:OpenGL 中主要有两种投影模式,分别是正交投影和透视投影
  • 相机:相机视图顾名思义就相当于站在相机的角度观察某个物体,相机会看到投影到近平面的物体

4.1 投影

OpenGL提供了两种投影变换矩阵如下
请添加图片描述

透视投影
请添加图片描述

学过素描的应该都知道透视图的概念,符合人眼习惯,呈现近大远小的效果。

/*** @param m 生成的投影矩阵,float[4*4]* @param mOffset 填充时候起始的偏移量* @param left  近平面left边的x坐标* @param right 近平面right边的x坐标* @param bottom  近平面bottom边的y坐标* @param top   近平面top边的y坐标* @param near  近平面距离摄像机的距离* @param far   远平面距离摄像机的距离*/
public static void frustumM(float[] m, int mOffset,float left, float right, float bottom, float top,float near, float far) {
}

正交投影
请添加图片描述

该投影方式图像大小不会随着距离变化而变化

/*** @param m 生成的投影矩阵,float[4*4]* @param mOffset 填充时候起始的偏移量* @param left  近平面left边的x坐标* @param right 近平面right边的x坐标* @param bottom  近平面bottom边的y坐标* @param top   近平面top边的y坐标* @param near  近平面距离摄像机的距离* @param far   远平面距离摄像机的距离*/
public static void orthoM(float[] m, int mOffset,float left, float right, float bottom, float top,float near, float far) {
}
  • 不管是正交投影还是透视投影,最终都是将视景体内的物体投影在近平面上,这也是 3D 坐标转换到 2D 坐标的关键一步。
  • 而近平面上的坐标接着也会转换成归一化设备坐标,再映射到屏幕视口上。
  • 为了解决之前的图像拉伸问题,就是要保证近平面的宽高比和视口的宽高比一致,而且是以较短的那一边作为 1 的标准,让图像保持居中。

4.2 相机

相机位置设置

/**** @param rm 生成的摄像机矩阵,float[16]* @param rmOffset 填充时候的起始偏移量* @param eyeX 摄像机x坐标* @param eyeY 摄像机y坐标* @param eyeZ 摄像机z坐标* @param centerX 观察目标点的x坐标* @param centerY 观察目标点的y坐标* @param centerZ 观察目标点的z坐标* @param upX 摄像机up向量在x上的分量* @param upY 摄像机up向量在y上的分量* @param upZ 摄像机up向量在z上的分量*/
public static void setLookAtM(float[] rm, int rmOffset,float eyeX, float eyeY, float eyeZ,float centerX, float centerY, float centerZ, float upX, float upY,float upZ) {
}
  • eyeXeyeYeyeZ:摄像机坐标。
  • centerXcenterYcenterZ:观察点坐标,和摄像机坐标一起决定了摄像机的观察方向,即向量(centerX - eyeX, centerY - eyeY, centerZ - eyeZ)。观察方向不朝向视景体是无法看到的。
  • upXupYupZ:摄像机up向量。相对于人眼观察物体中,人头的朝向,头的朝向影响了最后的成像。同样以图来说明:

请添加图片描述
当up向量为Y的正方向时,正如我们头顶对着天花板,所以观察到的物体是正的,投影在近平面的样子就是正的,如右图

请添加图片描述
当up向量为X正方向时,正如我们向右90度歪着脑袋去看这个三角形,看到的三角形就会是向左旋转了90度的三角形

再比如up向量如果为Z轴正方向,就相当于仰着头去看这个三角形,但是因为我们的up向量和观察方向平行了,所以我们什么也看不到,就比如仰着头去看你身前的物体时,你什么也看不到。

所以在设置up向量时,一般总是设置为(0,1,0),这是大多数观察时头朝上的方向。注意:up向量的大小无关紧要,有意义的只有方向。

4.3 near、far的取值范围规定

  • 正交投影时,摄像机可位于视景体中间,此时near < 0,far > 0,近平面位于视点后面(Z轴正方向),远平面位于视点前面(Z轴负方向)
  • 正交投影时,视景体也可位于视点后面(Z轴正方向),此时near < 0, far < 0
  • 正交投影时,far 和 near没有规定的大小关系,既可以far > near 也可以 far < near,只要物体在视景体内都可以被观察到。
  • 透视投影时,far>near>0;我们不考虑其他情况,我们默认就在Z轴上看物体
    当centerZ - eyeZ>0时:近平面nearZ坐标=eyeZ+near,远平面farZ坐标=eyeZ+far;
    当centerZ - eyeZ<0时:近平面nearZ坐标=eyeZ-near,远平面farZ坐标=eyeZ-far;
    我们要保证物体Z坐标在nearZ和farZ之间就能看到,也就是物体在视景里就能看到。

4.4 构造模型矩阵

根据上面的理论知识,我们不用再手动构造一个缩放矩阵了,我们定义如下三个矩阵并进行变换

public class Triangle {...// vPMatrix是“模型视图投影矩阵”的缩写// 最终变化矩阵private final float[] mMVPMatrix = new float[16];// 投影矩阵private final float[] mProjectionMatrix = new float[16];// 相机矩阵private final float[] mViewMatrix = new float[16];public void surfaceChanged(int width, int height) {// 设置OpenGL ES画布大小GLES20.glViewport(0, 0, width, height);float ratio;if (width > height) {ratio = (float) width / height;// 横屏使用// 透视投影,特点:物体离视点越远,呈现出来的越小。离视点越近,呈现出来的越大// 该投影矩阵应用于对象坐标Matrix.frustumM(mProjectionMatrix, 0, -ratio, ratio, -1, 1, 3, 7);} else {ratio = (float) height / width;// 竖屏使用// 透视投影,特点:物体离视点越远,呈现出来的越小。离视点越近,呈现出来的越大// 该投影矩阵应用于对象坐标Matrix.frustumM(mProjectionMatrix, 0, -1, 1, -ratio, ratio, 3, 7);}Matrix.setLookAtM(mViewMatrix, 0,0, 0, 3f,0f, 0f, 0f,0f, 1.0f, 0.0f);// Calculate the projection and view transformationMatrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mViewMatrix, 0);}
}

说明:我们使用frustumM做透视矩阵时,我们要把近平面宽高和屏幕宽高比例对应,我们以较短的一边作为1,按比例拉升即可。而far>near>0遵循此原则即可。

设置相机setLookAtM参数,我们只需要注意eyeZ的取值,根据上面的说明正确取值。上面方法取值为3那么近平面的nearZ坐标=eyeZ-near=0和物体z坐标重合,看到的物体比例正好不会变大也不会缩小。

最后我们用multiplyMM方法将投影矩阵和相机矩阵转换为最终的矩阵,然后传给着色器程序即可。

运行程序也可得到正三角形。

相机和投影概念实际上是为3D模型准备的,现在我们把他用在2D图形上,着实有中降维打击,大炮打苍蝇的感觉。但是我们不得不了解这个强大的工具,为将来遇到的3D场景变换做准备。

最后

我们都知道独孤九剑,剑法的最高境界是无招。上述介绍的几种方式都可谓是剑招,当我们了解了事物运行的本质后,这几种方式皆可为我所用,在适当的场景下选择合适的方式,甚至可以创造招式。

《黑客帝国》中尼奥复活后了解了虚拟世界的本质,整个世界的运行不过就是一串串数字。原来难以翻越的高山,现在也只是眼下的风景。还记的我们第一篇章吗Android OpenGLES2.0开发(一):艰难的开始,现在的我觉得脚下有路、心中有光,也期待未来会更美好。

参考:

  1. https://juejin.cn/post/6844903614838751240
  2. https://cloud.tencent.com/developer/article/1015587
  3. https://www.nxrte.com/jishu/13722.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/444596.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

YoloV10改进策略:BackBone改进|CAFormer在YoloV10中的创新应用,显著提升目标检测性能

摘要 在目标检测领域,模型性能的提升一直是研究者和开发者们关注的重点。近期,我们尝试将CAFormer模块引入YoloV10模型中,以替换其原有的主干网络,这一创新性的改进带来了显著的性能提升。 CAFormer,作为MetaFormer框架下的一个变体,结合了深度可分离卷积和普通自注意力…

学习博客写作

欢迎使用Markdown编辑器 你好&#xff01; 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章&#xff0c;了解一下Markdown的基本语法知识。 新的改变 我们对Markdown编辑器进行了一些功能拓展与语法支持&#x…

Vue】Vue扫盲(四)组件化思想与简单应用

【Vue】Vue扫盲&#xff08;一&#xff09;事件标签、事件修饰符&#xff1a;click.prevent click.stop click.stop.prevent、按键修饰符、及常用指令 【Vue】Vue扫盲&#xff08;二&#xff09;指令&#xff1a;v-for 、v-if、v-else-if、v-else、v-show 【Vue】Vue扫盲&…

解决银河麒麟桌面操作系统V10(ARM)中`apt-get update`“正在等待报头”问题

解决银河麒麟桌面操作系统V10&#xff08;ARM&#xff09;中apt-get update“正在等待报头”问题 1、问题描述2、 解决方法步骤一&#xff1a;打开终端步骤二&#xff1a;清理APT缓存步骤三&#xff1a;再次尝试更新软件源 &#x1f496;The Begin&#x1f496;点点关注&#x…

spring面试之2024

1、什么是spring? Spring是一个Java开发框架&#xff0c;它提供了一种可扩展的模型来开发Java应用程序。Spring框架的目标是提供一个全面的解决方案&#xff0c;用于构建企业级应用程序。Spring框架的核心特点包括依赖注入&#xff08;DI&#xff09;、面向切面编程&#xff…

django的路由分发

前言&#xff1a; 在前面我们已经学习了基础的Django了&#xff0c;今天我们将继续学习&#xff0c;我们今天学习的是路由分发&#xff1a; 路由分发是Web框架中的一个核心概念&#xff0c;它指的是将不同的URL请求映射到对应的处理函数&#xff08;视图&#xff09;的过程。…

如何提高专利申请的成功率?

在当今充满创新与竞争的时代&#xff0c;专利成为了保护智力成果、赢得市场优势的重要武器。然而&#xff0c;专利申请并非一帆风顺&#xff0c;许多申请人在这一过程中面临诸多挑战&#xff0c;导致申请成功率不尽如人意。那么&#xff0c;如何才能在这复杂的专利申请之路上提…

宝塔 进程守护管理器 神坑,再次跌入。thinkphp-queue队列 勤勤学长

如果&#xff0c;你有在使用【进程守护管理器】&#xff0c;记得在更新/重启&#xff0c;甚至卸载重新安装后&#xff0c;重启服务器。 事情的起因是&#xff0c;昨日服务器突然异常&#xff0c;网站无法正常访问&#xff0c;进入宝塔面板&#xff0c;发现 cpu和负载率均超过1…

2024.10月11日--- SpringMVC拦截器

拦截器 1 回顾过滤器&#xff1a; Servlet规范中的三大接口&#xff1a;Servlet接口&#xff0c;Filter接口、Listener接口。 过滤器接口&#xff0c;是Servlet2.3版本以来&#xff0c;定义的一种小型的&#xff0c;可插拔的Web组件&#xff0c;可以用来拦截和处理Servlet容…

1000题-计算机网络系统概述

术语定义与其他术语的关系SDU&#xff08;服务数据单元&#xff09;相邻层间交换的数据单元&#xff0c;是服务原语的表现形式。在OSI模型中&#xff0c;SDU是某一层待传送和处理的数据单元&#xff0c;即该层接口数据的总和。 - SDU是某一层的数据集&#xff0c;准备传递给下一…

ICDE 2024最新论文分享|BEEP:容量约束下能够对抗异常干扰的航运动态定价系统

论文简介 本推文详细介绍了上海交通大学高晓沨教授和陈贵海教授团队发表在顶级学术会议ICDE 2024上发表的最新论文《Corruption Robust Dynamic Pricing in Liner Shipping under Capacity Constraint》&#xff0c;该论文的学生作者为胡永祎、李雪嫣、魏熙锴&#xff0c;合作…

Midjourney零基础学习

Midjourney学习笔记TOP04 Midjourney的各种参数设置 Midjourney的用户操作界面没有醒目的工具栏、属性栏&#xff0c;所有的操作都是通过调用各种指令和参数进行的。 【MJ Version】 Midjourney在2023年3月份就已经更新到了V5版本&#xff0c;V5版本除了画质有所提升外&#…

MacOS 同时配置github、gitee和gitlab密钥

MacOS 同时配置github、gitee和gitlab密钥 1 在终端中新建 ~/.ssh目录 1.1 生成GitHub、Gitee和Gitlab的SSH密钥对 ssh-keygen -t ed25519 -C "xxxxxxxxxxx.com" -f ~/.ssh/id_ed25519_gitee ssh-keygen -t ed25519 -C "xxxxxxxxxxx.com" -f ~/.ssh/id_…

TikTok代理IP全面使用指南

对于那些希望通过社交媒体打造个人品牌的人来说&#xff0c;TikTok是现在热门的平台&#xff0c;他的流量与曝光不可小觑&#xff0c;相信很多跨境营销会选择他进行多账号营销。问题是&#xff0c;TikTok多账号很容易遇到封禁问题&#xff0c;那么如何解决&#xff1f; 一、什么…

无人机集群路径规划:5种优化算法(SFOA、APO、GOOSE、CO、PIO)求解无人机集群路径规划,提供MATLAB代码

一、单个无人机路径规划模型介绍 无人机三维路径规划是指在三维空间中为无人机规划一条合理的飞行路径&#xff0c;使其能够安全、高效地完成任务。路径规划是无人机自主飞行的关键技术之一&#xff0c;它可以通过算法和模型来确定无人机的航迹&#xff0c;以避开障碍物、优化…

pytest:4种方法实现 - 重复执行用例 - 展示迭代次数

简介&#xff1a;在软件测试中&#xff0c;我们经常需要重复执行测试用例&#xff0c;以确保代码的稳定性和可靠性。在本文中&#xff0c;我们将介绍四种方法来实现重复执行测试用例&#xff0c;并显示当前迭代次数和剩余执行次数。这些方法将帮助你更好地追踪测试执行过程&…

Python | Leetcode Python题解之第468题验证IP地址

题目&#xff1a; 题解&#xff1a; class Solution:def validIPAddress(self, queryIP: str) -> str:if queryIP.find(".") ! -1:# IPv4last -1for i in range(4):cur (len(queryIP) if i 3 else queryIP.find(".", last 1))if cur -1:return &q…

Jupyter的使用分享

文章目录 碎碎念安装方法1.安装Anaconda方法2.通过库的安装方式 启动使用教程1.指定目录打开2.启动后的简单使用 小结 碎碎念 前情提示 之前与许多小伙伴交流的时候&#xff0c;发现大家对于pycharm更容易上手&#xff08;可能是比较好设置中文的原因&#xff09;&#xff0c;在…

【MySQL】-- 表的操作

文章目录 1. 查看所有表1.1 语法 2. 创建表2.1 语法2.2 示例2.3 表在磁盘上对应的文件 3. 查看表结构3.1 语法3.2 示例 4. 查看创建表的语句5. 修改表5.1 语法5.2 示例5.2.1 向表中添加一列5.2.2 修改某列的长度5.2.3 重命名某列5.2.4 删除某个字段5.2.5 修改表名 6. 删除表6.1…

CANoe_调用C#控件的方法_DEMO方法演示

1、DEMO存放位置 D:\Users\Public\Documents\Vector\CANoe\Sample Configurations 11.0.96\CAN\MoreExamples\ActiveX_DotNET_Panels 每个人的电脑因为有区别存放位置不一样 2、控件制作--使用C#控件可以直接制作 3、控件代码 using System; using System.Collections; usi…