news 2026/10/26 7:24:13

用Qt构建跨平台迷你看盘软件:C++新手练手项目实战解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用Qt构建跨平台迷你看盘软件:C++新手练手项目实战解析

简介:基于Qt开发的跨平台迷你看盘软件项目,支持Windows、Linux与macOS三大桌面系统,主打轻量实用,适合希望借助真实项目快速上手C++与Qt开发的初学者。压缩包内含五百零九个文件,以C/C++源文件为主体,包括大量头文件、C程序文件、C++实现文件,同时配有Qt界面布局文件、工程构建脚本、Markdown及PDF说明文档等,整体体积约十四点七三兆字节,目录划分清晰,便于按模块查阅。已有135人下载学习,该项目覆盖信号与槽、事件处理、实时数据刷新、网络请求与解析、跨平台打包部署等关键知识点,能帮助读者系统理解从界面搭建到发布上线的完整流程。对于想要深入Qt开发或准备相关项目的学习者,这是一份难得的实战参考资料,可跟随源码逐步调试,掌握不同操作系统下的注意事项与常见排错方法。

1. 一个 Qt 迷你看盘软件,凭什么值得新手拿来当 C++ 练手项目

如果你刚把 C++ 语法啃完,正愁没有一块能真正跑起来的练手地,那么一个用 Qt 写的跨平台迷你看盘软件是个相当合适的落点。这东西不涉及复杂的服务器架构,却把 C++ 和 Qt 最常用的一批能力——窗口布局、信号槽、自绘图形、网络请求、定时刷新、文件读写——全部串在一起,而且一份代码能同时编译出 Windows、Linux、macOS 三个平台的版本。适合谁呢?适合学完语法想上手第一个完整项目的新手,也适合想看看 Qt 项目怎么组织工程结构、怎么处理平台差异的转岗工程师。标题里这个 zip 包装的正是这样一个可读可改的小软件,代码量不大,改造成本低,踩坑的性价比却很高。

2. 跨平台工程骨架:三套系统跑同一份代码的构建要点

拿到这样一个看盘项目,第一件事不是急着读业务逻辑,而是先把它编译起来,看一眼界面长什么样。做跨平台 Qt 项目,工程组织方式直接决定你后面折腾三套系统时的心智负担。我一般会先把工程收敛成 CMake 组织,再用条件编译处理平台差异,最后在三个系统上各跑一遍最小构建,确认环境没有隐性依赖。

2.1 用 CMake 而不是 qmake 来组织你的看盘工程

Qt Creator 默认新建项目是 qmake。但只要你打算同时照顾 Windows、Linux、macOS 三套环境,手写 .pro 文件很快会遇到两个麻烦:一是条件分支语法不够直观,二是后续想接第三方库(比如后面接个行情 SDK)时,CMake 的生态明显更省心。所以第一步就是把工程结构定成 CMake,这个决定能让你少走很多弯路。

