资讯动态

基于C++与Qt的Word编辑器开发实践:从选型到源码实现

发布时间:2026/10/23 4:52:29 来源:尧图企业网站定制
用C写一个Word编辑器这个念头大概率在很多学C的朋友脑子里冒出来过。但真正敢把它做成一个能跑、能排版、能存盘、能撤销的完整桌面项目而不是停留在控制台里读字符串的练手程序要跨过的坎比想象中多得多。这类项目最吸引人的地方在于文本编辑器的每一个功能都足够熟悉但实现每一个功能都涉及字符串处理、内存管理、GUI事件循环、文本排版和序列化几乎把C的常见战场全串起来了。如果你正好想找一个能写进简历、能演示、能长期维护的实际项目这个方向性价比极高。这篇文章就用这个“基于C的Word编辑器附源码”项目来拆解整个实现过程从技术选型、架构设计、核心功能实现到界面打磨、问题排查、性能优化完整走一遍。它不是教程式的逐行讲解更像是我做这类项目时沉淀下来的思路和经验。不同基础的读者都能拿它当参考哪怕你刚开始学Qt也能跟着把架子搭起来再慢慢填功能。1. 从需求到方案C编辑器怎么选型1.1 先说清“Word编辑器”到底要做什么很多人拿到这个需求第一反应是打开一个空白窗口开始写代码。我建议反过来先花半小时把目标写清楚用户日常打开Word真正高频使用的功能有哪些我列了一份清单基本上就是输入文字、设置字体字号字色、加粗斜体下划线、段落对齐、项目符号、查找替换、撤销重做、保存打开、打印导出。再梳理一下这些功能可以归为四层文本层输入、删除、选区、剪贴板、光标移动。格式层字符格式、段落格式、列表、表格、图片。交互层菜单、工具栏、快捷键、右键菜单、状态栏反馈。序列化层打开保存各种格式内部数据结构与磁盘格式之间的互相转换。这个分类看起来简单但它决定了后面架构的走向。文本编辑器最核心的概念不是“显示”而是“文档模型与视图分离”——你输入文字时修改的是内存里的文档模型界面上的窗口只是模型的一个投影。理解这一点后面处理光标、撤销、格式化才不会乱成一锅粥。1.2 为什么选了Qt而不是纯Win32或MFC技术选型是这个项目最关键的决定。常见的方案有三个纯Win32 API手写、MFC、Qt。方案优点痛点适合人群纯Win32底层完全可控消息循环、句柄管理都能摸透任何控件都要自己画滚动条、字体对话框、文本排版全都从零写代码量爆炸想彻底打通Windows底层的人MFC老牌封装资料多年代感强现代C体验差界面观感停留在十年前需要维护老系统的人Qt自带完整的富文本引擎跨平台社区活跃需要花时间学习框架自身的对象模型和信号槽机制想做完整产品而非研究底层的人我的选择是Qt理由很实际这个项目定位是“编辑器”重点应该放在编辑逻辑和用户体验上而不是把时间耗在控件绘制和窗口消息处理上。Qt的QTextDocument是非常成熟的富文本模型内置段落布局、字符格式、表格、图片、撤销栈这些恰好是Word编辑器的地基。站在它的肩膀上我们可以把精力聚焦在真正的业务逻辑上而不是自己重造一个文本布局引擎。我用的具体版本是Qt 6配CMake构建开发体验比早年的qmake顺滑很多。1.3 功能边界怎么划哪些必须做哪些不值得做好项目的第二个关键是敢砍需求。第一版我强烈建议把范围控制在这些地方文件功能新建、打开、保存、另存为、编辑功能撤销、重做、剪切、复制、粘贴、格式功能字体、字号、颜色、粗体、斜体、下划线、对齐、项目符号、视图功能缩放、状态栏、查找替换。这些做扎实已经是一个可用的编辑器了。不建议第一版碰拼写检查、公式编辑、目录生成、分栏、宏录制。拼写检查需要语言词典公式渲染是一整套数学排版系统宏需要嵌入脚本引擎这三个任何一个都能单独占掉一个学期的精力。划边界不等于偷懒而是把资源投向高杠杆的地方。先把富文本的地基打牢后续扩展才有意义。项目做完以后你会发现真正费时间的不是某个功能本身而是功能之间的状态协调——比如选中一段带格式的文字后再输入新文字格式应该保留还是清空这种小问题最考验设计。2. 编辑器架构别一上来就写界面2.1 五个模块的职责划分很多初学者拿到这个项目会直接往MainWindow里堆代码菜单写在构造函数里逻辑写在槽函数里文档操作写在按钮事件里。这样做原型很快但功能一多就开始痛苦——查找替换要访问文档状态栏要监听修改保存按钮要判断文件路径所有东西乱成一团。我的做法是先区分五层模块文档模型层DocumentModel负责封装QTextDocument对外提供新建、打开、保存、修改标记等高层接口。编辑控制层RichTextEdit继承QTextEdit处理光标、选区、键盘事件、右键菜单。界面呈现层MainWindow负责所有QAction的注册、菜单栏、工具栏、状态栏、快捷键的搭建。查找替换层FindBar作为独立小组件承载查找、替换、高亮逻辑。应用服务层Settings保存窗口大小、最近文件列表、自动保存间隔等配置。这样划分的核心原则是MainWindow不直接操作QTextDocument所有对文档的修改一律经过DocumentModel。这么做看起来多绕了一层但好处是如果以后要给编辑器加命令行模式、脚本接口或者多窗口预览只需要对接DocumentModel而不需要拽上整套UI。模块之间通过信号和槽通信界面只管展示业务逻辑只管数据。2.2 核心类设计与协作流程DocumentModel的头文件大致长这样它是整个编辑器的门面// DocumentModel.h class DocumentModel : public QObject { Q_OBJECT public: explicit DocumentModel(QObject* parent nullptr); QTextDocument* document() const { return m_doc; } void newDocument(); bool loadFile(const QString filePath, QString* errorOut nullptr); bool saveFile(const QString filePath, QString* errorOut nullptr); QString currentFile() const { return m_filePath; } bool isModified() const { return m_doc-isModified(); } signals: void modificationChanged(bool modified); void undoAvailable(bool available); void redoAvailable(bool available); private: QTextDocument* m_doc nullptr; QString m_filePath; };RichTextEdit则专注编辑交互它持有DocumentModel的指针对外暴露textCursor、mergeFormatOnSelection这类接口。MainWindow做的事情就是创建这三个核心对象然后用QAction把菜单和编辑器的功能连接起来。这里有个细节DocumentModel构造时就把m_doc初始化好不要让MainWindow去new QTextDocument再传进来否则很容易出现两处都持有Document指针、改了一处另一处不刷新的问题。2.3 一个操作从鼠标点击到落盘的完整链路理解了模块划分一个操作是怎么走完整个链路的就很清晰了。拿“加粗选中的文字”举例用户点击工具栏的加粗按钮这个按钮是一个QAction它触发信号后MainWindow的槽函数拿到RichTextEdit当前的光标构造一个QTextCharFormat并设置字体权重为粗体然后调用mergeCharFormat把格式合并到光标选区上。QTextDocument检测到内容发生变化发出contentsChange信号RichTextEdit的视图随之刷新。同时DocumentModel监听到修改状态变化把信号转发给主窗口标题栏出现星号自动保存计时器启动。用户按下CtrlSMainWindow把保存请求转发给DocumentModelDocumentModel从QTextDocument取出内容序列化到磁盘。这条链路最大的价值在于任何功能都不属于某一行代码而是属于整个系统的协作。后期排查bug的时候你能很快判断问题出现在显示层、数据层还是交互层。这是编辑器项目能长期演进的骨架。3. 核心功能实现每个功能背后都在解决同一类问题3.1 文档模型把文本当成一棵树QTextDocument的内部结构我的理解是一棵类似HTML DOM的树。段落是树干段落内部的字符片段是树叶格式信息挂在节点上。QTextBlock代表一个段落它的blockFormat管理缩进、对齐、行距这些段落级属性每个block内部被分成若干个QTextFragment每个fragment有独立的QTextCharFormat记录字体、颜色、字号等字符级属性。段落之间还可以插入QTextTable、QTextFrame这类容器节点实现表格、文本框、分栏这类复杂排版。理解这棵树最大的好处是明白一个原理当你修改某个fragment的字体时只有受影响的文本需要重新布局其他段落完全不受影响。这就是为什么大文档修改格式时界面依然能保持流畅的原因。操作这棵树靠的是QTextCursor它就像文件系统里移动的指针你可以在树的任意位置插入节点、删除节点、修改节点属性。每次修改之后cursor的位置会变化如果不注意保存和恢复position很容易出现“改错地方”的诡异bug。3.2 格式编辑为什么clear、merge、set不能乱用刚开始做格式功能时我在QTextCharFormat的三个接口上栽过跟头setCharFormat、mergeCharFormat、clearCharFormat它们语义完全不同。setCharFormat会把整个选区的格式替换成当前提供的格式选区原有的字号、颜色、下划线等等属性全部丢失。mergeCharFormat只会更新你显式设置的属性没提到的属性原样保留。clearCharFormat会把格式重置为默认样式相当于Word里的“清除格式”。举个例子你想把选中文字变成红色粗体但保留它们原来的字号正确做法是构造一个只设置了foreground和fontWeight的QTextCharFormat然后调用mergeCharFormat。如果误用setCharFormat选区的字号会被重置为默认值所有手动调过的行距、字符间距也会被冲掉。这个bug的隐蔽性在于界面不会报错只是样式悄悄变了排查起来很费劲。一个完整的文字加粗函数大致是这样void RichTextEdit::makeSelectionBold() { QTextCursor cursor textCursor(); if (!cursor.hasSelection()) { cursor.select(QTextCursor::WordUnderCursor); } QTextCharFormat fmt; fmt.setFontWeight(cursor.charFormat().fontWeight() QFont::Bold ? QFont::Normal : QFont::Bold); cursor.mergeCharFormat(fmt); setTextCursor(cursor); mergeCurrentCharFormat(fmt); }注意最后两步setTextCursor把修改后的光标状态同步回编辑器mergeCurrentCharFormat则把当前格式同步到输入状态。如果不做最后一步你会发现加粗后的文字后面继续输入新输入的文字并不会继承粗体用户会觉得很莫名其妙。这两步缺一不可。3.3 剪贴板与撤销重做让一系列操作变成一条命令QTextDocument自带一套完整的撤销栈和重做栈不需要自己实现命令模式。但默认的撤销粒度有一个坑每次insertText、removeSelectedText都会生成一条独立的撤销记录。如果你用循环粘贴100次文本那么用户按一次撤销只能退一格要按100下才能回到粘贴前体验非常糟糕。解决办法是把一组逻辑上相关的操作包在编辑块里。QTextCursor提供了beginEditBlock和endEditBlock在这个区间内做的所有修改会被合并成单条撤销命令QTextCursor cursor textCursor(); cursor.beginEditBlock(); cursor.removeSelectedText(); cursor.insertText(newText); cursor.endEditBlock(); setTextCursor(cursor);这样一次粘贴操作就是一条撤销命令用户按一次CtrlZ就能整体撤销。踩过的另一个坑是调用了beginEditBlock之后忘了配对endEditBlock。一旦忘了后续所有修改都会被吞进同一个事务块里撤销时整片内容一起消失。排查这个问题的方法很直接监听QTextDocument的undoCommandAdded信号在每次操作后打印撤销栈的命令数量检查粒度是否符合预期。3.4 查找与替换迭代器失效是最大的坑查找功能看起来简单但做高亮和替换时有个经典问题一边修改文档一边继续用旧的cursor查找位置会偏移甚至导致死循环。实现“查找所有匹配项并高亮”的核心代码是这样的QListQTextEdit::ExtraSelection selections; QTextDocument* doc editor-document(); QTextCursor cursor(doc); while (!cursor.isNull() !cursor.atEnd()) { cursor doc-find(keyword, cursor, QTextDocument::FindCaseSensitively); if (cursor.isNull()) break; QTextEdit::ExtraSelection sel; sel.cursor cursor; sel.format.setBackground(QColor(#fff3a0)); selections.append(sel); } editor-setExtraSelections(selections);这里的关键是循环条件写成cursor.isNull()因为QTextDocument::find内部会基于传入cursor的位置继续向后搜索只要找到匹配项就会返回一个新的有效cursor。如果你粗心写成了while(!cursor.atEnd())并且循环体内没有正确更新cursor就会造成无限循环导致界面卡死。替换操作比查找更容易翻车。在循环里对匹配文本调用insertText之后当前cursor的内容和位置都会被改变如果继续用这个被修改过的cursor去查找下一个关键词很可能跳过某些匹配项或者重复处理同一段文本。安全做法是每次替换完成后都从匹配文本的结束位置重新创建一个全新的QTextCursor再用这个新cursor继续查找。另外当匹配数量超过一定阈值比如3000个时我建议停止继续高亮只在状态栏提示“匹配项过多仅显示前3000个”。这个限制能避免大文件执行全部替换时界面明显卡顿。3.5 文档落盘与PDF导出保存格式怎么做选择QTextEdit自带的toPlainText只能存纯文本格式全丢toHtml能存HTML但图片资源的管理很麻烦。我在项目里做了三种导出并存Markdown适合日常编辑文件干净兼容性好。HTML带完整样式适合复制到博客或网页。PDF通过QPrinter打印输出用于交付给其他人查看。PDF导出的代码不复杂QPrinter printer(QPrinter::HighResolution); printer.setPageSize(QPageSize(QPageSize::A4)); printer.setOutputFormat(QPrinter::PdfFormat); printer.setOutputFileName(export.pdf); QTextDocument* doc model-document(); doc-print(printer);为什么用print而不是直接截屏拼图因为QTextDocument::print会利用QPrinter的页面尺寸和页边距重新排版内容自动分页而截图方式无法处理跨页的长文档。实现时有个必须注意的点printer的页面尺寸一定要显式设置不设置的话某些系统会默认成Letter大小打印出来的表格会横向溢出一页挤不下观感很差。打开文件时的编码问题也要提前想清楚。实战中遇到的中文乱码百分之八十是因为源码文件编码和打开方式不一致。我的处理方式是先用QFile读取前三个字节判断BOMUTF-8 BOM直接解码无BOM的文件默认按UTF-8尝试解码解码结果里如果出现大量替换字符就回退到本地编码。Qt 6里QTextStream配合QStringConverter可以比较干净地处理这件事避免使用已经被移出核心模块的旧接口。3.6 图片与表格给编辑器装上“富文本引擎”插入图片的实现QTextImageFormat imgFormat; imgFormat.setName(filePath); imgFormat.setWidth(400); QTextCursor cursor textCursor(); cursor.insertImage(imgFormat);插入图片后图片是作为文档的一个特殊字符存在的。第一次做的时候我踩了一个大坑图片插入后保存文档再打开时图片“消失”了。原因是最常见的保存格式根本不携带图片资源HTML保存方式下图片是否能保留取决于外部资源是否可访问。我的解决方案是在项目里引入了一个资源收集器序列化文档时遍历文档内所有QTextImageFormat对应的图片路径把图片文件和一个XML描述文档一起打包成zip自定义一个.edx扩展名。这样整个项目迁移、分发都很方便不会出现图片丢在某个临时路径的情况。表格操作同样需要小心。插入表格是cursor.insertTable(rows, cols)它会返回一个QTextTable对象你可以通过cellAt(row, col)拿到每个单元格并设置文本。但真正的难点不在插入而在光标导航当用户的光标在表格内部时按Tab应该跳转到下一个单元格而不是插入一个制表符在最后一行的最后一个单元格按Tab应该自动新增一行。这些都需要在RichTextEdit里重写键盘事件判断当前光标位置是否在表格内再决定是插入制表符还是转移焦点。一开始不对这个问题做处理的话用户会觉得表格操作“卡住了”体验非常差。4. 界面与交互打磨编辑器是给人用的4.1 菜单、工具栏与快捷键的统一配置在Qt里QAction是连接菜单、工具栏和快捷键的枢纽。我的习惯是所有动作都在MainWindow的createActions方法里集中创建菜单、工具栏、快捷键都指向同一个QAction实例m_boldAction new QAction(tr(加粗), this); m_boldAction-setShortcut(QKeySequence::Bold); m_boldAction-setIcon(QIcon(:/icons/bold.png)); m_boldAction-setCheckable(true); connect(m_boldAction, QAction::triggered, this, MainWindow::onBoldTriggered); menuFormat-addAction(m_boldAction); toolbarFormat-addAction(m_boldAction);这样做的直接好处是快捷键、图标、提示文本、勾选状态只需要在一处维护。新手经常犯的错是在菜单里addAction时直接传文本在工具栏里又新建一个QAction结果同一个功能有两份独立状态加粗按钮的勾选状态和菜单项不一致修改起来还得两处同步纯属给自己制造麻烦。4.2 状态栏、便签、自动保存这些“不起眼”的功能状态栏实时显示字数、行数、列数是编辑器里特别体现细节的功能。如果直接监听QTextDocument的contentsChange信号每敲一个字符都会触发一次统计文档大的时候状态栏会频繁闪烁甚至拖慢输入。我的优化策略是内容变化后先置一个dirty标志启动一个短定时器比如300ms定时器到点才统一计算一次字数并刷新状态栏。这样连续输入时最多每秒刷新几次次而不是每个字符都刷新体验明显更稳。自动保存也是同样的思路。项目里用QTimer实现每30秒检查一次document-isModified()如果修改过就自动保存。保存动作放在后台不打断用户的编辑操作。要注意的细节是自动保存前如果当前还没有关联的文件路径就不要弹另存为对话框打断用户而是只打一个警告标记等用户手动CtrlS时再处理。自动保存的目标是防丢数据不是替代用户操作。4.3 还有哪些交互细节会影响手感QTextEdit默认不支持Ctrl滚轮缩放需要自己处理。实现方式是在RichTextEdit里重写wheelEvent或者安装事件过滤器bool RichTextEdit::eventFilter(QObject* obj, QEvent* ev) { if (ev-type() QEvent::Wheel) { QWheelEvent* wheelEvent static_castQWheelEvent*(ev); if (wheelEvent-modifiers() Qt::ControlModifier) { int delta wheelEvent-angleDelta().y(); if (delta ! 0) { delta 0 ? zoomIn() : zoomOut(); return true; } } } return QTextEdit::eventFilter(obj, ev); }缩放之后要限制缩放范围否则一旦缩放到离谱的程度整个文档布局会重新计算大文件会卡到几乎不可用。另外中文输入法在Linux和Windows下的候选框位置偶尔会错乱我实测加上setInputMethodHints并确保编辑器控件持有焦点之后问题缓解了很多。还有右键菜单默认的QTextEdit右键菜单太简陋我重写了contextMenuEvent把剪切、复制、粘贴、字体设置、段落设置、插入图片这些高频操作全部塞进去顺手又加了一个“全选”。这些不起眼的交互细节恰恰是编辑器手感的分水岭。5. 常见问题与排查技巧实录5.1 一张问题速查表先拿走做这个项目的过程中我把反复遇到的问题整理成了一张速查表优先排查这几个方向能省下大量时间。症状根因解决方案中文乱码源码文件编码与读取编码不一致统一UTF-8打开文件时做BOM检测撤销历史丢失通过setDocument换过document对象不要替换doc始终操作同一个文档实例全文高亮卡顿循环find且无上限匹配限制匹配数量分批设置ExtraSelection打印表格溢出QPrinter页面尺寸未设置显式setPageSize并设置页边距图片保存后消失保存格式不带图片资源用资源收集器把图片和描述文件打包粘贴格式全乱外部HTML样式污染当前文档粘贴时先剥离富文本或限制外部样式状态栏闪烁每个字符变化都触发统计刷新用短定时器合并刷新减少计算次数表格Tab无反应QTextEdit默认把Tab当制表符处理重写键盘事件识别表格内光标位置并跳格5.2 展开说几个值得反复琢磨的翻车过程第一个是撤销栈的“神秘消失”。某次我在重构代码时把QTextDocument的创建挪到了DocumentModel里结果发现之前的撤销历史全没了。一开始以为是信号连接断了排查到后面才意识到setDocument会断开旧文档和视图之间的关联而默认的QTextEdit构造函数里已经有一个document相当于换了一个全新的撤销栈。从那以后我定下一条规则DocumentModel的构造函数里初始化一次文档之后的整个生命周期里绝不替换document对象。如果确实需要重置内容使用clear()然后重新插入而不是new新对象。第二个是中文输入法候选框闪现。现象是在某些环境下输入几个字母之后候选框会在屏幕左上角闪一下然后又出现在正确位置。排查下来发现是输入法的预编辑字符串和QTextEdit的光标位置更新之间有竞态窗口重绘时机不对。临时方案是给编辑器设置合理的inputMethodHints并在候选框弹出时强制刷新一次光标矩形效果能接受但说实话这个问题在不同平台上的表现不一样需要在目标平台上多测几个输入法。如果打算把编辑器做得特别严肃这块值得单独立项优化。第三个是外部HTML粘贴导致的格式灾难。从网页复制一段文字粘进来所有字体、字号、颜色、背景全被带过来了文档风格瞬间被搅乱。这不是bug是QTextEdit默认就支持富文本粘贴而外部内容的样式表往往很乱。我的处理方案是在粘贴事件里加一个选项默认保留纯文本只有用户明确选择“保留格式粘贴”时才带入HTML样式。这个取舍牺牲了一部分顺手性换来的是文档格式的整体稳定我觉得值得。5.3 性能优化经验当文档超过一定字数编辑器就进入性能敏感区。我的实测经验来自一份约500页的纯文本文档滚动和输入在未优化前都有肉眼可见的迟滞。优化分三个层面第一查找高亮不能一次全量算完。匹配超过3000个就停止继续高亮改成提示“匹配项过多仅显示前3000个”。第二不要在GUI线程里做任何文件吞吐和文本统计工作。打开大文件时先用一个后台线程做IO读取和编码识别解析完成后再通过信号回到主线程构建文档模型。构建过程也要分批进行每插入几万字符就处理一次事件循环否则界面会一直转圈圈。第三插入大量文本时一定要配合beginEditBlock使用既减少撤销栈碎片也避免每次都触发完整的布局计算。QTextDocument内部对撤销命令的存储和重建布局都有开销批量操作能同时缓解这两个问题。老老实实地用这些手段之后500页文档的滚动和输入体验才有了比较稳定的提升但距离商业编辑器那种丝滑动还是有差距这也说明一个成熟的编辑引擎有多难。6. 从这份源码里还能拿走什么6.1 源码结构导读完整项目建议用CMake Qt 6组织目录结构我贴在这里方便你对照自己的工程EditorX/ ├── CMakeLists.txt ├── resources/ │ ├── icons/ # 工具栏图标 │ └── styles.qss # 全局样式表 ├── src/ │ ├── main.cpp # 应用入口 │ ├── MainWindow.cpp # 主窗口、菜单、工具栏、状态栏 │ ├── DocumentModel.cpp# 文档模型封装 │ ├── RichTextEdit.cpp # 编辑控件处理光标与键盘事件 │ ├── FindBar.cpp # 查找替换面板 │ └── Settings.cpp # 应用配置读写 └── tests/ # 自动化测试阅读源码时建议按这个顺序来先看DocumentModel理解文档模型对外暴露的能力再看RichTextEdit搞清楚光标和选区的交互接着看MainWindow看菜单和功能是如何连接的最后看FindBar和Settings理解一个辅助功能是怎么插入主流程的。不要一开始就盯着一百多行的实现细节看那会把注意力锁死在某个局部。6.2 个人经验与后续可扩展的方向这个项目模块化做得比较顺之后我最后又补了两个东西收益很大一个是把文档和界面彻底解耦后的自动化测试另一个是“最近打开文件”列表。前者让我在改动格式逻辑时不再害怕破坏旧功能后者是每个人都会用到、但一开始总被忽略的小功能。给它配上持久化存储之后编辑器的专业感立刻上升一个档次。从后续扩展的角度看最值得投入的方向大概率是“给编辑器一个独有的使用场景”而不是继续堆砌通用功能。我自己最想加的是词频统计和写作排版辅助——因为这类功能没有现成控件可用必须基于字节码和文本分析去思考这是最锻炼人的地方。此外嵌入一个脚本引擎来实现宏功能也是一个能把项目提升到新台阶的方向只是要注意控制复杂度别让脚本系统喧宾夺主。如果你准备自己动手做一遍我最后想分享的一条经验是写编辑器别钻牛角尖最好的做法是把它变成一个你会天天打开的工具。给它加一个让你自己觉得方便的小功能比复制一堆高大上的特性更有价值。还有一句话想送给你任何你觉得“这里不可能出bug”的地方大概率都藏着一个等着坑你的问题比如只用一次就丢光历史的撤销栈。把一个个平凡细节攒成一个耐用的工具这个过程本身就是学习C最扎实的路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