Od teme magazina do projektne prakse
Povezane stranice usluga i tehnologije za članak
QR Code skener Delphi FMX u praksi
U demo verziji QR Code skener Delphi FMX brzo je sastavljen: prikazati preview kamere, preuzeti Bitmap, pustiti ZXing da ga obradi. U stvarnom poslovnom softveru (npr. prijem robe, dodjela uređaja, ticketing, procesi pristupa) pojavljuju se dodatni uvjeti: aplikacija ide u pozadinu, kamera gubi fokus, korisnik drži uređaj ukošeno, format slike se mijenja – i odjednom skenirate isti kod dvaput u sekundi ili sučelje se trzaji jer se dekodiranje vrši u UI-threadu.
Tipični problemi nisu toliko „ZXing kann nicht lesen“, koliko lifecycle i arhitektura: oslobađanje resursa kamere, taktiranje frejmova, sigurnost niti pri pristupu TBitmap (GPU/CPU) te jasan Stop/Start koji ostaje čist i kada korisnik brzo navigira ili OS privremeno oduzme kameru.
Architekturüberblick: Pipeline statt „OnSampleBufferReady macht alles“
U praksi se pokazala mala pipeline s jasno definiranim odgovornostima:
- Kamera-Adapter: dostavlja frejmove (ili njihove kopije) u definiranom formatu.
- Decoder: radi u pozadinskoj niti i vraća rezultate preko callbacka.
- Gate/Debounce: sprječava dvostruka skeniranja i regulira opterećenje (Throttle).
- UI-Schicht: prikazuje preview, opcionalno fokusni pravokutnik (ROI, „Region of InteREST“) i reagira na rezultate.
Time izbjegavate da se UI, kamera i decoder međusobno blokiraju. „ROI“ ovdje znači izrezano tražilno polje (npr. centriranih 60 %), koje rasterećuje decoder i smanjuje lažno pozitivne rezultate. Važno: ROI je alat za performanse i upotrebljivost, a ne sigurnosni mehanizam.
Source-Schnipsel: Robuster QR Code Scanner (FMX + ZXing) mit Debounce und sauberem Stop
Sljedeći kod zamišljen je kao kompaktan, ali projektnoprihvatljiv modul. Koristi ZXing (Delphi-Port) preko ZXing.ScanManager i veže se na TCameraComponent.OnSampleBufferReady. Presudna su tri aspekta:
- Frejmovi se ograničavaju (ne dekodirati svaki uzorak).
- Dekodiranje se ne izvodi u UI-niti.
- Stop/Start je idempotent (moguće ga je pozvati više puta bez kaosa s resursima).
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>
/// Kontroler QR skenera za FMX (Android/iOS).
/// Brine se o ograničavanju obrade okvira kamere, pozadinskom dekodiranju i urednom zaustavljanju/pokretanju.
/// </summary>
TQrScannerController = class
private
FCamera: TCameraComponent;
FScanManager: TScanManager;
FBitmap: TBitmap;
FLock: TObject;
FOnResult: TQrScanResultEvent;
// Ograničavanje brzine (throttle)
FIsRunning: Boolean;
FIsDecoding: Integer; // 0/1 kao Interlocked-zastavica
FLastDecodeTick: Int64;
FMinIntervalMs: Cardinal;
// Debounce protiv ponovljenih istih kodova
FLastText: string;
FLastTextTick: Int64;
FDebounceMs: Cardinal;
// ROI: udio slike koji se skenira (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; // npr. 120
property DebounceMs: Cardinal read FDebounceMs write FDebounceMs; // npr. 1200
property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
property RoiScale: Single read FRoiScale write FRoiScale; // npr. 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;
// Inicijalizirati ScanManager i ograničiti na QR (performanse + manje lažno pozitivnih)
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;
// Aktivirati kameru: U pravim aplikacijama unaprijed provjeriti dozvole (Android) i uzeti u obzir tok korisničkog sučelja.
if Assigned(FCamera) then
FCamera.Active := True;
end;
procedure TQrScannerController.Stop;
begin
if not FIsRunning then
Exit;
FIsRunning := False;
// Uredno isključivanje
if Assigned(FCamera) then
FCamera.Active := False;
// Resetirati zastavicu dekodera ako Stop dođe u nepovoljnoj fazi
TInterlocked.Exchange(FIsDecoding, 0);
end;
function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
// Ograničavanje brzine: ne dekodirati svaki okvir
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);
// isti tekst unutar debounce-prozora – ignorirati
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;
// Samo jedno dekodiranje istovremeno (inače zagušenje reda na slabim uređajima)
if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
Exit;
// Kopirati uzorak kamere u FBitmap. Zaključavanje, jer se isti bitmap-buffer ne smije koristiti paralelno.
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;
// Dekodiranje u pozadini
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
// Izdvojiti ROI centrirano: smanjuje opterećenje računanja i usmjerava korisnika.
// Pažnja: kod vrlo malih QR kodova ROI može biti preuska.
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-thread: navigacija, zvuk, popunjavanje polja itd.
TThread.Queue(nil,
procedure
begin
if FIsRunning and Assigned(FOnResult) then
FOnResult(AText);
end);
end;
end.
Što kod rješava (i zašto je potrebno)
Throttle (MinIntervalMs) smanjuje opterećenje CPU-a i isijavanje topline. Bez ograničenja neki uređaji pokušavaju dekodirati 30–60 frames/s; u praksi je dovoljno 5–10/s, često i manje. Debounce (DebounceMs) sprječava da stabilno držani QR-kod bude pokrenut više puta (npr. dvostruko knjiženje u jednom koraku procesa).
Interlocked-Flag (FIsDecoding) osigurava da radi najviše jedan Decode-Task. To je arhitektonski trik protiv „zagušenja queue-a“: ako dekodiranje traje 200 ms, ali se task pokreće svaka 120 ms, red čekanja raste i rezultati dolaze s odgodom, što u produkciji izgleda kao „skener reagira pogrešno“.
Uvjeti i zamke
- TBitmap i multithreading: FMX-Bitmaps mogu biti GPU-backed. Pristup kopira frame u lokalni bitmap i dekodira u pozadini. Ovisno o verziji Delphi/platformi ipak treba oprez: ako vidite artefakte, forsirajte CPU-bitmapu (npr. preko Pixel-Read/Write) ili radite s ByteBufferom iz SampleBuffer-a (bliže platformi, ali stabilnije).
- Stop/Start kod navigacije: U mobilnim aplikacijama često se zaustavlja pri promjeni form-a ili prilikom App-Pause eventa. Važno je da se
Stopmože pozvati više puta bez iznimki (idempotentno). Također bi rezultat-callback trebao provjeriti radi li skener još uvijek (radiDoResultOnMainThread). - ROI preuzak: Središnji ROI ubrzava skeniranje, ali može zakazati ako korisnik drži kod izvan njega ili je kod vrlo malen. Zato je
EnableRoikonfigurabilan, aRoiScaleograničen. - Format-lock na QR: Ograničavanje na
QR_CODEje u većini slučajeva ispravno. Ako trebate i Code128/EAN, proširite formate – računajte međutim na više false positive i veće opterećenje CPU-a.
Delphi FMX kamera-lifecycle: dozvole, background, rotacija
Najčešće greške ne nastaju pri dekodiranju, nego oko kamere:
- Dozvole na Androidu: Dozvole za kameru moraju se tražiti za vrijeme rada aplikacije. Planirajte i slučaj da korisnik odbije ili odabere „Samo ovaj put“. Tehnički to znači: držite stanje korisničkog sučelja („Scanner bereit?“) odvojeno od stanja kamere, inače ćete zapeti u poluzavršenim stanjima.
- Aplikacija ide u background: Pri
OnApplicationEvent(npr.EnteredBackground) trebate pozvatiStop. Pri povratku svjesno pozoviteStart(i po potrebi kratko zakašnjenje) kako bi preview bio stabilan. - Rotacija/ogledanje: Za QR-kodove rotacija često nije kritična, ali u nekim camera-pipelineima bitmapa može biti zrcaljena ili rotirana. Ako se skenovi rade „samo u jednom položaju“, to je pokazatelj. U tom slučaju: prije skeniranja rotirajte/zrcalite ili koristite decoder koji koristi orientation-metadata.
Debugging u produkciji: kako pronaći stvarne uzroke
Ako skener „ponekad“ ne čita, reproducibilni debugging vrijedi zlata. Tri mjere koje daju rezultate:
- Logiranje frame-samplinga: Zabilježite (samo u Debug/Support-modu) tick, veličinu slike, veličinu ROI-ja, trajanje dekodiranja. Tako odmah vidite je li problem u Throttle/Debounce ili u CPU-opterećenju.
- Spremanje testnih slika: Pohranite svake N sekundi ROI-sliku (privremeno). Time možete bez kamere analizirati je li problem u kontrastu/nena oštrini slike.
- Razdvojite opterećenje: Ne osvježavajte UI-azuriranja (Preview-Overlay, statusni tekst) velikom frekvencijom. „Zadrhtavanje“ sučelja često nastaje zbog previše
Queue-događaja.
Varijante: Ako trebate više od „skeniraj i gotovo“
Više rezultata, ali kontrolirano
Za serijske procese (npr. mnoge naljepnice jedna za drugom) smanjite DebounceMs i dopunite jednim Whitelist/State-Machine: QR-kod smije se prihvatiti samo ako ga trenutačna faza procesa očekuje. To nije logika korisničkog sučelja, nego logika domene – pripada u zasebni sloj kako bi skener i proces ostali neovisno testabilni.
Offline-validacija i sigurni korisnički podaci
U poslovnim procesima QR-kodovi često sadrže ID-e ili tokene. Ne oslanjajte se na pretpostavku „QR = ispravno“. Validirajte lokalno (format, kontrolni zbroj, očekivani prefiksi) i na strani servera (REST-API). Ako koristite tokene: postavite vrijeme isteka, zaštitu od replay-napada i pažljivo postupanje s logiranjem (bez tokena u običnom tekstu u logovima za podršku).
Legacy-situacije: FMX-skener kao modul u miješanim bazama koda
Ako imate razvijeni VCL-svijet, FMX kao mobilni klijent često je zaseban tok. Držite skener kao controller-klasu bez ovisnosti o formama (kao gore), tada ga možete integrirati u različite ekrane. To se isplati i pri modernizaciji: poslovna logika ostaje testabilna, a kamera je samo ulazni kanal. Posebno u legacy-situacijama vrijedi jasno razdvajanje za logiranje, feature-flagove i udaljenu konfiguraciju.
Zaključak: Stabilno FMX-QR-skeniranje je problem životnog ciklusa – ne samo poziv ZXing-a
QR-kod skener u Delphi FMX postaje stabilan ako ga tretirate kao malu pipeline: kamera isporučuje okvire, pozadinski dekoder radi kontrolirano, a Debounce/Throttle sprječavaju duple i zakašnjele događaje. Gornji isječak izvornog koda adresira točno ona mjesta koja u stvarnim mobilnim poslovnim procesima pokleknu: previše zadataka dekodiranja, neuredno zaustavljanje, blokade UI-niti i nepotreban teret.
Ograničenja primjene: Ako trebate ekstremno visoke stope skeniranja (npr. industrijsko skeniranje na proizvodnoj traci) ili imate stroge zahtjeve za obradu slike, FMX-standardna kamera + bitmap-pipeline često je preskupa. U tom slučaju isplati se platformno-približan pristup (Native Camera API, YUV-Buffer izravno, SIMD/NEON) ili specijalizirani scanner-SDK. Za većinu mobilnih aplikacija bliskih procesu prikazani pristup je dovoljan, pod uvjetom da su lifecycle, dozvole i threading čisto integrirani – i da su procesi iza toga jasno definirani.
Ako morate uklopiti QR-sken u postojeću Delphi arhitekturu (uključujući rubne slučajeve poput navigacije, backgroundinga, logiranja i validacije procesa), rado ćemo to razjasniti strukturirano:
U stručnom kontekstu Zxing Delphi i Fmx Tcameracomponent također igraju važnu ulogu kad integracije, tokovi podataka i daljnji razvoj moraju glatko surađivati.
Razgovarajte o projektu ili modernizacijskom pothvatu s Net-Base.
Sljedeći korak
Kad se tema pretvori u stvarni projekt, arhitektura, postojeći sustav i operativni rad trebaju se rano sagledati zajedno.
Podržavamo vas ne samo u pojedinačnim pitanjima, već i kada iz isječaka izvornog koda, naslijeđenih sustava ili ideja za portale treba nastati pouzdan poslovni projekt.
- Postojeće stanje, ciljna slika i tehnički rizici procjenjuju se zajedno.
- REST, pristup podacima, portali i Rollout neće biti odgođeni kao kasne posljedice.
- Vidite rano koji je put ekonomski i operativno održiv.