ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

DWFToolkit-7.7源码编译与集成指南:嵌入式开发中的DWF格式处理利器

DWFToolkit-7.7源码编译与集成指南:嵌入式开发中的DWF格式处理利器 简介DWFToolkit-7.7-src 是 Autodesk 官方发布的开源 DWF 格式开发库面向建筑、工程与制造领域的 C 开发者用于在自有应用中集成 DWF 文件的查看、转换、测量、图层控制及安全管控能力解决设计数据跨平台分发与交互的技术瓶颈。压缩包为 ZIP 格式大小 32.5MB包含完整源码、头文件、静态/动态链接库及示例工程核心文件类型涵盖 .cpp/.h可二次开发的底层实现、.lib/.dllWindows 平台可直接调用的二进制组件以及 Visual Studio 项目配置文件结构清晰便于编译接入与调试验证。目前已有 599 人学习下载适合具备 C 基础并熟悉 CAD 数据处理逻辑的中高级开发者可直接基于该库构建轻量级 DWF 查看器、CAD 文档转换工具或 BIM 协同系统中的元数据管理模块。1. 项目概述DWFToolkit一个被低估的DWF格式开发利器如果你在嵌入式开发特别是STM32这类项目中经常需要处理设计图纸、查看PCB布局或者你的应用需要集成一个轻量级的CAD图纸查看器那么你很可能听说过或者正在寻找一种合适的文件格式和解析库。PDF太通用但缺乏专业设计信息原始的DWG/DXF又过于庞大和复杂。这时DWFDesign Web Format格式和它的官方开发库DWFToolkit就悄然进入了视野。DWFToolkit-7.7-src顾名思义是Autodesk官方发布的DWF格式处理工具包的7.7版本源代码。它不是一个成品软件而是一套C库允许开发者在其应用程序中读取、创建、甚至编辑DWF文件。对于需要将设计数据如二维图纸、三维模型视图、BOM表整合进自己系统的开发者来说它就像一把打开专业设计数据宝库的钥匙。很多人第一次接触它可能是因为某个遗留项目或者在网上搜索“如何打开.dwf文件”时顺藤摸瓜找到的。它的官方文档不算特别友好网络上的中文资料更是稀少这让不少开发者望而却步。但事实上一旦你理解了它的架构和基本用法它会成为一个非常强大的工具。尤其是在当前“软硬结合”的趋势下比如你用STM32做了一套工业设备上位机软件需要能展示设备对应的机械图纸或电气原理图直接集成DWFToolkit来解析DWF文件远比要求用户安装一个庞大的AutoCAD或专门的DWF Viewer要优雅和高效得多。结合现在流行的VSCode进行C/C开发配置好这个库就能为你的项目增添专业的图形数据处理能力。2. DWFToolkit核心架构与设计思路拆解2.1 DWF格式与DWFToolkit的定位要理解DWFToolkit必须先理解DWF。你可以把DWF想象成设计领域的“PDF”但它更专注于工程和设计数据的轻量化发布与协作。它由Autodesk创建本质上是一个压缩的、包含多种数据类型的容器格式。一个DWF文件里可以打包进二维矢量图形线条、多边形、文字等保证缩放不失真。光栅图像嵌入的图片、渲染快照。三维模型视图并非完整的3D模型数据而是特定的视图和快照。元数据图层信息、属性、标记红线圈阅等。DWFToolkit就是用来读写这个容器的“瑞士军刀”。它的设计思路是面向对象的C API提供了一系列的“阅读器”Reader和“书写器”Writer类以及代表DWF文件中各种元素的“对象”类如DWFGraphicDWFLayer。它的定位非常明确为第三方应用提供嵌入式的DWF处理能力而不是做一个独立的应用程序。因此它的API设计更偏向于底层和灵活你需要自己处理图形渲染、用户交互等上层逻辑。2.2 源代码包-src的价值与挑战我们拿到的是DWFToolkit-7.7-src即源代码版本。这与预编译的库.dll, .so, .a有本质区别。价值在于跨平台编译你可以针对Windows、Linux、macOS甚至嵌入式系统进行编译确保与你的目标环境完美兼容。深度定制与调试你可以根据需求修改源码需遵守许可协议或者当遇到诡异问题时能深入库内部进行调试这是黑盒库无法比拟的优势。理解内部机制阅读其源码是学习复杂文件格式解析和图形数据结构组织的绝佳案例。挑战也同样明显编译依赖复杂它依赖特定的第三方库如用于压缩的zlib用于XML解析的expat或libxml2。在7.7版本搭建编译环境是第一个拦路虎。版本与兼容性7.7是一个相对较老的版本Autodesk后期转向了其他技术。虽然稳定但可能缺少对新系统或编译器的最新支持需要一些适配工作。文档匮乏官方SDK文档主要介绍API对于如何从零开始编译、链接以及一些高级用例需要开发者自己摸索。2.3 与现代开发环境如VSCode的整合思路“stm32基于标准库开发vscode”这个热词提示了我们一个典型场景开发者使用VSCode配合ARM GCC工具链进行STM32开发。虽然DWFToolkit本身是x86/64桌面端的库但整合思路是相通的。通常DWFToolkit会被用于设备对应的上位机软件运行在Windows/Linux PC上而不是直接运行在STM32单片机里。上位机软件用C编写集成DWFToolkit解析DWF图纸并通过串口、网络等方式与STM32设备交互。在VSCode中管理这样一个C项目关键点在于配置好编译构建系统如CMake和头文件/库文件的路径。你需要先成功编译出DWFToolkit的静态库.a或.lib然后在你上位机项目的CMakeLists.txt中通过include_directories和target_link_libraries命令将其引入。这个过程正是从“拿到源码”到“为我所用”的核心桥梁。3. 从源码到库编译与配置实战详解这是使用DWFToolkit的第一步也是最容易卡住的一步。下面以在Ubuntu Linux系统下编译为例Windows思路类似但工具和路径不同。3.1 环境准备与依赖项安装首先确保你的系统有基本的开发工具和DWFToolkit所需的依赖。# 更新包列表并安装编译工具和基础依赖 sudo apt-get update sudo apt-get install build-essential cmake git # 安装DWFToolkit核心依赖zlib (压缩), libxml2 (XML解析) sudo apt-get install zlib1g-dev libxml2-dev注意DWFToolkit 7.7默认使用libxml2进行XML解析。早期版本可能使用expat请根据源码包内的README或构建文件确认。libxml2功能更全面是推荐的选择。接下来获取源码。假设你已经下载了DWFToolkit-7.7-src.zip并解压到~/dwf目录。3.2 源码结构分析与编译配置进入源码目录你会看到典型的旧式C项目结构可能包含多个子目录Source,Include,Build等和不同平台的解决方案文件如.slnfor Visual Studio。关键目录通常包括Include/所有公共头文件。你的项目需要包含这个路径。Source/所有C源代码文件按模块组织。Build/或Make/可能包含旧的Makefile或项目文件。对于现代编译我们更倾向于使用CMake来管理。但DWFToolkit 7.7可能不自带CMakeLists.txt。我们需要自己编写一个或者使用它提供的Makefile。方案一使用原有Makefile如果存在且可用cd ~/dwf/DWFToolkit-7.7-src # 查找是否有Makefile或makefile ls -la | grep -i makefile # 如果有尝试执行 make。可能需要指定配置如 make release make如果成功会在Lib/或Bin/目录下生成libDWFToolkit.soLinux动态库或libDWFToolkit.a静态库。方案二创建简单的CMakeLists.txt更推荐便于集成在源码根目录创建CMakeLists.txtcmake_minimum_required(VERSION 3.10) project(DWFToolkit LANGUAGES CXX) # 设置C标准根据源码要求C98或C11 set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 包含头文件目录 include_directories(Include) # 查找所有源文件 file(GLOB_RECURSE SOURCE_FILES Source/*.cpp) # 添加库目标 add_library(DWFToolkit STATIC ${SOURCE_FILES}) # 查找并链接依赖库 find_package(ZLIB REQUIRED) find_package(LibXml2 REQUIRED) target_include_directories(DWFToolkit PUBLIC $BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/Include ${LIBXML2_INCLUDE_DIR} ) target_link_libraries(DWFToolkit PUBLIC ${ZLIB_LIBRARIES} ${LIBXML2_LIBRARIES}) # 安装规则可选 install(TARGETS DWFToolkit ARCHIVE DESTINATION lib) install(DIRECTORY Include/ DESTINATION include)然后进行编译cd ~/dwf/DWFToolkit-7.7-src mkdir build cd build cmake .. -DCMAKE_BUILD_TYPERelease make -j$(nproc)编译成功后在build目录下你会找到libDWFToolkit.a。3.3 编译过程中的常见陷阱与解决错误找不到zlib.h或xml2.h原因依赖库的开发包未安装。解决确保已执行sudo apt-get install zlib1g-dev libxml2-devLinux或正确配置了Windows下的包含路径和库路径。错误undefined reference toXML_...原因链接时找不到libxml2的库。解决在CMakeLists.txt中确保find_package(LibXml2 REQUIRED)和target_link_libraries正确执行。如果使用Makefile可能需要手动在LDFLAGS中添加-lxml2。错误C语法错误如‘for’ loop initial declarations are only allowed in C99 mode原因源码中使用了较新的C语法如在for循环内声明变量但编译器被设置为较旧的标准。解决在CMake中明确设置set(CMAKE_CXX_STANDARD 11)或在GCC命令行中添加-stdc11。警告众多但编译通过DWFToolkit 7.7源码可能包含一些旧式写法如strcpy未检查长度编译器会产生安全警告。只要不是错误通常可以忽略。如果希望静默可以在CMake中针对该目标添加编译选项-Wno-deprecated-declarations等。4. 核心API解析与基础使用范例成功编译出库文件后我们来看看如何在自己的C程序中使用它。核心流程是初始化库 - 打开DWF文件 - 遍历内容 - 提取或处理所需数据 - 清理资源。4.1 初始化与资源管理DWFToolkit需要显式初始化和终止以管理内部全局状态。#include DWFToolkit/DWF.h // 主要头文件 int main() { // 1. 初始化工具包 try { DWF::Initialize(); } catch (DWF::Exception e) { std::cerr 初始化失败: e.message() std::endl; return -1; } // ... 你的业务逻辑 ... // 最后终止工具包 DWF::Terminate(); return 0; }实操心得一定要将DWF::Initialize()和DWF::Terminate()放在最外层确保它们只被调用一次。可以将它们放在main函数的开头和结尾或者封装成一个单例的RAII资源获取即初始化类利用构造函数和析构函数自动管理。4.2 读取DWF文件与遍历内容树这是最常用的功能。DWF文件内部像一棵树有包Package、节Section、资源Resource等层级。#include DWFToolkit/DWF.h #include DWFToolkit/DWFPackageReader.h #include iostream void readDWF(const char* filePath) { try { // 2. 创建包阅读器 DWF::DWFPackageReader reader(filePath); // 3. 打开包 reader.open(); // 4. 获取包对象 DWF::DWFPackage* pPackage reader.getPackage(); if (pPackage NULL) { std::cerr 无法获取包对象 std::endl; return; } // 5. 遍历包内的所有“节”Section例如图纸节、标记节 DWF::DWFSection::tIterator* piSections pPackage-getSections(); if (piSections) { for (; piSections-valid(); piSections-next()) { DWF::DWFSection* pSection piSections-get(); std::wcout L找到节: pSection-name() std::endl; // 6. 遍历节内的所有“对象”Object例如图形、图像 DWF::DWFObject::tIterator* piObjects pSection-getObjects(); if (piObjects) { for (; piObjects-valid(); piObjects-next()) { DWF::DWFObject* pObject piObjects-get(); // 判断对象类型并处理 if (pObject-type() DWF::Object::eGraphic) { DWF::DWFGraphic* pGraphic dynamic_castDWF::DWFGraphic*(pObject); if (pGraphic) { std::wcout L - 图形对象: pGraphic-label() std::endl; // 可以进一步获取图形的几何数据顶点、路径等 } } else if (pObject-type() DWF::Object::eImage) { DWF::DWFImage* pImage dynamic_castDWF::DWFImage*(pObject); if (pImage) { std::wcout L - 图像对象格式: pImage-format() std::endl; // 可以提取图像的原始像素数据 } } } // 务必手动删除迭代器 DWFCORE_FREE_OBJECT(piObjects); } } DWFCORE_FREE_OBJECT(piSections); } // 7. 关闭阅读器 reader.close(); } catch (DWF::Exception e) { std::cerr 处理DWF文件时出错: e.message() std::endl; } }关键点解析DWFCORE_FREE_OBJECT这是DWFToolkit内部的内存释放宏。非常重要对于它返回的迭代器Iterator对象使用完毕后必须用这个宏来释放而不是C的delete。这是新手最容易导致内存泄漏的地方。宽字符DWFToolkit内部大量使用wchar_t宽字符来处理字符串因为设计图纸中的文本可能是多语言的。所以你会看到std::wcout和L”字符串“。动态类型转换通过type()判断对象类型后使用dynamic_cast进行安全的向下转型以调用特定子类如DWFGraphic的方法。4.3 提取图形数据与元信息获取到DWFGraphic对象后如何拿到具体的线条、多边形数据呢这涉及到更底层的“几何体”Geometry访问。void processGraphic(DWF::DWFGraphic* pGraphic) { // 获取图形的几何迭代器 DWF::DWFGeometry::tIterator* piGeometries pGraphic-getGeometry(); if (piGeometries) { for (; piGeometries-valid(); piGeometries-next()) { DWF::DWFGeometry* pGeometry piGeometries-get(); // 几何体也有类型如路径、多段线、文本等 switch (pGeometry-type()) { case DWF::Geometry::ePath: { DWF::DWFSegment* pSegment dynamic_castDWF::DWFSegment*(pGeometry); if (pSegment) { // 获取路径的顶点 const double* pVertices pSegment-vertices(); size_t numVertices pSegment-verticesCount(); std::cout 找到一条路径包含 numVertices 个顶点。 std::endl; for (size_t i 0; i numVertices; i) { std::cout 顶点 i : ( pVertices[i*2] , pVertices[i*21] ) std::endl; } } break; } case DWF::Geometry::eText: { DWF::DWFText* pText dynamic_castDWF::DWFText*(pGeometry); if (pText) { std::wcout L文本内容: pText-string() L, 位置: ( pText-position()[0] L, pText-position()[1] L) std::endl; } break; } // 处理其他几何类型... } } DWFCORE_FREE_OBJECT(piGeometries); } // 获取图形属性元数据 DWF::DWFPropertyContainer* pProps pGraphic-getProperties(); if (pProps) { DWF::DWFProperty::tIterator* piProps pProps-getProperties(); if (piProps) { for (; piProps-valid(); piProps-next()) { DWF::DWFProperty* pProp piProps-get(); std::wcout L属性 - pProp-name() L: pProp-value() std::endl; } DWFCORE_FREE_OBJECT(piProps); } } }通过这种方式你可以将DWF中的矢量图形数据提取出来转换成你自己的图形引擎如OpenGL, Qt Graphics, 甚至简单的SVG可以理解的格式进行渲染。5. 集成到实际项目以VSCode CMake上位机项目为例假设我们有一个STM32设备的上位机软件项目使用VSCode进行开发需要集成DWFToolkit来显示设备对应的DWF图纸。5.1 项目结构规划你的上位机项目/ ├── CMakeLists.txt # 项目主CMake文件 ├── src/ │ ├── main.cpp │ ├── DWFViewer.cpp │ └── DWFViewer.h ├── lib/ │ └── DWFToolkit/ # 将编译好的DWFToolkit库和头文件放在这里 │ ├── include/ # 从源码包拷贝的Include目录内容 │ │ └── DWFToolkit/ │ │ ├── DWF.h │ │ └── ... │ └── lib/ │ ├── libDWFToolkit.a # 编译生成的静态库 │ └── (或 .lib / .so) └── assets/ └── example.dwf # 示例DWF文件5.2 CMakeLists.txt 配置cmake_minimum_required(VERSION 3.10) project(MyDeviceViewer LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) # 1. 添加DWFToolkit库 # 假设你将编译好的库和头文件放在了项目下的 lib/DWFToolkit 目录 set(DWFTOOLKIT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/lib/DWFToolkit) include_directories(${DWFTOOLKIT_DIR}/include) # 添加DWFToolkit静态库 add_library(DWFToolkit STATIC IMPORTED) set_target_properties(DWFToolkit PROPERTIES IMPORTED_LOCATION ${DWFTOOLKIT_DIR}/lib/libDWFToolkit.a INTERFACE_INCLUDE_DIRECTORIES ${DWFTOOLKIT_DIR}/include ) # 2. 查找DWFToolkit的依赖项 (zlib, libxml2) find_package(ZLIB REQUIRED) find_package(LibXml2 REQUIRED) # 3. 创建你的可执行文件 add_executable(MyDeviceViewer src/main.cpp src/DWFViewer.cpp src/DWFViewer.h) # 4. 链接所有库 target_link_libraries(MyDeviceViewer PRIVATE DWFToolkit ${ZLIB_LIBRARIES} ${LIBXML2_LIBRARIES} # 如果你的GUI用了Qt还需要链接Qt库例如 # Qt5::Widgets Qt5::Core )在VSCode中你需要安装CMake Tools扩展然后打开项目文件夹。VSCode会自动识别CMakeLists.txt并让你选择编译工具链如GCC。配置好之后按F7或点击底部状态栏的“Build”即可编译整个项目。5.3 编写一个简单的DWF查看器类在DWFViewer.h/cpp中封装DWFToolkit的基本操作。// DWFViewer.h #pragma once #include string #include vector #include memory class DWFViewer { public: DWFViewer(); ~DWFViewer(); bool openFile(const std::string filePath); void closeFile(); std::vectorstd::wstring listSheetNames() const; // ... 其他接口如获取图形数据、渲染等 private: class Impl; // 使用Pimpl惯用法隐藏DWFToolkit头文件细节 std::unique_ptrImpl pImpl; };使用PimplPointer to Implementation惯用法是个好主意因为它可以将DWFToolkit庞大的头文件和复杂的依赖关系隔离在.cpp文件中保持主项目代码的整洁并加快编译速度。// DWFViewer.cpp #include DWFViewer.h #include DWFToolkit/DWF.h #include DWFToolkit/DWFPackageReader.h // ... 其他DWFToolkit头文件 class DWFViewer::Impl { public: DWF::DWFPackageReader* pReader nullptr; DWF::DWFPackage* pPackage nullptr; // ... 其他状态数据 }; DWFViewer::DWFViewer() : pImpl(std::make_uniqueImpl()) { DWF::Initialize(); } DWFViewer::~DWFViewer() { closeFile(); DWF::Terminate(); } bool DWFViewer::openFile(const std::string filePath) { closeFile(); // 先关闭已打开的文件 try { pImpl-pReader new DWF::DWFPackageReader(filePath.c_str()); pImpl-pReader-open(); pImpl-pPackage pImpl-pReader-getPackage(); return (pImpl-pPackage ! nullptr); } catch (DWF::Exception e) { // 记录错误日志 return false; } } // ... 实现其他成员函数这样在你的main.cpp或GUI代码中就可以简单地创建DWFViewer对象调用openFile然后获取数据再与你选择的图形界面库如Qt的QGraphicsScene、ImGui等结合将图形绘制出来。6. 高级应用、性能优化与疑难排坑6.1 处理大型DWF文件与性能考量当DWF文件包含数百张图纸或复杂三维视图时一次性加载所有内容到内存可能导致卡顿甚至崩溃。优化策略按需加载不要在一开始就遍历和解析所有对象。利用DWFSection和DWFObject的层级关系先快速扫描出图纸列表通常每个图纸是一个DWFGraphic或位于一个独立的DWFSection中。当用户点击某张图纸时再深入加载和解析该图纸的几何数据。数据缓存将解析后的几何数据如顶点数组缓存起来避免重复解析。对于静态图纸这是非常有效的。简化渲染不是所有几何细节都需要原样渲染。对于缩放级别很小时的视图可以只渲染图形的包围盒或使用更简化的几何表示LOD Level of Detail。这需要你在提取数据后在自己的渲染管线中实现。多线程解析将文件IO和XML解析放在独立线程防止阻塞UI。但注意DWFToolkit的对象模型可能不是线程安全的最好在一个线程内完成对一个DWF包的所有操作。6.2 坐标系与单位转换DWF文件内部使用的坐标系是“世界坐标系”通常是双精度浮点数单位可能是英寸、毫米或其他设计单位。你需要将这些坐标转换到你的屏幕或渲染视口坐标系。获取图形范围DWFGraphic对象通常有extents()方法可以获取其最小和最大的X、Y坐标即包围盒。计算变换矩阵根据图形范围和你视图窗口的大小计算一个缩放和平移矩阵。应用变换在渲染每个顶点时手动应用这个矩阵或者将矩阵传递给你的图形API如OpenGL的模型视图矩阵。void calculateTransform(const DWF::DWFGraphic* pGraphic, double viewportWidth, double viewportHeight, double scale, double offsetX, double offsetY) { if (!pGraphic) return; const double* pExtents pGraphic-extents(); // [minX, minY, maxX, maxY] double graphicWidth pExtents[2] - pExtents[0]; double graphicHeight pExtents[3] - pExtents[1]; // 计算保持宽高比的缩放因子 double scaleX viewportWidth / graphicWidth; double scaleY viewportHeight / graphicHeight; scale std::min(scaleX, scaleY) * 0.9; // 留一些边距 // 计算居中偏移 offsetX (viewportWidth - graphicWidth * scale) / 2.0 - pExtents[0] * scale; offsetY (viewportHeight - graphicHeight * scale) / 2.0 - pExtents[1] * scale; }6.3 常见错误与问题排查实录问题1程序崩溃错误信息模糊。排查十有八九是内存管理问题。请严格检查所有通过getSections(),getObjects(),getGeometry()等获得的迭代器是否都用DWFCORE_FREE_OBJECT释放了是否在DWF::Initialize()之前或DWF::Terminate()之后尝试调用任何DWFToolkit API是否在多线程中不安全地访问了同一个DWF对象工具在Linux下使用valgrind在Windows下使用Visual Studio的调试器或Dr. Memory来检测内存错误。问题2打开某些DWF文件失败提示“Invalid package”或“Unsupported version”。原因DWFToolkit 7.7可能不支持太新或太旧版本的DWF格式。DWF格式本身有多个版本。解决尝试用Autodesk官方的免费DWF Viewer打开该文件。如果能打开说明文件本身没问题。可能是该文件使用了某些高级特性如特定压缩、加密或版本而7.7版本不支持。考虑寻找更新版本的DWFToolkit如果存在或者要求文件提供方导出为兼容版本。问题3提取出的文字是乱码。原因字符编码问题。DWF内部文本可能使用UTF-8、UTF-16或系统本地编码。解决DWFText::string()返回的是宽字符串(const wchar_t*)。确保你的程序在编译和运行时宽字符设置正确。在Linux/Unix系统确保区域设置支持如setlocale(LC_ALL, en_US.UTF-8)。如果仍有问题尝试使用DWFString类提供的其他转换方法。问题4编译链接时出现大量“undefined reference”错误但库路径明明对了。原因依赖库的链接顺序可能很重要或者你需要链接额外的系统库。解决在CMake的target_link_libraries中调整库的顺序。一般规则是被依赖的库放在后面。尝试target_link_libraries(MyApp DWFToolkit ${LIBXML2_LIBRARIES} ${ZLIB_LIBRARIES} pthread dl)。在Linux下pthread线程库和dl动态加载库有时是DWFToolkit或它的依赖所需要的。问题5在Windows下编译遇到“安全编译警告”或“函数过时警告”导致编译失败。原因新版本Visual Studio的SDL检查或安全开发生命周期检查更严格而DWFToolkit 7.7源码使用了如sprintf等“不安全”函数。解决临时禁用项目属性中的SDL检查不推荐长期。定义宏_CRT_SECURE_NO_WARNINGS来屏蔽这些警告。在CMake中可以通过add_definitions(-D_CRT_SECURE_NO_WARNINGS)实现。高级修改源码将不安全的函数调用替换为安全版本如sprintf_s但这需要仔细测试。将DWFToolkit这样一个有一定年代的工业级库成功集成到现代开发环境中本身就是一个充满挑战但极具成就感的过程。它就像一座桥梁连接了专业的工程设计数据和灵活的现代软件开发。当你看到自己编写的程序能够流畅地展示出复杂的机械图纸时之前为解决编译问题、内存泄漏和坐标转换所熬的夜就都值了。本文还有配套的精品资源点击获取
返回列表