当前位置: 首页 > 工具软件 > Skia > 使用案例 >

Skia4Dephi 的 Demo 程序界面架构分析

邰棋
2023-12-01

前言

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,都无需额外写代码。随便挑一个按钮菜单的代码看看:

procedure TfrmControls.pnlTSkSVGClick(Sender: TObject);
begin
  ChildForm<TfrmTSkSVG>.Show;
end;

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

function TfrmBase.ChildForm<T>: T;
var
  LSelfIndex: Integer;
begin
  Assert(T.InheritsFrom(TfrmBase));
  LSelfIndex := FCreatedFormsList.IndexOf(Self);
  if (LSelfIndex >= 0) and (LSelfIndex < FCreatedFormsList.Count - 1) and (FCreatedFormsList[LSelfIndex + 1].ClassType = T) then
    Exit(T(FCreatedFormsList[LSelfIndex + 1]));
  Result := CreateForm<T>;
  TfrmBase(Result).pnlContent.Align := TAlign.alClient;
  TfrmBase(Result).pnlBack.Visible := FShowingFormsList.Count > 0;
  FCreatedFormsList.Add(TfrmBase(Result));
end;

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

泛型

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

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

procedure TfrmBase.Show;
begin
  DoShow;
end;

//这个 DoShow 的代码:
procedure TfrmBase.DoShow;
begin
  pnlTip.Visible := not lblTipDescription.Caption.IsEmpty;
  if Self = Application.MainForm then
  begin
    pnlBack.Visible := False;
    FShowingFormsList.Add(Self);
  end
  else
  begin
    if FShowingFormsList.Count > 0 then
      FShowingFormsList.Last.pnlContent.Visible := False;
    FShowingFormsList.Add(Self);
    pnlContent.Parent := Application.MainForm; // 关键一行代码在这里
  end;
  inherited;
  pnlContentResize(nil);
end;

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

pnlContent.Parent := Application.MainForm;

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

显示界面的方法

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

回退按钮呢?

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

procedure TfrmBase.pnlBackClick(Sender: TObject);
begin
  {$IF CompilerVersion >= 32}
  TThread.ForceQueue(nil,
    procedure
    begin
      CloseForm(Self);  //调用 CloseForm 方法,传入的参数是当前这个 Form 的实例指针。
    end);
  {$ELSE}
  TThread.CreateAnonymousThread(
    procedure
    begin
      TThread.Queue(nil,
        procedure
        begin
          CloseForm(Self);
        end);
    end).Start;
  {$ENDIF}
end;


//真正的关闭 Form 的代码在这里,这是一个类方法

class procedure TfrmBase.CloseForm(const AForm: TfrmBase);
var
  LFormIndex: Integer;
  LAction: TCloseAction;
  I: Integer;
begin
  LFormIndex := FShowingFormsList.IndexOf(AForm);
  if LFormIndex < 0 then
    Exit;
  LAction := TCloseAction.caFree;
  AForm.DoClose(LAction);
  if LAction = TCloseAction.caNone then
    Exit;
  if LFormIndex = 0 then
    Application.Terminate
  else
  begin
    LFormIndex := FCreatedFormsList.IndexOf(AForm);
    Assert(LFormIndex > -1);
    for I := FCreatedFormsList.Count - 1 downto LFormIndex do
    begin
      FCreatedFormsList[I].Free;
      FShowingFormsList.Remove(FCreatedFormsList[I]);
      FCreatedFormsList.Delete(I);
    end;
    FShowingFormsList.Last.pnlContent.Visible := True;
  end;
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 来做,也是可以实现的。

 类似资料: