ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

VC++ 6.0实战:获取并显示沪深股市五档实时行情

VC++ 6.0实战:获取并显示沪深股市五档实时行情 简介这是一份面向C初、中级学习者的VC 6.0项目源码围绕沪深股市五档行情获取与显示展开。五档行情所涉买卖价档对盯盘与决策有直接参考意义资源完整演示了如何在老牌MFC框架下通过HTTP或FTP与行情服务器交互并封装了HttpClient类适合想了解WinInet网络编程、XML/JSON解析及多线程界面更新的读者。压缩包内共18个文件以5个头文件、4个C源文件为主体另含dsp/dsw工程文件、rc/rc2界面资源、图标与ReadMe说明结构紧凑便于直接打开工程对照学习。整体仅33KB清晰展示了从网络请求、数据解析到CListCtrl界面刷新的完整链路。目前已有578人学习适合作为股票行情类软件开发的入门参考或课设模板。1. 五档行情是什么VC 6.0 这个老技术为什么还能干这件事看到“五档行情---VC 6.0获取并显示沪深股市实时行情数据.rar”这个标题多数人第一反应是“都什么年代了还用VC 6.0”。但如果你维护过2005年前后的交易辅助工具、行情监控屏或者刚从公司代码仓库里翻出这类老包就会明白它解决的是一个至今没消失的需求不打开券商客户端用一个自己写的桌面小程序定时拉取沪深股票买一到买五、卖一到卖五的盘口数据并实时刷新到界面上。选择VC 6.0在这个场景里反而有它的合理性一是大量存量工具就是MFC写的改动成本最低二是新浪、腾讯这类公开HTTP行情接口返回的是GBK编码的纯文本VC6的CString默认就是ANSI/MBCS拿来直接解析省掉了新版VS里UTF-8转码的麻烦。本文会按数据源选型、MFC界面搭建、WinINet请求、工作线程刷新、避坑排查这条线给出一套能编译、能联网、能显示五档行情的最小落地实现。适合正在维护老代码的工程师也适合想用C理解行情协议的新手。2. 数据源选型与五档字段先搞清楚接口里每个数字是什么2.1 五档行情到底有哪五档字段清单“五档”指的是买一至买五、卖一至卖五这十个报价位。买一是当前最高的未成交买入委托价和对应委托量买二低一档以此类推卖一是最低的未成交卖出委托价。行情客户端展示时通常把卖档放在上边、买档放在下边中间是最新成交价。写程序之前必须先把接口返回的字段顺序弄清楚否则解析出来的盘口全是错位数字。我一般先用新浪的http://hq.sinajs.cn/listsh600000做验证浏览器直接打开会看到一行文本var hq_str_sh600000浦发银行,10.230,10.200,10.250,10.300,10.150,10.240,1000,10.230,2000,10.220,1500,10.210,800,10.200,3000,10.250,1200,10.260,900,10.270,1600,10.280,500,10.290,700,2025-01-15,10:23:45,00,;这行文本从引号开始按照逗号分隔每个位置对应一个固定含义。下面是常用下标表下标含义示例0股票名称浦发银行1今开10.2302昨收10.2003最新价10.2504最高10.3005最低10.1506买一价10.2407买一量手10008-9买二价/量10.230 / 200010-15买三至买五依次类推16-21卖一至卖三10.250 / 120022-25卖四、卖五10.280 / 500、10.290 / 70030日期2025-01-1531时间10:23:45注意括号里的“手”是委托量单位一手等于100股。有些博客会写“股”以实测为准。我的习惯是把原始字符串先落盘再按下标取字段不要凭记忆写死。2.2 三种数据源对比为什么首选新浪/腾讯 HTTP 接口沪深五档行情的数据来源常见有三类新浪财经 HTTP 接口、腾讯财经 HTTP 接口、券商或行情软件自有的 TCP 私有协议。对VC 6.0这个开发环境来说选型的核心指标是“协议可见、依赖为零、反制不狠”。数据源协议形态学习门槛延迟稳定性新浪 hq.sinajs.cnHTTP GET逗号分隔文本低1-3秒中等需带 Referer腾讯 qt.gtimg.cnHTTP GET波浪号分隔文本低1-3秒中等通达信/同花顺本地协议TCP 私有封包有加密和版本校验高毫秒级高但协议变化频繁HTTP接口的本质是服务端已经帮你维持好了会话你的程序只负责发起 GET 请求、读回文本。这对老项目最友好因为你不需要理解 TCP 封包、校验码、心跳这些机制私有协议则是一个黑匣子光“行情登录报文”就够研究好几天而且版本一升级可能就要重新逆向。结论很直接做五档行情显示、盯盘筛选、数据落盘这一类秒级刷新工具公开HTTP接口是最划算的路径。等跑通了再考虑要不要引券商级别的行情源。2.3 请求格式与频率限制一次拉多只股票的正确姿势新浪接口和腾讯接口都支持一次查询多只股票代码前缀必须带sh上海或sz深圳中间用英文逗号连接。常见做法是http://hq.sinajs.cn/listsh600000,sz000001,sz300750腾讯的对应地址是http://qt.gtimg.cn/qsh600000,sz000001,sz300750两个接口对 HTTP 请求头的要求不一样。新浪在缺少Referer头时经常返回 403所以我做GetHttpText这个函数时会把Referer: https://finance.sina.com.cn/和User-Agent: Mozilla/5.0一起带上腾讯相对宽松但带全头也没坏处。频率控制上我一般把每只股票的轮询间隔控制在 3 秒以上一批 20 只以内。不要每 100 毫秒请求一次公开接口不是给你做高频用的短时间大量请求会触发 IP 级别的拒绝而且这在单机工具里根本没意义。3. 用 MFC 搭建一个能跑的最小五档行情窗口3.1 工程与界面对话框 CListCtrl 放盘口VC 6.0 里新建一个 MFC AppWizard(exe) 工程选择 Dialog based界面不需要画得太复杂。我常用的布局是一个CListCtrl报表模式列依次是股票名称、最新价、买一量、买一价、卖一价、卖一量、买二价、买二量、卖二价、卖二量。更多档位放不下就先不管先把五档的“第一档最新价”跑通。OnInitDialog里初始化列表列的代码// 设置列表控件为全行选中、网格线、报表样式 m_list.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES); // 行首一列显示股票名后续各列对应盘口字段 m_list.InsertColumn(0, 名称, LVCFMT_LEFT, 100); m_list.InsertColumn(1, 最新价, LVCFMT_RIGHT, 70); m_list.InsertColumn(2, 买一量, LVCFMT_RIGHT, 70); m_list.InsertColumn(3, 买一价, LVCFMT_RIGHT, 70); m_list.InsertColumn(4, 卖一价, LVCFMT_RIGHT, 70); m_list.InsertColumn(5, 卖一量, LVCFMT_RIGHT, 70); m_list.InsertColumn(6, 买二价, LVCFMT_RIGHT, 70); m_list.InsertColumn(7, 买二量, LVCFMT_RIGHT, 70); m_list.InsertColumn(8, 卖二价, LVCFMT_RIGHT, 70); m_list.InsertColumn(9, 卖二量, LVCFMT_RIGHT, 70);InsertColumn的第一个参数是列序号第二个是列标题后面依次是对齐方式和列宽。这里的列宽是按 1024 分辨率调的如果你在 4K 屏上调试会显得偏窄可以把数值整体放大到 100-120。3.2 用 WinINet 拉 HTTP 文本的 CQuoteFetcherVC6 环境下最省心的网络方案是 WinINet 而不是裸 Winsock。WinINet 自动处理 HTTP 头、代理、重定向代码量少很多。我封装了一个GetHttpText函数专门负责把 URL 的内容读取成 CString。下面是完整实现bool GetHttpText(const char* url, CString out) { CString strAgent Mozilla/5.0; CString strHeaders Referer: https://finance.sina.com.cn/\r\n Accept: */*\r\n; // 打开WinINet会话参数依次是应用名、代理名、本地/远端、访问类型 HINTERNET hSession InternetOpenA( strAgent.GetBuffer(0), INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); if (hSession NULL) return false; // INTERNET_FLAG_RELOAD 强制从服务器拉取不读本地缓存 // INTERNET_FLAG_NO_CACHE_WRITE 不把响应写入临时文件 HINTERNET hUrl InternetOpenUrlA(hSession, url, strHeaders.GetBuffer(0), -1, INTERNET_FLAG_RELOAD | INTERNET_FLAG_NO_CACHE_WRITE, 0); if (hUrl NULL) { InternetCloseHandle(hSession); return false; } char szBuf[2048]; DWORD dwRead 0; out.Empty(); // InternetReadFile 返回 TRUE 且 dwRead 0 时才是正常读完 // 不要把 dwRead 0 当成出错否则会提前退出循环 while (InternetReadFile(hUrl, szBuf, sizeof(szBuf) - 1, dwRead) dwRead 0) { szBuf[dwRead] \0; out szBuf; dwRead 0; } InternetCloseHandle(hUrl); InternetCloseHandle(hSession); return !out.IsEmpty(); }解释几个关键参数。InternetOpenA的INTERNET_OPEN_TYPE_PRECONFIG表示使用 IE 的代理设置内网用户如果走代理能直接生效如果你确定本机直连互联网可以用INTERNET_OPEN_TYPE_DIRECT。InternetOpenUrlA的第五个参数是INTERNET_FLAG_RELOAD | INTERNET_FLAG_NO_CACHE_WRITE前者防止拿到 IE 缓存里的旧行情后者避免每次请求都往临时目录写文件——这两点对实时性都很重要。这里没有用HttpOpenRequest那一套因为InternetOpenUrl已经足够对付 GET 请求少封装一层就少一个错误来源。3.3 解析五档把逗号分隔的字符串变成结构体拿到原始文本后需要写一个解析函数。核心思路是先把var hq_str_sh600000前面的前缀和最后的分号去掉再按逗号截取子串。下面的结构体保存一只股票的完整五档struct QUOTE_INFO { char szName[32]; // 股票名称 double dLast; // 最新价 double dBidPrice[5]; // 买一价..买五价 double dBidVolume[5]; // 买一量..买五量 double dAskPrice[5]; // 卖一价..卖五价 double dAskVolume[5]; // 卖一量..卖五量 }; void ParseSinaText(const CString raw, QUOTE_INFO q) { memset(q, 0, sizeof(q)); // 找到第一个引号真正的字段串从引号后面开始 int nPos raw.Find(); if (nPos 0) return; // 截掉引号后内容再去掉末尾的分号 CString data raw.Mid(nPos 1); data.TrimRight(;); // 用 Tokenize 按逗号切字段 CString strField; int nIdx 0; int nStart 0; while (!(strField data.Tokenize(,, nStart)).IsEmpty()) { switch (nIdx) { case 0: lstrcpyn(q.szName, strField, 31); break; case 3: q.dLast atof(strField); break; case 6: q.dBidPrice[0] atof(strField); break; case 7: q.dBidVolume[0] atof(strField); break; case 8: q.dBidPrice[1] atof(strField); break; case 9: q.dBidVolume[1] atof(strField); break; // 买三到买五的下标依次是 10-15 case 16: q.dAskPrice[0] atof(strField); break; case 17: q.dAskVolume[0] atof(strField); break; case 18: q.dAskPrice[1] atof(strField); break; case 19: q.dAskVolume[1] atof(strField); break; // 卖三到卖五的下标依次是 20-25 } nIdx; } }CString::Tokenize是 VC6 里 MFC 提供的按分隔符切分函数比strtok安全因为它不会修改原始字符串的内部状态。上面代码只列了买一、买二和卖一、卖二买三到卖五的下标规律和你看到的表格完全一致——买三价是下标10买三量是11卖三价是20卖三量是21延续下去就能覆盖全部十档。解析完成后先用一个TRACE或者OutputDebugString把szName、dLast打出来确认没有解析错位再往界面上写。3.4 定时器轮询先跑通同步版再把延时留下来最简单的刷新方式是SetTimerOnTimer。在OnInitDialog里启动定时器3 秒一次SetTimer(1, 3000, NULL);然后在OnTimer里同步拉取并刷新列表。为了验证解析逻辑我会先写一个同步版本哪怕它会在网络慢时卡一下先把“数据对不对”这个问题解决掉。4. 实时更新不卡界面工作线程 自定义消息的刷新模型4.1 为什么不能在 OnTimer 里直接发 HTTP 请求同步版跑通之后你会遇到一个典型的用户体验问题当接口响应慢或者网络波动时InternetReadFile会阻塞住 UI 线程窗口拖不动、按钮点不了严重时甚至直接提示“未响应”。如果这次请求耗费了 5 秒OnTimer又在下一次 3 秒后触发程序会叠加重入越来越多的请求排队界面彻底假死。核心原则是两条网络请求永远放在工作线程里控件更新永远回到 UI 线程里。后面所有设计都围绕这两条展开。4.2 工作线程的启动、运行与安全退出我封装了一个CQuoteWorker类里面对外暴露Start()和Stop()。线程函数用 MFC 的AfxBeginThread启动因为它在 VC6 的 DLL 版 MFC 里会被自动管理不需要手动调_beginthreadex。下面是线程主体static UINT QuoteThreadProc(LPVOID pParam) { CQuoteWorker* pWorker (CQuoteWorker*)pParam; while (pWorker-m_bRunning) { // 每次循环只拉一批股票拉完就睡3秒 pWorker-FetchOnce(); Sleep(3000); } return 0; } void CQuoteWorker::Start(HWND hNotifyWnd) { m_hNotifyWnd hNotifyWnd; m_bRunning TRUE; AfxBeginThread(QuoteThreadProc, this, THREAD_PRIORITY_NORMAL, 0); } void CQuoteWorker::Stop() { m_bRunning FALSE; // 等待线程退出避免窗口销毁后线程还在 PostMessage WaitForSingleObject(m_hThread, 2000); }m_bRunning必须声明为volatile BOOL避免编译器把它优化进寄存器导致线程看不到退出标记。Stop()里的WaitForSingleObject是防止销毁对话框时工作线程仍在运行向已销毁的窗口发消息导致崩溃。4.3 用 PostMessage 把数据送给主线程更新界面工作线程解析完五档数据后不能直接调用CListCtrl::SetItemText必须把数据封装成指针通过PostMessage发到主线程。PostMessage是异步的所以指针的生命周期需要约定清楚线程 new 出来UI 线程处理完 delete。// 工作线程里解析完一只股票后发消息 QUOTE_INFO* pInfo new QUOTE_INFO; *pInfo q; ::PostMessage(pWorker-m_hNotifyWnd, WM_UPDATE_QUOTE, 0, (LPARAM)pInfo);主对话框里的消息处理函数LRESULT CMainDialog::OnUpdateQuote(WPARAM wParam, LPARAM lParam) { QUOTE_INFO* pInfo (QUOTE_INFO*)lParam; if (pInfo) { UpdateListCtrl(*pInfo); delete pInfo; // 用完立即释放不能延期 } return 0; }这里有个血泪经验千万别在工作线程里delete这个指针也别让 UI 线程和子线程同时访问它。PostMessage把地址传过去之后所有对这份数据的读和释放都应当在 UI 线程完成。4.4 刷新频率和防抖参数频率控制不只是“3 秒一次”这么简单。我在项目里会维护两个时间戳上次请求开始时间、上次请求结束时间。计算实际耗时如果单次请求超过了 2 秒下一次等待时间就自动加到 5 秒如果连续三次超时就按 10 秒退避直到恢复。这样既能保证界面不卡也能降低被接口封禁的概率。代码片段void CQuoteWorker::FetchOnce() { DWORD dwStart GetTickCount(); CString raw; if (GetHttpText(m_strUrl, raw)) { ParseAndPost(raw); m_nConsecutiveFails 0; } else { m_nConsecutiveFails; } DWORD dwCost GetTickCount() - dwStart; if (dwCost 2000) m_nNextInterval 5000; else if (m_nConsecutiveFails 3) m_nNextInterval 10000; else m_nNextInterval 3000; Sleep(m_nNextInterval); }5. VC 6.0 行情采集的5个常见坑现象、原因、定位方法5.1 HTTP 403 被拒现象用浏览器打开新浪行情地址正常返回文本但程序里GetHttpText返回的是一串 HTML 或直接返回空抓包一看状态是 403。原因新浪接口会校验Referer头默认的 WinINet 请求头里没有这个字段被服务端的反爬策略挡下。个人电脑的固定 IP 更容易中招机房 IP 更敏感。解决把Referer: https://finance.sina.com.cn/和Accept: */*写死在请求头里。如果还是 403把User-Agent改成浏览器全串比如Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36。我见过只加Referer就能通过的案例也见过必须三个头齐全的情况建议全带上。5.2 中文股票名称乱码现象界面列表里其他数字都正常唯独股票名称显示成“娑挖”之类的乱码。原因新浪接口返回的是 GBK 编码VC6 的 CString 在默认 MBCS 字符集下可以直接显示但如果你的工程被设置成 Unicode 字符集CString 内部是宽字符直接读取窄字符文本就会乱码。解决检查工程设置的字符集。项目设置 - C/C - 预处理器定义里如果定义了_UNICODE和UNICODE就在解析前用MultiByteToWideChar把 GBK 字节转成宽字符或者干脆把工程改回“使用多字节字符集”。老代码库大多是多字节字符集这个坑主要在从新版 VS 移植工程时出现。5.3 工作线程里改控件直接崩溃现象程序运行几秒后崩溃报错位置在CListCtrl::SetItemText但代码逻辑看起来没问题。原因工作线程里直接访问了CWnd系控件。MFC 的控件对象绝大多数绑定到创建它的线程跨线程访问内部句柄表会造成访问冲突。尤其是在调试器里报错往往不在真正出错的地方而是在下一个消息处理处。解决严格执行“工作线程只组装数据、PostMessage通知 UI 线程刷新”的原则。不要图方便在子线程里发一个自定义消息给控件控件更新必须回到 UI 线程的OnUpdateQuote里。5.4 五档数据错位下标和网络流传的版本对不上现象列表里“买一价”显示成了“今开”“买一量”显示成 0而最新价却是对的。原因行情接口的字段下标在历次改版中发生过调整网上能找到的文档有的写的是老下标。比如有版本把下标 6 记为“买入价”下标 7 记为“买入量”但有的版本把这几列整体后移。你按旧帖子写死自然错位。解决写解析函数前先把原始字符串完整输出到日志文件一行一个下标和实际界面核对后再定常数。我当时写“先 dump再解析”这个习惯就是从一次对错位数据分析了一晚上之后养成的。别信记忆信打印出来的原始串。5.5 返回一堆空串InternetReadFile 的循环写法错了现象GetHttpText返回值里只有零星几个字符或者循环只读了一次就退出但在线抓包发现响应体很大。原因InternetReadFile的返回值只表示“本次读取调用成功”不代表“已经读到数据”。当缓冲区小于响应体时多次调用之间dwRead会清零正确判断结束的标志是InternetReadFile返回 TRUE 且dwRead 0。代码里如果写成while (InternetReadFile(..., dwRead))第二次调用会因缓冲区数据已读完而返回 TRUE但dwRead是 0循环误以为失败退出。解决参考 3.2 节代码块里的写法读完后立即把dwRead置零循环条件加上 dwRead 0。还要注意InternetCloseHandle一定要在循环结束后调用句柄泄漏多了会触发 10061 连接失败。6. 进阶把五档数据变成可留存的盯盘工具6.1 多股票轮询与 CSV 日志跑通单只股票后下一步是把 URL 改成逗号拼接的多股票地址并把每次拉取的数据追加写入 CSV。CSV 的作用不只是存档它还方便你回溯某一天的盘口变化用来校验收盘后复盘。写入代码很简单void AppendQuoteToCsv(const QUOTE_INFO* pArr, int nCount) { FILE* fp fopen(quote_log.csv, a); if (!fp) return; for (int i 0; i nCount; i) { fprintf(fp, %s,%.2f,%.2f,%.0f,%.2f,%.0f,%d\n, pArr[i].szName, pArr[i].dLast, pArr[i].dBidPrice[0], pArr[i].dBidVolume[0], pArr[i].dAskPrice[0], pArr[i].dAskVolume[0], (int)time(NULL)); } fclose(fp); }time(NULL)写入的是 Unix 时间戳也可以用localtime_s转成可读格式。CSV 第一行建议手工写表头方便后面用 Excel 或 pandas 读。6.2 验证数据正确性和券商客户端比一比判断接口解析是否正确的最终标准不是代码看起来对不对而是和真实行情对得上。我的验证方法是在同一时间点打开同花顺或主力自选股软件看同一只股票的买一价、卖一价、最新价数值和界面显示一致就说明解析没问题。延迟方面新浪接口返回的时间字段是服务器时间可以拿本地时间和它做差// data 是接口返回的 10:23:45 时间串 // 解析成小时、分钟、秒后计算和本地时间的差值 COleDateTime dtServer, dtLocal; dtServer.ParseDateTime(data); dtLocal COleDateTime::GetCurrentTime(); int nDiff (int)(dtLocal - dtServer);这个差值一般在 1 到 3 秒内属于正常超过 10 秒说明接口侧有延迟或你的刷新频率太高被限流了。6.3 价格异动提醒当前价穿破买一/卖一五档数据最有用的地方之一是判断价格异动。当最新价突然穿破买一价时说明有资金主动砸盘当最新价上穿卖一价时说明有主动买盘。我通常在UpdateListCtrl里维护上一轮的买一价数组比较当前价和上一轮买一价的关系if (q.dLast prevBidPrice q.dLast q.dBidPrice[0]) { // 最新价跌破买一弹出红色闪烁提示 SetItemColor(nRow, 0, RGB(255, 0, 0)); } else if (q.dLast prevAskPrice q.dLast q.dAskPrice[0]) { // 最新价上穿卖一用绿色标记 SetItemColor(nRow, 0, RGB(0, 160, 0)); }颜色标记可以用CListCtrl::SetItemText配合自定义绘制也可以在列表行前面加一个状态列显示“破位”两个字。这一步不需要改请求逻辑只是把已有的五档结构体多利用了一层。我现在写这类工具的习惯是解析下标一律先打印原始报文验证一遍再看界面同时把每次改动的下标常量集中放在文件顶部加注释说明来源协议。这样即便是维护三年前的老工程翻车半天的情况也能控制在一小时内定位。希望这篇笔记对你有帮助。本文还有配套的精品资源点击获取
返回列表