Od teme magazina do projektne prakse
Povezane stranice usluga i tehnologije za članak
QR Code skener Delphi FMX u praksi
Jedan QR Code Scanner Delphi FMX se u demo primjeru brzo složi: prikazati predpregled kamere, dohvatiti Bitmap, pustiti ZXing da dekodira. U stvarnoj poslovnoj softverskoj primjeni (npr. prijem robe, dodjela uređaja, ticketing, procesi pristupa) pojavljuju se dodatni ograničavajući uvjeti: aplikacija prelazi u pozadinu, kamera gubi fokus, korisnik drži uređaj ukošeno, format slike se mijenja – i odjednom skenirate istu šifru dva puta u sekundi ili se UI trzaju jer dekodiranje radi u UI-threadu.
Tipični problemi nisu toliko „ZXing kann nicht lesen“, koliko lifecycle i arhitektura: oslobađanje resursa kamere, taktovanje frameova, thread-sigurnost pri pristupu TBitmap (GPU/CPU), i jasan Stop/Start koji ostaje čist čak i kada korisnici brzo navigiraju ili OS privremeno oduzme kameru.
Pregled arhitekture: Pipeline umjesto „OnSampleBufferReady macht alles“
U praksi se pokazala efikasnom mala pipeline sa jasnim odgovornostima:
- Kamera-Adapter: isporučuje Frames (ili njihove kopije) u definisanom formatu.
- Decoder: radi u pozadinskoj niti i vraća rezultate preko callback-a.
- Gate/Debounce: sprječava dvostruko skeniranje i upravlja opterećenjem (Throttle).
- UI-sloj: prikazuje preview, opcionalno okvir fokusa (ROI, „Region of InteREST“) i reagira na rezultate.
Time izbjegavate međusobno blokiranje UI-ja, kamere i decodera. „ROI“ ovdje znači izrezano pretraživačko polje (npr. centralno 60 %), koje rasterećuje decoder i smanjuje lažno pozitivne rezultate. Važno: ROI je alat za performanse i upotrebljivost, a ne sigurnosni mehanizam.
Isječak izvornog koda: robustan QR Code skener (FMX + ZXing) s Debounce-om i urednim Stop/Start
Slijedeći kod zamišljen je kao kompaktan, ali projektnu upotrebu podržavajući modul. On koristi ZXing (Delphi-port) preko ZXing.ScanManager i povezuje se na TCameraComponent.OnSampleBufferReady. Presudna su tri aspekta:
- Frameovi se throttlen (ne dekodira se svaki sample).
- Dekodiranje ne odvija se u UI-threadu.
- Stop/Start je idempotentan (može se pozivati više puta bez resursnog kaosa).
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 o upravljanju frekvencijom okvira kamere, dekodiranju u pozadini i urednom zaustavljanju/pokretanju.
/// </summary>
TQrScannerController = class
private
FCamera: TCameraComponent;
FScanManager: TScanManager;
FBitmap: TBitmap;
FLock: TObject;
FOnResult: TQrScanResultEvent;
// Gating/Throttle
FIsRunning: Boolean;
FIsDecoding: Integer; // 0/1 als Interlocked-Flag
FLastDecodeTick: Int64;
FMinIntervalMs: Cardinal;
// Debounce gegen wiederholte gleiche Codes
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;
// Inicijalizacija ScanManagera i ograničavanje 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 stvarnim aplikacijama prethodno provjeriti dozvole (Android) i uzeti u obzir tok korisničkog sučelja (UI).
if Assigned(FCamera) then
FCamera.Active := True;
end;
procedure TQrScannerController.Stop;
begin
if not FIsRunning then
Exit;
FIsRunning := False;
// Ispravno isključiti
if Assigned(FCamera) then
FCamera.Active := False;
// Resetirati flag dekodera ako Stop dođe u nepovoljnoj fazi
TInterlocked.Exchange(FIsDecoding, 0);
end;
function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
// Throttle: ne dekodirati svaki Frame
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 - zanemari
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;
// Nur ein Decode gleichzeitig (sonst Queue-Stau bei schwachen Geräten)
if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
Exit;
// Kamera-Sample in FBitmap kopieren. Lock, weil derselbe Bitmap-Buffer nicht parallel benutzt werden soll.
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
// ROI izrezati u sredini: smanjuje računarsko opterećenje i usmjerava korisnika.
// Pažnja: kod vrlo malih QR-kodova ROI može biti preuski.
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, zvučni signal, popunjavanje polja itd.
TThread.Queue(nil,
procedure
begin
if FIsRunning and Assigned(FOnResult) then
FOnResult(AText);
end);
end;
end.
Šta kod rješava (i zašto je potrebno)
Throttle (MinIntervalMs) smanjuje opterećenje CPU‑a i generisanje topline. Bez ograničenja neki uređaji pokušavaju dekodirati 30–60 frameova/s; u praksi je dovoljno 5–10/s, često i manje. Debounce (DebounceMs) sprečava da stabilno držani QR‑kod bude pokrenut više puta (npr. dvostruko evidentiranje u jednom procesnom koraku).
Ovo Interlocked‑Flag (FIsDecoding) osigurava da maksimalno jedan Decode‑Task radi. To je arhitektonski trik protiv „zagušenja reda“: ako dekodiranje traje 200 ms, a svakiih 120 ms se pokreće novi task, red se gomila i rezultati stižu s odgodom, što u radu izgleda kao „skener reagira pogrešno“.
Ograničenja i zamke
- TBitmap i Threading: FMX‑Bitmaps mogu biti GPU‑backed. Pristup kopira frame u lokalnu bitmapu i dekodira u pozadini. Ovisno o verziji/platformi Delphi može ipak biti potrebna opreznost: ako vidite artefakte, prisilite CPU‑bitmapu (npr. preko Pixel‑Read/Write) ili radite s ByteBuffer‑om iz SampleBuffer‑a (bliže platformi, ali stabilnije).
- Stop/Start pri navigaciji: U mobilnim aplikacijama se često zaustavlja pri promjeni form‑a ili pri App‑Pause eventu. Važno je da se
Stopmože pozvati više puta i da ne baca iznimke (idempotentno). Također bi rezultat‑callback trebao provjeriti da li skener još radi (to radiDoResultOnMainThread). - ROI previše uzak: Centralni ROI ubrzava dekodiranje, ali može ne uspjeti ako korisnik drži kod izvan regiona ili je kod vrlo mali. Zato je
EnableRoikonfigurabilan iRoiScaleograničen. - Format‑lock na QR: Ograničavanje na
QR_CODEje najčešće ispravno. Ako trebate i Code128/EAN, proširite podržane formate – računajte međutim na više lažno pozitivnih rezultata i veće opterećenje CPU‑a.
Delphi FMX Kamera-Lifecycle: Dozvole, pozadina, rotacija
Najčešće greške ne nastaju pri dekodiranju, već u okolini kamere:
- Android dozvole: Dozvole za kameru moraju se zatražiti u runtime‑u. Planirajte slučaj da korisnik odbije ili izabere „Samo ovaj put“. Tehnički to znači: držite UI‑stanje („Scanner bereit?“) odvojeno od stanja kamere, inače ćete zapeti u polu‑dovršenim stanjima.
- Aplikacija prelazi u pozadinu: U
OnApplicationEvent(npr.EnteredBackground) trebate pozvatiStop. Pri povratku svjesno pozoviteStart(i po potrebi kratko zakašnjenje), kako bi pregled uživo bio stabilan. - Rotacija/Zrcaljenje: Za QR‑kodove rotacija često nije kritična, ali u nekim kamera‑pipelineima bitmapa može biti zrcaljena ili rotirana. Ako skenovi rade „samo u jednom položaju“, to je pokazatelj. U tom slučaju: prije skeniranja rotirajte/zrcalite ili koristite dekoder koji koristi metapodatke o orijentaciji.
Debugiranje u radu: Kako pronaći stvarne uzroke
Ako skener „ponekad“ ne čita, reproducibilno debugiranje vrijedi zlata. Tri mjere koje se pokažu korisnim:
- Logovanje frame‑sampling‑a: Logirajte (samo u Debug/Support modu) Tick, veličinu slike, veličinu ROI‑a, trajanje dekodiranja. Tako odmah vidite da li su Throttle/Debounce ili opterećenje CPU‑a uzrok problema.
- Sačuvajte testne slike: Spremite svake N sekundi jednu ROI‑sliku (privremeno). Tako možete bez kamera‑hardvera analizirati da li je problem kontrast ili zamućenje.
- Workload trennen: UI-Updates (Preview-Overlay, Status-Text) nicht in hoher Frequenz aktualisieren. Das „UI-Zittern“ kommt oft von zu vielen
Queue-Events.
Varianten: Wenn Sie mehr brauchen als „Scan und fertig“
Mehrere Ergebnisse, aber kontrolliert
Für Stapelprozesse (z. B. viele Labels nacheinander) reduzieren Sie DebounceMs und ergänzen eine Whitelist/State-Machine: Ein QR-Code darf nur dann akzeptiert werden, wenn der aktuelle Prozessschritt ihn erwartet. Das ist keine UI-Logik, sondern Domänenlogik – sie gehört in eine eigene Schicht, damit Scanner und Prozess unabhängig testbar bleiben.
Offline-Validierung und sichere Nutzdaten
In Unternehmensprozessen enthalten QR-Codes oft IDs oder Token. Verlassen Sie sich nicht darauf, dass „QR = korrekt“. Validieren Sie lokal (Format, Prüfsumme, erwartete Prefixe) und serverseitig (REST-API). Wenn Sie Token verwenden: Ablaufzeiten, Replay-Schutz, und Logging mit Vorsicht (keine Tokens im Klartext in Support-Logs).
Legacy-Situationen: FMX-Scanner als Modul in gemischten Codebasen
Wenn Sie eine gewachsene VCL-Welt haben, ist FMX als Mobile-Client oft ein separater Strang. Halten Sie den Scanner als Controller-Klasse ohne Form-Abhängigkeiten (wie oben), dann können Sie ihn in unterschiedliche Screens integrieren. Das zahlt sich auch bei Modernisierung aus: Die Business-Logik bleibt testbar, die Kamera ist nur ein Input-Kanal. Gerade in Legacy-Situationen lohnt außerdem ein klarer Schnitt für Logging, Feature-Flags und Remote-Konfiguration.
Fazit: Solider FMX-QR-Scan ist ein Lifecycle-Problem – nicht nur ein ZXing-Aufruf
Ein QR Code Scanner in Delphi FMX wird stabil, wenn Sie ihn wie eine kleine Pipeline behandeln: Kamera liefert Frames, ein Hintergrund-Decoder arbeitet kontrolliert, und Debounce/Throttle verhindern Doppel- und Spät-Events. Der Source-Schnipsel oben adressiert genau die Stellen, die in echten mobilen Business-Prozessen kippen: zu viele Decode-Tasks, unsauberer Stop, UI-Thread-Blockaden und unnötige Last.
Einsatzgrenzen: Wenn Sie extrem hohe Scanraten brauchen (z. B. Industrie-Scanning am Fließband) oder harte Anforderungen an Bildverarbeitung haben, ist die FMX-Standardkamera + Bitmap-Pipeline oft zu teuer. Dann lohnt ein plattformnaher Ansatz (Native Camera API, YUV-Buffer direkt, SIMD/NEON) oder ein spezialisierter Scanner-SDK. Für die meisten prozessnahen mobilen Anwendungen reicht der gezeigte Ansatz jedoch, sofern Lifecycle, Rechte und Threading sauber integriert sind – und die Prozesse dahinter eindeutig sind.
Wenn Sie einen QR-Scan in eine bestehende Delphi-Architektur einpassen müssen (inklusive Randfällen wie Navigation, Backgrounding, Logging und Prozessvalidierung), klären wir das gerne strukturiert:
Im fachlichen Umfeld spielen auch Zxing Delphi und Fmx Tcameracomponent eine wichtige Rolle, wenn Integrationen, Datenflüsse und Weiterentwicklung sauber zusammenspielen müssen.
Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.
Sljedeći korak
Ako se tema pretvori u stvarni projekat, arhitekturu, postojeći sistem i operacije trebalo bi rano zajednički razmotriti.
Wir unterstuetzen nicht nur bei Einzelfragen, sondern auch dann, wenn aus Source-Schnipseln, Legacy-Themen oder Portalideen ein belastbares Unternehmensprojekt werden soll.
- Postojeće stanje, ciljno stanje i tehnički rizici procjenjuju se zajedno.
- REST, pristup podacima, portali i Rollout neće se odgađati za kasnije faze.
- Pravovremeno prepoznajete koji pristup je ekonomski i operativno održiv.