简介:这是一套基于VC6.0开发的股票软件源代码,聚焦股票列表实时行情刷新功能,实现每3秒刷新一次,并以中远海控为例演示脱机使用场景。数据接口对接腾讯股票实时行情数据,适合具备一定C++与MFC基础、希望研究行情推送与界面刷新机制的开发者参考。压缩包共2034个文件,以h头文件、cpp源文件为主,辅以obj、sbr等编译中间文件,以及bmp、gif、ico等界面资源,另有dsp、dsw、vcproj等工程文件与dll、lib依赖库,整体约64.12MB,工程结构完整。目前已有1347人学习下载。读者可从中获取行情刷新线程调度、网络数据解析、列表控件更新等实现思路,并借助现成工程直接编译调试,快速理解实时行情模块的落地方式。
1. 一份 VC6.0 时代的股票行情源码,为什么现在还有人翻出来跑
前阵子有个做量化接口的朋友问我,手里有一份老掉牙的 VC6.0 股票软件源代码,编译都费劲,还有没有必要折腾。我让他把包发过来,解压一看,目录结构是典型的 MFC 单文档工程,核心逻辑全压在几个 View 和自定义的行情类里,股票列表用 CListCtrl 撑着,定时器挂在主窗口上,3 秒触发一次行情刷新。这东西放在今天当然不算先进,但它有一个现在很多框架给不了的东西:整条链路是裸的,从 socket 收包、解析、入列表、重绘,一行都没藏。
这份资源适合两类人。一类是想搞明白「股票列表实时行情刷新」到底怎么落地的人,市面上的行情框架封装太厚,你调个 API 就出数据,中间发生了什么完全是黑匣子,拿这份源码当解剖样本,比看十篇原理文章都直接。另一类是手里有老系统要维护的,VC6.0 的工程还在产线上跑,想加个列表刷新或者改刷新频率,得先看懂它原来是怎么组织的。它解决的不是「做一个新行情软件」的问题,而是「让你看清一个行情刷新循环的最小闭环长什么样」。
2. 拆开工程看结构:MFC 单文档里行情数据是怎么流动的
2.1 先认清这套 VC6.0 工程的骨架
VC6.0 生成的 MFC 单文档工程,标准结构是 App、MainFrame、Doc、View 四件套。这份源码没有跳出这个框架,但它把行情相关的逻辑塞得比较集中,主要落在 View 和几个自定义类里。你打开工程后,先别急着编译,按下面的顺序把文件过一遍,能省掉后面大量找代码的时间。
| 文件/类 | 职责 | 你要重点关注的地方 |
|---|---|---|
| xxxApp.cpp | 应用入口,初始化 | InitInstance 里有没有提前起线程或定时器 |
| MainFrame.cpp | 主框架窗口 | 消息映射、菜单命令、定时器挂载点 |
| xxxView.cpp | 列表显示与刷新 | OnTimer、列表填充、重绘触发 |
| 行情数据类 | 收包与解析 | socket 接收、缓冲区处理、字段映射 |
| 股票列表结构 | 数据容器 | 用数组还是 CArray,增删改的同步方式 |
常见做法是,行情线程收到数据后不直接碰 UI,而是往一个共享结构里写,View 的定时器到点去读。这份源码基本也是这个路子,但具体用的是全局变量还是成员变量,得你自己确认,因为这直接决定了后面加锁的位置。
2.2 行情刷新的触发链路:定时器、线程与重绘
3 秒刷新一次,这个「3 秒」在代码里通常不是一个孤立的常量,而是串在一条链上。你要顺着这条链走一遍,才能知道改哪里、哪里不能乱改。
// 在主窗口或 View 的初始化里设置定时器 // 参数 1 是定时器 ID,参数 3 是间隔毫秒数 SetTimer(TIMER_REFRESH_ID, 3000, NULL); // 定时器消息处理,通常在 View 的 OnTimer 里 void CStockView::OnTimer(UINT nIDEvent) { if (nIDEvent == TIMER_REFRESH_ID) { // 从共享数据区取最新行情 RefreshStockList(); // 触发列表重绘 m_StockList.Invalidate(); } CView::OnTimer(nIDEvent); }这段逻辑说明几件事。第一,刷新的节奏由 SetTimer 的第三个参数控制,3000 就是 3 秒,改成 1000 就是 1 秒,但别急着改,后面避坑章节会说为什么。第二,OnTimer 里做的是「取数据 + 重绘」,真正收数据的活儿不在这里,通常在一个独立线程里跑。第三,Invalidate 只是标记重绘,不是立刻画,MFC 会在消息队列空闲时处理 WM_PAINT。
参数上你要盯两个地方。一个是定时器 ID,如果工程里还有别的定时器,ID 冲突会导致刷新逻辑根本不进这个分支。另一个是共享数据的读写时机,如果收数据的线程和 OnTimer 不在同一个线程,读的时候不加保护,列表里偶尔冒出脏数据或者程序直接崩,都是这么来的。
2.3 股票列表控件的填充与性能边界
CListCtrl 填充数据,新手最容易写成每来一条就 InsertItem 一次,数据量小的时候看不出问题,股票列表一多,界面就开始卡。这份源码里如果用的是先 DeleteAllItems 再整体重填,那每次刷新都是一次全量重建,3 秒一次还能忍,频率一高就露馅。
void CStockView::RefreshStockList() { // 先冻结重绘,避免填充过程中界面闪烁 m_StockList.SetRedraw(FALSE); m_StockList.DeleteAllItems(); int nCount = m_StockData.GetSize(); for (int i = 0; i < nCount; i++) { // 插入一行,第一列是股票代码 m_StockList.InsertItem(i, m_StockData[i].szCode); // 后续列用 SetItemText 填 m_StockList.SetItemText(i, 1, m_StockData[i].szName); m_StockList.SetItemText(i, 2, m_StockData[i].szPrice); } // 恢复重绘,一次性刷新 m_StockList.SetRedraw(TRUE); m_StockList.Invalidate(); }SetRedraw(FALSE) 和 SetRedraw(TRUE) 这一对是血泪经验,不加的话填充过程中界面会疯狂闪,看起来像程序要挂了。DeleteAllItems 加整体重填的写法,优点是逻辑简单、不会出现行错位,缺点是数据量大时开销全花在重建上。如果你要把它改成增量更新,得自己维护「代码到行号」的映射,改错了就会出现价格串行,这种 bug 比崩溃还难查。
3. 让这份源码在你机器上跑起来:VC6.0 编译与依赖处理
3.1 VC6.0 环境准备与工程打开
VC6.0 在现在的系统上直接装,十有八九会卡在安装程序兼容性上。常见做法是先用兼容模式跑安装包,装完之后如果 IDE 打开工程报错,多半是工作空间文件(.dsw)和项目文件(.dsp)的路径对不上。你拿到的是 .rar,解压后先看目录里有没有 .dsw,没有的话就找 .dsp 单独打开。
打开工程后,先别点编译,做三件事。第一,确认工程设置里的字符集,VC6.0 默认是 MBCS,如果源码里有中文注释或字符串,改成 Unicode 反而会乱码。第二,看链接器里依赖的库,MFC 工程一般会链上 mfc42.lib 之类,缺库会在链接阶段报一堆 unresolved external。第三,确认输出目录,VC6.0 默认把中间文件丢在 Debug 或 Release 下,路径里有中文或空格有时会出玄学问题。
# 如果解压出来的工程文件名带空格或中文,先重命名成纯英文 # 例如把 "策略为王 股票软件" 改成 stock_demo mv "策略为王 股票软件" stock_demo # 进入目录确认工程文件 ls *.dsp *.dsw重命名这一步看着多余,但 VC6.0 对路径的处理确实不如今天的工具稳,路径里带空格导致编译失败的案例我见过不止一次。改完之后用 VC6.0 打开 .dsp,如果提示转换工程,选「是」,让它按当前版本重新生成工作空间。
3.2 编译报错的常见类型与处理顺序
VC6.0 编译老工程,报错基本集中在三类:头文件找不到、类型不识别、链接缺符号。处理顺序建议从第一类开始,因为头文件问题会连带引发后面两类,你先修后面的等于白修。
头文件找不到,先看工程设置里的 Additional include directories 有没有把源码自带的 include 目录加进去。有些工程把自定义头文件放在子目录里,但 .dsp 里没配路径,换台机器就找不到。类型不识别,常见于源码里用了较新的写法,比如 for 循环里声明变量,VC6.0 的编译器对 C++ 标准支持有限,这种得把变量声明提到循环外面。链接缺符号,看报错里的函数名,去 MSDN 或者源码里搜,确认是哪个库没链上。
// VC6.0 不支持的写法 for (int i = 0; i < n; i++) { ... } // 改成这样 int i; for (i = 0; i < n; i++) { ... }这个改动看着琐碎,但老工程里这种点很多,一个个改比整体重构风险小。改的时候别顺手把逻辑也动了,先让它编过,再谈优化。
3.3 运行起来后先验证行情刷新链路
编译通过只是第一步,跑起来能不能刷新才是关键。运行后先看股票列表有没有数据,如果没有,按「数据源 → 解析 → 填充」的顺序排查。数据源这块,这份源码大概率是连某个行情服务器或者读本地模拟数据,你得先确认它连的是什么。如果是网络源,看 socket 有没有连上;如果是本地文件,看路径对不对。
列表有数据但不刷新,重点看定时器有没有真的触发。在 OnTimer 里加个断点或者输出,跑起来看它是不是每 3 秒进一次。如果断点根本不进,回去查 SetTimer 的返回值和定时器 ID。如果进了但界面没变,问题在重绘或者数据没更新,这时候把 RefreshStockList 里的数据打印出来,看取到的是不是新值。
提示:验证刷新链路时,先把刷新间隔临时改成 1 秒,这样不用等太久就能看出问题。确认逻辑通了再改回 3 秒,别一上来就盯着 3 秒的节奏干等。
4. 避坑与排查:这份老源码最容易翻车的几个点
4.1 现象:列表数据偶尔串行或显示错位
原因:收数据的线程和 UI 线程同时操作同一块数据,没有做同步。行情线程写的时候 View 正在读,读到的可能是写了一半的结构,代码和价格对不上。
解决:在共享数据的读写处加临界区。VC6.0 里可以用 CRITICAL_SECTION,初始化和删除配对,读写前 EnterCriticalSection,读完 LeaveCriticalSection。别用 Sleep 去躲,那是掩耳盗铃。
4.2 现象:刷新频率改成 1 秒后界面卡死
原因:全量重建列表的开销跟不上刷新频率,消息队列里堆满了 WM_PAINT 和定时器消息,界面线程被拖死。
解决:要么改增量更新,只刷新变化的行;要么把刷新和重绘解耦,数据更新归数据更新,界面按自己的节奏重绘。3 秒这个值不是随便定的,它是在「看起来实时」和「界面扛得住」之间取的平衡。
4.3 现象:编译通过但运行时报内存错误
原因:老工程里大量裸指针和手动 new/delete,某处 delete 之后没置空,后面又用了一次;或者数组越界写坏了相邻内存。
解决:先用 VC6.0 自带的调试器跑,看崩溃时的调用栈。定位到具体行之后,检查指针的生命周期。如果工程里用了 CArray 之类的容器,注意 GetSize 返回的是元素个数,不是字节数,索引别越界。
4.4 现象:换台机器编译,报一堆找不到的库
原因:工程设置里用了绝对路径引用库或头文件,换机器路径对不上。
解决:把工程设置里的路径改成相对路径,或者用环境变量。VC6.0 的工程设置里,include 和 lib 路径都支持相对写法,以工程文件所在目录为基准。改完之后把 .dsp 里残留的绝对路径也清一遍。
4.5 现象:行情数据能收到,但列表里全是乱码
原因:字符集不匹配。行情源发的是 GBK,工程按 Unicode 处理,或者反过来。
解决:先确认源码里字符串的类型,char 还是 TCHAR。如果是 char,工程设置保持 MBCS;如果是 TCHAR,确认有没有定义 _UNICODE。两边对齐之后,乱码问题基本就没了。
5. 把 3 秒刷新改成可配置:一个具体技巧和验证方法
老工程里把 3000 写死在 SetTimer 里,改一次要重新编译,这在实际维护里很烦。我一般会把它抽成一个配置项,从 ini 或者注册表读,这样现场调刷新频率不用动代码。下面这个改法不复杂,但能省掉很多来回编译的时间。
// 从 ini 文件读取刷新间隔,默认 3000 毫秒 int GetRefreshInterval() { int nInterval = GetPrivateProfileInt( _T("Setting"), // 节名 _T("RefreshMs"), // 键名 3000, // 默认值 _T(".\\config.ini") // 配置文件路径 ); // 做个下限保护,别让人填个 0 或负数 if (nInterval < 500) nInterval = 500; return nInterval; } // 初始化时用读到的值设置定时器 int nMs = GetRefreshInterval(); SetTimer(TIMER_REFRESH_ID, nMs, NULL);GetPrivateProfileInt 是 Win32 的老接口,VC6.0 直接能用,不用额外依赖。第三个参数是默认值,配置文件不存在或者键没写的时候用它。第四个参数是 ini 路径,用相对路径的话,注意工作目录是不是工程目录,不然读不到。下限保护那两行别省,现场有人填 0 进去,定时器行为会变得很怪,排查起来又是一轮。
改完之后怎么验证?把 config.ini 里的 RefreshMs 分别设成 500、3000、10000,跑起来看列表刷新的节奏是不是跟着变。500 的时候界面如果卡,说明你的机器或者当前实现扛不住这个频率,那就别硬上。10000 的时候如果数据明显滞后,说明这个值对当前场景太慢。找到那个「不卡又不太慢」的点,比抄一个固定值有意义。
从那以后我每次拿到这种带定时刷新的老工程,都强制先把刷新间隔抽成配置,再谈别的优化。因为刷新频率这个东西,跟机器性能、数据量、用户预期都挂钩,写死在代码里等于把调优的路堵死了。希望这份拆解能帮你把这份源码真正跑起来、改得动。
本文还有配套的精品资源,点击获取