cmake_minimum_required(VERSION 3.16) project(MiniKLineViewer VERSION 0.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 这四行是 Qt 工程的必修课,少一个都过不了编译 set(CMAKE_AUTOMOC ON) # 处理 Q_OBJECT 宏,生成 moc 文件 set(CMAKE_AUTORCC ON) # 处理 .qrc 资源文件 set(CMAKE_AUTOUIC ON) # 处理 .ui 界面文件 find_package(Qt6 QUIET COMPONENTS Widgets Network) # 很多机器还停在 Qt5,找不到 Qt6 时回退是常见做法 if(NOT Qt6_FOUND) find_package(Qt5 REQUIRED COMPONENTS Widgets Network) endif() add_executable(MiniKLineViewer main.cpp MainWindow.cpp MainWindow.h KLineWidget.cpp KLineWidget.h QuoteModel.cpp QuoteModel.h ) target_link_libraries(MiniKLineViewer PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network )

set(CMAKE_AUTOMOC ON) 是 Qt 工程里最容易漏的一行。没有它,任何带 Q_OBJECT 的类都会报未定义引用,新手常误以为是链接库没配对,其实是 moc 预处理没跑。find_package(Qt6 QUIET ...) 后面跟 if(NOT Qt6_FOUND) 回退到 Qt5,是因为不少 Linux 发行版和培训机构还在用 Qt5.15,直接 REQUIRED Qt6 会把一批环境卡死。target_link_libraries 里用 Qt${QT_VERSION_MAJOR} 而不是写死 Qt6::Widgets,是为了让上面的回退逻辑真正生效,这个变量由 find_package 自动带出。

如果你拿到的源码是 .pro 格式,我会建议花半小时转成 CMake。转换时注意 .ui 和 .qrc 文件不用手动列进去,AUTOUIC 和 AUTORCC 会自动处理。唯一要手工维护的是 .cpp/.h 清单,这也是新手练习文件组织的好机会。

2.2 平台差异藏在三个地方:字体、目录、网络库

Qt 把大部分跨平台差异封装掉了,但看盘软件这种带中文、有数据落地需求的程序,真正需要动手处理的主要是三个点。

第一是字体。Windows 上默认的 GUI 字体渲染中文没问题,Linux 上经常出现满屏方块,原因是代码里写死了“Microsoft YaHei”,而 Linux 发行版默认没装这套字体。常见做法是按平台选字体族,放到一个公共函数里:

#include <QFontDatabase> #include <QSysInfo> QString defaultFontFamily() { #if defined(Q_OS_WIN) return QStringLiteral("Microsoft YaHei"); #elif defined(Q_OS_MACOS) return QStringLiteral("PingFang SC"); #else return QStringLiteral("Noto Sans CJK SC"); #endif }

在 MainWindow 构造函数里调一次 qApp->setFont(QFont(defaultFontFamily())),全局生效。Linux 侧的系统依赖是 fonts-noto-cjk,安装后中文字体就稳了。如果对这套宏不熟,可以跑一遍三端验证,你很快会发现不是所有发行版都自带中文字体,这也是跨平台项目的第一个玄学来源。

第二是数据目录。配置文件和行情落地不能写死路径,Windows 的 AppData 和 Linux 的 .local/share 完全是两套约定。正确做法是用 QStandardPaths 统一定位,后面第 3 章落数据时会看到具体用法。

第三是网络库。Qt Network 模块在三个平台上行为一致,但在 Windows 上发布 HTTPS 接口时,必须把 OpenSSL 的 DLL 一起打包,否则 QNetworkReply 会报 TLS 初始化失败。这个坑不到发布基本踩不到,先记住有这回事,第 5 章会展开说。

2.3 在 Windows、Linux、macOS 上跑通最小构建的检查清单

这里给一张我在三个平台上跑同类 Qt 项目时的对照表,照着走一遍能避免很多低效排查。

平台依赖构建命令最容易翻车的点
Windows(MSVC)Visual Studio 2022 生成器 + Qt 6(msvc2019_64)cmake -B build -G "Visual Studio 17 2022" -A x64 && cmake --build build --config ReleaseQt 的 bin 没进 PATH,或编译器架构与 Qt 包架构不匹配
Windows(MinGW)Qt mingw 包 + MinGW 工具链cmake -B build -G "MinGW Makefiles" -DCMAKE_PREFIX_PATH=...用了 MSVC 的 Qt 包去配 MinGW,链接器直接报错
Linux(Debian/Ubuntu)build-essential cmake qt6-base-dev(或 qtbase5-dev)cmake -B build && cmake --build build -j$(nproc)缺 libgl1-mesa-dev,链接 QtWidgets 时报 OpenGL 相关错误
macOSbrew install qt cmakeexport PATH="/opt/homebrew/opt/qt/bin:$PATH"; cmake -B build && cmake --build buildApple Silicon 和 Intel 的 Homebrew 路径不同,find_package 找不到 Qt

Linux 那个 OpenGL 报错是最典型的:错误信息里写着 undefined reference to 'glXGetProcAddress',大概率不是代码问题,而是少装 libgl1-mesa-dev。macOS 上如果 cmake 找不到 Qt,先检查 export 的路径是不是本机架构对应的那一条。Windows 上 MSVC 和 MinGW 混用是翻车重灾区,总之 Qt 包、编译器和生成器三者必须同一套。

这一套走下来,你的工程骨架就算稳了。接下来才轮到真正的业务逻辑:行情数据从哪来、怎么刷新。

3. 行情数据层:拉数据、缓存、刷新这三件事怎么做才不卡界面

看盘软件的核心不是界面多好看,而是数据链路顺畅:定时拉行情、解析、更新界面、存本地。这一层做不好,界面再漂亮也是个花瓶。考虑到这是新手练手项目,数据源优先选免费的公开 HTTP 接口,不引入需要注册鉴权的重型 SDK,把精力放在解析健壮性和刷新策略上。

3.1 行情源怎么选:公开 HTTP 接口的解析与容错

常见的免费行情接口有新浪和腾讯财经。新浪的接口返回 GBK 编码文本,腾讯的 qt.gtimg.cn 返回按 ~ 分隔的文本,字段多且稳定,适合练手。tushare 这类需要 token 的库更适合做历史数据回测,用来做实时快照反而不顺手。看盘软件最需要的是当前价格、涨跌幅和分时走势,一个 HTTP GET 就能搞定。

void QuoteClient::fetchSnapshot(const QString& code) { QUrl url(QStringLiteral("https://qt.gtimg.cn/q=%1").arg(code)); QNetworkRequest req(url); req.setRawHeader("User-Agent", "Mozilla/5.0"); req.setTransferTimeout(3000); // 超过 3 秒没响应直接判失败 QNetworkReply* reply = m_nam->get(req); connect(reply, &QNetworkReply::finished, this, [this, reply]() { reply->deleteLater(); // 必须释放,否则内存只进不出 if (reply->error() != QNetworkReply::NoError) { qWarning() << "quote error" << reply->errorString(); return; } const QByteArray raw = reply->readAll(); QString text = QStringDecoder(QStringDecoder::Utf8).decode(raw); // 该接口历史上返回 GBK,遇到乱码再按系统编码兜底 if (text.contains(QChar::ReplacementCharacter)) text = QStringDecoder(QStringDecoder::System).decode(raw); const QStringList fields = text.split(QLatin1Char('~')); if (fields.size() < 32) return; // 字段不够说明响应异常,丢弃 emit quoteReady(fields.value(3), // 当前价 fields.value(4), // 昨收 fields.value(31)); // 涨跌幅 }); }

这里有几个关键点。m_nam 必须是 QuoteClient 的成员变量,不能每次请求都 new 一个 QNetworkAccessManager,否则底层连接池不够用,请求一多就报错。setTransferTimeout(3000) 让超时由 Qt 内部计时处理,不需要自己再套一层 QTimer。解码那一段是血泪经验:同一个接口在不同时期返回过不同编码,先按 UTF-8 解,发现替换字符再退回系统编码,比写死一种编码更抗变。

为什么强调字段数量校验?因为行情接口偶发返回空串或半截数据,直接 split 后取下标会得到空值。宁可丢弃这次响应等下一轮刷新,也不要拿脏数据更新界面。这是数据层最基本的一条纪律。

3.2 用 QTimer 做周期刷新,但别把网络请求塞进 UI 线程

看盘软件的刷新节奏一般是 3 秒一次。QNetworkAccessManager 本身就是异步的,网络收发发生在 Qt 的事件循环里,不会阻塞界面,所以不需要额外开线程。新手常见的错误反而是画蛇添足地开一个 QThread,在线程里创建 QNetworkAccessManager,结果信号跨线程连接没处理好,界面更新反而乱掉。

// MainWindow 构造函数里 m_refreshTimer = new QTimer(this); m_refreshTimer->setInterval(3000); connect(m_refreshTimer, &QTimer::timeout, this, &MainWindow::refreshQuotes); m_refreshTimer->start(); void MainWindow::refreshQuotes() { if (!isVisible()) return; // 最小化时不刷新,省电也少打扰服务器 for (const QString& code : m_watchlist) m_client->fetchSnapshot(code); }

这个实现里唯一的技巧是 isVisible() 判断:窗口最小化或隐藏时跳过刷新,这是公开接口限频下很实用的自我保护。定时器的间隔参数值得说道一下。3 秒是行情快照的常见节奏,如果你做的是收盘后复盘,间隔可以放大到 30 秒甚至更长;但不要小于 1 秒,一来免费接口扛不住,二来 1 秒内的价格变化对迷你看盘没有意义。

另一个要注意的误用是阻塞式读取。QNetworkReply 提供了 waitForReadyRead(),看起来能“同步拿结果”,但调用它会让当前线程卡住直到数据到达,UI 线程一旦调用,界面直接冻结。如果发现程序拉行情时窗口拖不动,基本就是这个 API 惹的祸。黑匣子式排查不背这个锅,Qt 的异步模型本来就是答案。

3.3 数据落地:QSettings 存配置,收盘数据存 CSV 而不是数据库

看盘软件要持久化的数据有两类:一类是自选股代码,量小、需要随时改;一类是行情记录,追加多、读取少。前者我用 QSettings,后者用一个 CSV 文件就够了,不推荐为了凑复杂度引入 SQLite。

void MainWindow::saveWatchlist(const QStringList& codes) { // 用 IniFormat 让配置文件可见、可手工编辑 QSettings s(QSettings::IniFormat, QSettings::UserScope, QStringLiteral("MiniK"), QStringLiteral("watchlist")); s.setValue("codes", codes); s.sync(); // 强制落盘,防止崩溃丢配置 } QStringList MainWindow::loadWatchlist() { QSettings s(QSettings::IniFormat, QSettings::UserScope, QStringLiteral("MiniK"), QStringLiteral("watchlist")); return s.value("codes").toStringList(); }

QSettings 的组织名和应用名决定了配置文件的具体路径:Windows 上通常是 C:/Users/xxx/AppData/Roaming/MiniK/watchlist.ini,Linux 上是 ~/.config/MiniK/watchlist.conf,macOS 上则是 ~/Library/Preferences 下的 plist。选 IniFormat 是为了三端行为一致,避免 Windows 默认写注册表,Linux 写 ini,macOS 写 plist 的分裂局面。UserScope 表示只对当前用户生效,不需要管理员权限。

行情记录用 QTextStream 追加写,注意两点:打开模式用 Append,用 Truncate 的话软件重启第一天数据就没了;文本编码统一 UTF-8,否则跨平台打开 CSV 中文乱码。

QFile f(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + QStringLiteral("/kline_600000.csv")); if (!f.open(QIODevice::Append | QIODevice::Text)) return; QTextStream out(&f); out.setEncoding(QStringConverter::Utf8); out << QDateTime::currentDateTime().toString(Qt::ISODateWithMs) << ',' << price << ',' << volume << '\n';

QStandardPaths::DocumentsLocation 在不同平台会定向到各自的文档目录,这是前面说的“数据目录不写死”的正规解法。如果以后想看历史列表,可以用 QFile 按行读回来;若数据量到了几万行以上,再考虑升级成 SQLite 并用 QSqlTableModel 接入表格视图,那是一条自然的演进路径,而不是第一步就上数据库。

数据链路通了,接下来才是用户每天盯着看的部分:K 线怎么画、自选股表格为什么滚动卡顿。

4. 界面层:自绘 K 线与自选股列表的实现取舍

看盘软件的界面层有两个关键组件:K 线图和自选股表格。K 线图用 QPainter 自绘,自选股表格用 QTableView 加自定义 Model。这两个选型的共同逻辑是:把数据量和视图解耦,保证数据再多界面也不卡。这也是整个界面层最值得新手学习的部分。

4.1 用 QPainter 画 K 线:坐标映射、蜡烛实体与均线叠加

新手画 K 线的第一反应是找现成的图表库,比如 QCustomPlot 或 Qt Charts。但练手项目我更推荐直接用 QPainter 自绘,因为看盘软件真正有价值的是坐标映射逻辑:把每根 K 线的开高低收映射到画布上的矩形位置。这个逻辑自己写过一遍,后面换任何图表库都能秒懂它的接口在干什么。

void KLineWidget::paintEvent(QPaintEvent*) { QPainter p(this); p.setRenderHint(QPainter::Antialiasing, true); const QRectF chartRect = QRectF(8, 8, width() - 16, height() - 40); const QVector<KBar>& bars = m_bars; if (bars.isEmpty()) { p.drawText(chartRect, Qt::AlignCenter, QStringLiteral("暂无数据")); return; } const int lastN = qBound(10, m_visibleCount, bars.size()); const int start = bars.size() - lastN; double hi = -1e9, lo = 1e9; for (int i = start; i < bars.size(); ++i) { hi = qMax(hi, bars.at(i).high); lo = qMin(lo, bars.at(i).low); } const double pad = (hi - lo) * 0.05; // 上下留 5% 边距,防止顶格贴边 hi += pad; lo -= pad; const double step = chartRect.width() / lastN; const double bodyW = step * 0.6; // 蜡烛实体占步长的 60% for (int i = start; i < bars.size(); ++i) { const KBar& b = bars.at(i); const double x = chartRect.left() + (i - start) * step + step / 2; const double yh = chartRect.bottom() - (b.high - lo) / (hi - lo) * chartRect.height(); const double yl = chartRect.bottom() - (b.low - lo) / (hi - lo) * chartRect.height(); const double yo = chartRect.bottom() - (b.open - lo) / (hi - lo) * chartRect.height(); const double yc = chartRect.bottom() - (b.close- lo) / (hi - lo) * chartRect.height(); const QColor color = (b.close >= b.open) ? QColor("#e05a5a") : QColor("#21a038"); p.setPen(QPen(color, 1)); p.drawLine(QPointF(x, yh), QPointF(x, yl)); // 影线 p.fillRect(QRectF(x - bodyW / 2, qMin(yo, yc), bodyW, qMax(1.0, qAbs(yo - yc))), color); // 实体 } drawMA(p, chartRect, start, 5); }

坐标映射的核心公式是 v 映射到 y:chartRect.bottom() - (v - lo) / (hi - lo) * chartRect.height()。含义是:价格越低,y 越大,因为屏幕坐标向下增长。qMax(1.0, qAbs(yo - yc)) 保证当 K 线缩到只剩一根时,实体至少画出 1 像素,否则十字缩到极端会出现实体消失的视觉 bug。qBound(10, m_visibleCount, bars.size()) 把可见 K 线数量限制在 10 到总数量之间,既避免数组越界,也避免滚轮无休止放大导致坐标计算溢出。

drawMA 绘制均线建议用 QPainterPath 而不是逐点 drawLine:遍历从 start 开始的每个点,把 MA5 的值加入 path,然后 p.drawPath(path)。这样画出的折线更平滑,而且可以用 setPen 单独设置颜色,与蜡烛体分离,后续想隐藏均线也只需要一个 bool 开关。这一步是在练手项目里体会 QPainterPath 价值的最短路径。

4.2 自选股表格的卡顿优化:从 QTableWidget 换到 QTableView + 自定义 Model

自选股表格是另一个高频交互点。很多新手从 QTableWidget 起步,塞进去几千行数据后滚动起来就掉帧。原因在于 QTableWidget 为每个单元格都创建独立的 QTableWidgetItem 对象,1000 行 6 列就是 6000 个对象,每次刷新还要遍历它们,内存和 CPU 双双告急。换到 QTableView + 自定义 Model 后,View 只向 Model 索要可见行数据,滚动开销与总行数基本脱钩。

class QuoteTableModel : public QAbstractTableModel { Q_OBJECT public: int rowCount(const QModelIndex& parent = QModelIndex()) const override { return parent.isValid() ? 0 : m_items.size(); } int columnCount(const QModelIndex& parent = QModelIndex()) const override { return parent.isValid() ? 0 : 6; } QVariant data(const QModelIndex& idx, int role) const override { if (!idx.isValid() || idx.row() >= m_items.size()) return {}; const QuoteItem& it = m_items.at(idx.row()); if (role == Qt::DisplayRole) { switch (idx.column()) { case 0: return it.code; case 1: return it.name; case 2: return QString::number(it.price, 'f', 2); case 3: return QString::number(it.changePercent, 'f', 2) + QLatin1Char('%'); default: return {}; } } if (role == Qt::ForegroundRole && (idx.column() == 2 || idx.column() == 3)) return it.changePercent >= 0 ? QColor("#e05a5a") : QColor("#21a038"); return {}; } void updateQuote(int row, const QuoteItem& it) { if (row < 0 || row >= m_items.size()) return; m_items[row] = it; // 只通知局部变化,不整表重置 emit dataChanged(index(row, 0), index(row, columnCount() - 1)); } private: QVector<QuoteItem> m_items; };

Model 的核心是 data() 按需返回,View 滚动到哪一行,Qt 才调哪一行的 data(),这是性能提升的根本。updateQuote 里用 dataChanged 局部刷新,而不是 beginResetModel/endResetModel 整表重置。整表重置会让 View 重建所有可见单元格,连续刷新时界面闪烁;局部刷新只有那一行的几个单元格重绘。涨跌颜色放在 ForegroundRole 里返回 QColor,比在委托里重绘文字更省事,这也是 Qt 表格最常见的上色方式。

视图端还有两个必调的设置。setUniformRowHeights(true) 告诉 QTableView 所有行高一致,跳过逐行计算行高,滚动性能立刻上一个台阶。setVerticalScrollMode(QAbstractItemView::ScrollPerPixel) 让滚动按像素而非整行滚动,观感更接近看盘软件。这两个参数用好了,2000 行自选股滚动跟 50 行没有肉眼区别。

4.3 十字光标与缩放:把鼠标交互做成看盘该有的样子

K 线图的交互,十字光标和滚轮缩放是标配。实现方式其实很简单:鼠标移动时记录坐标,触发重绘,在 paintEvent 里补两条线;滚轮改变可见 K 线数量,重新计算坐标。

void KLineWidget::mouseMoveEvent(QMouseEvent* e) { m_crossPos = e->pos(); update(); // 触发重绘,绘制十字线 QToolTip::showText(e->globalPos(), QStringLiteral("%1 %2").arg(xToDate(e->pos().x()), yToPrice(e->pos().y())), this); } void KLineWidget::wheelEvent(QWheelEvent* e) { const int delta = e->angleDelta().y() > 0 ? -3 : 3; m_visibleCount = qBound(10, m_visibleCount + delta, m_bars.size()); e->accept(); update(); }

滚轮缩放的步长取 3 根,一次滚轮大概缩放 10% 的视野,是比较自然的节奏。qBound 在这里既是保护也是边界定义,把 visibleCount 限制在 10 到总行数之间,避免数组越界。十字线绘制在 paintEvent 里用 p.drawLine 画两条水平垂直线即可,不需要单独挂 QLabel。

值得强调的是坐标反算。4.1 里把价格映射到 y,这里 xToDate 和 yToPrice 要做逆运算。我一般会把映射和反算写成一对纯函数,例如 priceToY 和 yToPrice,共用同一套 hi、lo、chartRect 参数。这样鼠标提示和绘制永远一致,不会出现光标指的位置和读出的价格对不上的诡异情况。很多“图上看懂了,点了没反应”的 bug,根源都是坐标算了两遍、公式各写各的。

界面层做完,一个能看的看盘软件已经成型。但把它从一个“能跑的 demo”变成“敢分发给别人的软件”,还有几个老坑绕不过去。

5. 避坑:新手在这个 Qt 看盘项目里最常见的 5 个翻车现场

这一章全是血泪经验。每一条都按现象、原因、解决的顺序写,你在自己机器上遇到类似问题,直接对照着排查。

5.1 一到 Release 发布就报“找不到 Qt6Widgets.dll”或 0xc000007b

现象:Debug 模式下程序跑得欢,把 build/Release 下的 exe 单独拷出来,双击不是报缺少 DLL,就是闪一个 0xc000007b 的错误。

原因:Qt 的库没有一起带上。Windows 上要通过 windeployqt 把 Qt 运行库、平台插件、编译器运行时全部拷到 exe 旁边;0xc000007b 常见于把 MinGW 编译的 Qt DLL 拷给了 MSVC 编出来的 exe,二进制不兼容。

解决:用 Qt 安装目录里与编译器匹配的 windeployqt 跑一遍,命令是 windeployqt --release MiniKLineViewer.exe。MSVC 编译的程序还要确保目标机器装了对应版本的 Microsoft Visual C++ Redistributable,否则报缺 VCRUNTIME140.dll。MinGW 编译的不依赖 VC 运行库,但要带上 libgcc_s_seh-1.dll、libstdc++-6.dll 这些运行时。发布时把这两套规则记牢,可以省掉大量挨个试 DLL 的时间。

5.2 中文界面在 Linux 上变成方块

现象:同一份源码,Windows 上标题和按钮的中文显示正常,换到 Ubuntu 上全是口字形方块。

原因:代码里指定的字体族不存在。Linux 默认不装微软雅黑,Qt 找不到指定字体时回退到无中文字形的字体,于是渲染成方块。

解决:按第 2.2 节的方式做字体回退,并确保系统装了 fonts-noto-cjk。如果程序中用了 QSS 样式表,样式表里写的 "Microsoft YaHei" 同样要改成按平台动态设置。排查思路很简单:在目标机器上执行 fc-list :lang=zh 看有没有中文字体,没有就装,有就检查代码里的字体名是否匹配实际字体族。这个问题的隐蔽点在于它不崩溃、不报错,只影响观感,容易拖到最后才被发现。

5.3 自选股超过 2000 行后滚动掉帧

现象:自选股列表第一版用 QTableWidget,几百行没事,等到导入 2000 行,拖滚动条时界面明显粘滞,CPU 占用还高。

原因:QTableWidget 为每个单元格创建对象,2000 行 6 列就是 12000 个 QTableWidgetItem,每次滚动 View 都要在这些对象上做命中检测和遍历。QTableWidget 的设计目标是简单交互的小表格,不是大数据列表。

解决:换 QTableView + QAbstractTableModel(第 4.2 节)。如果短时间内不想动这个结构,有一个临时优化是关掉逐行计算行高,直接 view->setUniformRowHeights(true),能缓解一部分卡顿。但根治方案还是换 Model,这个方案的数据结构只改一次,后续加列、加排序都很自然。

5.4 行情接口偶发返回空串,K 线图整段消失

现象:软件挂机一整天,偶尔发现 K 线图白屏,窗口里一个字都没有,过几分钟刷新后又自己恢复。

原因:HTTP 响应偶发为空或半截数据,解析代码直接取 split 后的字段,拿到了空字符串;更糟的是界面代码一拿到“数据”就 clear 旧数据再更新,于是白屏。

解决:三层防护。第一层,req.setTransferTimeout(5000) 让超时请求直接走错误分支。第二层,解析前检查字段数量,不够 32 段就丢弃这次响应,保持旧数据不动。第三层,UI 更新统一走“下一条完整数据到了再重绘”的节奏,不要在开始拉取时就清空图表。这条经验可以推广到所有轮询类应用:宁可显示旧数据也不白屏,用户只会认为数据没更新,不会认为软件坏了。

5.5 窗口关闭后 QTimer 还在触发,析构时崩溃

现象:关掉主窗口,一按 Ctrl+C 退出,终端刷出一串 “QObject::startTimer: Timers cannot be stopped from another thread”,或者程序直接段错误退出。

原因:MainWindow 析构时 m_refreshTimer 没停止,定时器在对象销毁过程中仍触发 timeout;更隐蔽的是网络回调的 lambda 里捕获了 this,reply 延迟返回时 MainWindow 已被销毁,this 悬空。

解决:MainWindow 析构函数里显式 m_refreshTimer->stop()。异步回调要利用 connect 的 context 重载,connect(reply, &QNetworkReply::finished, this, lambda),这样 this 销毁时 Qt 自动断开连接,lambda 不会被调用。检查自己代码里有没有 leaked 的 connect,可以全局搜一遍 connect( 后面第三参数是不是 this,如果有些地方传了 nullptr 或省略,那基本就是隐患。这类崩溃不是必现的,往往在退出时偶发,属于最折腾人的一类问题。

6. 最后一个技巧:给这个项目再加一个能验证你读懂它的功能

如果你已经把前面的骨架、数据层、界面层都走通了,我建议给这个看盘软件补一个联动成交量副图,顺便搭一个自测入口。这个功能的密度刚刚好:它要用到已经写过的坐标映射、又要新增一个绘图区间的划分,是检验你是否真读懂 K 线绘制逻辑的好题目。

做法是把绘图区一分为二,上部 65% 画 K 线,下部 25% 画成交量柱,中间留出坐标轴间隙。成交量的核心逻辑是复用 K 线的时间轴,但用独立的量纲映射柱高,否则成交量和价格混在一个坐标里就没法看了。关键代码只有一段,却要把图例说透:

const QRectF volRect(8, chartRect.bottom() + 12, chartRect.width(), height() - chartRect.bottom() - 24); double maxVol = 0; for (int i = start; i < bars.size(); ++i) maxVol = qMax(maxVol, bars.at(i).volume); for (int i = start; i < bars.size(); ++i) { const double x = volRect.left() + (i - start) * step + step / 2; const double h = bars.at(i).volume / maxVol * volRect.height(); const QColor color = bars.at(i).close >= bars.at(i).open ? QColor("#e05a5a") : QColor("#21a038"); p.fillRect(QRectF(x - bodyW / 2, volRect.bottom() - h, bodyW, h), color); }

滚动缩放时,两个区域共享同一个 m_visibleCount,所以时间轴天然对齐。这里最容易出的错是 volRect 计算时和 chartRect 的高重叠,导致成交量柱画出绘图区。验证方法也很直接:把坐标映射抽成一对纯函数后,在 main 里加一个 --selftest 参数,用随机生成的一批 K 线数据跑一遍断言,确认成交量柱数量和可见 K 线数量相等、最高价映射后落在图表区最顶端。这样每次改完坐标逻辑,跑一遍就知道有没有弄坏东西,而不是盯着界面肉眼找偏移。

我第一次给 K 线图加成交量副图时,直接把两段绘制代码硬塞进 paintEvent,结果缩放后上下两幅图的坐标肉眼可见地错位。后来把价格转 y、y 转价格这些映射全部抽成纯函数,自测入口放在 main 函数里,才算把这事做踏实。一个练手项目值不值得投入,就看它有没有逼你从“能跑”走到“知道为什么能跑”。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 7:23:30

OpenMontage:用AI代理与Remotion实现视频批量自动化生产

1. 从 OpenMontage 这个名字说起&#xff1a;它到底想解决什么问题第一次看到 OpenMontage 这个词&#xff0c;我脑子里蹦出来的第一反应是“开源 蒙太奇”。蒙太奇是影视剪辑里最核心的概念之一——把不同镜头、不同素材按照某种逻辑拼接在一起&#xff0c;形成一段有节奏、有…

作者头像 李华
网站建设 2026/10/26 7:23:29

e2e端到端测试与AI评测实战:从profile1配置化到落地避坑指南

1. 从“e2e”这个标题说起&#xff1a;它到底指什么第一次看到“e2e”这三个字符&#xff0c;很多人会愣一下。它不像“Docker”“Kubernetes”那样一眼能看出领域&#xff0c;也不像“React”“Vue”那样自带技术栈标签。但如果你在软件工程、测试、或者近几年火起来的AI评测圈…

作者头像 李华
网站建设 2026/10/26 7:23:20

Text-to-CAD本质是工程语义逆向编译,非AI画图

1. “Text-to-CAD”不是AI画图&#xff0c;而是工程语义的逆向编译最近在几个工业软件开发者群和CAD二次开发论坛里&#xff0c;几乎每天都有人问&#xff1a;“现在能用文字直接生成SolidWorks模型吗&#xff1f;”“有没有类似DALLE但输出STEP文件的工具&#xff1f;”——这…

作者头像 李华
网站建设 2026/10/26 7:22:27

Docker+CentOS7离线部署ElasticSearch单机版实战

1. 为什么要走离线这条路&#xff1a;方案选型与整体设计docker centos7离线安装ElasticSearch单机版&#xff0c;这件事听起来像是一道普通的部署题&#xff0c;真到了现场才发现它是一整套约束条件下的取舍。我第一次接到这类需求是在一个完全断网的内网机房里&#xff0c;服…

作者头像 李华
网站建设 2026/10/26 7:22:21

Claude本地记忆增强:结构化语义记忆工程实践

1. 项目概述&#xff1a;这不是一个官方产品&#xff0c;而是一次对Claude记忆机制的逆向观察与工程化尝试“claude-mem”这个名称在当前技术社区里没有对应任何官方发布的工具、API或开源项目——Anthropic从未推出过名为“Claude-Mem”的独立服务、客户端或记忆管理插件。它不…

作者头像 李华
网站建设 2026/10/26 7:22:08

Transformer架构与Attention机制:从原理到LLM实战优化

1. 从零理解Transformer架构&#xff1a;为什么Attention机制改变了AI格局2017年那篇《Attention Is All You Need》论文像一颗核弹引爆了AI领域。当时我在团队内部分享会上第一次看到这个架构时&#xff0c;完全没意识到它会在五年后成为大语言模型的基石。现在回头看&#xf…

作者头像 李华