简介这份资源是一套基于C#开发的个人股票实时查看工具源码面向希望学习桌面应用开发、网络数据获取与XML解析的C#初学者和进阶开发者。项目通过WebService获取股票行情并支持根据买入成本价动态计算盈亏是理解金融数据与编程结合的实用案例。压缩包共39个文件约175KB以13个cs源代码文件为核心辅以resx与resources资源文件、config配置、wsdl与disco服务描述文件以及sln解决方案和csproj工程文件结构完整便于直接编译运行与二次开发。目前已有624人学习下载。通过研读源码读者可以掌握C#中网络请求、XML数据解析、用户界面交互与盈亏计算逻辑的实现方式了解如何将实时行情数据转化为直观的监控界面适合作为课程设计、毕业设计或自学练手项目帮助提升桌面软件与金融工具开发能力。1. 用 C# 写一个自己用的股票及时查看工具为什么我最后没选网页版盯盘这件事很多人第一反应是打开浏览器收藏夹里那几个行情页或者装一个手机 App。但真到需要「及时」两个字的时候网页版的问题就暴露了刷新靠手点切标签页就断多只股票要来回翻想加个自己算的指标还得等平台支持。我最早也是这么凑合的直到某天同时盯五只票来回切页面切到眼花才下决心用 C# 自己写一个桌面端的小工具。这个工具要解决的核心问题很朴素把关心的几只股票代码固定下来程序按固定间隔去拉最新价界面上实时刷新涨跌用颜色区分最好还能在价格穿过某个阈值时弹个提醒。它适合两类人一类是有一定 C# 基础、想练手又想做点有用东西的开发者另一类是纯粹想要一个轻量、可控、不依赖第三方客户端盯盘方式的从业者。整篇我会按「先跑通最小版本再补实时刷新和提醒最后讲坑」的顺序讲源码结构也会给出来你照着改就能用。需要先说明一点行情数据从哪来是这个工具能不能落地的第一道坎。常见做法是接公开的行情接口或者用券商提供的行情推送。不同来源的字段格式、刷新频率、是否需要鉴权都不一样。我下面用的是一种通用的 HTTP 拉取思路具体地址和字段你替换成自己能拿到的即可重点是整套拉取、解析、刷新、提醒的骨架。2. 先把最小可运行版本跑起来项目结构与拉取解析2.1 技术选型为什么用 WinForms 而不是 WPF桌面端 C# 做界面主流就 WinForms 和 WPF 两条路。这个工具我选了 WinForms理由很实际界面简单就一个表格加几个按钮WinForms 的 DataGridView 拖上去就能用数据绑定和刷新逻辑几行代码搞定WPF 的 XAML 数据绑定和 MVVM 更优雅但对一个自己用的小工具来说前期搭架子的成本偏高。如果你后续想做成多窗口、复杂样式、动画提醒那 WPF 更合适但第一版没必要。数据存储用内存里的一个 List 就够不需要数据库。配置股票代码列表、刷新间隔、提醒阈值用 JSON 文件存本地程序启动时读改完写回。JSON 序列化用 System.Text.Json.NET 自带不用引第三方包。网络请求用 HttpClient。这里有个血泪经验HttpClient 不要每次请求都 new 一个频繁创建会耗尽 socket正确做法是全局单例或者用 IHttpClientFactory。小工具里我直接用一个静态的 HttpClient 实例。2.2 项目骨架与核心数据结构先建一个 WinForms 项目目标框架选 .NET 6 或 .NET 8 都行我用的 .NET 8。核心就三个类股票数据模型、行情拉取服务、主窗体。// StockQuote.cs —— 单只股票的行情快照 public class StockQuote { public string Code { get; set; } ; // 股票代码如 600000 public string Name { get; set; } ; // 股票名称 public decimal Price { get; set; } // 最新价 public decimal PrevClose { get; set; } // 昨收价用来算涨跌幅 public decimal ChangePercent // 涨跌幅计算属性 PrevClose 0 ? 0 : (Price - PrevClose) / PrevClose * 100; public DateTime UpdatedAt { get; set; } // 本次数据的时间戳 }这个模型里 ChangePercent 用计算属性而不是存字段好处是 Price 或 PrevClose 一变涨跌幅自动跟着变不用手动同步。PrevClose 为 0 时返回 0避免除零。UpdatedAt 用来在界面上显示数据新鲜度后面排查「数据不刷新」时全靠它。// QuoteService.cs —— 行情拉取与解析 public class QuoteService { // 全局单例避免 socket 耗尽 private static readonly HttpClient _http new HttpClient { Timeout TimeSpan.FromSeconds(5) // 超时设短避免界面卡死 }; public async TaskStockQuote FetchAsync(string code) { // 这里的 url 换成你自己能拿到的行情接口地址 string url $https://example-quote-api.com/quote?code{code}; string json await _http.GetStringAsync(url); // 用 JsonDocument 解析字段名按实际接口替换 using var doc JsonDocument.Parse(json); var root doc.RootElement; return new StockQuote { Code code, Name root.GetProperty(name).GetString() ?? code, Price root.GetProperty(price).GetDecimal(), PrevClose root.GetProperty(prevClose).GetDecimal(), UpdatedAt DateTime.Now }; } }逻辑说明FetchAsync 是异步的返回 Task这样界面线程不会被网络请求阻塞。Timeout 设 5 秒是个经验值太长会让界面在接口卡住时显得假死太短又容易在弱网下误判失败。解析用 JsonDocument 而不是强类型反序列化是因为不同接口字段差异大用 GetProperty 按需取更灵活字段缺失时抛异常也能被上层捕获。参数说明code 是股票代码字符串注意有些接口要带市场前缀如 sh600000有些不要这个必须对着接口文档确认搞错了会返回空数据而不是报错属于典型的「静默失败」。2.3 主窗体定时刷新与界面绑定主窗体上放一个 DataGridView、一个「开始/停止」按钮、一个 NumericUpDown 控制刷新间隔秒。刷新用 System.Windows.Forms.Timer注意这个 Timer 的回调跑在 UI 线程上所以里面不能做同步阻塞的网络请求得用 async void 配合 await。// MainForm.cs 关键片段 private readonly QuoteService _service new QuoteService(); private readonly Liststring _codes new() { 600000, 000001, 300750 }; private readonly BindingListStockQuote _quotes new(); private System.Windows.Forms.Timer _timer new(); public MainForm() { InitializeComponent(); grid.DataSource _quotes; // BindingList 变更会自动刷新界面 _timer.Interval 3000; // 默认 3 秒 _timer.Tick async (s, e) await RefreshAllAsync(); } private async Task RefreshAllAsync() { foreach (var code in _codes) { try { var q await _service.FetchAsync(code); // 找到已有记录就更新没有就新增 var exist _quotes.FirstOrDefault(x x.Code code); if (exist null) _quotes.Add(q); else { exist.Price q.Price; exist.PrevClose q.PrevClose; exist.UpdatedAt q.UpdatedAt; } } catch (Exception ex) { // 单只失败不影响其他记录到日志区 Log(${code} 拉取失败: {ex.Message}); } } }逻辑说明用 BindingList 而不是普通 List是因为它实现了 IBindingList元素属性变化时 DataGridView 会自动重绘省掉手动调 Refresh。刷新时先查再更新避免每次 Add 导致列表越来越长。每只股票单独 try-catch一只失败不拖累其他这是多标的拉取必须做的隔离。参数说明Timer.Interval 单位是毫秒3000 就是 3 秒。这个值不要设太小公开接口一般有频率限制1 秒一次很容易被限流甚至封 IP也不要太大超过 10 秒就谈不上「及时」了。我一般设 3 到 5 秒。3. 让数据真正「及时」刷新策略、涨跌染色与阈值提醒3.1 刷新策略串行还是并发间隔怎么定最小版本里我是串行拉取五只股票每只 200 毫秒一轮下来一秒左右还能接受。但如果你盯二十只串行就会让一轮刷新超过间隔Timer 会堆积回调界面越来越卡。这时候要改成并发拉取。private async Task RefreshAllAsync() { // 并发拉取所有标的Task.WhenAll 等全部完成 var tasks _codes.Select(async code { try { return await _service.FetchAsync(code); } catch { return null; } // 失败返回 null后面过滤 }); var results await Task.WhenAll(tasks); foreach (var q in results.Where(x x ! null)) { var exist _quotes.FirstOrDefault(x x.Code q!.Code); if (exist null) _quotes.Add(q!); else { exist.Price q.Price; exist.PrevClose q.PrevClose; exist.UpdatedAt q.UpdatedAt; } } }逻辑说明Task.WhenAll 并发发起所有请求总耗时约等于最慢的那一只而不是所有之和。失败返回 null 而不是抛异常是为了让 WhenAll 不被单个异常打断。注意并发数别无限大二十只以内没问题上百只建议用 SemaphoreSlim 限流否则对接口压力太大也容易触发风控。参数说明并发拉取时HttpClient 的连接数限制默认每服务器 2 个可能成为瓶颈可以在程序启动时调 ServicePointManager.DefaultConnectionLimit或者用 SocketsHttpHandler 配置 MaxConnectionsPerServer。这个属于进阶调优标的少的时候不用管。3.2 涨跌染色DataGridView 的 CellFormatting行情工具不染色等于白做红涨绿跌一眼扫过去才有意义。WinForms 里用 CellFormatting 事件按涨跌幅设置前景色。private void Grid_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) { if (grid.Columns[e.ColumnIndex].Name ! ChangePercent) return; if (e.Value null) return; decimal pct Convert.ToDecimal(e.Value); // 国内习惯涨红跌绿平盘黑 if (pct 0) e.CellStyle.ForeColor Color.Red; else if (pct 0) e.CellStyle.ForeColor Color.Green; else e.CellStyle.ForeColor Color.Black; }逻辑说明CellFormatting 在单元格绘制时触发只处理涨跌幅那一列其他列直接 return避免无谓开销。颜色用 Color.Red 和 Color.Green如果你习惯欧美配色涨绿跌红把两个分支对调即可。注意 e.Value 可能是格式化后的字符串如果列绑定的就是 decimal这里能直接转。参数说明判断列用 Columns[e.ColumnIndex].Name前提是你在设计器里给列起了名字或者用 DataPropertyName 绑定。如果列名对不上染色会静默失效这是新手最容易踩的坑之一。3.3 阈值提醒价格穿越时弹窗还是托盘通知提醒逻辑的关键是「穿越」而不是「大于」。如果只是判断 Price 阈值那价格一直在阈值上方时每次刷新都会弹烦到你想砸键盘。正确做法是记录上一次的状态只在状态翻转时触发。// 每只股票维护一个是否已提醒的标记 private readonly Dictionarystring, bool _alerted new(); private void CheckAlert(StockQuote q, decimal threshold) { bool above q.Price threshold; bool wasAbove _alerted.TryGetValue(q.Code, out var v) v; if (above !wasAbove) // 从下方穿到上方触发一次 { notifyIcon.ShowBalloonTip(3000, 价格提醒, ${q.Name} 已涨到 {q.Price}, ToolTipIcon.Info); } _alerted[q.Code] above; // 更新状态 }逻辑说明_alerted 字典记录每只股票上一次是否在阈值上方。只有「这次在上方、上次不在」才提醒穿越一次弹一次。用托盘气泡通知NotifyIcon而不是 MessageBox是因为 MessageBox 会阻塞界面行情刷新就停了这是很多人第一版会犯的错。参数说明ShowBalloonTip 第一个参数是显示毫秒数3000 即 3 秒。阈值 threshold 建议做成可配置每只股票一个存在 JSON 配置里。提醒只做单向向上穿越还是双向上下都提醒看你自己需求双向就把判断反过来再加一个分支。4. 避坑与排查那些让我熬夜的静默失败4.1 界面卡死但没报错现象点了开始之后界面过几秒就点不动了任务管理器看 CPU 不高也没异常弹窗。原因多半是在 UI 线程里做了同步网络请求比如用了 _http.GetStringAsync(url).Result 或者 .Wait()。这会造成死锁——UI 线程等网络结果而网络回调又要回到 UI 线程互相等。解决全程用 async/await从事件处理到服务方法一路 async 到底绝不混用 .Result 和 .Wait()。Timer 的 Tick 事件写成 async (s, e) await RefreshAllAsync()。4.2 数据一直不刷新但也没报错现象程序跑着界面上的价格就是不动UpdatedAt 也不变。原因接口返回的字段名和你解析时用的名字对不上GetProperty 抛了 KeyNotFoundException但被外层 catch 吞了只记了日志你没看。或者接口对代码格式有要求要带前缀你传的格式不对返回了空数据。解决先把 catch 里的异常完整打出来别只记 Message把 StackTrace 也记上。然后手动用浏览器或 curl 请求一次接口对比返回的 JSON 字段名和你的解析代码。代码格式问题就对着接口文档加前缀。4.3 刷新频率一高就被限流现象刚开始正常跑几分钟后全部拉取失败过一会儿又恢复。原因请求太频繁触发了接口的频率限制返回 429 或直接拒绝连接。解决把刷新间隔调大或者做退避重试——失败后等 2 秒、4 秒、8 秒再试而不是死循环硬刚。多标的并发时也要控制并发数别一次性全发出去。4.4 涨跌染色时有时无现象有时候颜色对有时候全是黑的。原因CellFormatting 里判断列名的方式不对或者 e.Value 的类型不是你以为的 decimal转换失败被吞。解决在事件开头加一句日志把 e.ColumnIndex、列名、e.Value 的类型打出来跑一次就知道问题在哪。列名建议用 DataPropertyName 绑定到模型属性比手写字符串可靠。4.5 提醒弹窗把界面卡住现象价格到阈值时弹了 MessageBox点确定之前行情完全不刷新。原因MessageBox.Show 是模态阻塞的它会开一个新的消息循环把原来的刷新逻辑挡住。解决改用 NotifyIcon 的 ShowBalloonTip非阻塞。如果一定要用窗口提醒就开一个独立的非模态窗体别用 MessageBox。5. 进阶把配置、日志和打包做扎实走到这一步工具已经能用了但离「自己长期用」还差几块。第一块是配置外置。把股票代码列表、刷新间隔、每只股票的提醒阈值写进一个 config.json程序启动读、退出写改股票不用重新编译。public class AppConfig { public Liststring Codes { get; set; } new(); public int RefreshSeconds { get; set; } 3; public Dictionarystring, decimal AlertThresholds { get; set; } new(); } // 读取 var cfg JsonSerializer.DeserializeAppConfig(File.ReadAllText(config.json)) ?? new AppConfig(); // 写入 File.WriteAllText(config.json, JsonSerializer.Serialize(cfg, new JsonSerializerOptions { WriteIndented true }));WriteIndented 让 JSON 带缩进方便你手动改。反序列化可能返回 null文件为空或格式错用 ?? 兜底成默认配置避免启动就崩。第二块是日志。别用 Console.WriteLineWinForms 没有控制台。用一个简单的写文件方法按天分文件出问题时能回溯。private static readonly object _logLock new(); private static void Log(string msg) { lock (_logLock) // 多线程并发写要加锁 { string line ${DateTime.Now:yyyy-MM-dd HH:mm:ss} {msg}; File.AppendAllText($log_{DateTime.Now:yyyyMMdd}.txt, line Environment.NewLine); } }lock 是必须的因为并发拉取时多个线程可能同时写日志不加锁会丢内容甚至抛 IO 异常。第三块是打包。自己用的话dotnet publish 成单文件就行不用装运行时。命令是 dotnet publish -c Release -r win-x64 --self-contained -p:PublishSingleFiletrue。这样产出一个 exe拷到哪都能跑。注意 self-contained 会让文件变大几十兆但省去了装 .NET 运行时的麻烦自己用值得。最后说个我自己的习惯这个工具我从来不接实盘交易只做查看和提醒。原因很简单自己写的程序稳定性没经过长期验证网络一抖、接口一改就可能出问题拿它做交易决策的输入可以拿它直接下单就是给自己埋雷。行情查看工具的价值在于「让你更快看到」不在于「替你做决定」。把刷新间隔、提醒阈值这些参数调到自己舒服的值比堆功能重要得多。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?