中午,有个货随手买的2块钱的彩票,尼玛中了540块,这是啥子*气运。稍微吐槽一下,现在开始正规的笔记录入。经常有朋友说为毛我的博客不更新了或者说更新的少了,为啥呢!一来自己懒了,没学习什么新的东西,二来平常琐事多,于是这个博客更新就少了。FMX目前已经更新了好几个版本,甚至连属性方法都改过了,从以前刚出来时候的拼音输入法支持都有Bug,到现在基本上比较流畅运行,说明了进步还是挺大的,那么学习这个东西也应该可以是提上日程了,或许不久的将来会用到。
FMX是一套UI类库,就相当于以前的VCL,但是相比VCL来说,支持了跨平台,同时也直接内部支持了各种特效动画甚至3D的效果,如果效率性能上来了,这个类库还是很有前景的。这次我主要学习的就是一个FMX窗体是如何绘制并显示出来的,相比较于VCL,有哪些不同之处,以及一个FMX程序的启动运转的最简单剖析。至于各种特效,动画,以及3D等,以后再慢慢的去啃食,贪多嚼不烂。
新建一个FireMonkey的HD Desktop Application,IDE会自动建立一个工程,进入工程,可以发现FMX的程序,各个单元前面都有FMX的名称空间进行标记,FMX的Form,Application以及各种控件都已经是重写的了,而不是VCL的那一套继承体系,至于这个FMX的整体继承结构,其他的都有介绍说明,可以去网上搜索,这里不记录。我这里主要剖析一个程序的运行以及显示。程序运行,首要的第一个要看的就是Application这个对象,这个对象在FMX.Forms中,一个FMX工程运行的最简单的工程代码结构为
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end.
这个基本代码和VCL模式差不多,那么关键就是在于内部的实现了,由于FMX的窗体也不是以前的VCL,所以我们先看看这个CreateForm,这个CreateForm的代码很有意思,也会很蛋疼的
procedure TApplication.CreateForm(const InstanceClass: TComponentClass; var Reference);
var
Instance: TComponent;
RegistryItems : TFormRegistryItems;
RegItem : TFormRegistryItem;
begin
if FRealCreateFormsCalled then
begin
Instance := TComponent(InstanceClass.NewInstance);
TComponent(Reference) := Instance;
try
Instance.Create(Self);
for RegItem in FCreateForms do
if RegItem.InstanceClass = InstanceClass then
begin
RegItem.Instance := Instance;
RegItem.Reference := @Reference;
end;
except
TComponent(Reference) := nil;
raise;
end;
end
else
begin
SetLength(FCreateForms, Length(FCreateForms) + );
FCreateForms[High(FCreateForms)] := TFormRegistryItem.Create;
FCreateForms[High(FCreateForms)].InstanceClass := InstanceClass;
FCreateForms[High(FCreateForms)].Reference := @Reference; // Add the form to form registry in case RegisterFormFamily will not be called
if FFormRegistry.ContainsKey(EmptyStr) then
begin
RegistryItems := FFormRegistry[EmptyStr];
end
else begin
RegistryItems := TFormRegistryItems.Create;
FFormRegistry.Add(EmptyStr, RegistryItems);
end; RegistryItems.Add(FCreateForms[High(FCreateForms)]);
end;
end;
如何,很有意思吧,不知道是为啥这样写。这个代码的意思是没有真正创建主窗体之前都只会产生一个窗体注册项保存到注册的一个内部数组中,然后Run之后Application会调用
RealCreateForms函数进行窗体创建,此时FRealCreateFormsCalled才会为True,然后使用Application.CreateForm创建的窗体的第二个参数才会返回实际的窗体对象,否则没有Run的时候,使用本方法并不会创建对象,也就是说我们以前在VCL中的工程代码中可以写
begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Form1.Caption := 'VCL窗体';//这句代码在VCL可以,FMX中此时Form1并未创建,所以这个属性赋值会出错!
Application.Run;
end.
但是在 FMX窗体中,我们在Run之前使用Form1对象就会出错了。这点事切记的。
然后看Run方法,这个代码写的很简洁
procedure TApplication.Run;
var
AppService: IFMXApplicationService;
begin
{$IFNDEF ANDROID}
AddExitProc(DoneApplication);
{$ENDIF}
FRunning := True;
try
if TPlatformServices.Current.SupportsPlatformService(IFMXApplicationService, IInterface(AppService)) then
AppService.Run;
finally
FRunning := False;
end;
end;
主要就是
if TPlatformServices.Current.SupportsPlatformService(IFMXApplicationService, IInterface(AppService)) then这个转换,然后调用AppService的Run。
这个是针对平台的。TPlatformServices在FMX.Platform单元中,可以知道这个Current实际上就是一个单例的TPlatformServices对象,然后SupportsPlatformService进行
IFMXApplicationService接口查询转换。那么是神马时候建立的这个
SupportsPlatformService并且注册进这个TPlatformServices中的呢,我们翻到这个单元最底部的Initialization中,可以发现会调用RegisterCorePlatformServices这个,这个就是注册这个平台服务接口的。,然后这个函数在Android,Windows,IOS等平台中都有,比如FMX.Platform.Win,FMX.Platform.Android,至于区分使用那个,使用的是编译预处理,看用户的Target选择的是什么平台就注册的什么函数。然后Windows下是TPlatformWin,Application也是在这个对象建立的时候建立,可以查看他的Create代码,然后建立AppHandle,使用CreateAppHandle函数,之后建立窗体,因为FMX中唯有一个窗体是类似于VCL WinControl的有句柄的GDI对象,所以那么必须会使用CreateWindow进行窗口建立,然后消息代理到Application上去,FMX中在Win下,这个也是必须的,所以找到对应的方法,就是CreateHandle这个,这个函数调用的实际上是TPlatformWin的CreateWindow,然后返回一个Handle,这个Handle不在是VCL中的一个DWORD的句柄值,而是一个TWindowHandle对象了。在这个创建窗体过程中,可以发现他直接将窗体的消息处理过程指定到了WndProc这个函数过程,所有的消息处理都由这个过程进行。中间的消息处理过程就不说了,下面说一个窗体以及窗体上的控件的绘制显示过程.
因为FMX窗体上的所有控件显示对象都是使用的窗体本身的设备场景句柄,所以我们要看他的绘制显示过程直接看上面的Wndproc中的WM_Paint消息就行了。然后找到WMPaint方法如下:
function WMPaint(hwnd: HWND; uMsg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
var
i, rgnStatus: Integer;
Region: HRgn;
RegionSize: Integer;
RegionData: PRgnData;
R: TRect;
LForm: TCommonCustomForm;
UpdateRects, InPaintUpdateRects: TUpdateRects;
PS: TPaintStruct;
Wnd: Winapi.Windows.HWND;
PaintControl: IPaintControl;
begin
LForm := FindWindow(hwnd);
if LForm <> nil then
begin
Wnd := FormToHWND(LForm);
GetUpdateRect(Wnd, R, False);
Region := CreateRectRgn(R.Left, R.Top, R.Right, R.Bottom);
if Region <> then
try
rgnStatus := GetUpdateRgn(Wnd, Region, False);
if (rgnStatus = ) or (rgnStatus = ) then
begin
RegionSize := GetRegionData(Region, $FFFF, nil);
if RegionSize > then
begin
GetMem(RegionData, RegionSize);
try
RegionSize := GetRegionData(Region, RegionSize, RegionData);
if RegionSize = RegionSize then
begin
SetLength(UpdateRects, RegionData.rdh.nCount);
for i := to RegionData.rdh.nCount - do
begin
R := PRgnRects(@RegionData.buffer[])[i];
UpdateRects[i] := RectF(R.Left, R.Top, R.Right, R.Bottom);
end;
end;
finally
FreeMem(RegionData, RegionSize);
end;
if Supports(LForm, IPaintControl, PaintControl) then
begin
PaintControl.ContextHandle := BeginPaint(Wnd, PS);
try
if PlatformWin.FInPaintUpdateRects.TryGetValue(LForm.Handle, InPaintUpdateRects) and (Length(InPaintUpdateRects) > ) then
begin
// add update rects from FInPaintUpdateRects
for I := to High(InPaintUpdateRects) do
begin
SetLength(UpdateRects, Length(UpdateRects) + );
UpdateRects[High(UpdateRects)] := InPaintUpdateRects[I];
end;
end;
PaintControl.PaintRects(UpdateRects);
if PlatformWin.FInPaintUpdateRects.TryGetValue(LForm.Handle, InPaintUpdateRects) and (Length(InPaintUpdateRects) > ) then
begin
// paint second time - when Repaint called in painting
PlatformWin.FInPaintUpdateRects.TryGetValue(LForm.Handle, UpdateRects);
SetLength(InPaintUpdateRects, );
PlatformWin.FInPaintUpdateRects.AddOrSetValue(LForm.Handle, InPaintUpdateRects);
PaintControl.PaintRects(UpdateRects);
end;
PaintControl.ContextHandle := ;
finally
EndPaint(Wnd, PS);
end;
end;
end;
end;
finally
DeleteObject(Region);
end;
Result := DefWindowProc(hwnd, uMsg, wParam, lParam);
end
else
Result := DefWindowProc(hwnd, uMsg, wParam, lParam);
end;
这个代码稍微有一点点长,可以看到如果要绘制控件,基本上需要继承IPaintControl这个接口,然后绘制的时候会调用这个接口的PaintRects方法,所以我们然后就看Form的PaintRects方法,在TCustomForm.PaintRects中,从这里就可以看到所有窗体上显示的控件的绘制处理。调试可以发现基本上所有的控件的第一个都是一个TStyleobject的对象,这个主要是针对那个皮肤管理的用来绘制皮肤特效的,然后进入到控件的绘制,绘制控件的时候会触发TControl的PaintInternal方法。窗体的PaintRects会执行一个PareforPaint函数,这个函数主要是用来准备各个子控件的绘制。然后在执行本方法的时候,如果是TStyledControl会执行ApplyStyleLookup方法,就是绘制外观的。这个函数的主要目的是获得一个外观样式Control,然后设置成控件大小,然后插入到子控件列表作为第一个项目,然后绘制这个插入的外观样式。基本上是这么个显示概念,比如绘制Button,会在绘制的时候插入一个Button外观样式,然后绘制这个外观