资讯动态

QTableView完全指南:模型视图架构、委托定制与性能优化

发布时间:2026/10/17 1:49:53 来源:尧图企业网站定制
QTableView 是我这几年做 Qt 桌面端最常用的控件之一说它是表格类需求的首选一点也不夸张。只要涉及数据列表展示、批量编辑、右键操作、自定义单元格QTableView 基本都能兜住。很多人一开始图省事直接拖一个 QTableWidget等到数据量上来、界面复杂了再想换就要动不少手术。这篇文章我把自己在实际项目中用 QTableView 的思路、代码、踩坑记录全部整理出来内容不绕弯子适合刚接触 Qt 的开发者也适合已经用过 QTableWidget 想往更灵活方向走的人。1. 为什么选 QTableView 而不是 QTableWidget1.1 模型/视图架构的核心认知先聊一个很多人都没细想的点QTableWidget 明明也能显示表格为什么非要选 QTableView根源在于 Qt 的模型/视图架构。QTableWidget 自带一个内部模型你往里面塞数据它自己消化。这个模式在几十行数据、固定列数的小场景下很舒服但它把数据存储和界面显示绑死了。QTableView 本身不存数据它只负责显示真正的数据放在模型里模型继承自 QAbstractItemModel。刚开始接触会觉得这层抽象有点绕但拆开理解其实很简单模型提供数据视图负责展示委托负责“怎么显示怎么编辑”。这种分离带来的直接好处是同一个模型可以挂多个视图比如左边一个树形导航右边一个表格数据改一个地方两个界面同步刷新。这在做数据管理类工具时几乎是刚需。另一个容易被忽略的好处是自定义能力。QTableWidget 如果你想彻底控制某个单元格的绘制逻辑通常要处理一堆细节而且很容易碰壁。QTableView 的思路则非常清晰写一个委托类重写 paint 或者 createEditor告诉视图这个列该长什么样。这就像一个渲染流水线每一环都能插进去改而不是在一个封闭黑盒里凑合。1.2 什么时候该用 QTableView我个人的判断标准很简单只要表格不是“一次性页面展示”而是需要跟后端数据联动、需要复杂交互、需要复用布局就直接上 QTableView。比如数据从数据库查询出来要支持排序、筛选、分页某个状态列要根据数值显示不同的颜色、图标或进度条需要对某几列做特殊编辑器比如日期选择器、下拉联动框、文件选择按钮需要同一个数据在不同窗口里以不同方式展示需要操作大量数据比如几千行甚至上万行实时刷新在这些场景里 QTableWidget 也能做但代码量和维护成本会明显上升。反之如果你只是做一个设置界面固定五行三列放几个可编辑项那 QTableWidget 确实更快没必要硬上 QTableView。工具选型从来都是平衡收益和成本而不是为了技术看起来高级。2. 快速上手从零搭建一个可用的表格2.1 准备模型和视图我习惯的写法是在代码里手动创建 QTableView而不是在 UI 文件里拖控件。原因很实际当你需要动态列、动态数据类型时代码创建比改 UI 文件灵活得多。界面上只需要放一个空控件或者直接 new 出来添加到布局里。// 创建一个表格视图 QTableView *tableView new QTableView(this); // 创建一个标准模型用来存储表格数据 QStandardItemModel *model new QStandardItemModel(this); // 设置表头 model-setHorizontalHeaderLabels({名称, 类型, 大小, 修改时间}); // 视图绑定模型 tableView-setModel(model);这里用的 QStandardItemModel 是 Qt 自带的通用模型适合中小数据量场景。它的使用成本最低大部分入门项目都从这个模型开始。如果你的数据显示量极大比如几百万行后续可能需要自定义模型但核心调用逻辑是一样的只是模型内部的数据获取方式不同而已。setModel 是视图和模型建立连接的关键。一旦绑定视图中显示的内容完全由模型决定视图自己的职责只是布局和交互。这个设计好处在于你可以在任意时刻替换整个模型视图会立刻刷新不需要手动清空再填充。2.2 把数据填进表格往 QStandardItemModel 里塞数据的常规操作是创建 QStandardItem设置内容然后放到指定行列。下面是一个简单的示例演示了字符串、数字和勾选状态三种常见数据形式。// 第一行 QStandardItem *item0 new QStandardItem(项目配置); QStandardItem *item1 new QStandardItem(配置文件); item1-setData(1024, Qt::UserRole); // 存额外数据文件大小字节数 QStandardItem *item2 new QStandardItem(1 KB); QStandardItem *item3 new QStandardItem(2025-01-12 10:30); model-appendRow({item0, item1, item2, item3}); // 第二行带勾选框 QStandardItem *checkItem new QStandardItem(启用日志); checkItem-setCheckable(true); checkItem-setCheckState(Qt::Checked); model-appendRow({checkItem, new QStandardItem(开关), new QStandardItem(--), new QStandardItem(--)});这里有个细节值得注意item1 我通过 setData 存了真正的文件大小数字 1024显示上仍然是“配置文件”这个字符串。这是模型/视图架构中非常实用的一招——显示给用户看的是一套数据背后记录逻辑判断的是另一套数据。QStandardItem 内部支持多种角色Qt::DisplayRole 控制显示文本Qt::UserRole 是留给开发者自定义数据的起点。排序的时候你可以让模型按 UserRole 的大小来排而不是按显示字符串的字典序排这样文件大小排序才是有效的。如果只是 foreach 一堆数据也可以直接通过 setItem 指定行列添加for (int row 0; row dataList.size(); row) { model-setItem(row, 0, new QStandardItem(dataList[row].name)); model-setItem(row, 1, new QStandardItem(dataList[row].type)); }用 setItem 的好处是模型会自动扩容到足够容纳该行列省去手动确定行数的烦恼。但对大量数据一次插入更推荐先用 beginInsertRows 和 endInsertRows 包裹批量插入操作或者在一次性填充前用 model-setRowCount(rows) 预设行数能显著减少界面重绘次数。2.3 表头、列宽、行高这些细节表格的观感很大程度上由表头、列宽和行高决定。默认情况下 QTableView 的列宽是均分的这在很多场景下并不合理。名称列应该宽一些数值列窄一些时间列居中这个需要手动调。// 设置表头拉伸模式 tableView-horizontalHeader()-setSectionResizeMode(0, QHeaderView::Stretch); // 名称列拉伸填满 tableView-horizontalHeader()-setSectionResizeMode(1, QHeaderView::ResizeToContents); // 类型列自适应内容 tableView-horizontalHeader()-setSectionResizeMode(2, QHeaderView::ResizeToContents); tableView-horizontalHeader()-setSectionResizeMode(3, QHeaderView::ResizeToContents); // 设置默认行高 tableView-verticalHeader()-setDefaultSectionSize(36); // 如果你不想要垂直表头可以隐藏 tableView-verticalHeader()-setVisible(false);这里要注意 QHeaderView::Interactive 与 Stretch 的区别。Interactive 表示用户可以手动拖拽调整列宽Stretch 表示自动拉伸填满剩余空间两者如果混合使用经常会出现拖拽后列宽被自动拉回的情况。我的建议是需要用户调整列宽的场景用 Interactive并给列设置好默认宽度希望看起来匀称的场景用 Stretch放弃手动调宽能力。不要两种混在一个表格里体验很割裂。行高有个细节如果字体字号和系统缩放比例有变化固定行高会显得拥挤或空旷。比较稳妥的做法是用纵向表头的默认行高配置并给内容加一点上下 padding视觉上才舒服。3. 核心能力排序、选择、编辑与右键菜单3.1 开启排序和整行选择表格不像静态文本用户天然期望点击表头就能按某一列排序。QTableView 开启排序非常容易但是有个前提模型的数据存取逻辑要配合。如果用 QStandardItemModel只要一行代码即可开启排序前提是你插入的数据类型统一。比如某一列存的都是数字字符串排序时默认按字符串排结果就是 10 排在 2 前面非常反直觉。解决方法是插入时把真正的数值通过 QStandardItem::setData 放入 Qt::EditRole 或 Qt::UserRole。// 开启点击表头排序 tableView-setSortingEnabled(true); model-setSortRole(Qt::UserRole); // 让排序使用自定义数据角色然后插入数据时QStandardItem *sizeItem new QStandardItem; sizeItem-setData(2048, Qt::UserRole); // 显示数字时用 DisplayRole 格式化 sizeItem-setData(2048, Qt::EditRole); sizeItem-setText(2 KB);这样用户无论点击哪个表头排序的底层依据都是数字大小而不是字符串字典序。选择行为也要根据实际需求设置。默认情况下 QTableView 允许选中单个单元格但在大多数业务中我们更需要整行选中并且只能选一行。tableView-setSelectionBehavior(QAbstractItemView::SelectRows); // 整行选中 tableView-setSelectionMode(QAbstractItemView::SingleSelection); // 单选 tableView-setEditTriggers(QAbstractItemView::DoubleClicked); // 双击进入编辑这行配置做完配合信号 currentRowChanged 或者 selectionModel 的选择变化信号就能在右侧面板展示选中行对应详情或者点击按钮对选中行进行操作。3.2 右键菜单的完整实现右键菜单是桌面应用逃不掉的环节。QTableView 里实现右键菜单的常规套路是绑定 customContextMenuRequested 信号信号会带出鼠标点击位置通过这个位置定位到行再弹出菜单。这里有一个很多人容易犯的错直接用 viewport 坐标做 rowAt而不是用全局坐标。connect(tableView, QTableView::customContextMenuRequested, this, [this](const QPoint pos) { // 通过视图坐标定位到当前行 QModelIndex index tableView-indexAt(pos); if (!index.isValid()) { // 点在空白区域不弹出菜单或弹通用菜单 return; } int row index.row(); // 记录当前操作的行 m_currentRow row; QMenu menu(this); QAction *actOpen menu.addAction(打开); QAction *actDelete menu.addAction(删除); QAction *actRename menu.addAction(重命名); QAction *selected menu.exec(QCursor::pos()); if (selected actOpen) { // do open } else if (selected actDelete) { // do delete } });这里务必记得把 tableView 的 setContextMenuPolicy 设置为 Qt::CustomContextMenu否则信号永远不会触发。另外一个细节右键菜单弹出的位置。有人用 mapToGlobal(pos) 获取全局坐标这没问题但用 QCursor::pos() 更直接鼠标在哪菜单就在哪不会因为表格顶部有工具栏导致偏移。若菜单要对齐到鼠标点击的单元格上用 mapToGlobal(pos) 更精准。两种我都试过按交互习惯选即可。删除行时要注意模型与视图的同步。如果你直接操作视图或者删除行前没有处理模型索引很容易出现索引失效的问题。正确做法是通过模型来删除model-removeRow(m_currentRow);删除完成后 QTableView 会自动刷新不需要额外通知。3.3 单元格编辑的控制策略QTableView 默认支持双击编辑单元格但控制粒度可以非常细。如果希望整列可编辑、整列只读或者按行控制需要操作模型的 flags 方法。QStandardItemModel 中每个 item 的 flags 决定了它是否可编辑、是否可选择。// 让第 0 列的 item 不可编辑 for (int row 0; row model-rowCount(); row) { QStandardItem *item model-item(row, 0); item-setEditable(false); } // 如果需要完全只读模型 tableView-setEditTriggers(QAbstractItemView::NoEditTriggers);具体业务里经常会遇到这一列是状态列用户不能改那一列是备注允许自由编辑。这里我的经验是用 setIndexWidget 或者委托来控制而不是粗暴地整表禁止编辑。setIndexWidget 可以往指定单元格塞任意 widget但要注意它会覆盖默认绘制逻辑性能比委托差不适合大表格。真正灵活又高效的方式是写一个自定义委托控制 createEditor 是否返回编辑器。后续章节我会专门讲委托。编辑过程中的数据校验也是一个常见需求。默认情况下用户在单元格里输入什么模型就存什么。如果你需要数值范围校验可以在委托的 setModelData 里做拦截或者通过 model 的 setData 方法中检查。最省事的方案是在委托的 validate 函数里有限校验但实际项目中我发现大多数人最终会在业务层统一校验因为跨字段联动校验在委托里写会非常啰嗦。4. 自定义显示委托Delegate实操4.1 为什么需要委托默认的 QTableView 只能用文本显示数据但真实业务需要各种五花八门的内容进度条、图片、按钮、下拉框、日期选择器、开关。这些用数据源强行拼接字符串也能显示但要么丑要么不可交互要么代码可维护性差。委托QStyledItemDelegate) 是 Qt 提供的一个扩展钩子它把“一个单元格怎么画、怎么编辑、怎么保存”的逻辑从模型和视图中解耦出来单独封装成类。理解委托的关键是明确几个回调函数各管什么事paint负责绘制单元格外观sizeHint告诉视图这个单元格需要多大空间createEditor双击/编辑时创建什么控件来编辑数据setEditorData把模型里的数据填充到编辑器控件上setModelData把编辑器控件里的数据写回模型这几个函数是委托的全部核心。你做自定义表格控件只要在这几个函数里按需重写即可。4.2 一个最简单的自定义委托假设需求是让某一列的文字在显示时自动变红并且双击时只允许输入数字。传统做法是生病地遍历所有 item 设置颜色而委托只需要改 paint 和 createEditor。实现方式如下class NumberDelegate : public QStyledItemDelegate { public: using QStyledItemDelegate::QStyledItemDelegate; void paint(QPainter *painter, const QStyleOptionViewItem option, const QModelIndex index) const override { QStyleOptionViewItem opt option; initStyleOption(opt, index); // 只对数值列做红色处理 if (index.column() 2) { int val index.data(Qt::DisplayRole).toInt(); opt.palette.setColor(QPalette::Text, val 0 ? Qt::red : Qt::darkGreen); } QStyledItemDelegate::paint(painter, opt, index); } QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem option, const QModelIndex index) const override { QLineEdit *editor new QLineEdit(parent); QDoubleValidator *validator new QDoubleValidator(editor); editor-setValidator(validator); return editor; } void setEditorData(QWidget *editor, const QModelIndex index) const override { QLineEdit *lineEdit qobject_castQLineEdit*(editor); lineEdit-setText(index.data(Qt::DisplayRole).toString()); } void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex index) const override { QLineEdit *lineEdit qobject_castQLineEdit*(editor); model-setData(index, lineEdit-text(), Qt::EditRole); } };然后在视图中指定某列使用这个委托tableView-setItemDelegateForColumn(2, new NumberDelegate(tableView));这样就实现了“第 3 列显示红色文本编辑时只能输入数字”。理解这套流程之后你会发现 QTableView 的单元格扩展能力几乎是无限的。你想做一个图片缩略图列只需要在 paint 里画 QPixmap你想做一个进度条列也只需要在 paint 里画进度条背景和填充块。4.3 用委托让图片和进度条直接显示进度条列在实际项目里非常实用。比如任务列表的完成度、磁盘使用率、下载进度等。用委托画进度条很简单核心是在 paint 里拿数值然后用 QStylePainter 绘制 QStyleOptionProgressBar。class ProgressDelegate : public QStyledItemDelegate { public: void paint(QPainter *painter, const QStyleOptionViewItem option, const QModelIndex index) const override { if (index.column() ! 3) { QStyledItemDelegate::paint(painter, option, index); return; } int progress index.data(Qt::DisplayRole).toInt(); QStyleOptionProgressBar bar; bar.initFrom(painter-device()); bar.rect option.rect.adjusted(6, 8, -6, -8); bar.minimum 0; bar.maximum 100; bar.progress progress; bar.text QString(%1%).arg(progress); bar.textVisible true; QStylePainter stylePainter(painter); stylePainter.drawControl(QStyle::CE_ProgressBar, bar); } };这里有个容易踩的坑自定义绘制时选中态背景会被你的绘制覆盖掉导致选中后列出看不清。解决办法是在绘制前调用 initStyleOption 获取标准选项并用 option.state 判断是否有 State_Selected然后在绘制颜色里叠加半透明高亮。这样交互和视觉才能统一。创建 editor 时同理可以在 createEditor 里返回一个 QProgressBar 作为编辑器虽然进度条不适合编辑但你可以返回一个 QSpinBox 或 QSlider编辑完成就把值写回模型真正做到可视化编辑。5. 样式美化让表格不再是默认的灰底白字5.1 表头和网格线的样式默认的 QTableView 长得很素如果做的是商业项目或者个人作品审美这关过不去。用 QSS 改表格样式的性价比很高不需要大动代码。下面是我比较常用的样式片段兼顾了表头和网格线的呈现效果。QTableView { background-color: #ffffff; alternate-background-color: #f7f9fc; border: 1px solid #dfe3e8; gridline-color: #e8eaed; selection-background-color: #3478F6; selection-color: #ffffff; font-size: 13px; } QTableView::item { padding: 2px 6px; } QHeaderView::section { background-color: #f2f4f7; color: #2b2f36; padding: 8px; border-right: 1px solid #dfe3e8; border-bottom: 2px solid #cfd4db; font-weight: 600; }QSS 里注意 QTableView::item 的 padding 只对纯文本单元格生效如果你用了委托自定义绘制padding 就得自己在 paint 里控制。表头部分如果想做圆角直接设置 border-radius 在某些 Qt 版本上会导致表头绘制异常稳妥的做法是用平滑的色块拼接或者使用 stylesheet 给 QHeaderView 的 segment 设置圆角但不要太依赖它。5.2 隔行变色与选中态高亮隔行变色在数据表格里能有效提升扫描效率。开启方式是在代码里设置tableView-setAlternatingRowColors(true);对应的 QSS 里 alternate-background-color 会在这个开关打开后才生效。如果你不想依赖 QSS也可以在 model 的 data() 方法里根据行号给 BackgroundRole 返回不同颜色。后者更灵活比如你可以在某行是非活跃用户时整行变灰某行是警告状态时整行淡红色。但这种方案要小心由于模型不知道当前视图滚动位置它对所有行都会执行颜色计算数据量大时可能带来额外开销。选中态的高亮我个人建议用 QSS 里的 selection-background-color因为默认的蓝色高亮在部分深色主题里不够明显。也可以让选中整行都有背景色但内部文字颜色保持黑色这种效果在需要区分选中行与其他高亮状态的场景里很好用QTableView { selection-background-color: rgba(52, 120, 246, 0.2); selection-color: #000000; }一定要记住QSS 中 rgba 的支持需要 Qt 开启相应编译选项常规 Qt 默认支持不用太担心。选中行用什么颜色要看你的界面主题对比色不要为了好看选择一个跟主题冲突的颜色那样反而分散注意力。6. 常见问题与排查技巧实录6.1 表格不显示数据或显示空白列绝大多数刚接触 QTableView 的开发者都会遇到“模型绑定了但界面什么都没有”。排查步骤很简单是否调用了 setModelmodel 实例是否因为栈上创建过早销毁是否向模型里插入了数据是否设置表头数据只设置数据不设置表头不会导致空白但会默认用数字当表头是否在数据插入后又调用了某个清空操作例如 model-clear()清空会把表头一起删掉经验里最尴尬的情况是QStandardItemModel 在函数内部作为局部变量创建函数结束就释放了视图自然没了数据源。这个坑出现频率极高。解决方式是保证 model 的生命周期跟随视图或者由父对象统一管理。6.2 单元格编辑后数据没变如果编辑完回车界面确实更新了但重新打开窗口又恢复原样十有八九是数据没有写回底层存储。逻辑上 setModelData 写回模型但模型只是内存里的容器没有持久化一样的道理 QStandardItemModel 的所有修改只存在于内存。要持久化就要自己监听模型数据变更信号比如 dataChanged然后同步到数据库或者配置文件。connect(model, QStandardItemModel::dataChanged, this, [this](const QModelIndex topLeft, const QModelIndex bottomRight) { // 根据当前编辑的单元格更新对应数据 QModelIndex index topLeft; QVariant val index.data(Qt::EditRole); // 写回到你的业务数据结构中 });6.3 排序后行号对不上QTableView 开启排序后界面上看到的行顺序和模型内部的行顺序很可能不一致。此时如果你用 view-currentIndex().row() 去数组里取业务数据取到的可能是错误的行。经验做法是不要在排序开启后依赖行号而是依赖 QModelIndex 对应数据里的唯一标识比如 ID。使用 index.data(Qt::UserRole) 拿到业务 ID再去列表里找到对应对象。这个教训我在做文件列表时深刻体会过表头一排序点删除就删错对象非常危险。6.4 委托编辑器不弹出或无法取值createEditor 写了却不调用第一检查有没有设置 setItemDelegate 相关接口第二检查是否设置了 setEditTriggers 为 NoEditTriggers第三检查当前单元格模型的 flags 里是否有 ItemIsEditable。这三处是常见拦截点任何一个没有到位编辑器都不会出现。编辑器值不更新的情况检查 setEditorData 是否没实现里直接用了 index.data(Qt::EditRole)但模型里存的是 UserRole或者编辑器控件没有被正确 cast。用一个 qobject_cast 调试一把问题基本立刻定位。6.5 大数据量加载卡顿加载几千行数据时 QTableView 不会比 QTableWidget 好到哪里去关键区别在加载策略。QTableView 的优势是配合自定义模型可以实现按需加载只加载可视区数据但 QStandardItemModel 本身还是一个全量容器。大数据场景我的做法是分页加载 滚动加载或者基于自定义抽象模型实时计算值。如果你暂时不需要动态加载至少把大段数据插入包在 beginInsertRows/endInsertRows 里避免每一行都通知视图刷新一次性能能提升数倍。7. 性能优化与扩展方向表头排序之外有经验的人还会关注另一个细节大量列时的初始渲染。如果你的表头有几十列QTableView 默认会全部渲染滚动基本正常但如果每列都要绘制复杂委托需要斟酌委托的绘制成本。减少渐变、避免频繁 QFontMetrics 计算能明显降低滚动时的卡顿。另一个技巧是 setUniformRowHeights(true)告诉视图所有行高相同这样滚动条计算可以跳跃式进行性能大幅提升。tableView-setUniformRowHeights(true); tableView-horizontalHeader()-setCascadingSectionResizes(true);开启这个开关的前提条件是你的表格每行高度一致如果有列内容高度不同比如多行文本换行这个优化会出错。需要按内容自适应高度的话这个优化不能开。响应到大数据的交互还有一个很多人没用到的功能滚动到指定行。比如你在另一个窗口选中某种类型希望主表格自动滚动到对应行。使用 scrollTo 可以达到效果QModelIndex idx model-index(row, 0); tableView-scrollTo(idx, QAbstractItemView::PositionAtCenter);这是非常顺滑的联动体验比手动算滚动条位置可靠得多。扩展方向方面如果你把 QTableView 的路走通了再去看 QTreeView、QColumnView、QListView 基本无障碍因为底层全部是同一个模型/视图框架。你可以把同样的 model 挂到 QTreeView 上瞬间把平面表格变成树形导航数据源不用换这是一种非常优雅的功能扩展。另外 QTableView 也支持拖拽排序、支持从外部拖文件进来这些无非是设置 dragDropMode、实现 dropEvent、配合 MIME 数据。我建议是先把基础显示、委托、右键菜单这些主干功能做扎实再逐步叠加拖拽不要一开始就复杂度拉满调试会很痛苦。8. 最后分享两个实用小技巧第一个是表头排序和过滤器组合。我常做一个搜索输入框实时过滤表格内容。用 QSortFilterProxyModel 是最省力的方案// 创建代理模型过滤后再显示到 QTableView QSortFilterProxyModel *proxy new QSortFilterProxyModel(this); proxy-setSourceModel(model); proxy-setFilterCaseSensitivity(Qt::CaseInsensitive); tableView-setModel(proxy);搜索框的 textChanged 信号里调用proxy-setFilterFixedString(text);注意排序启用时要操作的是 proxy 作为视图模型而不是直接对 source model 做排序。视图排序开在 proxy 上才能既过滤又有表头排序。初学者很容易把这两个模型搞混我在项目里也吃过亏后来习惯取名的时候写清楚 sourceModel 和 proxyModel 的区别。第二个技巧是数据展示时结合主题切换。如果你的应用有深色模式和浅色模式QSS 可以对 QTableView、QHeaderView、QTableView::item 分别定义两套属性切换时通过 qApp-setStyleSheet 生效。但注意样式切换不能覆盖委托内部绘制的颜色所以自定义委托里尽量用 QPalette 取色而不是写死颜色值。这样主题切换时委托绘制的内容也会跟着变不用写两套代码。我在实际项目里遇到最满意的效果是表格数据联动数据库、右键操作完整、双击编辑、委托绘制进度条、隔行变色、搜索过滤、树形扩展全走一套模型。这套组合拳适应了很多种业务界面维护起来也轻松因为数据逻辑都在模型层视图层只是表现。如果你现在正在做一个数据密集型的桌面应用不妨直接从 QTableView 起步别再用 QTableWidget 将就了。开始可能多点代码一旦框架建立起来后面改界面、加功能会顺畅太多。像表格这种基础能力越早把模型/视图思维吃透你后面写的业务界面就越省心。

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

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

免费获取报价 →
↑