☰
Delphi股票分析系统源码拆解:图形控件与K线绘制实战
2026/10/19 3:19:04 网站建设 项目流程

简介:这是一套用Delphi原生开发的股票分析系统主站与客户端源码,面向希望研究大型桌面金融软件架构的Delphi开发者及证券行情分析爱好者,解决“Delphi能否承载复杂业务系统”的实践疑问。资源包共87个文件,约2.32MB,包含60个cel模板文件(覆盖股票、基金、债券、期货、汇率等行情与财务数据模板)、14个dat数据文件、5个dll动态库、3个ini配置、2个exe可执行程序及xml、txt等辅助文件,结构上区分了行情数据、财务指标、信息地雷与业务模板等模块。系统实现股票行情分析、实时财务数据与实时信息地雷等功能,且基本未依赖第三方控件,登录时随意填写用户名即可进入。已有984人学习下载,适合通过阅读源码理解Delphi在证券行情、财务数据组织与界面控件设计上的工程化思路,也可作为二次开发与架构参考的素材。

1. 一套用 Delphi 写客户端的股票分析系统,到底能跑出什么花样

有个做桌面端的老哥跟我吐槽,说他手里攒了一堆 Delphi 的图形处理控件源码,想拼一个能看盘、能画指标、能拉行情的股票分析系统,结果卡在“客户端怎么把 K 线画得跟主流行情软件一样顺滑”这一步。这套股票分析系统就是冲着这个场景来的:主站负责行情数据聚合与分发,客户端全用 Delphi 开发,图形处理类控件源码直接嵌进绘图层,指标计算、分时图、K 线叠加、画线工具都在本地完成。它适合两类人——一类是想拿现成 Delphi 图形控件做金融可视化的桌面开发者,另一类是想拆一套完整 C/S 行情系统、看行情推送和本地渲染怎么配合的熟手。下面我按“能干什么 → 怎么跑 → 怎么改”把这份源码拆开讲。

2. 架构与目录:主站、客户端、图形控件三层怎么咬合

2.1 整体分层:数据从主站到 K 线画布走了哪条路

这套系统的骨架是典型 C/S:主站是数据源,客户端是渲染与交互终端,图形处理控件是客户端里的绘图底座。主站不负责画图,它只做三件事——拉取或接收行情、按股票代码和周期做缓存、通过长连接把增量推给订阅了的客户端。客户端拿到数据后不直接怼到画布上,而是先过一层本地数据模型,再由图形控件按“视口 + 缩放级别 + 指标参数”重绘。

常见做法是主站用 TCP 长连接做推送,HTTP 做历史 K 线补拉。为什么不全用 HTTP 轮询?因为分时图对延迟敏感,轮询在开盘时段会把主站打出一堆无效请求。客户端这边,Delphi 的TClientSocket或 Indy 的TIdTCPClient都能扛,源码里大概率是前者,因为老 Delphi 项目对TClientSocket的依赖更顺手。

数据流大致是这样:

主站行情源 -> 主站缓存(按 code+period) -> 推送通道 -> 客户端接收线程 -> 本地行情模型(TQuote/TBar) -> 指标计算 -> 图形控件重绘

这里有个关键点:客户端接收线程和 UI 线程必须分开。Delphi 的 VCL 控件不是线程安全的,接收线程只负责把数据塞进线程安全队列,UI 线程用TTimer或自定义消息触发重绘。我见过有人直接在接收回调里Canvas.LineTo,开盘一忙就闪退,这是血泪经验。

2.2 目录结构:拿到源码先看哪几个文件夹

虽然输入没给完整目录树,但按这类 Delphi 行情项目的惯例,结构基本跑不出下面这套。你拿到源码后先按这个对照,缺哪个文件夹就说明那份源码被裁剪过。

StockAnalysis/ ├── Server/ # 主站 │ ├── Source/ # 主站源码 │ ├── DataCache/ # 行情缓存模块 │ └── PushService/ # 推送服务 ├── Client/ # Delphi 客户端 │ ├── Source/ │ │ ├── Main.pas # 主窗体 │ │ ├── QuoteModel.pas # 行情数据模型 │ │ ├── Indicator.pas # 指标计算 │ │ └── ChartView.pas # 图形控件封装 │ ├── Controls/ # 图形处理类控件源码 │ └── Client.dpr # 客户端工程文件 ├── Common/ # 公共协议、常量 │ └── Protocol.pas └── Docs/ # 协议与接口说明

Controls/是这份资源的核心卖点。图形处理类控件源码通常包含 K 线绘制、坐标轴、十字光标、画线工具这几类。ChartView.pas是封装层,把控件包成业务能调用的接口,比如DrawKLine(ABars: TArray<TBar>)。Indicator.pas里放 MA、MACD、KDJ 这些指标,注意它应该只算数值,不碰画布,否则指标和绘图耦合,改一个参数要动两处。

