尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Windows平台CVI SDK集成实战:从环境配置到硬件抽象层设计

Windows平台CVI SDK集成实战:从环境配置到硬件抽象层设计 简介本资源是面向CVICommon Vision Blox Interactive开发者的Windows平台SDK集成包专为使用C语言进行工业视觉与图像处理应用开发的工程师设计解决CVI环境下调用原生Windows API如窗口管理、文件操作、系统信息获取、音频控制、共享内存等的技术衔接问题。压缩包共43个文件含11个C源码实现核心功能逻辑、9个CWS工程配置文件、9个PRJ项目文件、8个H头文件定义接口与结构体及6个UIR界面资源总大小仅56KB轻量紧凑且模块清晰——涵盖sharemem进程间通信、winshape窗口定制、sndplay/cdplayer音频播放、glauxdemOpenGL辅助绘图、whoami安全上下文识别等典型场景示例。已有136人学习下载提供开箱即用的完整可编译项目结构、跨层调用封装思路与配套头文件引用范式助开发者快速打通CVI与Windows底层能力构建高性能、高集成度的视觉系统应用。1. 从“sdk.rar_CVI windows SDK”说起一个老牌开发工具的现代困境最近在整理一个尘封已久的项目备份时我翻到了一个名为sdk.rar_CVI windows SDK的压缩包。这个文件名瞬间把我拉回了十几年前那个还在用CVILabWindows/CVI做测控上位机软件的年代。对于很多从事工业自动化、测试测量、仪器控制的老工程师来说CVI和它的SDK软件开发工具包是绕不开的“老朋友”。这个压缩包很可能就是某个特定硬件比如数据采集卡、运动控制卡、或者像海康威视这类厂商的工业相机为CVI环境提供的专用开发接口。今天我想借这个由头深入聊聊在Windows平台上面对这类“历史悠久”的专用SDK我们如何从零开始理解、集成、调试并最终让它在现代开发环境中焕发新生。无论你是正在维护一个遗留系统还是需要集成一个只有老旧SDK的新设备这篇文章里的踩坑经验和实操路径或许能帮你省下不少折腾的时间。2. 解构“CVI Windows SDK”它到底是什么又能做什么首先我们必须厘清这个压缩包名字里的几个关键信息点。CVI全称是 LabWindows/CVI是美国国家仪器NI公司推出的一款面向测量和自动化领域的ANSI C集成开发环境。它的核心优势在于提供了大量现成的、针对数据采集、仪器控制、信号分析的库函数和UI控件特别适合开发测试、测量和控制系统的人机界面HMI和后台服务。而Windows SDK则指明了这个开发包的目标平台是微软的Windows操作系统。那么一个典型的CVI Windows SDK压缩包里通常会包含什么呢根据我多年接触各类厂商SDK的经验它绝不仅仅是一堆.h和.lib文件那么简单。一个完整的、可供直接使用的SDK包通常具备以下骨架头文件Include Files 这是SDK的“说明书”以.h文件形式存在。里面定义了所有你可以调用的函数原型、数据结构、常量宏。例如对于一个相机SDK你可能会找到Camera_Open(),Camera_GrabImage(),Camera_SetExposureTime()等函数的声明以及像IMAGE_FORMAT_MONO8这样的枚举常量。库文件Library Files 这是SDK的“实现本体”。在Windows的C/C世界里这通常表现为静态库.lib文件或动态链接库.dll文件及其对应的导入库.lib。你的程序在编译时需要链接这些.lib文件在运行时则需要相应的.dll文件存在于系统路径或程序目录下。文档Documentation 可能是最宝贵也最容易被忽视的部分。形式多样从简单的Readme.txt到结构化的CHM帮助文件甚至是完整的PDF手册。文档里会详细说明每个函数的参数含义、返回值、调用时序以及常见的编程范例。示例程序Samples/Demos 这是学习的捷径。一个负责任的SDK提供商会提供多个从简到繁的示例项目通常是.prj或.cws格式的CVI工程文件。通过这些例子你可以最快地看到如何初始化设备、进行基本操作、处理回调事件等。工具与驱动Tools Drivers 有时SDK包里会附带一些配置工具如相机参数配置软件、诊断工具或者明确指明需要预先安装的特定版本设备驱动程序。这一点至关重要很多“找不到设备”的问题根源就在于驱动没装对。当我们拿到sdk.rar_CVI windows SDK这样一个包时首先要做的不是急于写代码而是像外科手术一样解压并审视其内部结构。建立一个清晰的项目目录将头文件、库文件、示例程序分门别类放好并第一时间阅读任何形式的文档这能为后续开发扫清至少50%的障碍。3. 环境搭建与项目配置避开第一个“深坑”假设我们已经解压了SDK并初步了解了它的内容。接下来就要在CVI中搭建开发环境了。这个过程看似简单却布满了新手容易踩进去的坑。我以最常见的场景——在CVI中创建一个新项目并集成第三方SDK为例拆解关键步骤和避坑点。3.1 CVI工程创建与基本设置打开LabWindows/CVI创建一个新的“Empty Project”。第一个关键决策点是选择正确的“Target Type”。如果你的程序最终是一个带界面的可执行文件就选“Windows GUI”如果是一个无界面的后台服务或DLL则选择“Console”或“Dynamic Link Library”。这里选错可能导致后续UI控件无法使用或程序行为异常。创建项目后我强烈建议立即进行两项全局设置设置字符集 在Build - Target Settings中检查“Character Set”选项。为了最大程度兼容老旧SDK和避免中文乱码问题这也是网络热词中“windows乱码”的高频问题通常建议选择“Use Multi-Byte Character Set”。虽然Unicode是趋势但很多老SDK的函数接口是基于ANSI多字节字符串设计的强行使用Unicode会导致字符串参数传递失败。设置运行库 在Build - Target Settings - Runtime中选择运行时库。为了部署方便避免要求用户安装特定版本的VC运行库通常选择“Multithreaded (/MT)”进行静态链接。但这会增大最终可执行文件的体积需要权衡。3.2 集成SDK头文件与库文件路径配置这是核心环节配置错误将直接导致编译和链接失败。头文件路径配置 在CVI的工程窗口Project Window中右键点击项目名选择 “Add Include Path”。将SDK解压目录下的include或header文件夹路径添加进去。这里有个细节如果SDK头文件还引用了其他子目录的头文件你需要确保这些子目录也被包含或者将最顶层的目录加入即可。一个常见的检查方法是在代码中#include一个SDK头文件后按F12Go to Definition如果能正确跳转到该头文件说明路径配置正确。库文件路径与链接库目录 同样在工程窗口中右键选择 “Add Library Search Path”添加SDK的lib或library文件夹路径。链接具体库 还是在工程窗口中右键选择 “Add Files to Project”文件类型选择Library Files (*.lib)然后导航到SDK的库目录添加需要的.lib文件。这里有一个巨坑很多SDK会提供多个.lib文件例如一个用于调试版本可能带_d后缀一个用于发布版本。你需要根据你当前CVI的编译配置Debug/Release来链接对应的库文件链接错误会导致运行时崩溃。一个稳妥的做法是在工程设置里为Debug和Release配置分别指定不同的库文件。关于动态链接库DLL 链接了.lib文件只是解决了编译期的问题。程序运行时必须能找到对应的.dll文件。你需要将SDK提供的.dll文件复制到你的可执行文件.exe所在的输出目录下。通常CVI的默认输出目录是项目文件夹下的bin目录。你可以通过Build - Target Settings - Output Directory来查看和修改。为了确保万无一失也可以将必要的.dll文件路径添加到系统的PATH环境变量中但这对于最终的用户部署来说并不友好更推荐随程序一起分发DLL。3.3 驱动与系统依赖检查在尝试运行任何示例或自己编写的代码前务必确认设备驱动已正确安装。以工业相机为例你需要去设备制造商官网下载对应型号和操作系统位数32/64位的最新驱动并安装。安装后通常可以在设备管理器中看到对应的设备并且厂商会提供一个配置工具来验证设备是否可被识别和通信。另一个容易被忽略的系统依赖是诸如Microsoft Visual C Redistributable这样的运行库。有些SDK本身是用Visual Studio编译的依赖于特定版本的VC运行库。你需要根据SDK文档或错误提示在目标机器上安装相应版本。例如如果运行时提示缺少msvcr100.dll或vcruntime140.dll你就需要安装对应版本的VC Redistributable。4. 从示例代码到自主开发理解SDK的编程模型环境配好了下一步就是让代码跑起来。最聪明的做法就是从SDK自带的示例程序开始。不要小看这些例子它们是理解该SDK“编程模型”和“最佳实践”的钥匙。4.1 解剖示例程序寻找通用模式打开一个示例工程不要急于运行。先从头文件包含和全局变量定义看起了解它引入了哪些SDK头文件定义了哪些重要的句柄Handle或上下文Context变量。在CVI和很多硬件SDK中设备或资源通常用一个句柄来代表所有操作都围绕这个句柄展开。接着看main函数或主回调函数的流程。一个典型的设备控制流程遵循以下模式初始化与发现 调用XXX_Initialize()或XXX_EnumerateDevices()初始化SDK或列出可用设备。创建设备句柄 调用XXX_CreateHandle()或XXX_Open()传入设备标识符如IP地址、序列号、索引号获得一个操作该设备的句柄。参数配置 调用一系列XXX_SetParameter()函数设置像分辨率、帧率、曝光时间、触发模式等工作参数。启动与数据获取 调用XXX_StartAcquisition()开始工作。数据获取通常有两种模式轮询Polling 在循环中不断调用XXX_GetData()或XXX_GrabFrame()来主动抓取数据。回调Callback 事先注册一个回调函数然后启动采集。当有新数据到达时SDK会自动在新线程中调用你的回调函数。这种方式效率更高编程模型更事件驱动。数据处理与显示 在轮询循环或回调函数中对获取到的图像或数据进行处理并可能显示在CVI的图形控件如Canvas上。停止与清理 调用XXX_StopAcquisition()停止采集然后调用XXX_Close()或XXX_ReleaseHandle()关闭设备句柄最后可能还需要调用XXX_Finalize()释放SDK全局资源。仔细阅读示例中每一步的返回值检查Error Checking。优秀的示例会展示如何判断每个函数调用是否成功并在失败时进行适当的错误处理或资源清理这是编写健壮工业软件的基础。4.2 编写自己的第一个功能以相机采集为例假设我们要实现一个简单的相机连续采集并显示的功能。在理解了示例后我们可以开始搭建自己的代码框架。#include ansi_c.h #include cvirte.h #include userint.h #include CameraSDK.h // 假设的SDK头文件 // 全局变量 static int g_hCamera -1; // 相机句柄-1表示无效 static int g_bAcquisitionRunning 0; // 主面板回调函数等... int CVICALLBACK StartAcquisitionCallback (int panel, int control, int event, void *callbackData, int eventData1, int eventData2) { switch (event) { case EVENT_COMMIT: if (g_hCamera 0) { // 1. 打开相机例如打开第一个发现的相机 int nDeviceCount 0; Camera_EnumerateDevices(NULL, nDeviceCount); if (nDeviceCount 0) { char deviceInfo[256]; Camera_GetDeviceInfo(0, deviceInfo, 256); int ret Camera_Open(deviceInfo, g_hCamera); if (ret ! CAMERA_SUCCESS) { MessagePopup(错误, 打开相机失败); g_hCamera -1; return 0; } } // 2. 配置参数示例设置分辨率 Camera_SetResolution(g_hCamera, 1920, 1080); Camera_SetPixelFormat(g_hCamera, PIXEL_FORMAT_MONO8); // 3. 注册图像回调函数 Camera_SetFrameCallback(g_hCamera, MyFrameCallback, NULL); } // 4. 开始采集 if (!g_bAcquisitionRunning) { int ret Camera_StartAcquisition(g_hCamera); if (ret CAMERA_SUCCESS) { g_bAcquisitionRunning 1; SetCtrlVal(panelHandle, PANEL_CMD_START, 停止采集); // 更新按钮文字 } } else { // 5. 停止采集 Camera_StopAcquisition(g_hCamera); g_bAcquisitionRunning 0; SetCtrlVal(panelHandle, PANEL_CMD_START, 开始采集); } break; } return 0; } // 图像回调函数 void CVICALLBACK MyFrameCallback(int hCamera, void* pFrameData, int width, int height, int pixelFormat, void* pUserParam) { // 注意此回调通常在SDK创建的独立线程中被调用 // 6. 在此处理或显示图像。直接操作UI控件可能线程不安全。 // 安全做法将图像数据复制到全局缓冲区并通过PostDeferredCall通知主线程更新UI。 // 例如将pFrameData拷贝到全局图像缓冲区g_imageBuffer... // PostDeferredCall(UpdateImageDisplay, NULL); // UpdateImageDisplay在主线程中更新UI }这段框架代码清晰地展示了从打开设备到开始采集的流程。其中线程安全是回调模式下的一个关键考量点。在MyFrameCallback中直接调用CVI的UI函数如PlotXY是不安全的可能导致程序崩溃或界面卡死。正确的做法是通过PostDeferredCall将UI更新任务抛给主线程执行。4.3 参数设置与查询读懂SDK的“语言”SDK函数中大量的是Set和Get函数。设置参数时务必查阅文档理解参数的单位和有效范围。例如设置曝光时间单位可能是微秒(μs)也可能是毫秒(ms)设置增益可能是一个线性值也可能是分贝(dB)。盲目设置一个超出范围的值可能导致函数返回错误甚至设备行为异常。一个实用的技巧是在程序初始化时先使用Get函数读取所有关键参数的当前值和允许范围并以此初始化你的软件界面如滑块控件的最大最小值这样可以提升用户体验和软件的健壮性。5. 高级话题调试、封装与跨平台考量当基本功能实现后我们会遇到更复杂的需求和挑战。5.1 调试与错误排查当程序不按预期工作时调试与硬件交互的程序比调试纯软件复杂得多。以下是我常用的排查链路检查返回值 每一个SDK函数调用后立即检查其返回值。不要假设它总是成功。将错误代码与SDK文档中的错误码列表对照这是定位问题的第一步。使用厂商工具 大多数硬件厂商会提供配置或诊断工具如海康相机的“设备网络搜索工具”或“客户端演示程序”。先用这些工具确认硬件本身工作正常、连接通畅、参数可设。如果工具都连不上那问题大概率在驱动、网络或硬件本身而非你的代码。日志输出 在关键函数调用前后添加日志输出CVI中可用FileLogPrintf或输出到调试窗口记录函数调用、参数和返回值。这对于追踪偶发性问题尤其有效。简化复现 如果问题复杂尝试剥离无关代码创建一个最小的、能复现问题的测试程序。这有助于排除是SDK调用问题还是你程序其他部分的交互问题。检查资源泄漏 确保Open和CloseStart和Stop成对出现。长时间运行后程序崩溃或设备无法再次打开往往是句柄或资源未正确释放导致的。关注多线程 如前所述回调函数在非主线程中运行。确保对共享数据如图像缓冲区、状态标志的访问是线程安全的可以使用互斥锁CVI中的CmtLock/CmtUnlock。5.2 封装SDK构建自己的硬件抽象层如果你需要在一个大型项目中使用该SDK或者未来可能更换硬件供应商直接在所有业务代码中散落着SDK的函数调用是一种糟糕的做法。更好的架构是封装一个硬件抽象层Hardware Abstraction Layer, HAL。具体做法是创建一组你自己的接口函数例如// my_camera.h typedef void* MY_CAMERA_HANDLE; MY_CAMERA_HANDLE MyCamera_Open(const char* sn); int MyCamera_SetExposure(MY_CAMERA_HANDLE hCam, double time_ms); int MyCamera_GrabFrame(MY_CAMERA_HANDLE hCam, unsigned char* buffer, int* width, int* height); void MyCamera_Close(MY_CAMERA_HANDLE* phCam);然后在my_camera.c的实现内部再去调用具体的CameraSDK的函数。这样做的巨大好处是业务逻辑与硬件解耦 上层应用代码只依赖你定义的MyCamera_接口不关心底层是A厂商还是B厂商的SDK。便于替换和测试 未来更换相机时只需重写my_camera.c的实现甚至可以实现一个“模拟相机”的实现用于单元测试。统一错误处理 可以在抽象层内部对底层SDK的错误进行转换和统一处理向上层提供更友好的错误信息。5.3 从CVI到更现代的环境SDK的复用与迁移CVI虽然稳定但其开发环境和生态已逐渐老旧。我们可能会考虑将核心算法或硬件控制模块迁移到更现代的环境如 Visual Studio C 甚至 Python。这时那个CVI Windows SDK还能用吗答案是有可能但需要技巧。直接使用C接口 如果SDK提供的是纯C语言的API通常以extern C方式声明那么它在任何C/C编译器下都是可用的。你可以在Visual Studio中创建一个C项目同样包含头文件、链接库文件、并放置DLL。关键在于确保编译器的位数32/64位与SDK库文件的位数匹配。32位程序必须链接32位的.lib和调用32位的.dll64位亦然混合使用会导致链接错误或运行时崩溃。封装为DLL供其他语言调用 如果SDK是C接口有类、模板等直接在其他C编译器中使用可能存在ABI应用程序二进制接口兼容性问题。一个稳妥的方案是利用CVI本身将你对SDK的调用封装成一个标准的C接口DLL。在CVI中创建一个Dynamic Link Library工程导出纯C函数使用__declspec(dllexport)这些函数内部调用复杂的CVI或SDK功能。然后这个DLL就可以被Python通过ctypes、C#通过P/Invoke、LabVIEW等几乎所有支持调用标准Windows DLL的环境所使用。这相当于把CVI当作一个“粘合剂”或“适配器”。关注许可证与授权 将SDK功能通过DLL二次分发可能需要关注原SDK的许可证是否允许这样做。6. 实战案例封装一个通用的工业相机采集模块为了将上述理论具体化我分享一个简化版的实战案例设计一个用于CVI的、支持多品牌以模拟和真实SDK为例的相机采集模块。设计目标统一接口上层应用用同一套代码控制不同相机。支持轮询和回调两种采集模式。自动处理图像格式转换如Bayer到RGB。提供基本的相机参数管理。目录结构MyProject/ ├── App/ // 主应用程序 ├── HAL/ // 硬件抽象层 │ ├── CameraHal.h // 抽象接口定义 │ ├── CameraHal.c │ ├── Implementations/ // 各厂商SDK具体实现 │ │ ├── DummyCamera/ // 模拟相机用于测试 │ │ ├── HikCamera/ // 海康相机实现 │ │ └── DaHengCamera/ // 大恒相机实现 │ └── Common/ // 公共数据结构、工具函数 └── SDKs/ // 存放各厂商原始SDK ├── HikVision/ └── DaHeng/核心接口CameraHal.h#ifndef CAMERA_HAL_H #define CAMERA_HAL_H #ifdef __cplusplus extern C { #endif // 相机句柄 typedef void* CAMERA_HANDLE; // 错误码 typedef enum { CAMERA_OK 0, CAMERA_ERROR_INIT, CAMERA_ERROR_NOT_FOUND, CAMERA_ERROR_PARAM, CAMERA_ERROR_GRAB, // ... 更多错误码 } CAMERA_STATUS; // 图像信息 typedef struct { int width; int height; int bitsPerPixel; // 8, 16, 24等 void* pData; // 图像数据指针 long long timestamp; // 时间戳可选 } CAMERA_FRAME; // 回调函数类型定义 typedef void (*FrameCallbackFunc)(CAMERA_HANDLE hCamera, CAMERA_FRAME* pFrame, void* pUserParam); // 初始化相机子系统可选 CAMERA_STATUS CameraHal_Initialize(void); // 枚举可用相机 CAMERA_STATUS CameraHal_EnumerateDevices(char*** pppDeviceInfo, int* pCount); // 根据唯一标识如序列号打开相机 CAMERA_STATUS CameraHal_Open(const char* deviceId, CAMERA_HANDLE* phCamera); // 关闭相机 CAMERA_STATUS CameraHal_Close(CAMERA_HANDLE* phCamera); // 参数设置/获取示例曝光时间单位ms CAMERA_STATUS CameraHal_SetExposureTime(CAMERA_HANDLE hCamera, double timeMs); CAMERA_STATUS CameraHal_GetExposureTime(CAMERA_HANDLE hCamera, double* pTimeMs); // 采集控制 CAMERA_STATUS CameraHal_StartAcquisition(CAMERA_HANDLE hCamera, FrameCallbackFunc callback, void* pUserParam); CAMERA_STATUS CameraHal_StopAcquisition(CAMERA_HANDLE hCamera); // 轮询模式抓取一帧如果使用回调模式则不需要此函数 CAMERA_STATUS CameraHal_GrabFrame(CAMERA_HANDLE hCamera, CAMERA_FRAME* pFrame, int timeoutMs); // 释放枚举设备时分配的内存 void CameraHal_FreeDeviceList(char*** pppDeviceInfo, int count); // 反初始化可选 void CameraHal_Finalize(void); #ifdef __cplusplus } #endif #endif // CAMERA_HAL_H厂商具体实现以模拟相机DummyCamera为例 在HAL/Implementations/DummyCamera/DummyCamera.c中你需要实现上述所有接口。例如CameraHal_Open函数它内部会创建一个模拟相机的上下文结构体分配内存初始化一些默认参数并返回一个不透明的句柄。StartAcquisition函数则会启动一个定时器定期生成模拟图像数据如渐变条纹、正弦波图案并通过回调函数传递给上层。在CVI主程序中使用#include CameraHal.h void CVICALLBACK MyAppFrameCallback(CAMERA_HANDLE hCamera, CAMERA_FRAME* pFrame, void* pUserParam) { // 注意线程安全将图像数据传递给主线程处理 // 例如复制到全局缓冲区并发送自定义消息 PostDeferredCall(UpdateDisplay, pFrame); // UpdateDisplay需自己实现 } int main() { CAMERA_HANDLE hCam NULL; // 1. 枚举设备 char** devices NULL; int count 0; CameraHal_EnumerateDevices(devices, count); if (count 0) { // 2. 打开第一个设备这里可以根据序列号选择 CAMERA_STATUS status CameraHal_Open(devices[0], hCam); if (status CAMERA_OK) { // 3. 设置参数 CameraHal_SetExposureTime(hCam, 30.0); // 4. 开始采集注册回调 CameraHal_StartAcquisition(hCam, MyAppFrameCallback, NULL); // 5. 主循环或事件处理... RunUserInterface(); // 6. 停止并关闭 CameraHal_StopAcquisition(hCam); CameraHal_Close(hCam); } CameraHal_FreeDeviceList(devices, count); } return 0; }通过这样的设计当我们需要从“模拟相机”切换到“海康真实相机”时理论上只需要更换链接的库文件从DummyCamera.lib换成HikCamera.lib而主应用程序代码一行都不用改。这极大地提高了代码的可维护性和可扩展性。7. 总结与资源推荐面对一个像sdk.rar_CVI windows SDK这样的“历史遗产”从茫然到熟练驾驭关键在于建立系统性的方法解构SDK内容 - 精心配置环境 - 深入学习示例 - 理解编程模型 - 封装抽象接口 - 掌握调试技巧。这个过程不仅是技术学习更是工程思维的锻炼。最后分享几个在Windows平台进行C/C硬件集成开发时对我帮助巨大的工具和资源它们能帮你解决从环境配置到深度调试的各种问题Dependency Walker (depends.exe) 老牌但极其强大的DLL依赖查看工具。当你的程序提示“找不到xxx.dll”时用它打开你的EXE或DLL可以清晰看到所有依赖的动态链接库以及它们依赖的下一级DLL快速定位缺失的环节。对于排查由于运行时库MSVCRT*.DLL, VCRUNTIME*.DLL版本不对导致的问题尤其有效。Process Monitor (ProcMon) 来自微软Sysinternals套件的神器。它可以实时监控系统上所有进程的文件系统、注册表、网络和进程活动。当你的程序调用SDK函数失败却无明确错误时用ProcMon过滤你的进程名观察它试图打开哪些文件、读取哪些注册表键值常常能发现权限问题、路径错误或配置读取失败等隐藏问题。厂商社区与知识库 不要只埋头看文档。积极搜索和浏览设备厂商的官方社区、技术支持论坛和知识库。你遇到的90%的常见问题很可能已经有其他开发者提问并得到了解答。例如海康、大恒、Basler等主流厂商都有活跃的开发者社区。虚拟仪器技术社区 虽然CVI本身社区不如主流语言活跃但美国国家仪器NI的官方论坛NI Community依然是寻找CVI特定问题和解决方案的宝库。很多关于内存管理、线程安全、UI刷新等通用性问题在那里有深入的讨论。处理这类SDK就像与一个经验丰富但沉默寡言的老工匠合作你需要耐心读懂他留下的工具头文件、图纸文档和样品示例尊重他的工作模式编程模型才能让他为你创造出可靠的作品。每一次成功的集成不仅是完成了一个项目功能更是对你解决复杂工程问题能力的一次扎实提升。本文还有配套的精品资源点击获取
返回列表