• Skia4Dephi 的 Demo 程序界面架构分析


    前言

    Skia 是一个高效率的 2D 画图引擎,由 Google 开源出来。目前可以运行在 Android, iOS 和 Win32 上面。

    Skia4Delphi 是一个开源的 Delphi 控件,它封装了对 Skia 的调用,让 Delphi 的代码可以很简单地使用 Skia 来代替 Delphi 原本使用的系统库(比如 Windows 的 GDI)。尤其是在安卓下,Delphi 自己的支持 FireMonkey 的图形库效果不是太好,使用 Skia 代替,基本上不需要修改程序就能获得更好的效果。比如 TArc 在安卓上圆弧不平滑的问题直接解决。

    界面框架

    Skia4Delphi 带来的 Demo 程序,不管是在 Windows 桌面还是在手机上,运行效果都类似一个标准的手机 APP:多层菜单,一层一层点菜单进入,一层一层点回退按钮退出。

    Sika4Delphi 的 Demo 程序包括 2 个,一个是 For VCL 的,一个是 For FMX 的。For VCL 的当然只能在 Windows 平台上运行,而 For FMX 的,当然可以编译发布到 Delphi FireMonkey 支持的多个不同的操作系统平台上,包括 Windows, MacOS, Android, iOS 。

    界面框架解析

    仔细查看它的 Demo 程序的代码,发现 VCL 的程序和 FMX 的程序,界面框架的实现方式基本相同,就是把界面的主要元素做成一个基本的 Form,其它 Form 都从这个 Form 继承。

    这种类似手机 APP 的界面,基本元素包括:顶部一个工具栏,工具栏的左侧是回退按钮(一个向左打尖头符号)。还包括一个底部工具栏,用于显示一些提示信息。中间则是主要的显示区域。

    不管界面是第几层,都采用这个框架。每一层,可能有进入下一层的按钮,点击后显示下一层。每一层的回退按钮,点击后本层消失,显示上一层的界面。

    抽象出共性后,Skia4Delphi 的 Demo 程序,将这些共性,实现为一个基本的 Form,这里它取名叫做:【TfrmBase】,如下:

    TfrmBase = class(TForm)

    这个 Form 有一个最底层的托盘控件,在 VCL 的 Demo 里面它是一个 TPanel,在 FMX 的 Demo 里面,它是一个 TLayout.。所有的界面控件都摆放在这个托盘上。基于 Delphi 的代码框架,所谓的摆放,就是:某个界面元素比如 Label1.Parent := 托盘控件。

    在此基础上,这个 TftmBase 还实现了几个类方法以及用于存放创建的 Form 实例的类变量。

    显示一个新的界面

    在设计期,每个新的界面(比如不同的菜单栏,或者显示不同内容的 Form),都是一个 Form,这些 Form 都继承自 TfrmBase。因此它们都包含了一个顶部的工具栏以及工具栏左侧的回退按钮。这个工具栏和回退按钮,又都摆放在最底层的托盘控件上,以它的 VCL Demo 程序为例,摆放在名字叫做 pnlContent 的一个 TPanel 上面。

    不管在哪个 Form 里面点击菜单按钮需要显示一个新的 Form,都是调用 TfrmBase 的方法。因此,不管在哪个 Form 里面需要显示下一层的 Form,都无需额外写代码。随便挑一个按钮菜单的代码看看:

    1. procedure TfrmControls.pnlTSkSVGClick(Sender: TObject);
    2. begin
    3. ChildForm.Show;
    4. end;

    上述代码中, ChildForm 是一个函数:

    1. function TfrmBase.ChildForm<T>: T;
    2. var
    3. LSelfIndex: Integer;
    4. begin
    5. Assert(T.InheritsFrom(TfrmBase));
    6. LSelfIndex := FCreatedFormsList.IndexOf(Self);
    7. if (LSelfIndex >= 0) and (LSelfIndex < FCreatedFormsList.Count - 1) and (FCreatedFormsList[LSelfIndex + 1].ClassType = T) then
    8. Exit(T(FCreatedFormsList[LSelfIndex + 1]));
    9. Result := CreateForm;
    10. TfrmBase(Result).pnlContent.Align := TAlign.alClient;
    11. TfrmBase(Result).pnlBack.Visible := FShowingFormsList.Count > 0;
    12. FCreatedFormsList.Add(TfrmBase(Result));
    13. end;

    这个函数把需要显示的下一层的界面(一个 Form)创建好,并放到类变量 FCreatedFormsList 里面去方便管理。因此,不管在哪一层的界面里面创建了下一层的界面,它的 Form 实例的一个指针,都在 FCreatedFormsList 这个变量里面。

    泛型

    上述代码中比较有意思的是,这个被所有真正用于显示的 Form 继承的父类基础 Form,它是没办法知道子类的。按照传统的 Delphi 的语法,这里根本没法这样写。但这里它利用了新的泛型语法,这里直接使用了尖括号加 T,也就是泛型,返回也是 T。因此,真正调用时填入子类 Form 的类名,在父类里面,可以创建子类!

    上述代码中的 Show 方法的代码则是:

    1. procedure TfrmBase.Show;
    2. begin
    3. DoShow;
    4. end;
    5. //这个 DoShow 的代码:
    6. procedure TfrmBase.DoShow;
    7. begin
    8. pnlTip.Visible := not lblTipDescription.Caption.IsEmpty;
    9. if Self = Application.MainForm then
    10. begin
    11. pnlBack.Visible := False;
    12. FShowingFormsList.Add(Self);
    13. end
    14. else
    15. begin
    16. if FShowingFormsList.Count > 0 then
    17. FShowingFormsList.Last.pnlContent.Visible := False;
    18. FShowingFormsList.Add(Self);
    19. pnlContent.Parent := Application.MainForm; // 关键一行代码在这里
    20. end;
    21. inherited;
    22. pnlContentResize(nil);
    23. end;

    上述代码,真正显示下一级 Form 界面的代码就是这一行:

    pnlContent.Parent := Application.MainForm;

    把这个被创建的下一级 Form 里面作为底层托盘的 Panel 的 Parent 设置为主Form,因此这个 Panel 被摆到主 Form 上面了。

    显示界面的方法

    所以,这里显示的下一层 Form 的界面,并不是下一层 Form 直接 Show 出来,而是把它内部的 Panel 摆放到主 Form 上显示出来。因此,这里的每一层的 Form 在设计期存在,仅仅是用来作为一个界面模块的容器!在运行期创建它的实例以后,并没有显示它,而是把它当内容(作为托盘的 Panel)直接拿出来显示到主 Form 上面。

    回退按钮呢?

    那么,显示出来的界面,点了左上角的回退按钮,它做了什么操作?看代码:

    1. procedure TfrmBase.pnlBackClick(Sender: TObject);
    2. begin
    3. {$IF CompilerVersion >= 32}
    4. TThread.ForceQueue(nil,
    5. procedure
    6. begin
    7. CloseForm(Self); //调用 CloseForm 方法,传入的参数是当前这个 Form 的实例指针。
    8. end);
    9. {$ELSE}
    10. TThread.CreateAnonymousThread(
    11. procedure
    12. begin
    13. TThread.Queue(nil,
    14. procedure
    15. begin
    16. CloseForm(Self);
    17. end);
    18. end).Start;
    19. {$ENDIF}
    20. end;
    21. //真正的关闭 Form 的代码在这里,这是一个类方法
    22. class procedure TfrmBase.CloseForm(const AForm: TfrmBase);
    23. var
    24. LFormIndex: Integer;
    25. LAction: TCloseAction;
    26. I: Integer;
    27. begin
    28. LFormIndex := FShowingFormsList.IndexOf(AForm);
    29. if LFormIndex < 0 then
    30. Exit;
    31. LAction := TCloseAction.caFree;
    32. AForm.DoClose(LAction);
    33. if LAction = TCloseAction.caNone then
    34. Exit;
    35. if LFormIndex = 0 then
    36. Application.Terminate
    37. else
    38. begin
    39. LFormIndex := FCreatedFormsList.IndexOf(AForm);
    40. Assert(LFormIndex > -1);
    41. for I := FCreatedFormsList.Count - 1 downto LFormIndex do
    42. begin
    43. FCreatedFormsList[I].Free;
    44. FShowingFormsList.Remove(FCreatedFormsList[I]);
    45. FCreatedFormsList.Delete(I);
    46. end;
    47. FShowingFormsList.Last.pnlContent.Visible := True;
    48. end;
    49. end;

    在上面的类方法 CloseForm 里面,它根据这个 Form 的实例指针,在之前保存指针的类变量 FCreatedFormsList 里面,把指针从 List 里面删除掉,并且把这个 Form 对象释放掉。

    Form 对象被释放掉,它内部被用于显示界面元素的托盘 Panel 也就被释放掉了,被它遮住的底下的界面就显示出来了。

    到这里,我们发现,多层界面,其实就是多层 Panel 的层层堆叠。上面一层被释放掉,下面一层就显示出来了。

    但这些多个 Panel,并不是在设计期,都挤在一个 Form 里面进行设计,而是分别在不同的 Form 里面设计,这样可以做到界面的模块化,便于代码管理。

    题外话:我也见过非常复杂很多层显示界面的程序,所有界面控件都放在一个 Form 里面设计,运行期用代码决定显示哪个控件以及每个控件的显示位置。这样做,界面上控件太多,拥挤在一起,设计期非常难找到想要的控件,不是一种好的开发模式。

    总结

    虽然 Skia4Delphi 的 Demo 程序中,运行在 Windows 上的 VCL 程序以及可以运行在手机上的 FMX 程序,都采用了相同的类似手机 APP 的层叠界面的设计模式,但最实用的还是手机 APP,因为屏幕太小,不可能一屏里面还划分几个区域,只能是一层一层的界面叠加。因此,这样的设计模式,在做手机 APP 的时候非常值得学习。实际上它的 FMX 的 Demo 程序的框架,可以直接用于我们自己开发的手机 APP。

    之前我自己开发的手机 APP 虽然也是多层界面叠加,但管理界面堆叠层次的代码就显得比较罗嗦,没有这个 Skia4Delphi 的 Demo 程序这样设计良好的框架,并且将所有界面的显示和回退的代码都集中到一个基类里面,在其它界面里面无需重复。

    又及:

    这个 Skia4Delphi 的 Demo 程序,每个界面模块是在 Form 里面实现的。要加载一个新的界面就要创建一个 Form 实例。而 Form 占用的资源较多。这里可以采用 Frame 来作为界面设计的基础框架。我自己写代码测试了一下,上述构思,基于 Frame 来做,也是可以实现的。

  • 相关阅读:
    【C++】list常用接口
    Win11中Yolo V10安装过程记录
    Vue 项目部署到GitHub Pages
    如何定义set的比较函数?
    HyperLynx(十八)DDR(一)DDR简介和DDR的数据仿真
    SSM+Vue+Element-UI实现外卖点餐系统
    查询快递单号物流,自动识别出物流是否签收
    使用relocation解决包冲突导致的java.lang.LinkageError: loader constraint violation
    算法入门教程(六、试探)
    一键部署(dhcp、dns、pxe、raid、nfs+apache+expect、lvm、磁盘分区、监控资源)
  • 原文地址:https://blog.csdn.net/pcplayer/article/details/126592420