简介:这是一套完全基于Delphi开发的股票分析系统主站与客户端源码,面向希望研究桌面端金融行情软件架构、或需要Delphi大型项目实战参考的开发者。系统涵盖股票行情分析、实时财务数据、实时信息地雷等核心功能,且基本未依赖第三方控件,对理解原生Delphi在复杂业务系统中的工程组织颇具价值。压缩包共87个文件,约2.32MB,包含60个cel模板文件(覆盖股票、基金、债券、期货、汇率等行情与财务模板)、14个dat数据文件、5个dll动态库、3个ini配置、2个exe可执行程序及xml、txt等辅助文件,目录按数据、模板、报表、更新模块分层组织。目前已有984人浏览学习。读者可从中获取完整的行情分析客户端源码、数据字典与报表模板体系、模块划分思路及Delphi大系统开发经验,适合中高级Delphi开发者研读与二次开发。
1. 拆开一个 Delphi 写的股票分析主站:图形控件源码到底能怎么用
前阵子有个做量化工具的朋友甩给我一个压缩包,说“你之前不是搞过一阵图形处理控件吗,这个股票分析系统主站是纯 Delphi 写的,客户端全用 Delphi 开发,里面图形处理类控件源码挺全,你看看能不能拆出点能复用的东西”。我本来以为又是个套壳 Demo,结果解压一看,K 线绘制、分时图、指标叠加、坐标轴自适应这些模块都是手写 GDI/GDI+ 逻辑,没有依赖任何第三方图表库。这就有点意思了——现在网上能找到的股票类源码,要么是 C# 调用的封装库,要么是 Python 的 matplotlib 套壳,真正用 Delphi 原生图形控件从零画 K 线的资源确实不多。这份资源适合两类人:一是手上还有 Delphi 存量项目、需要给系统加行情图表的开发者;二是想研究图形处理类控件底层绘制逻辑、把思路迁移到其他语言的人。它解决的核心问题不是“怎么分析股票”,而是“怎么用原生图形控件把行情数据画得又快又稳”。
2. 图形处理类控件的绘制原理与选型逻辑
2.1 为什么不用第三方图表库
Delphi 生态里能画 K 线的第三方库不是没有,TeeChart、FastReport 的图表模块都能用,但这个项目选择了纯手写。原因很实际:股票行情图表的刷新频率和交互需求跟通用图表库的设计目标不一样。通用图表库为了兼容各种图表类型,抽象层很厚,每次重绘要走一遍完整的布局计算;而行情图需要的是“数据变了只重画变化区域”,K 线图在盘中每秒可能刷新多次,用通用库很容易出现闪烁和卡顿。
手写图形控件的另一个好处是可控。比如分时图里那条均价线,通用库通常只给你一个 Series 对象,你想改线宽、改拐点样式、改超出画布时的裁剪逻辑,都得翻文档找属性。而自己用 Canvas 画,MoveTo、LineTo直接控制,想怎么改就怎么改。这个项目的图形处理类控件源码里,核心绘制逻辑集中在几个单元文件里,每个单元负责一类图形元素,职责切得很干净。
常见做法是继承TCustomControl或TGraphicControl。前者有窗口句柄,能接收焦点和鼠标滚轮事件,适合做主图容器;后者没有句柄,更轻量,适合做指标副图或图例。这个项目主图用的是TCustomControl派生,副图用TGraphicControl,算是比较标准的选型。
2.2 双缓冲与局部重绘的实现
Delphi 里做图形绘制,绕不开闪烁问题。默认情况下,Canvas直接往屏幕画,数据一多就能看到明显的抖动。解决办法是双缓冲:先在内存位图上画好,再一次性BitBlt到屏幕。
// 双缓冲绘制核心逻辑(简化示意) procedure TStockChart.Paint; var MemDC: HDC; MemBitmap: TBitmap; begin // 创建内存位图,尺寸与控件客户区一致 MemBitmap := TBitmap.Create; try MemBitmap.Width := ClientWidth; MemBitmap.Height := ClientHeight; MemDC := MemBitmap.Canvas.Handle; // 所有绘制操作先画到内存 DC DrawBackground(MemDC); DrawGrid(MemDC); DrawCandles(MemDC); // 画 K 线实体 DrawIndicators(MemDC); // 画均线、成交量等 // 一次性拷贝到屏幕,避免逐元素刷新造成的闪烁 BitBlt(Canvas.Handle, 0, 0, ClientWidth, ClientHeight, MemDC, 0, 0, SRCCOPY); finally MemBitmap.Free; end; end;这段代码的关键在BitBlt那一步。SRCCOPY表示直接覆盖,不做混合。如果要做半透明叠加效果,可以改用AlphaBlend,但性能会下降。参数MemBitmap.Width和Height必须跟客户区一致,否则会出现拉伸或留白。我一般会在Resize事件里重建位图,而不是每次Paint都创建,减少内存分配开销。
局部重绘是另一个优化点。盘中最后一根 K 线在变,前面的历史 K 线不动,就没必要全量重画。项目里用了一个InvalidateRect调用,只把最后一根 K 线所在的矩形区域标记为无效,Paint里再判断裁剪区域,只画那部分。这个技巧在数据量大时效果很明显,全量重绘 2000 根 K 线大概要 30 毫秒,局部重绘能压到 5 毫秒以内。
2.3 坐标映射与数据缩放
行情图表的本质是把价格和时间映射到像素坐标。价格轴是线性的,时间轴在分时图里是线性的,在 K 线图里是等间距的(每根 K 线占固定宽度)。这个项目里封装了一个TChartAxis类,负责维护数据范围到像素范围的转换。
// 坐标映射核心方法 function TChartAxis.DataToPixel(Value: Double): Integer; begin // FMinValue/FMaxValue 是当前可见数据的最小最大值 // FPixelStart/FPixelEnd 是轴在画布上的起止像素 if FMaxValue = FMinValue then Result := FPixelStart else Result := FPixelStart + Round( (Value - FMinValue) / (FMaxValue - FMinValue) * (FPixelEnd - FPixelStart) ); end;这里有个边界坑:当FMaxValue等于FMinValue时(比如停牌期间没有新数据),除法会出异常,所以要先判断。另外Round取整会导致同一像素对应多个价格值,画细线时看不出来,但画散点图时会出现重叠,需要做去重处理。
数据缩放方面,项目支持鼠标滚轮缩放和拖拽平移。缩放时改变的是FMinValue和FMaxValue,然后触发重绘。注意缩放中心要跟着鼠标位置走,否则用户体验会很别扭——滚轮放大时应该以鼠标所在价格为锚点,而不是以图表中心为锚点。
3. 把图形控件源码跑起来:环境配置与最小复现
3.1 编译环境与依赖检查
这份源码是 Delphi 项目,主站部分用的是 VCL 框架。我实测用的环境是 Delphi 10.4 Sydney,低版本比如 Delphi 7 理论上也能编译,但需要注意字符串类型差异(AnsiString和UnicodeString)。项目里没有用到太新的语法特性,主要是传统的 Object Pascal 风格。
打开项目前先检查几个东西:一是Graphics、Math、Windows这几个单元是否在uses里,图形处理类控件依赖它们;二是项目选项里的Runtime Library要设为Dynamic RTL还是Static,如果目标机器没装 Delphi 运行库,就选静态编译;三是Output Directory和Unit Output Directory要分开,否则编译中间文件会跟源码混在一起。
# 用命令行编译(如果装了 Delphi 命令行工具) dcc32 -B StockChartMain.dpr # -B 表示全量编译,避免增量编译的缓存问题 # 如果报找不到单元,检查 -U 参数指定的搜索路径命令行编译适合做自动化构建,但日常开发还是用 IDE 方便。-B参数我一般只在清理重建时用,平时增量编译快得多。
3.2 核心单元文件结构
解压后源码目录大概是这样组织的:
| 目录/文件 | 作用 |
|---|---|
Source\Chart\ | 图形处理类控件核心单元 |
Source\Chart\ChartBase.pas | 图表基类,定义坐标轴和画布 |
Source\Chart\CandleChart.pas | K 线绘制实现 |
Source\Chart\TimeChart.pas | 分时图绘制实现 |
Source\Data\ | 数据层,负责行情数据读取和缓存 |
Source\Indicator\ | 技术指标计算单元 |
MainForm.pas | 主窗口,组装图表和工具栏 |
ChartBase.pas是理解整个图形体系的入口。它定义了TChartBase抽象类,声明了Draw、DataToPixel、PixelToData等虚方法,子类只需要实现具体绘制逻辑。这种设计的好处是新增图表类型时不用改主窗口代码,注册一下就行。
3.3 最小可运行示例
如果想快速验证图形控件能不能用,可以新建一个空白 VCL 项目,把ChartBase.pas和CandleChart.pas加进去,然后在窗体上放一个TImage或者直接继承TCustomControl。
// 最小复现:在窗体上画一根 K 线 procedure TForm1.FormPaint(Sender: TObject); var Candle: TCandleChart; begin Candle := TCandleChart.Create(Self); try Candle.Parent := Self; Candle.Align := alClient; // 构造模拟数据:开盘、最高、最低、收盘 Candle.AddBar(10.0, 10.5, 9.8, 10.2); Candle.AddBar(10.2, 10.8, 10.1, 10.6); Candle.AddBar(10.6, 10.9, 10.3, 10.4); Candle.Draw; // 触发绘制 finally Candle.Free; end; end;这段代码里AddBar的参数顺序是开、高、低、收,别搞反了,否则画出来的 K 线颜色会乱。Draw方法内部会调用Paint,但如果你是在FormPaint里手动调,注意避免递归重绘。实际项目中一般用Invalidate触发系统重绘,而不是直接调Draw。
提示:如果编译时报
TCandleChart未声明,检查CandleChart.pas是否在uses子句里,以及单元文件的搜索路径是否配好。
4. 避坑排查:图形控件在真实项目里的五个翻车点
4.1 现象:图表闪烁严重,拖动窗口时尤其明显
原因:没有启用双缓冲,或者双缓冲位图在每次Paint时重新创建,导致内存分配和释放的开销比绘制本身还大。
解决:在控件构造函数里创建一次TBitmap,在Resize事件里调整尺寸,Paint里直接复用。另外把DoubleBuffered属性设为True只能解决控件级别的闪烁,对Canvas内部的自绘逻辑没用,必须自己实现内存位图。
4.2 现象:K 线数量超过 1000 根后,鼠标移动卡顿
原因:鼠标移动事件里触发了全量重绘,而且每次重绘都重新计算所有 K 线的坐标映射。
解决:把坐标映射结果缓存起来,数据不变时不重复计算。鼠标移动只更新十字光标位置,用InvalidateRect只重画光标所在的窄条区域。我一般会加一个FMouseMoveInvalidate标志,避免高频事件里反复调用Invalidate。
4.3 现象:分时图均价线在数据断点处画出了直线连接
原因:行情数据中间有停牌或集合竞价时段,时间轴不连续,但绘制时直接按数组索引连线,没有判断时间间隔。
解决:在DrawLine之前检查相邻两个数据点的时间差,如果超过阈值(比如 5 分钟),就断开重新MoveTo。这个阈值要根据品种调整,股票和期货不一样。
4.4 现象:高 DPI 显示器上图表模糊或错位
原因:Delphi 老项目默认不感知 DPI,Windows 会把整个窗口拉伸,导致像素级绘制失真。
解决:在项目选项里启用High DPI Awareness,然后在代码里根据Screen.PixelsPerInch动态调整线宽和字体大小。注意Canvas.Pen.Width设成 1 在高 DPI 下会显得很细,可以按比例放大到 2 或 3。
4.5 现象:编译通过但运行时提示“Canvas does not allow drawing”
原因:在Paint事件之外的地方调用了Canvas绘制,比如在Create构造函数里直接画。
解决:所有绘制操作必须放在Paint或OnPaint事件里,或者用Invalidate请求重绘。如果确实需要在非绘制时机准备图形资源,用TBitmap离屏画好,再在Paint里Draw上去。
5. 进阶技巧:把 K 线绘制逻辑迁移到其他语言
这份源码最有价值的地方不是 Delphi 本身,而是图形处理类控件的绘制思路。我把 K 线绘制的核心逻辑抽出来,用 Python 的QPainter重写了一版,发现几个关键点可以直接复用。
首先是坐标映射的分离。不管用什么语言,数据到像素的转换都应该独立成一个类或函数,不要跟绘制代码混在一起。这样换绘图库时只需要改绘制部分,映射逻辑不动。
其次是局部重绘的触发条件。Python 的 Qt 框架里对应的是update(QRect),跟 Delphi 的InvalidateRect思路一样。判断哪些区域需要重绘的逻辑可以完全照搬:数据追加时只重绘最后一根 K 线区域,鼠标移动时只重绘光标区域。
最后是双缓冲的实现方式。Qt 里可以用QPixmap做离屏缓冲,绘制完成后drawPixmap一次性贴到QWidget上。性能测试下来,2000 根 K 线的全量重绘在 Python 里大概 15 毫秒,比 Delphi 慢一倍,但局部重绘能压到 3 毫秒以内,交互完全够用。
| 迁移点 | Delphi 实现 | Python/Qt 对应 |
|---|---|---|
| 坐标映射 | TChartAxis.DataToPixel | 自定义AxisMapper类 |
| 双缓冲 | TBitmap+BitBlt | QPixmap+drawPixmap |
| 局部重绘 | InvalidateRect | update(QRect) |
| 绘制入口 | Paint事件 | paintEvent重写 |
从那以后我每次拿到图形类源码,都强制先跑一遍最小复现,确认绘制链路通了再往业务里塞。很多资源看着功能全,实际上手才发现依赖缺失或者版本不兼容,先验证再集成能省掉大量返工。希望帮到你。
本文还有配套的精品资源,点击获取