Од теме часописа до пројектне праксе
Одговарајуће странице услуга и техничке странице за чланак
QR код скенер Delphi FMX у пракси
У демо-верзији се QR код скенер Delphi FMX брзо састави: приказати preview камере, извући Bitmap, пустити ZXing да обради. У правом бизнис софтверу (нпр. при примопредаји робе, додели уређаја, тикетинг, процеси приступа) појављују се додатни услови: апликација иде у позадину, камера губи фокус, корисник држи уређај косо, формат слике се мења – и одједном скенирате исти код два пута у секунди или UI заостаје јер декодирање тече у UI-ниту.
Типични проблеми нису толико „ZXing не може да чита“, колико животни циклус и архитектура: ослобађање ресурса камере, регулација темпа фрејмова, сигурност нити при приступу TBitmap (GPU/CPU), и јасан Stop/Start који остаје чист и ако корисник брзо навигира или ОС привремено одузме камеру.
Преглед архитектуре: Pipeline уместо „OnSampleBufferReady macht alles“
У пракси се показао мали pipeline са јасним одговорностима:
- Адаптер камере: испоручује фрејмове (или њихове копије) у дефинисаном формату.
- Декодер: ради у позадинском треду и враћа резултате преко callback-а.
- Gate/Debounce: спречава дупле скенове и регулише оптерећење (Throttle).
- UI-слој: приказује preview, опционално оквир фокуса (ROI, „Region of InteREST“) и реагује на резултате.
Тако избегавате да се UI, камера и декодер међусобно блокирају. „ROI“ овде означава исечено поље претраге (нпр. централно 60 %) које растерети декодер и смањује лажно-позитивне резултате. Важно: ROI је алат за перформансе и употребљивост, а не механизам безбедности.
Source-Schnipsel: Robuster QR Code Scanner (FMX + ZXing) mit Debounce und sauberem Stop
Следећи код је замишљен као компактни, али пројектно-погодан грађевни блок. Користи ZXing (Delphi-Port) преко ZXing.ScanManager и везује се за TCameraComponent.OnSampleBufferReady. Кључна су три аспекта:
- Фрејмови се ограничавају (не декодира се сваки sample).
- Декодирање не ради у UI-ниту.
- Stop/Start је идемпотентан (може се позвати више пута без хаоса са ресурсима).
unit UQrScanner;
interface
uses
System.SysUtils, System.Classes, System.Types, System.UITypes, System.SyncObjs,
System.Diagnostics, System.Threading,
FMX.Types, FMX.Graphics, FMX.Media,
ZXing.BarcodeFormat, ZXing.ReadResult, ZXing.ScanManager;
type
TQrScanResultEvent = reference to procedure(const AText: string);
/// <summary>
/// Контролер QR скенера за FMX (Android/iOS).
/// Управља контролом учесталости обраде кадрова камере, декодирањем у позадини и поузданим покретањем/заустављањем.
/// </summary>
TQrScannerController = class
private
FCamera: TCameraComponent;
FScanManager: TScanManager;
FBitmap: TBitmap;
FLock: TObject;
FOnResult: TQrScanResultEvent;
// Контрола учесталости (gating/throttle)
FIsRunning: Boolean;
FIsDecoding: Integer; // 0/1 као Interlocked флаг
FLastDecodeTick: Int64;
FMinIntervalMs: Cardinal;
// Debounce против понављања идентичних кодова
FLastText: string;
FLastTextTick: Int64;
FDebounceMs: Cardinal;
// ROI: део слике који се скенира (0..1)
FEnableRoi: Boolean;
FRoiScale: Single;
procedure CameraSampleBufferReady(Sender: TObject; const ATime: TMediaTime);
function ShouldDecodeNow(const ANowTick: Int64): Boolean;
function IsDebounced(const AText: string; const ANowTick: Int64): Boolean;
function ExtractRoiBitmap(const ASrc: TBitmap): TBitmap;
procedure DoResultOnMainThread(const AText: string);
public
constructor Create(const ACamera: TCameraComponent);
destructor Destroy; override;
procedure Start;
procedure Stop;
property MinIntervalMs: Cardinal read FMinIntervalMs write FMinIntervalMs; // нпр. 120
property DebounceMs: Cardinal read FDebounceMs write FDebounceMs; // нпр. 1200
property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
property RoiScale: Single read FRoiScale write FRoiScale; // нпр. 0.6
property OnResult: TQrScanResultEvent read FOnResult write FOnResult;
end;
implementation
uses
System.Math;
{ TQrScannerController }
constructor TQrScannerController.Create(const ACamera: TCameraComponent);
var
Formats: TArray<TBarcodeFormat>;
begin
inherited Create;
FLock := TObject.Create;
FCamera := ACamera;
FCamera.OnSampleBufferReady := CameraSampleBufferReady;
// Иницијализовати ScanManager и ограничити га на QR (боља перформанса и мање лажно позитивних детекција)
Formats := TArray<TBarcodeFormat>.Create(TBarcodeFormat.QR_CODE);
FScanManager := TScanManager.Create(Formats);
FBitmap := TBitmap.Create;
FMinIntervalMs := 120;
FDebounceMs := 1200;
FEnableRoi := True;
FRoiScale := 0.6;
FLastDecodeTick := 0;
FLastText := “;
FLastTextTick := 0;
FIsDecoding := 0;
FIsRunning := False;
end;
destructor TQrScannerController.Destroy;
begin
Stop;
FBitmap.Free;
FScanManager.Free;
FLock.Free;
inherited;
end;
procedure TQrScannerController.Start;
begin
if FIsRunning then
Exit;
FIsRunning := True;
// Активирати камеру: у реалним апликацијама претходно проверити дозволе (Android) и узети у обзир UI ток.
if Assigned(FCamera) then
FCamera.Active := True;
end;
procedure TQrScannerController.Stop;
begin
if not FIsRunning then
Exit;
FIsRunning := False;
// Деактивирати поуздано
if Assigned(FCamera) then
FCamera.Active := False;
// Ресетовати decoder-флаг ако Stop дође у неповољној фази
TInterlocked.Exchange(FIsDecoding, 0);
end;
function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
// Тротлинг: не декодирати сваки кадар
Result := (ANowTick – FLastDecodeTick) >= FMinIntervalMs;
if Result then
FLastDecodeTick := ANowTick;
end;
function TQrScannerController.IsDebounced(const AText: string; const ANowTick: Int64): Boolean;
begin
Result := False;
if AText = “ then
Exit(True);
// исти текст унутар debounce прозора – игнорисати
if SameText(AText, FLastText) and ((ANowTick – FLastTextTick) <= FDebounceMs) then
Exit(True);
FLastText := AText;
FLastTextTick := ANowTick;
end;
procedure TQrScannerController.CameraSampleBufferReady(Sender: TObject; const ATime: TMediaTime);
var
NowTick: Int64;
LocalCopy: TBitmap;
begin
if not FIsRunning then
Exit;
NowTick := TThread.GetTickCount64;
if not ShouldDecodeNow(NowTick) then
Exit;
// Само један decode истовремено (иначе загушење реда на слабијим уређајима)
if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
Exit;
// Копирање узорка камере у FBitmap. Закључавање јер се исти bitmap-буфер не сме користити паралелно.
TMonitor.Enter(FLock);
try
FCamera.SampleBufferToBitmap(FBitmap, True);
LocalCopy := TBitmap.Create;
try
LocalCopy.Assign(FBitmap);
except
LocalCopy.Free;
raise;
end;
finally
TMonitor.Exit(FLock);
end;
// Декодирање у позадини
TTask.Run(
procedure
var
ScanBmp: TBitmap;
Res: TReadResult;
Text: string;
Tick: Int64;
begin
try
Tick := TThread.GetTickCount64;
if FEnableRoi then
ScanBmp := ExtractRoiBitmap(LocalCopy)
else
ScanBmp := LocalCopy;
try
Res := FScanManager.Scan(ScanBmp);
if Assigned(Res) then
Text := Res.Text
else
Text := “;
finally
if ScanBmp <> LocalCopy then
ScanBmp.Free;
end;
if (Text <> “) and (not IsDebounced(Text, Tick)) then
DoResultOnMainThread(Text);
finally
LocalCopy.Free;
TInterlocked.Exchange(FIsDecoding, 0);
end;
end);
end;
function TQrScannerController.ExtractRoiBitmap(const ASrc: TBitmap): TBitmap;
var
R: TRectF;
W, H: Single;
RoiW, RoiH: Single;
X, Y: Single;
begin
// Исецање ROI-а централно: смањује оптерећење процесора и усмерава корисника.
// Упозорење: код веома малих QR кодова ROI може бити преблизу (превише уско).
W := ASrc.Width;
H := ASrc.Height;
RoiW := Max(16, W * EnsureRange(FRoiScale, 0.2, 1.0));
RoiH := Max(16, H * EnsureRange(FRoiScale, 0.2, 1.0));
X := (W – RoiW) / 2;
Y := (H – RoiH) / 2;
R := TRectF.Create(X, Y, X + RoiW, Y + RoiH);
Result := TBitmap.Create(Round(RoiW), Round(RoiH));
Result.Canvas.BeginScene;
try
Result.Canvas.Clear(TAlphaColors.Black);
Result.Canvas.DrawBitmap(ASrc, R, TRectF.Create(0, 0, Result.Width, Result.Height), 1.0, True);
finally
Result.Canvas.EndScene;
end;
end;
procedure TQrScannerController.DoResultOnMainThread(const AText: string);
begin
if not Assigned(FOnResult) then
Exit;
// UI-нит: навигација, звук, попуњавање поља итд.
TThread.Queue(nil,
procedure
begin
if FIsRunning and Assigned(FOnResult) then
FOnResult(AText);
end);
end;
end.
Šta kod rešava (i zašto je to potrebno)
Throttle (MinIntervalMs) smanjuje opterećenje CPU-a i zagrevanje. Bez ograničenja, neki uređaji pokušavaju da dekodiraju 30–60 sličica/s; u praksi je dovoljno 5–10/s, često i manje. Debounce (DebounceMs) sprečava da stabilno držani QR-kod pokreće događaj više puta (npr. dvostruko knjiženje u jednom koraku procesa).
Interlocked-Flag (FIsDecoding) obezbeđuje da u svakom trenutku radi najviše jedan Decode-Task. To je arhitektonski trik protiv „zagushenja reda“: ako dekodiranje traje 200 ms, a svaki 120 ms se startuje novi task, red raste i rezultati stižu sa zakašnjenjem, što u radu deluje kao „skener odgovara pogrešno“.
Okviri i prepreke
- TBitmap i threading: FMX-bitmape mogu biti podržane od strane GPU-a. Pristup kopira frejm u lokalnu bitmapu i dekodira u pozadini. U zavisnosti od verzije/ platforme Delphi može ipak biti potrebna opreznost: ako vidite artefakte, forsirajte CPU-bitmapu (npr. preko čitanja/pisanja piksela) ili radite sa ByteBuffer-om iz SampleBuffer-a (bliže platformi, ali stabilnije).
- Stop/Start pri navigaciji: U mobilnim aplikacijama se često poziva stop pri promeni form-a ili pri OnPause događaju. Važno je da se
Stopmože pozivati više puta bez izbacivanja izuzetaka (idempotentno). Takođe, callback za rezultate bi trebalo da proveri da li skener još radi (to radiDoResultOnMainThread). - ROI preuzak: Centrirani ROI ubrzava, ali može propasti ako korisnik drži kod van centra ili je kod veoma mali. Zato je
EnableRoikonfigurisiv iRoiScaleograničen. - Format-Lock na QR: Ograničavanje na
QR_CODEje u većini slučajeva ispravno. Ako vam trebaju i Code128/EAN, proširite formate – računajte međutim na više false positive-a i veće opterećenje CPU-a.
Delphi FMX Kamera-Lifecycle: Berechtigungen, Hintergrund, Rotation
Najčešće greške ne nastaju pri dekodiranju, već oko kamere:
- Android Permissions: Dozvole za kameru moraju se tražiti u runtime-u. Planirajte slučaj da korisnik odbije ili izabere „Samo ovaj put“. Tehnički to znači: držite UI-state („Skener spreman?“) odvojeno od kamera-state-a, inače ostajete u polu-završenim stanjima.
- Aplikacija ide u pozadinu: Pri
OnApplicationEvent(npr.EnteredBackground) trebalo bi pozvatiStop. Pri povratku svesno pozoviteStart(i eventualno kratko zakasnite), da bi preview bio stabilan. - Rotacija/Mirroring: Za QR-kodove rotacija često nije kritična, ali u nekim kamera-pipeline-ovima bitmapa može biti zrcaljena ili rotirana. Ako skenovi rade „samo u jednom položaju“, to je indikator. U tom slučaju: pre skana rotirajte/zrcalite sliku ili koristite decoder koji koristi orientation-metapodatke.
Debugging u radu: Kako naći prave uzroke
Ako skener „povremeno“ ne čita, reproduktivno debagovanje je zlata vredno. Tri mere koje su se pokazale efikasnim:
- Logovanje frame-samplinga: Logujte (samo u Debug/Support modu) tick, veličinu slike, veličinu ROI, trajanje dekodiranja. Tako odmah vidite da li su Throttle/Debounce ili opterećenje CPU-a uzrok problema.
- Čuvanje test-snimaka: Sačuvajte svake N sekundi ROI-sliku (privremeno). Time bez kamere možete analizirati da li su kontrast/zamećenost uzrok problema.
- Раздвојите оптерећење: не освежавајте UI-ажурирања (Preview-Overlay, Status-Text) великом фреквенцијом. „Треперење UI‑ја“ често потиче од превише
Queue-догађаја.
Варијанте: Ако вам треба више од „скенирај и готово“
Више резултата, али контролисано
За серијске процесе (нпр. много налепница узастопно) смањите DebounceMs и допуните Whitelist/State-Machine: QR-код сме бити прихваћен само ако га очекује тренутна фаза процеса. То није UI-логика, већ доменска логика — треба да буде у посебном слоју да би скенер и процес остали независно тестирани.
Офлајн-валидација и сигурни кориснички подаци
У пословним процесима QR-кодови често садрже ID-ове или токене. Не ослањајте се на „QR = тачно“. Валидацију извршавајте локално (формат, контролни збир, очекивани префикси) и на серверу (REST-API). Ако користите токене: рокови важења, заштита од реплеја и опрезно логовање (не стављајте токене у чистом тексту у логове подршке).
Legacy-ситуације: FMX-скенер као модул у мешовитим кодним базама
Aко имате развијен VCL-свет, FMX као мобилни клијент често је засебан ток. Држите скенер као Controller-klasu bez zavisnosti od forme (као горе), тако ћете га моћи интегрисати у различите екране. То се исплати и при модернизацији: бизнис-логика остаје тестабилна, камера је само улазни канал. Управо у legacy-ситуацијама вреди јасан рез за логовање, feature-флагове и удаљену конфигурацију.
Закључак: Поуздан FMX-QR-скен је проблем животног циклуса — не само позив ZXing-а
QR-код скенер у Delphi FMX биће стабилан ако га третирајете као малу пипелaјн: камера испоручује фрејмове, позадински декодер ради контролисано, а Debounce/Throttle спречавају дупле и касне догађаје. Горњи исечак изворног кода адресира управо оне тачке које у правим мобилним бизнис-процесима изазивају пад: превише Decode-таскова, непажљив Stop, блокаде UI-треда и непотребно оптерећење.
Границе примене: Ако вам требају екстремно високе стопе скенирања (нпр. индустријско скенирање на траци) или строги захтеви за обраду слике, FMX-стандардна камера + bitmap-процес често је прескупа. Тада вреди приступ нативни на нивоу платформе (Native Camera API, YUV-Buffer директно, SIMD/NEON) или специјализовани scanner-SDK. За већину процесно-приближних мобилних апликација приказани приступ је довољан, под условом да су lifecycle, дозволе и threading уредно интегрисани — и да су процеси иза тога јасно дефинисани.
Ако треба да уклопите QR-скен у постојећу Delphi архитектуру (укључујући и рубне случајеве као што су навигација, backgrounding, логовање и валидација процеса), рецимо да то разрадимо структуирано:
У стручном окружењу Zxing Delphi и Fmx Tcameracomponent такође играју важну улогу када интеграције, токови података и даљи развој морају да се уклопе чисто и предвидиво.
Разговарајте о пројекту или пројекту модернизације са Net-Base.
Следећи корак
Када тема прерасте у реалан пројекат, архитектуру, постојеће системе и операције треба рано разматрати заједно.
Подржавамо не само у појединачним питањима, већ и када из исечака изворног кода, застарелих тема или идеја за портале треба да настане поуздан корпоративни пројекат.
- Постојеће стање, циљано стање и технички ризици оцењују се заједно.
- REST, приступ подацима, портали и роллаут се неће одлагати као накнадне последице.
- Ви рано видите који пут је економски и оперативно одржив.