2.3 通信协议:主站和客户端约定了什么

协议层在Common/Protocol.pas。这类系统一般自定义二进制包,而不是 JSON,因为行情推送频率高,JSON 的解析开销在 Delphi 里不划算。一个典型包结构是:包头(长度+命令字) + 股票代码 + 周期 + 数据体。

// Common/Protocol.pas 片段示意 type TQuotePacket = packed record Head: Word; // 包头标识 Cmd: Byte; // 命令字: 1=订阅 2=推送 3=历史 Code: array[0..7] of AnsiChar; // 股票代码 Period: Byte; // 周期: 1=分时 5=5分钟 9=日线 BodyLen: Integer; // 数据体长度 end;

Cmd决定客户端怎么处理这个包,Period决定用哪套缓存。参数说明:Code定长 8 字节是为了避免变长字符串在流里解析出错,这是老 C/S 项目的常见做法。BodyLen必须校验,否则粘包时会把下一条包的头当数据读,直接画出乱线。客户端解析时先读固定头,再按BodyLen读体,读满才交给上层。

3. 跑起来:主站启动、客户端编译、行情连通的完整步骤

3.1 主站启动:先把数据源和推送端口配好

主站一般是个控制台或服务程序。启动前要确认两件事:行情数据源地址、推送监听端口。源码里通常有个config.ini或常量单元。

; Server/config.ini [DataSource] Url=http://127.0.0.1:8080/quote PollInterval=3000 [Push] Port=9100 MaxClients=200

PollInterval是主站主动拉行情的间隔,单位毫秒。分时场景可以设 1000,日线场景 3000 足够。MaxClients是推送连接上限,超过就拒绝,避免单台主站被拖垮。启动主站后先看日志有没有“listen on 9100”这类输出,没有就是端口被占或配置没读到。

主站启动命令按工程类型不同,常见是直接运行编译好的 exe:

# 进入主站目录后运行 ./StockServer.exe --config ./config.ini

如果源码是 Delphi 工程,先在 IDE 里编译Server.dpr,再运行生成的 exe。注意主站和客户端要用同一份Protocol.pas,否则命令字对不上,客户端会一直收到“未知包”然后断开。

3.2 客户端编译:Delphi 版本和控件包要先对齐

客户端全用 Delphi 开发,这里第一个坑就是版本。图形处理类控件源码如果用了较新的语法(比如泛型、匿名方法),老版本 Delphi 7 编译不过;反过来,如果控件是为 Delphi 7 写的,用较新版本打开可能报“单元找不到”。

编译顺序建议:

  1. 先打开Controls/下的控件包工程(通常是.dpk),编译并安装到 IDE。
  2. 再打开Client.dpr,在项目选项里确认搜索路径包含Controls/和Common/。
  3. 编译前检查QuoteModel.pas里有没有硬编码的服务器 IP。
// Client/Source/QuoteModel.pas 片段 const SERVER_HOST = '127.0.0.1'; // 改成主站实际地址 SERVER_PORT = 9100;

SERVER_HOST和SERVER_PORT必须和主站config.ini一致。改完重新编译,运行客户端,主窗体应该能出来。如果一启动就报“控件未注册”,说明第 1 步的控件包没装好,回 IDE 的 Component 菜单确认。

3.3 连通验证:订阅一只股票,看 K 线有没有出来

客户端跑起来后,在代码里或界面上触发订阅。订阅逻辑一般在主窗体创建时或按钮事件里:

// 订阅示意 procedure TMainForm.Subscribe(const ACode: string; APeriod: Byte); var Pkt: TQuotePacket; begin FillChar(Pkt, SizeOf(Pkt), 0); Pkt.Head := $AA55; Pkt.Cmd := 1; // 1=订阅 StrPCopy(Pkt.Code, ACode); Pkt.Period := APeriod; Pkt.BodyLen := 0; FTcpClient.Socket.SendBuf(Pkt, SizeOf(Pkt)); end;

Head是包头标识,主站收到先校验这个值,不对就丢。Cmd=1表示订阅,主站收到后把该代码加入推送列表。订阅成功后,主站会回一个Cmd=2的推送包,客户端接收线程解析后塞进队列,UI 线程定时取出来重绘。

验证是否连通,看两个地方:客户端日志有没有“recv packet cmd=2”,以及 K 线画布有没有出现第一根柱子。如果日志有包但画布空白,问题在绘图层,不在通信层。这时候去ChartView.pas看DrawKLine有没有被调用,以及传入的ABars长度是不是 0。

