尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

Qt 5.14.2 通讯录示例:Model/View 与教程双路线

发布时间:2026/9/29 1:49:30

资讯中心
01
ARTICLE

Qt 5.14.2 通讯录示例:Model/View 与教程双路线

Qt 5.14.2 通讯录示例:Model/View 与教程双路线
Qt Creator 欢迎页左边那排按钮里Examples 大概是最容易被忽略的一个。我见过不少人装完 Qt 5.14.2 之后教程看了一堆官方文档也翻过却从没点开过那个示例列表——直到某天要给内部工具加一个通讯录模块才发现 QT5.14.2 自带的 Examples 里就躺着一个叫 Address Book 的完整工程从界面布局、信号槽到 Model/View 分层一应俱全。它不是什么玩具这套示例里其实有两条完全不同的实现路线一条在 tutorials 目录下按步骤拆成好几个小工程教你手写界面另一条在 itemviews 目录里用 QAbstractTableModel 把数据层和 QTableView 彻底解耦。把这两套吃透基本等于把 Qt Widgets 里最核心的那套东西过了一遍。这篇文章我会把它们的目录位置、类结构、关键代码、我编译和改装时踩过的坑都摊开讲适合刚配好 Qt5.14.2 环境想找个小项目练手的人也适合平时写惯了拖控件、想搞清楚 Model/View 到底怎么落地的人。1. 从 Qt Creator 的欢迎页点进去Address Book 到底装在哪1.1 用 Examples 按钮定位比翻安装目录快得多最省事的入口是 Qt Creator 的欢迎页。左边一列按钮里点Examples顶上会出现一个搜索框输入address book列表会立刻过滤出几个同名条目。每个条目点开之后右侧能看到这个示例的简介、它属于哪个模块下面一行小字写着源码目录的绝对路径还有一个Open按钮点下去直接把工程加载到编辑器里。之所以推荐用这个入口是因为它能顺带帮你确认一件很关键的事你到底装没装 Examples。很多人为了省空间安装时只勾了编译器套件Examples 和 Sources 两个组件都没选结果就是欢迎页的 Examples 里空空如也然后到处问“Qt5.14.2 下载完了怎么没有示例”。这种情况下不用重新装打开安装目录下的维护工具在组件列表里把 Examples 勾上补齐就行。离线安装包一般会把这些组件一起打包但安装向导里依然需要你手动勾选这一步很容易漏。如果你更喜欢直接在文件管理器里找路径规律是这样的Windows 下在C:\Qt\Qt5.14.2\5.14.2\套件名\examples\widgets\下面套件名可能是msvc2017_64、mingw73_64之类Linux 下通常在~/Qt5.14.2/5.14.2/gcc_64/examples/widgets/macOS 则是~/Qt5.14.2/5.14.2/clang_64/examples/widgets/。进去之后你会看到两个相关目录tutorials/addressbook和itemviews/addressbook。注意不要直接在安装目录里编译示例。Qt 的安装目录在 Windows 上往往位于 Program Files 之下写权限受限构建产物还会污染原始源码。Qt Creator 默认开启影子构建会把编译输出放到单独的构建目录这是正确做法别手动关掉它。1.2 tutorials 和 itemviews 两套同名示例差别比想象中大同一本官方文档里出现两个叫 Address Book 的示例很容易让人困惑。它们的定位完全不同我在第一次接触时也是绕了一圈才理清楚。对比项tutorials/addressbookitemviews/addressbook组织形式按 part 拆成多个独立小工程单个完整工程数据容器QMapQString, QString封装在自定义 Model 里界面构建纯代码手写 QGridLayoutQTableView Model核心教学点布局、信号槽、状态机QAbstractTableModel、视图委托适合谁刚上手 Widgets 的人想搞懂 Model/View 的人代码量级每个 part 一两百行五六个类结构更完整tutorials 那一套的价值在于它把过程拆开了。你打开 part1看到的就是一个只会显示两个输入框和几个按钮的窗口什么功能都没有part2 加了新增往后依次加上下一条、编辑删除、查找、存盘。每一步只动一小块代码出了问题很容易定位是哪一步引入的。itemviews 那一套则是逆向的它一上来就是完整形态但用一层 Model 把“数据长什么样”和“界面怎么显示”隔开了代码看起来更抽象却更接近真实项目的组织方式。我个人的建议是先跑 tutorials把它当成理解 Qt 事件和信号槽的练习等你能自己解释清楚为什么它用了 QMap 而不是 QList再去看 itemviews那时候 Model/View 的那些虚函数就不难理解了。2. tutorials 那一套界面是怎么一步步长出来的2.1 part1 只干一件事——把控件摆到网格里part1 的构造函数短得有点过分但正是这种短让你能看清 QGridLayout 的排布逻辑AddressBook::AddressBook(QWidget *parent) : QWidget(parent) { QLabel *nameLabel new QLabel(tr(Name:)); QLineEdit *nameLine new QLineEdit; QLabel *addressLabel new QLabel(tr(Address:)); QTextEdit *addressText new QTextEdit; QGridLayout *mainLayout new QGridLayout; mainLayout-addWidget(nameLabel, 0, 0); mainLayout-addWidget(nameLine, 0, 1); mainLayout-addWidget(addressLabel, 1, 0, Qt::AlignTop); mainLayout-addWidget(addressText, 1, 1); setLayout(mainLayout); setWindowTitle(tr(Simple Address Book)); }这段代码里有三个细节值得停下来想一想。第一地址用的是QTextEdit而不是 QLineEdit因为现实中的地址经常要换行单行输入框装不下第二地址标签在 addWidget 时多传了一个Qt::AlignTop原因是 QTextEdit 比一行文字高得多不指定对齐方式的话标签会垂直居中看起来像浮在输入框中间第三所有会被翻译的字符串都套了tr()这不是可选项而是 Qt 项目的硬性习惯后期做多语言时你会庆幸当初没偷懒。还有一点新手常犯错所有控件都 new 出来了但一个都没 delete。这不是内存泄漏因为 Qt 的对象树会在父对象析构时自动清理子对象——当你把mainLayout通过setLayout交给 this 之后布局和布局里的控件都挂到了 this 这棵树上。理解这套父子关系是理解 Qt 内存管理的第一步。2.2 part2 引入对话框同时把数据结构定了下来到了 part2界面上多了一个 Add 按钮点击之后弹出一个独立对话框让你填姓名和地址。这个对话框被单独放在adddialog.h/cpp里继承自 QDialog内部也是两个输入框加 OK/Cancel 两个按钮用 QDialogButtonBox 收尾。关键调用是这一段void AddressBook::addContact() { AddDialog dialog(this); if (dialog.exec() QDialog::Accepted) { QString name dialog.getName(); QString address dialog.getAddress(); if (!name.isEmpty() !contacts.contains(name)) { contacts.insert(name, address); // 刷新界面显示 } } }exec()是模态执行它会阻塞当前函数直到对话框关闭返回值告诉你用户是点了确认还是取消。这是模态对话框最直观的用法代价是调用点会被“挂住”。如果你的对话框里要做耗时操作记得改成open()加信号连接的异步方式否则主界面会假死。数据容器用的是QMapQString, QStringkey 是姓名value 是地址。选 QMap 而不是 QList理由有三条一是姓名天然唯一用 map 可以直接靠contains()做重名校验二是 QMap 内部按 key 排序显示出来天然有序三是它的迭代器在插入删除时行为可预期后续做“上一条/下一条”导航时特别顺手。当然代价是查询复杂度虽然是对数级但插入顺序被打乱了——如果你需要“最近添加的排最前”那就得换 QList 或者自己维护一个顺序数组。2.3 从导航到编辑删除一个 Mode 枚举撑起了整个状态机part3 加了 Previous 和 Next 两个按钮靠 QMap 的迭代器在记录之间移动。QMap 的迭代器支持和--这一点比 QHash 友好得多后者只有单向迭代器。代码里会保存一个当前的迭代器成员每次移动后调用一个统一的刷新函数把内容写回输入框。真正让这套代码变得“像样”的是 part4。它引入了这么一个小枚举enum Mode { Navigation, Adding, Editing };然后所有按钮和输入框的启用状态全部由一个updateInterface(Mode mode)函数统一控制。Navigation 模式下输入框只读Add 和 Edit 可用Adding 模式下输入框可写Submit 和 Cancel 可用其他按钮全禁用。这就是一个最朴素的界面状态机。我见过太多项目里按钮的setEnabled散落在十几个函数里改一处忘一处最后出现“正在编辑状态下还能点删除”这种尴尬。示例里这个写法虽然简单但方向是对的界面状态只在一处决定其他地方都只是触发状态切换。part5 加了查找对话框思路是遍历 QMap用QString::contains()做子串匹配命中就跳转过去。part6 则补上了持久化用 QFile 配合 QDataStream 把整个 map 写进二进制文件再读回来。整个 tutorial 走完你手里就有了一个功能完整、能存盘的桌面通讯录代码总量还不大非常适合拿来当模板改。3. itemviews 那一套把数据从界面里彻底剥出来3.1 TableModel 里那几个必须重写的虚函数itemviews 版本的灵魂是一个继承自 QAbstractTableModel 的类骨架大概长这样class TableModel : public QAbstractTableModel { Q_OBJECT public: explicit TableModel(QObject *parent nullptr); int rowCount(const QModelIndex parent QModelIndex()) const override; int columnCount(const QModelIndex parent QModelIndex()) const override; QVariant data(const QModelIndex index, int role) const override; QVariant headerData(int section, Qt::Orientation orientation, int role) const override; Qt::ItemFlags flags(const QModelIndex index) const override; bool setData(const QModelIndex index, const QVariant value, int role Qt::EditRole) override; bool insertRows(int row, int count, const QModelIndex parent QModelIndex()) override; bool removeRows(int row, int count, const QModelIndex parent QModelIndex()) override; private: QListQPairQString, QString m_contacts; // 姓名、地址 };这七个函数是表格模型的“最小可用集”。rowCount返回数据条数columnCount返回 2姓名、地址data根据 role 决定返回什么——Qt::DisplayRole决定显示什么Qt::EditRole决定编辑时初始值是什么两个角色通常返回同一个字符串。headerData负责把列头文字设成 “Name” 和 “Address”注意要判断 orientation否则行号那边也会被塞进去奇怪的文字。flags是最容易被漏掉的一个。如果不在这里返回Qt::ItemIsEditable表格就是只读的你双击单元格没有任何反应而setData也永远不会被调用——很多人改了半天 setData 没生效问题就出在这里。示例里的写法一般是判断 index 合法后再或上编辑标志。至于底层容器示例用的是QListQPairQString, QString这类结构意思是第一格存姓名、第二格存地址。不同小版本的字段命名可能略有出入但思路是一致的模型内部只关心数据结构完全不关心它会被谁显示。这就是 Model/View 存在的意义。3.2 beginInsertRows 和 endInsertRows 的顺序是这套机制里最容易出人命的地方在 Model/View 里增删数据绝对不能直接改容器就完事必须把修改动作夹在一对函数之间bool TableModel::insertRows(int row, int count, const QModelIndex parent) { beginInsertRows(QModelIndex(), row, row count - 1); for (int i 0; i count; i) m_contacts.insert(row, qMakePair(QString(), QString())); endInsertRows(); return true; }beginInsertRows的作用是提前通知视图“我马上要往这几行塞数据了你先把内部的持久索引和选中状态收一收。”视图收到通知后会把相关缓存置为无效。等你改完容器endInsertRows再告诉视图重新去问模型要数据。顺序反了、或者只调用其中一个轻则界面显示错乱、选中行跳到别处重则直接崩溃——因为视图手里握着指向已经失效位置的索引。删除同理用beginRemoveRows/endRemoveRows而且必须先通知、后删除。修改已有内容则用dataChanged信号emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});这里有个可以优化的点如果你只改了一列就不要把整行两个索引都发出去只发被改那一列的 index视图刷新的范围更小大数据量下差别很明显。这个技巧在几十条记录的通讯录上看不出效果但等你哪天要处理几万行日志就会感谢当初多写的那一行。3.3 界面层因此变得几乎没有逻辑有了模型之后主界面的组装就变得很干净创建一个 QTableView把模型塞进去开个排序加两个按钮连上信号槽收工。整个视图文件里看不到任何“数据存在哪”“怎么增删”之类的逻辑它只负责把模型给的东西画出来把用户的操作转发回去。示例里还做了一个挺贴心的小设计当通讯录一条记录都没有时不显示一张空荡荡的表格而是显示一段提示文字加一个添加按钮作为引导页对应类通常叫 NewAddressTab 之类。这种空状态设计在实际产品里非常常见但很多入门示例都会忽略。添加联系人的对话框则复用了和 tutorial 里类似的结构收集完姓名和地址后先insertRows插一行空记录再通过setData把两个字段填进去最后顺手选中新加的那一行并滚动到可见位置。提示如果新增之后界面没更新先检查是不是只改了容器没调模型接口或者忘记endInsertRows了。这两个原因占了新手问题的一大半。4. 编译和运行这两个示例时我踩过的那些坑4.1 构建套件没配好报错信息却完全看不懂第一次打开示例直接点运行最常见的报错是No valid kits found或者一堆 “Qt version is not properly installed”。这不是示例的问题是 Qt Creator 没找到可用的编译器套件。解决办法是进工具 - 选项 - 构建和运行看构建套件页签里每个套件前面是不是绿勾。如果 Qt 版本那一栏是空的说明安装目录没有被自动识别手动添加一下 qmake 路径即可。还有一种情况是套件配好了但打开示例时忘了选——Qt Creator 打开 .pro 文件后会弹出套件选择对话框很多人随手点确定结果用了一个错误的套件比如在 Windows 上选了 gcc 的套件却装的只有 MSVC编译时就会报一堆找不到头文件的错误。养成习惯打开工程后先看一眼左下角的套件选择器和构建模式。4.2 用 MSVC 编译时中文乱码根因在源文件编码如果你在示例基础上改代码加了几行中文字符串用 MSVC 编译出来可能全是乱码。原因是 MSVC 默认按照本地代码页简体中文环境下是 GBK去解析源文件而 Qt Creator 保存的文件是 UTF-8。两种编码对不上中文自然就废了。解决办法是在 .pro 里加一段条件编译msvc { QMAKE_CXXFLAGS /utf-8 }/utf-8是 MSVC 2015 之后提供的选项等于同时告诉编译器“源码是 UTF-8、执行字符集也用 UTF-8”。加上之后乱码基本就没了。如果你用的是 MinGW 套件一般不会有这个问题MinGW 默认按 UTF-8 处理。另外注意QString::fromLocal8Bit和QStringLiteral在处理中文时的行为不同写死了不带 tr() 的中文字面量时更要留意源文件编码。4.3 改了代码却没生效八成是跑错了目录这个坑我自己踩过不止一次。示例的构建是影子构建源码目录里的文件是原始的编译产物在另一个构建目录里。如果你直接双击源码目录下那个陈旧的 exe看到的永远是旧版本。还有一种更隐蔽的情况tutorials 目录里有七八个 part每个 part 的工程名和窗口标题都非常接近你改的是 part3运行的却是 part2那自然是白忙一场。我的习惯是改完代码后看两个东西一是 Qt Creator 底部编译输出的最后一行有没有 “Finished”二是运行窗口的标题栏。示例里每个 part 的窗口标题都不太一样对一下就知道跑对没有。4.4 高分屏下界面糊成一片在 4K 显示器上跑这些老示例会发现字体和控件边缘发虚。Qt 5.14 里高分屏缩放不是默认开启的需要在 main 函数最开头显式打开int main(int argc, char *argv[]) { QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); QApplication app(argc, argv); // ... }这两个属性必须在 QApplication 对象构造之前设置写在构造之后是完全无效的而且不会有任何报错提示你写错了。另外如果你手动加过QT_SCALE_FACTOR之类的环境变量做测试记得清理掉否则会和这里的设置叠加界面会变得异常巨大。5. 把示例改成真正能用的通讯录还差哪几步5.1 存盘方案怎么选二进制、JSON 还是配置项tutorial 最后用的是 QDataStream 写二进制代码短、速度快但有两个明显缺点文件不可读人想手动改一条记录都不行跨版本读取有风险如果写入和读取两端的 QDataStream 版本号不一致可能读出垃圾数据甚至抛出异常。改良写法是写入时显式锁定版本号QFile file(path); if (!file.open(QIODevice::WriteOnly)) return; QDataStream out(file); out.setVersion(QDataStream::Qt_5_14); // 固定版本避免跨环境读不出来 out quint32(data.size()); for (const auto p : data) out p.first p.second;对于通讯录这种量级的数据我更倾向于用 JSON可读性带来的调试便利远大于那点性能损失QJsonArray arr; for (const auto p : data) arr.append(QJsonObject{{name, p.first}, {address, p.second}}); QJsonDocument doc(arr); file.write(doc.toJson(QJsonDocument::Indented));三种方案的取舍大致如下方案优点缺点适合场景QDataStream代码最少、体积小、读写快不可读、跨版本需锁版本号数据量大、只给程序自己看JSON可读、跨语言、结构灵活体积偏大、需要解析配置化数据、需要人工查看QSettings平台原生存储、无需管路径只适合小数据、不支持复杂结构窗口位置、最近打开等零星状态我实际的做法是通讯录主体用 JSON 存窗口大小、上次打开的窗口位置这类零碎状态塞进 QSettings。两者各干各的事不用纠结统一。5.2 加一个搜索框只需要一个代理模型itemviews 版本的示例本身支持点击表头排序但没有搜索。加搜索框不需要动模型一行代码套一层 QSortFilterProxyModel 就行QSortFilterProxyModel *proxy new QSortFilterProxyModel(this); proxy-setSourceModel(model); proxy-setFilterCaseSensitivity(Qt::CaseInsensitive); proxy-setFilterKeyColumn(-1); // 所有列都参与匹配 tableView-setModel(proxy); connect(searchEdit, QLineEdit::textChanged, proxy, QSortFilterProxyModel::setFilterFixedString);这里最值得记的是setFilterKeyColumn(-1)。默认值只对第 0 列做过滤也就是说用户搜地址里的内容会一条都搜不到传 -1 表示所有列都参与匹配这才是通讯录该有的行为。如果需求更复杂比如“姓名必须开头匹配、地址只要包含就行”那就得自己继承 QSortFilterProxyModel 重写filterAcceptsRow在函数里拿到源模型的 index 自己判断。这套扩展路径是 Qt Model/View 体系里非常经典的一环理解了代理模型夹在中间的位置很多“模型和视图之间还想插一层逻辑”的需求就有标准答案了。5.3 输入校验和删除确认示例里都没有官方示例为了让代码短基本不做校验也不做删除确认。但真实项目里这两件事必须补上。校验有两个层次一是对话框层在点击确认时判断姓名非空、格式合法不合法就弹提示、不关对话框二是模型层在setData里判断 value 是否为空为空直接返回 false。后一层更重要因为别人可能绕过你的对话框直接操作模型。删除确认则简单得多在触发删除前插一个 QMessageBox::question注意把默认按钮设成“取消”而不是“确定”防止用户一路回车误删。这个细节看起来微不足道但用小键盘快速操作时真的能救命。我还习惯在删除后把被删记录的姓名和地址拼成一句提示显示出来用户至少知道刚才删掉的是谁。6. 为什么我建议把 Qt 自带的 Examples 当成第一手资料6.1 能编译、能运行、能调试这三条就把大部分教程比下去了网上讲 Qt 的文章多如牛毛但真正能编译通过的完整工程并不多很多代码片段是截取出来的缺头文件、缺 .pro 配置、缺信号槽连接抄下来跑不通你甚至不知道是文章写错了还是自己环境有问题。Qt 自带的 Examples 不一样它是和这个版本一起发布、一起做过编译验证的你打开就能跑跑起来就能下断点单步调试。这个价值其实和 Unity 生态里那些官方示例工程是一个道理。比如做深度相机开发的人会去翻 Azure Kinect 或者 Femto Bolt 的官方示例官方一般也会提供一套 Unity 示例工程让你先跑通再改。这类示例的定位都是“一个能跑通的骨架”而不是一份可以直接上线的产品代码。区别在于Qt 的示例是可以直接在 IDE 里点开源码逐行看的连 .pro 里每一行是干什么都摆在那里学习成本更低。顺带说一句阅读示例的顺序很重要。我的习惯是先原样跑一遍看它到底长什么样再从头单步走一遍构造流程看清楚对象树是怎么搭起来的然后故意改坏一行代码看看会报什么错——比如把endInsertRows()注释掉会怎样。这一步看着有点自虐但它能让你把“错误现象”和“错误原因”对上号下次在真实项目里遇到同样报错反应速度完全不一样。6.2 别把示例当成产品代码尤其是这几个方面示例为了简洁做了很多让步。比如数据全部放在内存里退出即丢tutorial 里有了存盘但 itemviews 版本没有没有做任何输入校验没有考虑并发或者多线程界面样式也是系统默认的没有做美化错误处理基本是“打不开文件就算了”没有给出用户可感知的提示。这些让步在示例里是合理的因为它的目的是教一个概念不是交付一个产品。所以拿示例改项目时我一般会先列一张清单哪些是我要保留的结构哪些是必须补的功能哪些是需要重写的部分。Model/View 那套分层结构基本可以原样保留改的是模型内部的数据源从内存容器换成数据库或网络请求以及视图层的交互细节。tutorial 那套手写界面的方案则适合规模小、界面固定的工具窗口一旦界面元素超过二三十个手写布局就开始痛苦了这时候老老实实上 .ui 文件配合 Designer 更划算。我个人在几个内部小工具里都沿用了 itemviews 这套骨架把底层容器换成 SQLite 之后几千条记录照样流畅因为视图只渲染可见区域模型只在需要时被查询。这个特性是 Model/View 体系最被低估的优势——它天生就是为大数据量设计的只是示例里的数据太少你感受不到而已。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。