中午,有個貨隨手買的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) + 1); 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 <> 0 then try rgnStatus := GetUpdateRgn(Wnd, Region, False); if (rgnStatus = 2) or (rgnStatus = 3) then begin RegionSize := GetRegionData(Region, $FFFF, nil); if RegionSize > 0 then begin GetMem(RegionData, RegionSize); try RegionSize := GetRegionData(Region, RegionSize, RegionData); if RegionSize = RegionSize then begin SetLength(UpdateRects, RegionData.rdh.nCount); for i := 0 to RegionData.rdh.nCount - 1 do begin R := PRgnRects(@RegionData.buffer[0])[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) > 0) then begin // add update rects from FInPaintUpdateRects for I := 0 to High(InPaintUpdateRects) do begin SetLength(UpdateRects, Length(UpdateRects) + 1); UpdateRects[High(UpdateRects)] := InPaintUpdateRects[I]; end; end; PaintControl.PaintRects(UpdateRects); if PlatformWin.FInPaintUpdateRects.TryGetValue(LForm.Handle, InPaintUpdateRects) and (Length(InPaintUpdateRects) > 0) then begin // paint second time - when Repaint called in painting PlatformWin.FInPaintUpdateRects.TryGetValue(LForm.Handle, UpdateRects); SetLength(InPaintUpdateRects, 0); PlatformWin.FInPaintUpdateRects.AddOrSetValue(LForm.Handle, InPaintUpdateRects); PaintControl.PaintRects(UpdateRects); end; PaintControl.ContextHandle := 0; 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外觀樣式,然後繪制這個外觀