提示:第一次跑建议先用主站自带的模拟数据源,别直接连真实行情。模拟源能稳定复现“订阅→推送→重绘”整条链路,排查问题时变量少。

4. 核心模块拆解:指标计算、K 线绘制、十字光标怎么实现

4.1 指标计算:MA 和 MACD 的数值层怎么写

指标计算放在Indicator.pas,原则是只进不出画布。以 MA 为例,输入是 K 线数组,输出是均线数组,长度对齐,前 N-1 个位置填 0 或 NaN。

// Indicator.pas 片段:简单移动平均 function CalcMA(const Bars: TArray<TBar>; N: Integer): TArray<Double>; var I, J: Integer; Sum: Double; begin SetLength(Result, Length(Bars)); for I := 0 to High(Bars) do begin if I < N - 1 then begin Result[I] := 0; // 数据不足,留空 Continue; end; Sum := 0; for J := I - N + 1 to I do Sum := Sum + Bars[J].Close; Result[I] := Sum / N; end; end;

N是均线周期,常见 5、10、20、60。Bars[J].Close是收盘价,TBar里至少要有 Open/High/Low/Close/Volume/Time 这几个字段。这段是 O(N*M) 的朴素写法,数据量大时可以在外层维护滑动窗口,把内层循环省掉。MACD 在此基础上多两步:先算 EMA,再算 DIF 和 DEA,最后画柱状图。注意 EMA 的初始值一般用第一个收盘价,别用 0,否则前几十根线的值会偏得离谱。

指标算完后不直接画,而是交给ChartView。这样改指标参数(比如把 MA5 改成 MA10)只需要重算数组,不用动绘图代码。

4.2 K 线绘制:图形控件怎么把一根柱子画准

图形处理类控件的核心是坐标映射:把价格映射到 Y 像素,把时间映射到 X 像素。ChartView.pas里通常有个PriceToY和TimeToX。

// ChartView.pas 片段:价格转 Y 坐标 function TChartView.PriceToY(APrice: Double): Integer; begin if FMaxPrice = FMinPrice then Exit(FPlotRect.Top); Result := FPlotRect.Bottom - Round((APrice - FMinPrice) / (FMaxPrice - FMinPrice) * FPlotRect.Height); end;

FMaxPrice和FMinPrice是当前视口内的最高最低价,每次重绘前根据可见 K 线重新算。FPlotRect是绘图区域,要留出上下边距,否则最高价那根柱子会贴边。画一根 K 线时,先画影线(High 到 Low),再画实体(Open 到 Close),阳线和阴线用不同颜色填充。

// 画单根 K 线 procedure TChartView.DrawBar(const ABar: TBar; X: Integer); var YHigh, YLow, YOpen, YClose: Integer; begin YHigh := PriceToY(ABar.High); YLow := PriceToY(ABar.Low); YOpen := PriceToY(ABar.Open); YClose := PriceToY(ABar.Close); Canvas.Pen.Color := clGray; Canvas.MoveTo(X, YHigh); Canvas.LineTo(X, YLow); // 影线 if ABar.Close >= ABar.Open then Canvas.Brush.Color := clRed else Canvas.Brush.Color := clGreen; Canvas.FillRect(Rect(X - 3, Min(YOpen, YClose), X + 3, Max(YOpen, YClose))); // 实体 end;

X是这根柱子的横坐标,柱宽这里写死 3 像素,实际项目里应该按缩放级别动态算。Min/Max保证阳线阴线实体方向一致。注意Canvas.FillRect之前要设Brush.Color,否则用的是上一次的颜色,画出来红绿乱套。

4.3 十字光标与画线:交互层怎么和绘图层解耦

十字光标是行情软件的高频交互。做法是在OnMouseMove里记录鼠标位置,换算成对应的 K 线索引和价格,然后重绘一层覆盖线。

// 鼠标移动更新十字光标 procedure TChartView.OnMouseMove(Sender: TObject; Shift: TShiftState; X, Y: Integer); begin FCrossX := X; FCrossY := Y; FCrossIndex := XToIndex(X); // 横坐标转 K 线索引 FCrossPrice := YToPrice(Y); // 纵坐标转价格 Invalidate; // 触发重绘 end;

XToIndex和YToPrice是PriceToY、TimeToX的逆运算。Invalidate会让整个画布重绘,数据量大时会有闪烁,常见优化是只重绘光标覆盖的区域,或者用双缓冲。画线工具(趋势线、水平线)建议单独存一个TList<TDrawLine>,重绘时先画 K 线再画线,这样线始终在最上层。

