Од тема во магазинот до проектна пракса
Соодветни страници за услуги и технички информации поврзани со објавата
Кој во Delphi FireMonkey мора да поддржува повеќе форм-фактори, брзо ќе наиде на Responsive Layouts FMX – и исто така брзо на мешавина од Align-каскади, скриени Layout-контейнери и Designer-работарaундови што при следната промена на DPI или ротација пропаѓаат. Во веќе развиени бизнис-клиенти тоа е особено проблематично: UI-то се одржува и надградува, тимовите се менуваат, и одеднаш логиката е поврзана со визуелни детали.
Суштината на проблемот: FireMonkey нуди многу градежни блокови (на пр. Align, Anchors, TScaledLayout, TFlowLayout, TGridPanelLayout), но нема „нативен“ Breakpoint-систем како во web‑от. Иако може да се реагира на промени на димензии, без јасна архитектура тоа завршува со „if Width < … then …“ распрснато по многу форми.
Овој напис покажува еден Layout-Router: мала компонента што централизирано ги управува Breakpoint‑ите и ги префрла Controls (или цели блокови на layout) меѓу подготвени Slots. Целта: состојбите да останат сочувани, кодот да биде одржлив, а работите како ротација, вложени layout‑и и реентрантност да бидат ублажени. Понатаму следуваат неколку помалку очигледни трикови кои во практика го прават пресвртниот момент помеѓу „работи во демо“ и „работи стабилно во производство”.
Зошто Breakpoints во FMX се поинакви од веб‑от
Во web‑распоредите Breakpoint‑ите обично се декларативни (CSS Media Queries). Во FMX одлуките за распоред често се империјативни во време на извршување: при OnResize се менува распоредот. Покрај тоа, постојат платформски специфики:
- Device‑Pixel vs. логички пиксели:
ClientWidth/ClientHeightсе во логички единици (зависно од скалирањето). Промени на DPI (на пр. Windows Per-Monitor-DPI) можат да предизвикаат повторно пресметување на layout‑от без да се измени „физички“ нешто. - Ротација и Safe Areas: Мобилните платформи даваат Insets (Notch/Safe Area) – зависно од OS и уредот. „Breakpoint само по ширина“ често е премногу поедноставен, бидејќи употребливата површина може да биде помала од чистата големина на прозорецот.
- Layout‑Pass: FireMonkey ги пресметува layout‑ите во фази. Ако се менува Parent/Align во погрешен момент, се јавуваат нусефекти (на пр. повеќекратно рефлоу или трепкање на димензии).
Layout‑Router го адресира ова така што (1) го декуплира „кога“ (Resize/Scale/Rotation) од „како“ (правила за layout) и (2) ги концентрира правилата на едно место. За техничките лидери најважниот ефект е јасен и проверлив центар за донесување одлуки наместо многу локални исклучоци.
Архитектура: Layout‑Router со Slots наместо создавање на Controls
Чистиот трик за FMX: не динамично да се создаваат нови Controls, туку постојните Controls да се префрлаат меѓу Slots. Slot е едноставно контејнер (на пр. TLayout) кој го репрезентира еден дел од UI‑то: Sidebar, Toolbar, Content, Footer, Details‑Pane.
Предности во индивидуален корпоративен софтвер:
- Состојбите остануваат сочувани (полето за уредување, позицијата на скрол, селектираните елементи), бидејќи инстанците не се реконструираат.
- Помал ризик од дупли поврзувања на Events, Timers или Bindings.
- Правилата за layout стануваат видливи: „кој блок е во кој Slot“ може да се следи по секој Breakpoint и да се ревидира.
Важно за практиката: сегментирајте UI-блокови доволно грубо. Ако префрлите 30 поединечни контроли, самата листа на рути станува извор на грешки. Подобри се контејнери како layFilterBar, layNavigation, layResultList, layDetails.
Изворен исечок: Breakpoint-Router за Responsive Layouts во FMX
Следниот код е замислен како помошна единица која можете да ја користите во FMX-формите. Тој пресметува еден Breakpoint (XS/SM/MD/LG/XL) и прикачува дефинирани контроли во дефинирани слот-контејнери. Важни детали:
- Debounce преку
TThread.ForceQueue: повеќе Resize-евенти се консолидираат во едно ажурирање (помалку треперење на UI, помалку reflow-цикли). - Re-Entrancy-Schutz: ажурирањето на layout често само предизвикува повторни Resize/Layout-настани.
- Опционално: ориентација (Portrait/Landscape) може да се вклучи во логиката на Breakpoint-от.
unit NB.FMX.LayoutRouter;
interface
uses
System.Classes, System.SysUtils, System.Types, System.Generics.Collections,
FMX.Types, FMX.Controls;
type
TNBLayoutBreakpoint = (bpXS, bpSM, bpMD, bpLG, bpXL);
// Мапирање: кое Control треба да се постави во кој Slot (контeјнер) за одреден Breakpoint.
TNBRoute = record
Control: TControl;
TargetSlot: TControl; // типично TLayout или TPresentedControl
Align: TAlignLayout;
end;
TNBRouteList = TList<TNBRoute>;
TNBGetBreakpointEvent = reference to function(const AClientSize: TSizeF): TNBLayoutBreakpoint;
TNBLayoutRouter = class(TComponent)
private
FRoot: TControl;
FPending: Boolean;
FUpdating: Boolean;
FCurrent: TNBLayoutBreakpoint;
FOnGetBreakpoint: TNBGetBreakpointEvent;
FRoutes: TObjectDictionary<Integer, TNBRouteList>;
function KeyOf(const ABp: TNBLayoutBreakpoint): Integer;
procedure RootResized(Sender: TObject);
procedure ApplyPending;
procedure ApplyRoutes(const ABp: TNBLayoutBreakpoint);
function DefaultBreakpoint(const AClientSize: TSizeF): TNBLayoutBreakpoint;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
procedure AttachRoot(const ARoot: TControl);
procedure DefineRoute(const ABp: TNBLayoutBreakpoint; const AControl, ASlot: TControl;
const AAlign: TAlignLayout = TAlignLayout.Client);
procedure Invalidate; // рачно повторно пресметување
property Current: TNBLayoutBreakpoint read FCurrent;
property OnGetBreakpoint: TNBGetBreakpointEvent read FOnGetBreakpoint write FOnGetBreakpoint;
end;
implementation
{ TNBLayoutRouter }
constructor TNBLayoutRouter.Create(AOwner: TComponent);
begin
inherited;
FRoutes := TObjectDictionary<Integer, TNBRouteList>.Create([doOwnsValues]);
FCurrent := bpMD;
end;
destructor TNBLayoutRouter.Destroy;
begin
if Assigned(FRoot) then
FRoot.OnResize := nil;
FRoutes.Free;
inherited;
end;
procedure TNBLayoutRouter.AttachRoot(const ARoot: TControl);
begin
if FRoot = ARoot then
Exit;
if Assigned(FRoot) then
FRoot.OnResize := nil;
FRoot := ARoot;
if Assigned(FRoot) then
FRoot.OnResize := RootResized;
Invalidate;
end;
procedure TNBLayoutRouter.DefineRoute(const ABp: TNBLayoutBreakpoint; const AControl,
ASlot: TControl; const AAlign: TAlignLayout);
var
LKey: Integer;
LList: TNBRouteList;
LRoute: TNBRoute;
begin
if (AControl = nil) or (ASlot = nil) then
raise EArgumentNilException.Create('Control/Slot не смеат да бидат nil');
LKey := KeyOf(ABp);
if not FRoutes.TryGetValue(LKey, LList) then
begin
LList := TNBRouteList.Create;
FRoutes.Add(LKey, LList);
end;
LRoute.Control := AControl;
LRoute.TargetSlot := ASlot;
LRoute.Align := AAlign;
LList.Add(LRoute);
end;
function TNBLayoutRouter.KeyOf(const ABp: TNBLayoutBreakpoint): Integer;
begin
Result := Ord(ABp);
end;
procedure TNBLayoutRouter.RootResized(Sender: TObject);
begin
Invalidate;
end;
procedure TNBLayoutRouter.Invalidate;
begin
if (FRoot = nil) or FUpdating then
Exit;
// Дебаунс: само еднаш по Message-Loop да се примени
if FPending then
Exit;
FPending := True;
TThread.ForceQueue(nil,
procedure
begin
ApplyPending;
end);
end;
procedure TNBLayoutRouter.ApplyPending;
var
LBp: TNBLayoutBreakpoint;
LSize: TSizeF;
begin
if (FRoot = nil) then
Exit;
if not FPending then
Exit;
FPending := False;
LSize := TSizeF.Create(FRoot.Width, FRoot.Height);
if Assigned(FOnGetBreakpoint) then
LBp := FOnGetBreakpoint(LSize)
else
LBp := DefaultBreakpoint(LSize);
if LBp = FCurrent then
Exit;
ApplyRoutes(LBp);
FCurrent := LBp;
end;
procedure TNBLayoutRouter.ApplyRoutes(const ABp: TNBLayoutBreakpoint);
var
LList: TNBRouteList;
LRoute: TNBRoute;
begin
if FUpdating then
Exit;
FUpdating := True;
try
if not FRoutes.TryGetValue(KeyOf(ABp), LList) then
Exit;
// Внимание: смена на Parent ја менува Z-Order.
// Ако редоследот е важен, повикајте DefineRoute во саканиот редослед.
for LRoute in LList do
begin
if (LRoute.Control.Parent <> LRoute.TargetSlot) then
LRoute.Control.Parent := LRoute.TargetSlot;
// Align поставувајте го откако ќе го поставите Parent, инаку Bounds може да бидат поинаку толкувани.
LRoute.Control.Align := LRoute.Align;
LRoute.Control.Visible := True;
end;
finally
FUpdating := False;
end;
end;
function TNBLayoutRouter.DefaultBreakpoint(const AClientSize: TSizeF): TNBLayoutBreakpoint;
var
W: Single;
begin
W := AClientSize.cx;
// Breakpoints намерно груби, бидејќи целните платформи на FMX значително варираат.
if W < 480 then Exit(bpXS);
if W < 768 then Exit(bpSM);
if W < 1024 then Exit(bpMD);
if W < 1440 then Exit(bpLG);
Result := bpXL;
end;
end.
Како да го користите рутерот во форма
Вие дефинирате слотови како TLayout (на пр. layTop, layLeft, layContent) и потоа за секој Breakpoint регистрирате каде кои блокови да стојат. Типично е дека страничната лента и панелот со детали во малите Breakpoint-и се префрлаат еден под друг.
procedure TFrmMain.FormCreate(Sender: TObject);
begin
FRouter := TNBLayoutRouter.Create(Self);
FRouter.AttachRoot(LayoutRoot); // z. B. ein TLayout, das Client-aligned ist
// XS: alles untereinander
FRouter.DefineRoute(bpXS, layToolbar, slotTop, TAlignLayout.Top);
FRouter.DefineRoute(bpXS, laySidebar, slotContent, TAlignLayout.Top);
FRouter.DefineRoute(bpXS, layDetails, slotContent, TAlignLayout.Top);
FRouter.DefineRoute(bpXS, layMain, slotContent, TAlignLayout.Client);
// MD: Sidebar links, Details rechts
FRouter.DefineRoute(bpMD, layToolbar, slotTop, TAlignLayout.Top);
FRouter.DefineRoute(bpMD, laySidebar, slotLeft, TAlignLayout.Client);
FRouter.DefineRoute(bpMD, layMain, slotCenter, TAlignLayout.Client);
FRouter.DefineRoute(bpMD, layDetails, slotRight, TAlignLayout.Client);
// Optional: eigene Breakpoint-Logik
FRouter.OnGetBreakpoint :=
function(const S: TSizeF): TNBLayoutBreakpoint
begin
if (S.cx < 700) or (S.cy < 420) then
Result := bpSM
else
Result := bpMD;
end;
end;Поставување: Зошто префрлувањето често е постабилно од менувањето на Visible
Чест пристап е за секоја варијанта да се одржуваат посебни дрва на layout и само да се менува Visible. Тоа во дизајнерот делува удобно, но има типични несакани ефекти:
- Дуплирано binding/настани: Два слични контроли мора да се одржуваат синхронизирани (на пр., две ленти за филтри).
- Редослед на табови и фокус: При префрлување се губи фокус или се завршува во невидливи контроли ако
TabStop/HitTestсе неповолно поставени. - Дивергенција на состојби: Позиции на скрол, состојби на селекција или уредувани текстови се разликуваат.
Префрлувањето ја задржува инстанцата единствена. Важно е да ги исечете layout-блоковите така што ќе можат независно да се преместуваат (на пр., странична лента како посебен контејнер наместо многу поединечни контроли). Токму тоа се исплаќа при одржување и анализа на грешки: вие дебагирате една инстанца, не две паралелни сенки-UI.
Замки во пракса (и како да ги дебагирате)
1) Бранови на Resize и реентрантност
FMX предизвикува OnResize не само при resize од корисникот, туку и при промени на стилот, промени на parent и делумно при промени на DPI. Без debounce апликацијата може да виси во layout-цикли. Рутерот користи TThread.ForceQueue за да ги помести промените во следниот UI-тик.
Совет за дебагирање: логирање (на пр. преку OutputDebugString) со breakpoint, големина и брояч на ажурирања помага да се најдат reflow-цикли. Ако дополнително логирате кога ApplyRoutes започнува и кога завршува, брзо ќе видите дали еден resize „каскадира“.
2) Z-Order, HitTest и „невидливи“ блокатори на клик
Промените на parent ја менуваат Z-Order. Ако Overlay-и (на пр. Flyouts) повеќе не реагираат на клик, често е затоа што над нив се наоѓа клиент-алинаен контејнер и HitTest е активен. Варијанта: за overlay-површините намерно обезбедете посебен слот целосно на врвот и само таму таквите контроли поставувајте како parent. Во FMX HitTest (дали контрола фаќа mouse-/touch-настани) почесто е причината отколку видливоста.
3) TGridPanelLayout и процентуални големини
TGridPanelLayout може при процентуални колони/редови во комбинација со Align=Client и динамично префрлување да предизвика неочекувани пресметки. Ако мора да користите Grid, поставете го Grid-от во слот и префрлувајте само цели Grid-блокови, а не Grid-децата. Тоа ја намалува комбинацијата на layout-пасови.
4) Фокус, виртуелна тастатура и „полиња за внес кои „скачаат“
Ретко сценарио кое се јавува во мобилни FMX-апликации и и на Windows-таблети: при префрлување фокусиран Edit-Control привремено може да го изгуби родителот. Тоа може да ја затвори виртуелната тастатура или да го ресетира курсорот. Практично се покажа: пред Routing да го зачувате тековниот фокус (Focused/IFMXFocusControl), а по Routing (во истиот UI-tick) да го вратите фокусот. Ова особено се исплати кај форми за внес кои се менуваат помеѓу „zweispaltig“ (Tablet/PC) и „einspaltig“ (Phone).
Варијанти: Breakpoints според форм-фактор наместо само според ширина
Во реални мултиплатформски клиенти „Breite“ само по себе често не е соодветен индикатор. Смислени варијанти:
- Breite und Höhe: многу плитки прозорци (на пр. касовни терминали, споделени екрани) бараат други правила.
- Orientierung:
Landscapeна таблети често е „desktop-ähnlich“, додека Portrait е повеќе „mobile-like“. - Safe-Area-Nutzfläche: На iOS/Android ефективно достапната висина може значително да се намали од системските ленти. Кој ги гледа само
Height, понекогаш рутува „премногу доцна“.
Рутерот е намерно дизајниран така што можете да ја замените функцијата за Breakpoint. Тоа е корисно и во legacy-ситуации кога иста форма се извршува во повеќе хостови (на пр. еднаш како нормален прозорец, еднаш вграден во контейнер).
Необично чисто: Layout-Routing како „Transaktion“
На поголеми екрани проблемот помалку лежи во самите Breakpoints, и повеќе во редоследот на UI-операциите. Практично применлив образец е да се третира Routing како трансакција: прво одлучете, потоа префрлете, па потоа наредно извршете ги споредните ефекти (видливост, фокус, освежување на податоци).
Конкретно тоа значи: избегнувајте поединечни контроли за време на префрлувањето да предизвикуваат свои настани кои пак стартуваат промени на распоред или пристап до податоци. Во FMX тоа се случува, на пр., кога при промена на родителот се пали OnEnter/OnExit или кога LiveBinding-израз се прекалкува поради обновување на Bounds. Ако ги забележите овие ефекти, помага централен прекинувач „Updating“ (како во рутерот) и јасен пост-степ: само по ApplyRoutes смеат да се извршуваат скапи операции (на пр. повторно вчитување на листа, поврзување на детален приказ).
Особено кај клиенти со REST-прием е ова релевантно: ненаметнато презарежување за време на resize може да води до непотребни requests. Тоа не се забележува во LAN, но во VPN или мобилно веднаш.
Кога пристапот се исплати – и каде има ограничувања
Layout-рутерот се исплати кога:
- една FMX-програма живее со години и неколку развивачи работат на истите екрани,
- UI-блоковите можат јасно да се разделат (Sidebar/Details/Content),
- Ви се потребни репродуцибилни правила за Breakpoint, наместо ad-hoc прилагодување на Align.
Ограничувања ќе забележите кога екранот мора да биде силно „fluid“ (многу динамички кaчки, вистински Masonry-Layouts). Тогаш се попогодни TFlowLayout/TGridPanelLayout или сопствени класи за layout. И кога многу индивидуални контроли се менуваат помеѓу слотовите, одржувањето на рутите станува нејасно – тогаш е подобро да се сечат поголеми блокови или да се воведе декларативен слој за конфигурација (н.пр. JSON-конфигурација за доделување слотови што се вчитува при старт).
Заклучок: За responsive layout-и во FMX, „префрлување со Breakpoints“ е прагматичен компромис: помалку хаос кај дизајнерот, јасни правила, стабилни состојби. Тоа не ја заменува добро осмислената UI-структура, но ви дава цврст скелет за контролирано понатамошно развивање на FMX-клиенти во дигитални корпоративни решенија преку различни форм-фактори.
Ако во постоечка Delphi- или FMX-апликација сакате чисто да воспоставите ваква архитектура на layout, без да ризикувате UI-регресии во оперативни сценарија, можеме тоа технички да го оцениме со вас: разговарајте за проект или модернизациски потфат со Net-Base.
Во стручната сфера, исто така, Delphi Fmx Breakpoints и Firemonkey Layout имаат важна улога кога интеграции, текови на податоци и натамошен развој треба да работат во тесна координација.
Разговарајте за проект или модернизациски потфат со Net-Base.
Следен чекор
Кога темата ќе прерасне во реален проект, архитектурата, постоечката средина и експлоатацијата треба рано да се разгледаат заедно.
Wir unterstuetzen nicht nur bei Einzelfragen, sondern auch dann, wenn aus Source-Schnipseln, Legacy-Themen oder Portalideen ein belastbares Unternehmensprojekt werden soll.
- Постоечката состојба, целната слика и техничките ризици се проценуваат заедно.
- REST, пристапот до податоци, порталите и Rollout не се одложуваат како подоцнежни последици.
- Уште рано идентификувате кој пат е економски и оперативно одржлив.