资讯详情 VC++6.0五档行情工程解析:从WinInet请求到界面刷新实战
📅 2026/10/8 3:47:20
简介面向使用 VC 6.0 开发行情界面的开发者这是一份可直接运行的沪深股市五档行情获取与显示示例工程定位是帮助入门者理解股票软件的数据链路与界面组织。压缩包共 18 个文件以 5 个头文件、4 个源文件为核心另有 .dsw/.dsp 工程文件、.rc/.rc2 资源脚本、.ico 图标和两个 .aps 状态文件完整还原了一个 MFC 对话框项目的标准目录整体仅 33KB代码量精炼适合在旧版 VC 工程中直接打开研读。工程封装了 HttpClient 网络请求类与 HttpDlg 主对话框界面演示了从 HTTP 接口获取实时买一买二等五档报价、解析响应数据并刷新列表展示的完整流程同时还给出了多线程刷新、网络异常处理和用户交互等实战处理方法。目前已有 578 人学习浏览对于想复刻经典炒股软件界面或熟悉 MFC 网络编程的开发者这是一份低门槛、可复用的参考代码。1. 为什么要拆这个VC 6.0的五档行情工程老代码里最完整的请求链路五档行情在股票软件里是最基础的展示模块但真正要拿到一个老旧的VC 6.0工程里从零做一遍很多人上手才发现坑比想象中多。这个资源是一份可以直接编译的MFC对话框程序文件里已经把HttpClient封装、HTTP请求、行情文本解析和界面刷新完整串在了一起打开工程就能看到一条“请求→解析→显示”的闭环链路。对想学网络编程、或者打算自研轻量看盘工具的开发者来说它的价值在于代码量不大、依赖少适合读也适合在自己机器上改着跑比东拼西凑看教程靠谱得多。下文按我拆项目的习惯从请求封装讲到界面刷新最后把最容易翻车的地方一条条列出来。2. 项目骨架与HttpClient封装先搞清楚请求从哪里发出去2.1 解压后哪几个文件是真正要看的这个工程的文件结构很干净核心代码集中在两个文件里其余都是VC 6.0工程附带的支撑文件。我一般建议按下表顺序读先看入口再追请求实现最后看界面逻辑。文件作用优先级Stock.dsw / Stock.dspVC 6.0工作区和工程文件打开入口高HttpClient.h / HttpClient.cpp基于WinInet的HTTP请求封装高HttpDlg.h / HttpDlg.cpp主对话框逻辑行情显示与刷新高Http.rc / resource.h / Http.rc2对话框模板、菜单、图标资源中StdAfx.h / StdAfx.cppMFC预编译头文件低ReadMe.txt工程说明低打开Stock.dsw时VC 6.0会把整个工作区加载进来。如果你遇到“文件打不开”或资源丢失多半是Http.rc2或者res目录没有和.dsw放在同一层级把整个目录原样解压、不要单独拖文件出来能避免掉一大半路径问题。2.2 HttpClient核心WinInet四步调用链HttpClient这个类的作用很纯粹给定一个URL返回一段字符串。工程里没有用MFC的CInternetSession那一套封装而是直接调WinInet API好处是缓冲区、超时、缓存策略都能自己控制出了问题也容易定位。// HttpClient.cpp 核心请求函数 int CHttpClient::GetData(const CString strUrl, CString strOut) { // 1. 打开一个Internet会话句柄 HINTERNET hSession InternetOpen( _T(StockQuoteAgent/1.0), INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0); if (hSession NULL) return -1; // 2. 直接打开发送的URL加RELOAD标志绕过缓存 HINTERNET hReq InternetOpenUrl( hSession, strUrl, NULL, 0, INTERNET_FLAG_RELOAD | INTERNET_FLAG_NO_CACHE_WRITE, 0); if (hReq NULL) { InternetCloseHandle(hSession); return -2; } // 3. 读取返回内容缓冲区要留出结尾\0的位置 char szBuf[2048] {0}; DWORD dwRead 0; strOut.Empty(); while (InternetReadFile(hReq, szBuf, 2047, dwRead) dwRead 0) { szBuf[dwRead] \0; strOut szBuf; } // 4. 关闭句柄释放会话 InternetCloseHandle(hReq); InternetCloseHandle(hSession); return 0; }调用链是标准的四步InternetOpen建立会话InternetOpenUrl发起请求InternetReadFile循环读取最后关闭句柄。这里的几个参数值得说明一下。第一个参数StockQuoteAgent/1.0是User-Agent有些行情接口会校验这个字段如果你的请求被服务器拒绝可以先排查这里。INTERNET_OPEN_TYPE_PRECONFIG表示使用系统代理设置对于大多数直接上网的环境够用。读取缓冲区设成2047字节是为了保证每次追加前都留下一个字节放字符串结束符防止内存越界。返回值里-1代表会话创建失败-2代表URL请求失败这两个错误码在排查时能快速缩小范围。2.3 超时参数与重试逻辑免费行情接口在开盘时段经常变慢如果不设超时InternetReadFile可能被卡住十几秒。网上最常见的做法是用InternetSetOption单独设置连接和接收超时。// 设置超时单位是毫秒 DWORD dwConnectTimeout 5000; // 连接超时 DWORD dwReceiveTimeout 10000; // 接收超时 InternetSetOption(hSession, INTERNET_OPTION_CONNECT_TIMEOUT, dwConnectTimeout, sizeof(dwConnectTimeout)); InternetSetOption(hSession, INTERNET_OPTION_RECEIVE_TIMEOUT, dwReceiveTimeout, sizeof(dwReceiveTimeout));连接超时5秒、接收超时10秒是我试下来比较稳的组合。设太短稍微波动就失败设太长界面会长时间没反应。如果接口走的是代理还要额外给INTERNET_OPTION_DATA_CONNECT_TIMEOUT设个值否则代理握手阶段可能不认前面这两个参数。重试也一样有讲究。我一般不会无脑重试三次以上而是用“最多2次 短等待”的方式// 请求重试封装 for (int nRetry 0; nRetry 2; nRetry) { if (hc.GetData(strUrl, strResp) 0) break; // 成功则跳出 Sleep(300); // 两次请求之间休息300ms }这里Sleep(300)的作用是给服务器和本地网络一个缓冲避免行情接口把你当恶意请求封掉。300毫秒是经验值太快没效果太慢影响刷新体验。3. 行情解析与界面绑定五档数据从文本到控件的完整映射3.1 先弄清楚响应文本里每一段是什么拿到HTTP响应后常见行情接口返回的是类似这样的文本var hq_str_sh600000浦发银行,10.320,10.280,10.310,10.350,10.250,...,2024-01-15,15:00:00,00;这段文本以逗号分隔字段前面几项是股票名称、开盘价、昨收价、最新价后面跟着一批买卖挂单数据。不同数据源的字段顺序会有差别所以解析代码最忌讳写死“神秘数字”最好的办法是把字段索引用常量定义清楚。下面是一份常见返回格式的索引对照表五档字段从索引6开始顺序排列索引含义索引含义0股票名称16卖一价1今日开盘价17卖一量2昨日收盘价18卖二价3最新价19卖二量4今日最高价20卖三价5今日最低价21卖三量6买一价22卖四价7买一量23卖四量8买二价24卖五价9买二量25卖五量买一到买五在索引6到15之间卖一到卖五紧跟在后面从16开始。这类格式在沪深行情的免费接口中很常见拿到数据后先按这个表核对一遍字段数量再做解析不迟。3.2 用CString拆字段并安全转浮点VC 6.0里没有现成的Split函数但CString自带的Tokenize足够完成这个任务。下面这段解析逻辑先把两个双引号之间的正文取出来再按逗号拆成CStringArray。// 解析入口strRaw是HTTP返回的原始文本 BOOL ParseStockQuote(const CString strRaw, CStringArray arrBidPrice, CStringArray arrAskPrice, CString strName) { // 1. 截取两个双引号之间的正文 int nL strRaw.Find(_T()); int nR strRaw.ReverseFind(_T()); if (nL 0 || nR nL 1) return FALSE; CString strBody strRaw.Mid(nL 1, nR - nL - 1); // 2. 按逗号拆分到CStringArray CStringArray arrFields; int nPos 0; while (nPos 0) { CString strItem strBody.Tokenize(_T(,), nPos); if (strItem.IsEmpty() nPos strBody.GetLength()) break; arrFields.Add(strItem); if (nPos 0) break; } // 3. 字段数量不够直接判定为失败 int nCount arrFields.GetCount(); if (nCount 26) return FALSE; strName arrFields[0]; // 4. 五档字段买一从6开始卖一从16开始 for (int i 0; i 5; i) { int nBid 6 i * 2; int nAsk 16 i * 2; arrBidPrice.Add(arrFields[nBid]); arrAskPrice.Add(arrFields[nAsk]); } return TRUE; }Tokenize的第一个参数是分隔符第二个参数nPos会移动传入的是分隔符前一字符的位置循环结束时nPos变为-1循环自然退出。字段数量这里判断了nCount 26因为如果接口返回的是停牌股或者无效代码字段往往不足提前返回FALSE比在界面上显示一堆空值好得多。这一步把买一价到买五价、卖一价到卖五价分别装进了两个数组。后续要转成浮点时在VC 6.0里用atof()要注意编码问题接口返回的是char类型直接atof((LPCSTR)strPrice)就可以但不要用它转整数量大时精度会有微妙的差异我习惯在赋值时才转。3.3 把数据填到控件SetDlgItemText和CListCtrl界面层有两种常见做法。如果只显示最新价和几个核心价位SetDlgItemText最省事// 在对话框的刷新逻辑里 CString strPrice; strPrice.Format(_T(%.2f), dCurPrice); SetDlgItemText(IDC_EDIT_PRICE, strPrice);如果要做成类似股票软件的五档盘口一般用CListCtrl分别建“档位”“价格”“数量”三列// 初始化列表设置三列表头 m_listQuote.SetExtendedStyle(LVS_EX_GRIDLINES | LVS_EX_FULLROWSELECT); m_listQuote.InsertColumn(0, _T(档位), LVCFMT_CENTER, 60); m_listQuote.InsertColumn(1, _T(价格), LVCFMT_CENTER, 80); m_listQuote.InsertColumn(2, _T(数量), LVCFMT_CENTER, 100); // 填充买一至买五卖一至卖五 for (int i 0; i 5; i) { int nRow m_listQuote.InsertItem(i, _T(买一)); m_listQuote.SetItemText(nRow, 1, arrBidPrice[i]); m_listQuote.SetItemText(nRow, 2, arrBidVol[i]); }注意LVCFMT_CENTER是列宽对齐方式价格那列别设太窄至少80像素否则五位数价格显示不全。填充前先DeleteAllItems()清空旧数据不然刷新几次后行数会一直往上堆。4. 刷新机制与刷新频率定时器、工作线程和UI更新策略4.1 SetTimer固定间隔刷新拿到行情数据后最直接的想法是“每隔几秒自动刷新一次”。MFC对话框里做定时刷新绕不开SetTimer// OnInitDialog中启动定时器 SetTimer(1, 3000, NULL); // 3秒触发一次WM_TIMER然后在OnTimer里处理刷新void CHttpDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent 1) { RequestAndRefresh(); // 发起请求并刷新界面 } CDialog::OnTimer(nIDEvent); }为什么是3秒而不是1秒免费行情接口的更新频率没那么高1秒一次除了给自己制造封号风险没有任何实际意义。3秒间隔既保证盘口变化能及时反映又不至于让界面闪烁得很厉害。如果你确实需要更高频率也要设计成“请求完成后再过3秒发起下一次”而不是固定每3秒无脑发请求否则慢接口会越积越多。4.2 把网络请求放到工作线程这是整个工程里最值得学的设计。HttpClient的读取操作是阻塞的如果直接放在主线程一旦服务器响应慢界面就会卡住。常见做法是用AfxBeginThread开一个工作线程去请求完成后通过PostMessage通知主线程刷新界面。// 传给工作线程的参数结构 struct QuoteRequest { CString strCode; // 股票代码例如sh600000 HWND hWndNotify; // 接收通知的窗口句柄 UINT nMsg; // 自定义消息ID }; // 工作线程函数 UINT ThreadRequestQuote(LPVOID pParam) { QuoteRequest* pReq (QuoteRequest*)pParam; CHttpClient hc; CString strUrl BuildUrl(pReq-strCode); CString strResp; int nRet hc.GetData(strUrl, strResp); // 用PostMessage把结果传回主线程 ::PostMessage(pReq-hWndNotify, pReq-nMsg, nRet, (LPARAM)new CString(strResp)); delete pReq; return 0; }启动线程的地方在按钮事件或定时器里QuoteRequest* pReq new QuoteRequest; pReq-strCode strCode; pReq-hWndNotify GetSafeHwnd(); pReq-nMsg WM_MY_QUOTE_RESULT; AfxBeginThread(ThreadRequestQuote, pReq);这里有几个关键点PostMessage是非阻塞的能立刻返回不会卡住界面结果用new CString传递是为了避免栈内存在线程结束后被回收主线程收到WM_MY_QUOTE_RESULT后在消息处理函数里解析CString并刷新控件。把网络请求挪到工作线程之后界面卡死的问题基本消除。哪怕接口完全没响应最多是界面上数据不更新不会出现“鼠标变沙漏”的糟糕体验。4.3 防止重复请求和显示刷新时间定时器和手动刷新会叠加触发比如定时器还没请求完用户又点了“刷新”按钮两个线程同时请求同一个接口返回顺序错乱后显示的数据可能是旧的。我一般用一个BOOL标志位来做互斥BOOL m_bRequesting; // 初始为FALSE void CHttpDlg::RequestAndRefresh() { if (m_bRequesting) return; // 已有请求在跑直接忽略本次触发 m_bRequesting TRUE; // 启线程…… } // 在WM_MY_QUOTE_RESULT消息处理函数末尾 m_bRequesting FALSE;再把上一次成功刷新的时间显示在界面上strTime.Format(_T(%02d:%02d:%02d), wHour, wMin, wSec)这样用户能判断当前数据到底新不新。如果请求失败界面保留旧数据同时把时间戳停住比直接清空显示要专业得多。5. 避坑手册VC 6.0环境下最常见的五个问题5.1 编译报错无法打开包含文件WinInet.h现象把工程拷到另一台机器编译报fatal error C1083: Cannot open include file: WinInet.h: No such file or directory。原因VC 6.0默认的Include路径里找不到WinInet.h或者整个环境是精简安装没把网络相关的SDK装全。解决在Project Settings的C/C选项卡里把C:\Program Files\Microsoft Visual Studio\VC98\INCLUDE加进Additional include directories同时在Link选项卡的Object/library modules里手动补上wininet.lib。如果你用的是vc 6.0 sp6安装包这些路径一般已配好但换机器重装后经常漏遇上了先查这两处。顺手把ws2_32.lib也加上不会错有些网络初始化逻辑会用到。5.2 程序运行一段时间后界面冻住现象连续刷新几次后程序标题栏出现“未响应”鼠标点哪里都没反应过十几秒自己恢复。原因网络请求做在了UI线程里某次接口响应特别慢InternetReadFile长时间阻塞主窗口的消息循环被卡死。解决把请求挪到工作线程用PostMessage回传数据。如果暂时不想改线程模型至少把2.3节的超时参数加上连接5秒、接收10秒这样即使阻塞也能在10秒内解套。我在实际项目里是两种方案一起用线程保证不卡界面超时保证线程不堆积。5.3 界面显示中文乱码现象实时行情都出来了但股票名称和涨跌幅显示成全问号或乱码。原因行情接口返回的编码和工程字符集不一致。VC 6.0默认工程是MBCS而不少接口返回的是UTF-8两者直接对接必然乱码。解决先确认接口编码格式如果是UTF-8用MultiByteToWideChar先转成宽字符再通过WideCharToMultiByte转成ANSI或者直接保持工程为MBCS字符集在解析阶段做一次Unicode到ANSI的转换。这里没有统一银弹关键是在HttpClient拿到响应后提前处理不要等到显示层再转那时代码会越改越乱。5.4 每次请求拿到的都是旧数据现象界面价格一直不变重启程序才更新但用浏览器访问同一地址却能刷新。原因WinInet默认启用了HTTP缓存短时间重复请求相同URL时直接返回了上次的结果。解决在InternetOpenUrl的第5个参数上加上INTERNET_FLAG_RELOAD | INTERNET_FLAG_NO_CACHE_WRITE。这两个标志位一个强制重新下载一个禁止写缓存组合使用后每次请求都是真实请求。这个问题在免费接口调试时很容易被忽视因为浏览器表现正常但WinInet的行为不一样。5.5 股票代码带前缀才能查询现象输入“600000”返回空数据输入“sh600000”正常输入“000001”结果跑到深发展上去了。原因行情接口要求明确市场沪市代码前面加sh深市加sz。如果用户不区分市场接口只能用代码前缀猜常把平安银行和上证指数搞混。解决在BuildUrl里做判断代码以6或9开头的补sh以0或3开头的补sz并提供手动覆盖入口。更稳妥的做法是在界面放一个“沪市/深市”下拉框把市场选择权交还给用户避免单靠代码猜测出错。6. 从能跑到跑稳验证方法、缓存快照和日志埋点6.1 数据正确性校验不能只看一眼行情程序最怕的不是没数据而是数据错了但界面照常显示。我在解析函数返回后一定会过一个校验逻辑很简单最新价必须大于0、买一价不能大于卖一价、成交量不为负。另外加一个常识性判断——最新价与昨收价的波动超过10%视为无效因为正常涨跌幅限制不会超过这个范围。// 简单但有效的行情校验 BOOL ValidateQuote(const QUOTE_DATA q) { if (q.dPrice 0.0) // 价格为正 return FALSE; if (q.dBidPrice q.dAskPrice) // 买一价不能高于卖一价 return FALSE; if (q.dVolume 0) // 成交量非负 return FALSE; if (fabs(q.dPrice - q.dPrevClose) q.dPrevClose * 0.11) return FALSE; return TRUE; }这四条规则能挡住大部分接口返回的脏数据。超过10%那条之所以用0.11而不是0.10是给ST股或异常波动留一点容差。6.2 内存里存一份最近行情快照网络波动时请求会失败这时候与其让界面显示空白不如把内存里最后一次成功的行情快照拿出来继续显示同时在界面标注数据时间。我的做法是定义一个全局结构体保存快照// 全局缓存最近一次成功的行情 struct QUOTE_SNAPSHOT { CString strCode; CString strName; CString strTime; double dPrice; CStringArray arrBidPrice; CStringArray arrAskPrice; }; QUOTE_SNAPSHOT g_LastQuote;请求成功时更新这个结构体请求失败时直接用里面的数据刷新界面用户至少还能看到最近的价格和报价时间。这个兜底策略在行情断线时非常有用用户会觉得这个程序“能扛事”。6.3 日志记录用最土的方式给自己留后路VC 6.0的工程一般不建议引入第三方日志库我习惯直接写一个几十行的日志函数把每次请求的关键状态追加到log.txtvoid WriteLog(const CString strMsg) { FILE* fp fopen(StockQuote.log, a); if (fp NULL) return; CString strTime COleDateTime::GetCurrentTime().Format(_T(%Y-%m-%d %H:%M:%S)); fprintf(fp, [%s] %s\n, (LPCSTR)strTime, (LPCSTR)strMsg); fclose(fp); }日志内容至少包括三样请求返回码、读取到的字节数、解析出的字段个数。遇到“数据明明拿到了但界面上是空的”这种问题打开日志一看字段数量不对立刻就能定位是解析逻辑的问题而不是请求的问题。从那以后我每次写完行情程序都会强制先补上日志和校验函数哪怕时间再紧张也要留这两行这个习惯帮我跳过了不少线上调试的坑。希望帮到你。本文还有配套的精品资源点击获取