注意:十字光标的价格换算要用和 K 线相同的FMaxPrice/FMinPrice,否则光标显示的价格和柱子对不上,这是新手最容易翻车的地方。

5. 避坑与排查:编译、连接、绘图三类问题怎么定位

5.1 控件包编译报“找不到单元”

现象:打开Controls/下的.dpk编译,提示某个.pas找不到。原因通常是控件包引用了Common/里的协议单元,但搜索路径没配。解决:在 IDE 的 Tools → Options → Library 里把Common/加进 Library Path,或者直接在.dpk的 requires 里确认引用关系。别急着改源码里的uses,先查路径。

5.2 客户端连上就断,日志显示“未知包”

现象:客户端能连上主站,但几秒后断开,日志里刷“unknown packet”。原因基本是主站和客户端的Protocol.pas不是同一份,命令字或包头标识对不上。解决:把Common/Protocol.pas复制到两边工程各一份,或者用同一个搜索路径引用,确保编译时用的是同一份。改完两边都要重新编译,只编一边没用。

5.3 K 线画出来是空白或只有一条直线

现象:订阅成功、数据也收到了,但画布上没柱子或只有一条水平线。原因有三种:FMaxPrice和FMinPrice没更新,导致所有价格映射到同一个 Y;ABars长度为 0,DrawKLine直接返回;柱宽算成 0,FillRect画不出东西。解决:在DrawKLine入口加日志打印Length(ABars)和FMaxPrice/FMinPrice,先确认数据进来了,再确认坐标范围有效。

5.4 开盘时段客户端卡死

现象:平时正常,一开盘就卡,界面无响应。原因是接收线程直接在回调里操作了 VCL 控件,或者重绘频率太高。解决:接收线程只入队,UI 线程用TTimer按固定间隔(比如 200ms)批量取数据重绘,不要每来一个包就Invalidate一次。另外把指标计算放到重绘前一次性算完,别在OnPaint里循环算。

5.5 历史 K 线和实时推送对不上

现象:补拉的历史数据和推送的最新一根柱子接不上,中间有跳空或重叠。原因是历史接口返回的最后一根和推送的第一根时间边界没对齐。解决:在客户端合并时按时间戳去重,同一时间戳以推送数据为准。主站侧也要保证历史缓存的最后一根和推送队列的第一根不重复发送。

6. 二次开发:换指标、接新数据源、把绘图控件抽出来复用

拿到这套源码后,最值钱的改造方向不是重写,而是把Controls/里的图形处理控件抽成独立包,让别的 Delphi 项目也能用。具体做法:把ChartView.pas里和股票业务相关的字段(比如FCode、FPeriod)剥掉,只留Bars、MaxPrice、MinPrice、PlotRect这些纯绘图参数,然后打成一个.dpk。这样以后做期货、做基金、做任何 K 线类可视化,直接引用这个包就行。

换指标是最常见的二次开发。以加一个布林带为例,在Indicator.pas里加函数:

// 布林带:中轨 MA(N),上下轨 ±K 倍标准差 procedure CalcBOLL(const Bars: TArray<TBar>; N: Integer; K: Double; out Mid, Upper, Lower: TArray<Double>); var I, J: Integer; Sum, Mean, Std: Double; begin SetLength(Mid, Length(Bars)); SetLength(Upper, Length(Bars)); SetLength(Lower, Length(Bars)); for I := 0 to High(Bars) do begin if I < N - 1 then Continue; Sum := 0; for J := I - N + 1 to I do Sum := Sum + Bars[J].Close; Mean := Sum / N; Std := 0; for J := I - N + 1 to I do Std := Std + Sqr(Bars[J].Close - Mean); Std := Sqrt(Std / N); Mid[I] := Mean; Upper[I] := Mean + K * Std; Lower[I] := Mean - K * Std; end; end;

N常用 20,K常用 2。算完后在ChartView里加三条线,颜色区分开。注意out参数在 Delphi 里要调用方先SetLength或由函数内部设置,这里函数内部设了,调用方直接用就行。

接新数据源也不难。主站的DataSource模块只要实现“拉取 → 转成TBar→ 塞进缓存”这三步,推送层不用动。如果你想把主站换成从本地 CSV 读历史数据做回测,就在DataSource里加一个 CSV 解析分支,按Code和Period返回TArray<TBar>。这样客户端完全无感,因为它只认协议包。

验证改造是否成功,我一般会走一遍固定流程:先用模拟数据源跑通订阅和重绘,再换成真实数据源看延迟和丢包,最后把指标参数改到极端值(比如 MA 周期设 1 或 250)看会不会崩。从那以后我每次动绘图层之前,都强制先用模拟数据跑一遍全链路,确认不是数据问题再改代码。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询