资讯动态

基于Qt的最小生成树Prim与Kruskal算法可视化演示项目

发布时间:2026/10/17 16:19:44 来源:尧图企业网站定制
最小生成树是我学图论时印象最深的算法之一但它也是最容易被“背下来”的几个算法。老师讲完流程、对着伪代码能算出一棵生成树可真让你把 Prim 和 Kruskal 的运行过程一步步画出来每一个决策都标清楚很多人反而会发现自己其实并没有完全理解。这个 Qt 项目就是为此做的把 Prim 和 Kruskal 算法变成屏幕上可以一步步回放、控制速度、甚至自己拖拽节点的动态演示程序同时附上一份完整的源代码报告。做完之后我最大的感受是算法可视化最大的难点不在算法实现而在于如何把算法内部的每一个决策转换成画面上的变化还要保证整个程序不卡、不闪、可交互。这篇文章就围绕这个项目把设计思路、算法拆解、Qt 实现细节和踩坑过程完整记录下来。1. 项目整体设计与技术选型1.1 为什么要做这样一个算法演示项目我在学习和辅导图论相关内容的时候发现大部分人对最小生成树的理解停留在“会做题”的层面。给一张图画两笔能求出来可一旦让学习者描述 Prim 算法执行到第 5 步时哪些边是候选边或者 Kruskal 算法在遇到回边时是怎么做出判断的就说不清楚了。做可视化演示本质上是把“隐藏的思考过程”挖出来摆在桌面上这是帮助理解算法最好的方式。最小生成树本身也很适合做可视化入门。它的输入输出足够直观给你若干个点和带权边找出连接所有点且总权值最小的树。这个目标不需要太多前置知识就能看懂而算法过程又有比较丰富的中间状态可展示比如集合扩张、候选边更新、连通分量合并、成环判断等。把这些状态做成动画观感清晰代码也不复杂是一个投入产出比很高的训练项目。这个项目面向的受众也比较明确正在学数据结构或算法课程的学生需要完成课程设计的人以及想用 Qt 做图形界面但还没找到合适练手项目的开发者。对第一类人来说动态展示能帮他们建立算法直觉对第二类人来说它提供了一个从算法到界面再到文档的完整范例对第三类人来说项目里面关于事件驱动动画、图元刷新、主线程阻塞这些问题的处理都是很实际的参考经验。1.2 为什么用 Qt 而不是其他方案我曾经纠结过到底用哪种技术栈来写这个演示工具。如果只是验证算法用 C 写一个控制台程序输出一行行“选中边 2-5权重 3”完全够用如果想做网页演示用 Canvas 或者 D3 也能做得很好看。但我最终选择了 Qt原因很直接Qt 的图形视图框架自带了一整套图元管理和事件分发机制不需要自己维护底层窗口和重绘细节而且它打包出来的桌面程序非常方便在课堂、实验室等场景离线演示。对比一下几类常见的实现方案选型的逻辑会更清楚。Python 加 Matplotlib 也能画图但交互事件的处理不够灵活而且做成的窗口程序在分发时往往要装一堆运行依赖Web 方案交互上限最高可你需要额外会前端那一套还要考虑动画帧控制和浏览器兼容问题QT 则是一个相对稳妥的“甜点区”。它对 C 工程化能力也有锻炼自己构造图数据结构自己管理视图和模型的分离等转型到其他 GUI 框架时很多思路可以复用。还有一个容易被忽略的点Qt 的 QGraphicsView 画布结构天然适合表达图和网这类数据关系。每个顶点可以是一个图元对象每条边可以是另一个图元对象图元自己持有坐标、颜色、文本等属性视图层只负责把它们放到场景里。后面做节点拖拽、高亮、闪烁这些功能时我几乎没有写过一行底层绘图代码全是靠图元自带的机制完成的。1.3 工程结构先想清楚三块再动手动手写代码之前我先把项目拆成了三个相对独立的模块图数据模块、算法执行模块、界面展示模块。这个决定后来被证明是整个项目最正确的架构决策。图数据模块负责维护顶点数量、邻接矩阵、边列表、随机图生成和文本导入导出。算法模块只接收图数据执行 Prim 或 Kruskal并把执行过程中的关键事件记录下来。界面展示模块负责画布、节点图元、边图元、按钮、日志区以及动画播放控制。三者之间通过一个“步骤事件数组”连接而不是让算法直接去调用刷新函数。为什么这样设计最直接的原因是代码可调试性和可测试性。如果算法函数里混入一堆scene-addItem()和update()调用算法逻辑和渲染逻辑搅在一起查 bug 时根本分不清是算错了还是画错了。我把算法模块设计成完全不依赖 Qt 类型只使用std::vector、std::pair这类标准结构这样将来甚至可以单独写一个控制台测试程序对算法输出做自动化断言。另一个原因是动画播放的天然需求。动态展示不是一次性把最终结果画出来而是要按顺序重现算法的每一步。所以算法执行时必须把中间状态都保留下来形成一个“步骤清单”界面层再按这个清单逐步播放。这种“先收集事件后播放动画”的模式正好要求算法与视图解耦。如果算法一边算一边直接刷新界面播放、暂停、步进这些功能就没法做了。2. 两种算法的核心拆解与动画建模2.1 Prim 算法的事件化拆解Prim 算法的核心思想是从单个顶点出发逐步扩张生成树。它每一步做的事情是从已经加入生成树的顶点集合出发找到一条连接“树内顶点”和“树外顶点”的最小权值边把这条边和对应的树外顶点加入生成树。想让动画表达出这个过程不能只记录最终选中了哪些边。我把 Prim 执行中每一个有视觉意义的事件都拆出来某个顶点被加入集合时要变色一条边被选中时要高亮如果我想让演示更细致还可以让“当前候选边集合”在每一步都更新显示让观众看到算法是怎么从一堆备选边里挑出最短的那条。这些事件按时间顺序存好动画就只是按顺序回放这些事件。Prim 实现时的关键数据结构是dist数组和parent数组。dist[v]记录的是顶点 v 到当前生成树集合的最小距离parent[v]记录这个最小距离是由哪条边贡献的。我常用一个生活类比来解释这一步把生成树看成一个已经占领的区域每个区域外的据点都有个距离牌写着它离占领区最近有多远。每次扩张就是看看所有距离牌挑出数字最小的那个据点占下来然后更新它周围据点的距离牌。// 伪代码示意实际实现中 n 为顶点数graph 为邻接矩阵 for (int cnt 0; cnt n; cnt) { int u -1; for (int v 0; v n; v) { if (!inTree[v] (u -1 || dist[v] dist[u])) { u v; } } inTree[u] true; if (parent[u] ! -1) { steps.push_back({EDGE_SELECT, parent[u], u, 选中边进入树}); } for (int v 0; v n; v) { if (!inTree[v] graph.weight(u, v) dist[v]) { dist[v] graph.weight(u, v); parent[v] u; } } }实际演示时我把初始点设置成 0 号顶点这样观看者能从同一个画面开始观察集合逐步扩张的过程。第一次运行这个演示时我发现画面变化太快根本看不清“候选边更新”这个关键环节后来特意在每次选中边后增加了一小段停顿并在日志区打印当前候选边的最短距离值观感明显好了很多。2.2 Kruskal 算法的事件化拆解Kruskal 的思路与 Prim 完全相反它是从全局出发的把所有边按权值从小到大排序然后一条一条试如果这条边连接的两端不在同一个连通分量里就把它们合并如果已经连通就说明加入这条边会成环必须跳过。它的辅助数据结构是并查集。在动画建模上Kruskal 的事件类型比 Prim 多了一种除了“选中边”还有“跳过边”。这对学习者理解成环判断非常重要。我设计画面时会让当前扫描到的边先变成醒目的橙色然后根据是否成环再变成绿色选中或灰色跳过。这样一来观众能看到排序后的边是一条条被检验的而不是一次性扎堆出现。“成环判断”这个点对很多初学者来说是难点。用生活类比解释就是连通分量像一个个小社团并查集就是社团花名册。每次来一条边就翻花名册看它两端的成员如今是不是已经在一个社团里。如果是加这条边会让社团内部出现两条路也就是环如果不是就合并两个社团更新花名册。std::vectorEdge edges graph.getAllEdges(); std::sort(edges.begin(), edges.end(), [](const Edge a, const Edge b) { return a.weight b.weight; }); DSU dsu(graph.vertexCount()); for (const Edge e : edges) { if (dsu.unite(e.u, e.v)) { steps.push_back({EDGE_SELECT, e.u, e.v, 连接两个连通分量}); } else { steps.push_back({EDGE_SKIP, e.u, e.v, 成环跳过这条边}); } }这里有个细节值得提醒并查集的路径压缩和按秩合并最好都写上否则在顶点较多的图上性能会退化。虽然演示用的图规模一般不超过二十个点跑不出明显差异但代码习惯还是应该一开始就规范。排序稳定性在实际工程里也值得注意如果两条边权重一样选择哪条都不影响正确性但会影响动画顺序的确定性所以我干脆按“权重相同按编号排序”来保证每次演示结果一致。2.3 两种算法在动画模型上的不同侧重两种算法实现原理不一样可视化的呈现重点也应该不一样。我做过一版把两个算法放在同一套渲染逻辑里的代码后来发现效果不好因为它们的“看点”完全不同。Prim 的“看点”是集合边界的变化观众应该关注哪些顶点已经确定哪些还没有以及候选距离怎么一步步变小Kruskal 的“看点”则是边的排序和连通分量合并观众应该关注当前在看哪条边这条边为什么被接受或拒绝。如果用一套画面去展示两种算法必然有一个的细节会丢失。这里把我实际使用的对比维度整理成表格方便做演示规划时参考维度PrimKruskal起点需要指定一个起始顶点从权值最小的边开始辅助结构dist 数组 parent 数组并查集 DSUUI 展现重点集合扩张、候选边更新边排序扫描、成环判断适合配合讲解的点贪心选择最近顶点贪心选择最小边 回路判定对画布布局的要求需要清晰的“树内/树外”视觉区分需要突出边的扫描顺序我实际做的时候还加了一个开关可以随时切换当前画布的高亮模式。在展示 Prim 时已加入生成树的顶点统一变成深色底候选节点保持白色在展示 Kruskal 时则把权值较小的边设置得更粗一些让观众感受排序带来的影响。这个开关虽然实现起来很简单但对演示效果提升非常大建议有同样需求的人优先考虑。3. Qt 渲染与交互的实操细节3.1 搭建绘图场景QGraphicsView 与 QGraphicsScene 的分工在 Qt 里画一个可交互的图最常用的组合是 QGraphicsView 加 QGraphicsScene。我的理解是QGraphicsScene 是一个装着各种图元Item的容器它负责管理图元之间的层级和碰撞QGraphicsView 是观察这个容器的窗口负责滚动、缩放、坐标变换。项目里我创建了一个自定义的GraphScene继承自 QGraphicsScene并在里面维护顶点图元和边图元的指针数组。顶点我用 QGraphicsEllipseItem 绘制每个顶点外圈是一个圆形圆形中央显示顶点编号。边用 QGraphicsLineItem 绘制边上还有一个 QGraphicsTextItem 显示权值。这里有一个很容易踩的坑如果你在创建边图元时把权值文本固定在某个坐标后续拖动顶点时文本不会跟着移动。必须把文本的父对象设置成边图元或者自定义一个复合图元让文本跟随线条的line()更新。坐标初始生成这部分我一开始用的是完全随机结果经常出现两个顶点重叠、边交叉得认不出来的情况。后来改成“随机 最小间距检测”随机生成一个候选坐标后如果和已有顶点距离小于某个阈值就重新生成直到位置满足要求。为了演示可复现我还支持从配置文件加载预设坐标这样测试动画功能时不会被随机性干扰。画布尺寸我设置成 800×600场景矩形固定在sceneRect范围内。窗口缩放时我调用view-fitInView(scene-sceneRect(), Qt::KeepAspectRatio)保持整体居中。这里特别注意不要在每次绘制时都调用 fitInView否则用户一旦手动缩放视图就会被立刻重置体验很怪。只需在窗口大小变化事件里处理一次即可。3.2 动画播放机制步骤驱动加定时器刷新如果让我说这个项目里最值得分享的经验那就是动画播放机制的设计。很多小白做算法演示时会犯同一个错误把算法执行放在按钮的槽函数里用一个while循环去跑算法每隔一段用sleep暂停。这种写法在控制台里没问题但在 GUI 里会让窗口直接失去响应因为整个事件循环被阻塞了鼠标键盘和重绘事件都处理不了。正确的思路是“步骤收集 定时器播放”。算法执行时不要实时刷新界面而是把所有要展示的事件都收集到一个std::vectorAlgoStep数组里。收集过程很快通常几十毫秒到几百毫秒就能结束。等收集完成后启动一个 QTimer 定时器每次定时器超时就从数组里取出一条事件更新画面和日志。这样做的好处非常多想暂停就直接停掉定时器想单步调试就手动触发一次定时器回调想调整速度就改定时器的间隔时间想重置就把索引归零。整个状态机非常清晰我在实际项目里连一个复杂条件判断都没用上。struct AlgoStep { int type; // 0: 节点状态变化, 1: 边选中, 2: 边跳过, 3: 日志 int id1, id2; // 涉及到的顶点或边编号 QString message; // 日志描述 }; void MainWindow::onTimerTick() { if (stepIndex steps.size()) { timer-stop(); statusLabel-setText(算法执行完成); return; } const AlgoStep step steps.at(stepIndex); scene-applyStep(step); }applyStep函数根据step.type调用对应的图元状态设置方法。比如遇到EDGE_SELECT就把对应边的颜色改成绿色、线宽加粗遇到EDGE_SKIP就把颜色改成灰色并设置透明度。为了让画面更有层次我还让选中的边常驻高亮被跳过的边在演示结束后统一变成虚线。播放速度我是通过一组预设间隔值实现的200 毫秒、400 毫秒、800 毫秒对应快速、中速、慢速三种节奏。课程演示时如果希望学员跟得上通常用 800 毫秒自己做功能验证时用 200 毫秒就够。这个间隔值不宜低于 100 毫秒否则画面会明显闪烁人眼也很难捕捉到变化。3.3 交互设计节点拖拽、重新布局与重置演示程序不能只是一张静态图否则观众就失去了参与感。我给节点图元设置了ItemIsMovable标志让用户可以直接拖拽顶点到任意位置。拖拽过程中边图元需要跟着更新坐标否则会出现“点走了线还在原地”的尴尬情况。实现边跟随的方法是给顶点图元重写itemChange事件。当位置变化时遍历这个顶点关联的所有边图元调用边的updatePosition()方法重新计算起点和终点坐标。关联关系我是在建图时同时建立了一个邻接表索引每个顶点保存它相邻的边 ID 列表。这个设计在后续很多功能里都用得上包括点击一个顶点时高亮它的所有邻接边。重置功能的细节容易被忽略。我遇到过一种情况用户拖拽了半天把图摆成自己想要的形状结果点“重新生成”按钮所有布局一下被随机数覆盖了。后来我明确区分了两个按钮一个叫“重新生成随机图”另一个叫“清空动画状态”。后者只恢复图元颜色和选中状态不改变顶点坐标和拓扑结构这样才能在演示中途反复播放动画而不丢失用户调好的布局。画面右侧我放了一个滚动日志区。每一条算法步骤都会附上一行文本说明比如“顶点 3 加入生成树当前最短候选边权值为 5”或者“边 4-7 权重 6成环跳过”。日志区还有一个“清空”按钮方便课堂上连续演示多个图例后整理画面。这个日志区虽然实现简单但课堂反馈时很多人表示比动画本身还直观因为文字能精确说出动画里发生了什么。4. 关键代码走读与踩坑记录4.1 工程配置与编译环境我用的开发环境是 Qt 6.5 加 CMake。Qt 6 从推广开始就主推 CMake 构建而旧一些的版本用 qmake 也很常见。我最后选择了 CMake因为新项目用它更符合习惯而且跨平台配置透明。CMakeLists 的核心内容很简短这里贴出来供参考cmake_minimum_required(VERSION 3.16) project(MSTDemo LANGUAGES CXX) set(CMAKE_AUTOMOC ON) find_package(Qt6 REQUIRED COMPONENTS Widgets) qt_add_executable(MSTDemo src/main.cpp src/MainWindow.cpp src/Graph.cpp src/GraphScene.cpp src/AlgorithmCollector.cpp ) target_link_libraries(MSTDemo PRIVATE Qt6::Widgets)第一次用 Qt6 的人可能会被一个细节卡住find_package(Qt6 REQUIRED COMPONENTS Widgets)里的 COMPONENTS 是必须写的如果不写Qt6 会报找不到 Qt6 包。这是因为 Qt6 把模块拆得更细了需要显式声明你要用哪些库。如果你还在用 Qt5可以用find_package(Qt5 REQUIRED COMPONENTS Widgets)其余的代码基本不变。工程目录我建议按职责分文件夹src放源代码resources放图标和样式表docs放设计文档和报告。这个项目虽然小但目录规范了之后后面加功能或写报告时找文件都省事很多。4.2 事件驱动的动画循环核心代码动画循环的核心其实不在 timer 本身而在“算法收集”和“动画播放”的衔接。这里给出一段在实际项目里验证过的核心流程框架void MainWindow::startAlgorithm(AlgorithmType type) { if (!graph || graph-vertexCount() 2) return; steps.clear(); stepIndex 0; scene-resetVisualState(); if (type ALGO_PRIM) { algorithmCollector-collectPrim(graph, steps, currentStartVertex); } else { algorithmCollector-collectKruskal(graph, steps); } statusLabel-setText(QString(共收集 %1 条步骤开始播放).arg(steps.size())); timer-start(speedIntervals[currentSpeed]); }我特意把算法收集和动画播放分成两个阶段有一个很实际的原因如果图规模大了收集过程可能耗时几百毫秒到一两秒。如果把收集放在 timer 回调里逐步执行代码复杂度会指数级上升而且暂停和步进的功能实现起来会非常别扭。反过来收集阶段一次性完成之后播放阶段就变成纯粹的“读数组刷新图元”逻辑简单到几乎不可能出 bug。还有一个小细节点击“开始”按钮前最好检查一下当前图是否合法。我遇到过图不连通的情况Prim 算法会漏掉一部分顶点动画播到最后看起来像没做完。我在算法开始时加了连通性检查和孤立点提示。如果用户随机生成了不连通的图程序会弹一个提示框建议重新生成或手动加边。4.3 我在实现中踩过的坑这个项目虽然不太大但我还是经历了几个印象很深的 bug值得写出来给大家避雷。第一个坑是界面卡死。最开始时我把算法执行和界面刷新全都放在了按钮槽函数里算法里面每选一条边就调用一次scene-update()。程序一运行点“开始”按钮窗口立刻变成“未响应”状态。这个问题不是算法错而是 GUI 循环被while占住了。后来我改成步骤收集加定时器播放卡死问题彻底消失。如果你的算法演示程序也出现了类似卡顿第一优先检查的就是有没有在槽函数里写了长循环或sleep。第二个坑是拖拽节点后边不跟随。我把边图元初始化时用固定的端点坐标画线之后拖动顶点线纹丝不动。后来我重写了节点图元的itemChange事件在位置变化时调用关联边的updatePosition()问题才解决。这里要特别提醒updatePosition()里要用edge-setLine(QLineF(start-scenePos(), end-scenePos()))而不是重新计算局部坐标否则有父子关系时坐标变换会出错。第三个坑是乱码和字体问题。工程文件保存编码如果不统一中文字符串在 Windows 上很容易变成乱码。我的处理方式是把所有源码文件统一保存为 UTF-8并且在main.cpp里设置QApplication::setFont指定一个支持中文的默认字体。这样无论是在哪个平台启动界面上的中文字符都不会因为系统默认字体差异而显示成方块。第四个坑是图元数量膨胀导致内存增长。早期版本每刷新一次状态就新建一个QGraphicsTextItem显示权值忘了删除旧图元。跑几遍动画后场景里堆了几百个残留文本图元窗口明显变卡。后来我把权值文本作为边图元的子对象只在构建时创建一次状态变化只改颜色和文本内容问题才解决。所以在 Qt 图形程序里图元复用是性能优化最直接的手段。5. 常见问题排查速查表我在调试过程中总结了一张问题排查表把现象、原因和解决方案放在一起遇到问题时可以直接对照着查比翻代码效率高很多。现象可能原因解决方案点击“开始”后窗口失去响应算法长循环或 sleep 占用了事件循环改为算法先生成步骤数组再用 QTimer 定时播放动画播放很快但画面闪烁播放间隔太短或每次刷新重建了过多图元间隔不低于 100ms优先复用已有图元而不是新建拖拽节点后相连的边没有变化边图元坐标没有跟随节点更新重写节点 itemChange调用 edge-updatePosition()某条权值相同的边每次演示顺序不同排序不稳定或未定义相等情况下的次序排序时增加“权重相同按边编号排序”的规则中文日志或按钮文字显示乱码源文件编码与编译器读取编码不一致统一使用 UTF-8 保存源码设置支持中文字体随机生成的图不连通导致算法漏掉部分顶点随机图生成时未做连通性校验生成后执行一次 BFS/DFS 校验不连通则重新生成暂停后再点击“下一步”画面没有变化stepIndex 没有正确维护在暂停状态下手动调用 timer 回调对应逻辑而不是启动定时器这张表里最有价值的是第一行和第四行。第一行对应我踩过的最大的坑第四行则提醒大家算法程序里每个细节都要考虑“确定性和可复现性”不然做演示时每次结果都不一样观众会以为程序有 bug。除了查表我还想补充一个通用的定位技巧当动画状态和预期不符时先在控制台把steps数组按顺序打印一遍只检查算法生成的步骤序列对不对。如果步骤序列正确那问题一定在渲染层如果步骤序列本身错了再去查算法的数据结构。这个分层排查的思路能省下大量时间比盯着画布猜原因强得多。6. 源代码报告的组织方式与扩展思考6.1 源代码报告该写哪些内容才不算白做做课程设计或项目验收时代码本身只是一部分写报告也是重头戏。不少同学的报告就是“把代码粘贴进去 几张截图”这种报告信息密度低评审老师翻两页就不想看了。我的经验是一份好的算法可视化项目报告应当包含项目概述、需求分析、总体设计、算法流程、模块设计、测试与演示、总结展望这几个部分。需求分析要写清楚“给谁用、解决什么问题”而不是简单说“实现两种算法”。比如这个项目的需求可以表述为提供一个面向算法学习者的交互式演示工具支持随机生成图、手动编辑图、以动画方式展示 Prim 和 Kruskal 算法执行过程并支持播放速度调节和步骤回放。这样写出来报告的读者能立刻理解产品的边界。总体设计部分建议画模块图和类图但不要贴太复杂的 UML。我实际使用的是简单的分层模块图数据层、算法层、展示层中间用步骤事件数组连接。配合一段文字说明为什么这样分层比贴大段代码更有说服力。算法流程部分必须把伪代码写清楚并且贴上运行截图。截图要有代表性能展示关键状态比如 Prim 执行到一半时部分顶点已着色、部分边已高亮Kruskal 显示某条边被跳过的瞬间。每张截图下面写一句话说明不需要长篇大论。报告里我还喜欢放一个“使用说明”小节包括编译方式、运行环境、按钮功能、操作步骤。别小看这段评审老师拿到项目后第一件事就是编译运行如果使用说明写得清楚第一印象会好很多。6.2 从算法演示到图算法实验平台做完这个项目之后我发现它天然可以往一个更通用的“图算法实验平台”扩展。当前只支持最小生成树的两种算法但代码架构已经支持继续加其他算法。比如可以加入最短路径相关的 Dijkstra、Bellman-Ford展示单源最短路径的松弛过程可以加入拓扑排序用动画展示入度变化还有连通分量、割点、桥这些结构相关的算法也都能用同一套“步骤事件数组”的模式表达。关键在于设计好统一的事件类型。除了当前定义的节点状态、边选中、边跳过、日志还可以扩展节点闪烁、子图高亮、路径染色等新类型。还有一个很实用的方向是把动画导出为图片序列或 GIF。在课堂上演示不方便实时运行时可以直接播放事先导出的动画。Qt 里可以用QImage和QPainter把画布内容逐帧保存成图片再交给外部工具合成 GIF。这个功能也不难加但实现时要注意保存时机最好在每次 timer 处理完一条步骤、画面已刷新完成后再抓图。如果对这个平台有更大的期待还可以做输入输出标准化支持读取文本格式的图数据、导出算法执行的详细日志。这样不仅可以在界面上播放动画还可以把步骤数据交给统计工具做分析和比较比如比较两种算法在不同密度图上的选边差异。架构上因为算法层已经和界面分离这些扩展基本不需要动界面代码这是当初分层的最大回报。最后再分享一个实际使用中的小技巧在用这个演示程序讲课或答辩时除了用鼠标逐帧演示也可以先拖拽几个关键节点的位置让画面更清楚再把播放速度调慢每走一步口头解释一步。配上右侧日志区滚动的文字观众的理解速度会比直接放完整动画快很多。这一点我在自己试用和分享给身边同学后得到了很一致的反馈也算是一个经过了实际检验的“使用心得”。

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

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

免费获取报价 →
↑