简介:这份资源面向使用 Delphi 与 Unigui 框架开发跨平台应用的开发者,聚焦图片轮播中「左推移」动画效果的实现,适合已掌握 VCL/FMX 基础、希望提升界面交互能力的中级开发者参考。压缩包共 26 个文件,约 256KB,以 pas 源码、dfm 窗体设计文件、dproj 工程文件为主,另含 jpg 示例图片、log 日志、html 说明及 js 脚本等,覆盖从工程配置到界面布局的完整结构。资源围绕 TImage 加载图片、TTimer 控制自动切换、手势识别触发手动滑动等核心思路展开,并涉及图片数组与索引管理、循环播放、布局适配及跨平台触摸交互等要点,可帮助读者理解动画过渡与事件驱动编程的配合方式。目前已有 338 人学习下载,适合作为 Unigui 轮播组件的入门范例,通过研读源码与窗体设计快速掌握左推移动画的实现路径。
1. 拆开这个 Delphi 图片轮播包:左推移效果到底怎么落地
做 Delphi 桌面项目的人大概都遇到过这种需求:客户要在主界面顶部放一组产品图或者广告位,要求自动播放、手动切换、切换时带点动效,最好还能循环。用 TImage 加 Timer 硬切当然能跑,但视觉上就是一张一张地闪,客户看一眼就说“太生硬”。这个unigui_图片轮播_左推移.rar就是冲着这个场景来的——它把图片轮播做成了“左推移”的动效,也就是新图从右侧推入、旧图向左退出,视觉上像一条横向传送带在走。
它适合两类人:一类是做 UniGUI 后台管理系统、需要在面板里嵌广告位或看板图的 Delphi 开发者;另一类是手上有现成 VCL 项目、想把这套轮播逻辑移植过去的人。包里是源码形式,不是编译好的组件,所以你得能看懂 Delphi 的 Canvas 绘制和定时器调度。如果你只是想拖个控件就完事,这个包会让你失望;但如果你想搞清楚“左推移”这种动效在 Delphi 里到底怎么用最少的重绘代价实现,它值得拆一遍。
2. 左推移的绘制原理:为什么不用 TImage 直接换图
2.1 双缓冲与偏移量:动效的核心不是换图,是画图
很多人第一反应是放两个 TImage,一个显示当前图,一个显示下一张,然后用 Timer 改 Left 属性做位移。这个思路在 VCL 里能跑,但在 UniGUI 的 Web 环境下会翻车——UniGUI 的控件位置变更会触发一次完整的 AJAX 回传,每移动一像素就回传一次,浏览器直接卡成幻灯片。所以这个包没有走控件位移的路子,而是走 Canvas 绘制。
核心逻辑是:准备一块和轮播区域等宽的离屏位图(双缓冲),每一帧根据当前偏移量FOffset把“当前图”和“下一张图”分别画到这块位图的左右两侧,然后一次性把整块位图刷到界面上。偏移量从 0 递增到轮播区宽度,视觉上就是新图从右边推进来、旧图往左边退出去。整个过程只触发一次界面刷新,而不是每像素一次。
// 双缓冲绘制核心:把两张图按偏移量拼到同一块位图上 procedure TSlideBanner.PaintFrame(ACanvas: TCanvas; AOffset: Integer); var LWidth: Integer; begin LWidth := Self.Width; // 先清空背景,避免残影 ACanvas.Brush.Color := FBackColor; ACanvas.FillRect(Rect(0, 0, LWidth, Self.Height)); // 当前图:向左偏移 AOffset 像素 if Assigned(FCurrentBitmap) then ACanvas.Draw(-AOffset, 0, FCurrentBitmap); // 下一张图:从右侧 LWidth - AOffset 处开始画 if Assigned(FNextBitmap) then ACanvas.Draw(LWidth - AOffset, 0, FNextBitmap); end;这段代码里AOffset是关键参数,取值范围是 0 到LWidth。当AOffset = 0时,画面上只有当前图;当AOffset = LWidth时,当前图完全移出左侧,下一张图正好占满整个区域。中间任何一帧都是两张图各占一部分。FBackColor是背景色,用来填充两张图之间的缝隙——如果两张图宽度不一致,这个填充色就会露出来,所以后面避坑章节会专门讲图片尺寸统一的问题。
2.2 定时器调度:帧率、步长和总时长的换算关系
偏移量每帧增加多少,决定了动效的快慢。这个包用的是 TTimer,间隔设成 15 毫秒,每帧偏移量增加FStep。假设轮播区宽度是 600 像素,你希望整个推移过程在 400 毫秒内完成,那么总帧数就是 400 / 15 ≈ 27 帧,每帧步长就是 600 / 27 ≈ 22 像素。
// 定时器回调:每帧推进偏移量,到达终点后切换图片 procedure TSlideBanner.OnTimerTick(Sender: TObject); begin Inc(FOffset, FStep); if FOffset >= Self.Width then begin // 推移结束,把下一张图扶正为当前图 FOffset := 0; FCurrentBitmap.Assign(FNextBitmap); LoadNextBitmap; // 预加载再下一张 FTimer.Enabled := False; // 停顿一段时间后再启动下一轮 FHoldTimer.Enabled := True; end; PaintFrame(Self.Canvas, FOffset); end;这里有个容易忽略的点:FStep不能整除Self.Width时,最后一帧的偏移量会超过宽度。代码里用>=判断并直接归零,避免了越界绘制。但如果你把FStep设得太大,比如 100 像素一帧,整个动效只有 6 帧,看起来就是一跳一跳的,失去了“推移”的顺滑感。我一般会把单帧步长控制在 15 到 30 像素之间,配合 15 毫秒的定时器间隔,视觉上最舒服。
2.3 图片预加载:别在动效开始时才去读文件
左推移动效对时序很敏感。如果你在FOffset归零的那一刻才去加载下一张图,磁盘 IO 或者网络请求的延迟会让画面卡住一拍。这个包的做法是维护一个图片队列,当前图显示的同时,下一张图已经在内存里准备好了。LoadNextBitmap只负责把队列里的下一张读进FNextBitmap,不参与绘制。
// 预加载:从文件列表里取下一张,读入内存位图 procedure TSlideBanner.LoadNextBitmap; var LFileName: string; begin if FFileList.Count = 0 then Exit; LFileName := FFileList[FCurrentIndex]; FCurrentIndex := (FCurrentIndex + 1) mod FFileList.Count; try FNextBitmap.LoadFromFile(LFileName); except // 单张图加载失败不能中断轮播,跳过继续 FNextBitmap.Assign(nil); end; end;FFileList是一个 TStringList,存的是图片的完整路径。FCurrentIndex用取模运算实现循环,这样图片播完最后一张会自动回到第一张。try...except那段是血泪经验——只要有一张图路径写错或者文件损坏,没有异常保护的话整个轮播就停在那里了,用户看到的就是一张死图。
3. 在 UniGUI 里接入轮播:从放控件到跑通动效
3.1 创建 UniGUI 工程并放置绘制容器
UniGUI 的页面结构和 VCL 不一样,你不能直接往 Form 上画。常见做法是放一个TUniImage或者TUniPanel作为绘制容器,然后在这个容器的OnPaint事件里调用轮播的绘制逻辑。如果你用的是TUniImage,把它的Stretch设为 False,避免它自己缩放图片干扰我们的偏移计算。
// 在 UniGUI 窗体里初始化轮播组件 procedure TMainForm.UniFormCreate(Sender: TObject); begin FSlideBanner := TSlideBanner.Create(Self); FSlideBanner.Parent := UniPanel1; // 挂到面板上 FSlideBanner.Align := alClient; // 填满面板 FSlideBanner.Interval := 15; // 定时器间隔,毫秒 FSlideBanner.Step := 20; // 每帧偏移像素 FSlideBanner.HoldTime := 3000; // 每张图停留时间 FSlideBanner.LoadImages('D:\banner\'); // 加载目录下所有图片 FSlideBanner.Start; end;Interval和Step这两个参数要配合着调。Interval越小、Step越小,动效越细腻,但 CPU 占用会上去。在 UniGUI 的 Web 环境下,Interval低于 10 毫秒意义不大,因为浏览器端的渲染帧率本身就在 16 毫秒左右。HoldTime是每张图推完之后停留多久再开始下一轮,设成 3000 就是停 3 秒。
3.2 图片目录规范与加载顺序控制
LoadImages这个方法接收一个目录路径,内部用FindFirst/FindNext遍历出所有.jpg、.png、.bmp文件。但这里有个坑:文件系统的遍历顺序是不确定的,不同机器上可能不一样。如果你对播放顺序有要求,得在文件名上做文章,比如01.jpg、02.jpg这样编号,然后在加载后对FFileList做一次排序。
// 按文件名排序,保证播放顺序可控 procedure TSlideBanner.LoadImages(const ADir: string); var LSearch: TSearchRec; begin FFileList.Clear; if FindFirst(IncludeTrailingPathDelimiter(ADir) + '*.jpg', faAnyFile, LSearch) = 0 then begin repeat if (LSearch.Attr and faDirectory) = 0 then FFileList.Add(IncludeTrailingPathDelimiter(ADir) + LSearch.Name); until FindNext(LSearch) <> 0; FindClose(LSearch); end; FFileList.Sort; // 按文件名升序,01 在 02 前面 FCurrentIndex := 0; LoadNextBitmap; end;FFileList.Sort用的是默认的字符串排序,对于01.jpg到10.jpg这种命名是有效的。但如果你用1.jpg、2.jpg、10.jpg,字符串排序会把10.jpg排在2.jpg前面。解决办法是文件名统一补零,或者自己写一个按数字部分排序的比较函数。我一般直接在命名规范里要求补零,省得后面改代码。
3.3 动效触发与手动切换的共存
自动轮播跑起来之后,用户可能想手动点一下切换到下一张。这时候不能直接改FCurrentIndex然后重启定时器,因为动效可能正在半路上。正确的做法是设一个标志位,让当前动效走完,再立即开始下一轮,而不是等HoldTime结束。
// 手动切换:不打断当前动效,走完后立即进入下一张 procedure TSlideBanner.NextSlide; begin FManualNext := True; // 标记手动触发 if not FTimer.Enabled then begin // 当前没有动效在跑,直接启动 FTimer.Enabled := True; FHoldTimer.Enabled := False; end; // 如果动效正在跑,等它自然结束后会检查 FManualNext end;在OnTimerTick的动效结束分支里,判断FManualNext是否为 True,如果是就跳过HoldTimer直接开始下一轮,然后把标志位清掉。这样手动切换的响应是“当前这张推完立刻推下一张”,而不是“等三秒再推”,体验上更跟手。
4. 避坑与排查:左推移轮播最容易翻车的五个地方
4.1 图片尺寸不统一导致推移时露出背景色
现象:轮播过程中,两张图交界处出现一条背景色的竖条,或者图片边缘有空白。
原因:PaintFrame是按固定宽度绘制的,如果当前图和下一张图的宽度不一致,窄的那张图旁边就会露出FBackColor。高度不一致时,上下也会露。
解决:在LoadNextBitmap里加一道尺寸校验,把加载进来的图统一缩放到轮播区尺寸。用Bitmap.Width和Self.Width比较,不一致就调用StretchDraw或者手动缩放。注意缩放会损失画质,所以最好在素材准备阶段就把图片裁成统一尺寸。
4.2 定时器间隔设成 1 毫秒导致界面无响应
现象:轮播跑起来后,整个 UniGUI 页面点什么都卡,浏览器标签页 CPU 占用飙升。
原因:TTimer的Interval设得太小,比如 1 毫秒,Delphi 会尽可能快地触发OnTimer,而每次触发都调用PaintFrame做一次全区域重绘。在 Web 环境下,每次重绘都可能触发一次 AJAX 回传,浏览器根本来不及渲染。
解决:Interval不要低于 10 毫秒,推荐 15 到 20 毫秒。同时确保PaintFrame里没有多余的Repaint或Invalidate调用,一帧只刷一次。
4.3 图片文件被占用导致加载失败
现象:某一张图始终显示不出来,轮播到它的时候画面空白或者直接跳过。
原因:FNextBitmap.LoadFromFile打开文件时,如果这个文件正被其他程序(比如图片编辑器、同步盘客户端)以独占方式打开,加载就会抛异常。代码里虽然有try...except,但异常被吞掉后FNextBitmap是空的,绘制时就是一片背景色。
解决:在异常处理里加日志,记录是哪张图加载失败。另外可以在加载前先判断文件是否存在、是否可读。如果图片放在网络盘或者同步盘目录里,建议先复制到本地临时目录再加载。
4.4 动效进行中窗口大小改变导致偏移量错乱
现象:轮播跑到一半时用户拖动了浏览器窗口大小,或者 UniGUI 面板因为布局变化改了宽度,动效就乱了,图片位置对不上。
原因:FOffset的终点是Self.Width,这个值在动效开始时是固定的。如果中途Self.Width变了,FOffset还在按旧宽度递增,就会提前或延后到达终点。
解决:在PaintFrame里不要用缓存的宽度,每次都从Self.Width实时读取。同时在OnResize事件里判断:如果动效正在跑,直接结束当前动效,把FOffset归零,重新开始一轮。
4.5 多实例共存时定时器互相干扰
现象:一个页面上放了两个轮播组件,结果一个跑一个不跑,或者两个的节奏完全同步了。
原因:如果TTimer是全局的或者挂在 Form 上而不是每个轮播实例自己持有,多个实例就会共享同一个定时器。一个实例启动定时器,另一个实例的OnTimer也被触发,但它的FOffset可能还是 0,就出现了“一个动一个不动”的怪象。
解决:每个TSlideBanner实例在Create时自己TTimer.Create(Self),把 Owner 设为自己。这样每个实例有独立的定时器,互不干扰。销毁时定时器随 Owner 一起释放,不会泄漏。
5. 进阶技巧:把左推移改成可配置的动效引擎
跑通基本轮播之后,你会发现“左推移”只是动效的一种。如果客户哪天说要“淡入淡出”或者“从下往上推”,你不想重写整个绘制逻辑。这个包的PaintFrame其实可以抽象成一个动效接口,把偏移量的计算方式抽出来。
// 动效类型枚举:不同动效只改偏移计算方式 type TSlideEffect = (seLeftPush, seRightPush, seTopPush, seFade); // 根据动效类型计算当前帧的绘制参数 procedure TSlideBanner.CalcFrameParams(AProgress: Double; out AOffsetX, AOffsetY: Integer; out AAlpha: Byte); begin case FEffect of seLeftPush: begin AOffsetX := Round(Self.Width * AProgress); AOffsetY := 0; AAlpha := 255; end; seRightPush: begin AOffsetX := -Round(Self.Width * AProgress); AOffsetY := 0; AAlpha := 255; end; seTopPush: begin AOffsetX := 0; AOffsetY := Round(Self.Height * AProgress); AAlpha := 255; end; seFade: begin AOffsetX := 0; AOffsetY := 0; AAlpha := Round(255 * (1 - AProgress)); end; end; end;AProgress是动效进度,从 0 到 1。左推移就是AOffsetX从 0 变到Self.Width;右推移是反向;上推移改AOffsetY;淡入淡出则不动位置,只改透明度。AAlpha参数需要PaintFrame支持 Alpha 混合绘制,Delphi 里可以用Graphics32库或者自己写AlphaBlend调用。
验证动效是否正常,我一般会做一个“慢放模式”:把Interval设成 100 毫秒,Step设成 5 像素,这样整个推移过程拉长到几秒钟,肉眼能看清每一帧的绘制是否正确。确认没问题后再把参数调回正常值。这个习惯帮我省了很多次“看起来卡了一下但不知道哪一帧出问题”的排查时间。
从那以后我每次接轮播类的需求,都强制先把动效参数做成可配置的,再开始写绘制逻辑。因为客户改主意的速度永远比你重写代码快,把偏移计算和绘制分离之后,改一个枚举值就能换一种动效,比从头改PaintFrame省事得多。希望帮到你。
本文还有配套的精品资源,点击获取