You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何提升Delphi中TScrollBox列表的UI渲染速度?

Delphi VCL 动态列表性能问题排查与优化

问题背景

使用TScrollBox作为列表容器,TFrame作为列表项,运行时动态生成Frame。每个Frame包含3.6KB的SVG图片、若干Label和EditBox。在FormShow事件中生成1000个列表项,代码如下:

var
  i: Integer;
begin
  for i := 1 to 1000 do
    with TFrameCDG.Create(Self) do
    begin
      Name := 'cdgFrame' + IntToStr(i);
      Parent := sbScrollBoxLeft;
    end;
end;

Frame的Align属性设为alTop,通过OnExit、OnEnter、OnClick事件控制背景色优化显示效果。

核心性能问题:

  • 表单加载耗时38秒
  • 窗口最大化耗时12秒
  • 滚动操作卡顿严重

硬件配置:i7-4790处理器、Radeon R7 430显卡、16GB内存,Windows 11系统,Delphi 10 Seattle开发。

删除SVG图片后,加载耗时仍有29秒;开启DoubleBuffered未达到预期优化效果。实际业务中列表最多仅需50个项,但运行依然缓慢,希望优化到接近C# WPF的流畅度。

最小复现代码

Project1.dpr

program Project1;

uses
  Vcl.Forms,
  Unit1 in 'Unit1.pas' {Form1},
  Unit2 in 'Unit2.pas' {Frame2: TFrame};

{$R *.res}

begin
  Application.Initialize;
  Application.MainFormOnTaskbar := True;
  Application.CreateForm(TForm1, Form1);
  Application.Run;
end.

Unit1.pas

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Unit2;

type
  TForm1 = class(TForm)
    ScrollBox1: TScrollBox;
    procedure FormShow(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormShow(Sender: TObject);
var
  i: Integer;
begin
  for i := 0 to 1000 do
    with TFrame2.Create(Self) do
    begin
      Name := 'Framea' + IntToStr(i);
      Parent := ScrollBox1;
    end;
end;

end.

Unit2.pas

unit Unit2;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
  Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls, Vcl.ComCtrls;

type
  TFrame2 = class(TFrame)
    ProgressBar1: TProgressBar;
    Label1: TLabel;
    Edit1: TEdit;
    Bevel1: TBevel;
    Edit2: TEdit;
    Label2: TLabel;
    Edit3: TEdit;
    Label3: TLabel;
    Button1: TButton;
    procedure FrameClick(Sender: TObject);
    procedure FrameEnter(Sender: TObject);
    procedure FrameExit(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

implementation

{$R *.dfm}

procedure TFrame2.FrameClick(Sender: TObject);
begin
  Self.SetFocus;
end;

procedure TFrame2.FrameEnter(Sender: TObject);
begin
  Color := clBlue;
end;

procedure TFrame2.FrameExit(Sender: TObject);
begin
  Color := clTeal;
end;

end.

Form1.dfm

object Form1: TForm1
  Left = 0
  Top = 0
  Caption = 'Form1'
  ClientHeight = 660
  ClientWidth = 1333
  Color = clBtnFace
  DoubleBuffered = True
  Font.Charset = DEFAULT_CHARSET
  Font.Color = clWindowText
  Font.Height = -11
  Font.Name = 'Tahoma'
  Font.Style = []
  OldCreateOrder = False
  OnShow = FormShow
  PixelsPerInch = 96
  TextHeight = 13
  object ScrollBox1: TScrollBox
    Left = 0
    Top = 0
    Width = 1333
    Height = 660
    HorzScrollBar.Visible = False
    VertScrollBar.Smooth = True
    VertScrollBar.Tracking = True
    Align = alClient
    TabOrder = 0
  end
end

Frame2.dfm

object Frame2: TFrame2
  Left = 0
  Top = 0
  Width = 451
  Height = 117
  Align = alTop
  Color = clTeal
  Font.Charset = ANSI_CHARSET
  Font.Color = clWindowText
  Font.Height = -19
  Font.Name = 'Segoe UI'
  Font.Style = []
  ParentBackground = False
  ParentColor = False
  ParentFont = False
  TabOrder = 0
  OnClick = FrameClick
  OnEnter = FrameEnter
  OnExit = FrameExit
  DesignSize = (
    451
    117)
  object Label1: TLabel
    Left = 24
    Top = 16
    Width = 55
    Height = 25
    Caption = 'Label1'
    Font.Charset = ANSI_CHARSET
    Font.Color = clWhite
    Font.Height = -19
    Font.Name = 'Segoe UI'
    Font.Style = []
    ParentFont = False
  end
  object Bevel1: TBevel
    Left = 0
    Top = 0
    Width = 451
    Height = 17
    Align = alTop
    Shape = bsTopLine
    ExplicitLeft = -44
    ExplicitTop = 24
  end
  object Label2: TLabel
    Left = 131
    Top = 16
    Width = 55
    Height = 25
    Caption = 'Label1'
    Font.Charset = ANSI_CHARSET
    Font.Color = clWhite
    Font.Height = -19
    Font.Name = 'Segoe UI'
    Font.Style = []
    ParentFont = False
  end
  object Label3: TLabel
    Left = 238
    Top = 16
    Width = 55
    Height = 25
    Caption = 'Label1'
    Font.Charset = ANSI_CHARSET
    Font.Color = clWhite
    Font.Height = -19
    Font.Name = 'Segoe UI'
    Font.Style = []
    ParentFont = False
  end
  object ProgressBar1: TProgressBar
    Left = 352
    Top = 73
    Width = 77
    Height = 21
    Anchors = [akLeft, akRight, akBottom]
    TabOrder = 0
  end
  object Edit1: TEdit
    Left = 24
    Top = 55
    Width = 101
    Height = 38
    BevelInner = bvNone
    BevelOuter = bvNone
    BorderStyle = bsNone
    Color = 11184810
    Ctl3D = True
    ParentCtl3D = False
    TabOrder = 1
    Text = 'Edit1'
  end
  object Edit2: TEdit
    Left = 131
    Top = 55
    Width = 101
    Height = 38
    BevelInner = bvNone
    BevelOuter = bvNone
    BorderStyle = bsNone
    Color = 11184810
    Ctl3D = True
    ParentCtl3D = False
    TabOrder = 2
    Text = 'Edit1'
  end
  object Edit3: TEdit
    Left = 238
    Top = 55
    Width = 101
    Height = 38
    BevelInner = bvNone
    BevelOuter = bvNone
    BorderStyle = bsNone
    Color = 11184810
    Ctl3D = True
    ParentCtl3D = False
    TabOrder = 3
    Text = 'Edit1'
  end
  object Button1: TButton
    Left = 354
    Top = 36
    Width = 75
    Height = 25
    Anchors = [akTop, akRight]
    Caption = 'Button1'
    TabOrder = 4
  end
end

问题根源分析

  1. TScrollBox架构的本质缺陷:
    TScrollBox会一次性渲染所有子控件,哪怕控件不在可视区域内。1000个Frame意味着上万个子控件,Windows消息循环需要处理大量绘制、布局消息,直接导致性能雪崩。
  2. 动态创建时的即时布局开销:
    每次设置Parent := ScrollBox1时,VCL会立即触发父控件的布局重算,1000次循环就会触发1000次布局,极大浪费CPU资源。
  3. 控件属性的额外绘制开销:
    Frame设置了ParentBackground = False、ParentColor = False,每个Frame都需要独立绘制背景;Edit控件的自定义颜色、Ctl3D = True等属性会增加绘制复杂度;即使删除SVG,Frame作为容器的固有开销依然存在。
  4. DoubleBuffered的局限性:
    VCL的DoubleBuffered仅针对单个控件的绘制缓冲,无法解决大量控件的布局和消息处理开销。

优化方案

1. 替换为虚拟列表控件(核心优化)

VCL原生的TListView(设置ViewStyle := vsReport)或TStringGrid支持虚拟模式,仅渲染可视区域内的项,能大幅降低控件数量和绘制开销。如果需要自定义项布局,可使用第三方虚拟列表控件(如DevExpress TcxGrid、TMS TAdvStringGrid),或自行实现虚拟滚动逻辑。

2. 优化动态创建流程

如果必须保留TFrame+ScrollBox架构:

  • 批量创建时暂停重绘:创建Frame前禁用ScrollBox的重绘,完成后再恢复,避免频繁布局:
procedure TForm1.FormShow(Sender: TObject);
var
  i: Integer;
begin
  ScrollBox1.Perform(WM_SETREDRAW, 0, 0); // 暂停重绘
  try
    for i := 0 to 49 do // 实际最多50个项
      with TFrame2.Create(Self) do
      begin
        Name := 'Framea' + IntToStr(i);
        Parent := ScrollBox1;
      end;
  finally
    ScrollBox1.Perform(WM_SETREDRAW, 1, 0); // 恢复重绘
    ScrollBox1.Invalidate; // 强制刷新
  end;
end;
  • 延迟创建不可见Frame:仅创建当前可视区域内的Frame,滚动时动态销毁/创建不可见项,模拟虚拟列表逻辑。

3. 精简Frame控件与属性

  • 减少Frame内控件数量:用TPaintBox自定义绘制文本和标签,替代多个TLabel、TEdit,减少控件句柄数量。
  • 启用继承属性:设置ParentBackground = True、ParentColor = True,让Frame继承父控件背景,减少独立绘制操作。
  • 简化控件样式:去掉不必要的Ctl3D、自定义边框等属性,使用默认样式降低绘制复杂度。

4. SVG图片优化

  • 预渲染为位图:程序启动时将SVG渲染为TBitmap,Frame直接使用位图而非SVG控件,避免重复解析SVG。
  • 转换为PNG格式:将SVG转为PNG,VCL对PNG的加载和渲染效率更高。

5. 其他优化技巧

  • 关闭ScrollBox的VertScrollBar.Tracking:跟踪滚动会导致频繁重绘,关闭后仅在滚动停止时刷新。
  • 用TPanel替代TFrame:TPanel的开销比TFrame略小,若不需要Frame的模块化特性可替换。
  • 高层级开启DoubleBuffered:给ScrollBox和Form都开启DoubleBuffered,减少闪烁(但无法解决布局开销问题)。

内容的提问来源于stack exchange,提问作者Mahan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 01:15:37