Net-Base Magazín

03.06.2026

Skenér QR kódov v Delphi FMX: robustné, vláknovo bezpečné kamerové skenovanie bez chvenia UI

Praxovo použiteľný skener QR kódov Delphi FMX závisí od životného cyklu kamery, správy vlákien a spoľahlivého zastavenia a spustenia. Príspevok ukazuje robustný prístup s ZXing, Debounce, Frame‑Throttling, orezom ROI, ako aj detailmi ladenia a prevádzky pre Android a iOS.

03.06.2026

Od témy magazínu k projektovej praxi

Súvisiace stránky služieb a technológií k príspevku

QR Code Scanner Delphi FMX v praxi

Jednoduchý QR Code Scanner Delphi FMX je v dema rýchlo poskladaný: zobraziť náhľad kamery, získať Bitmap, spustiť ZXing. V reálnom podnikových aplikáciách (napr. príjem tovaru, priradenie zariadení, ticketing, procesy vstupu) sa však objavia okrajové podmienky: aplikácia prejde do pozadia, kamera stratí fokus, používateľ drží zariadenie nakoso, zmení sa formát obrazu – a zrazu skenujete dvakrát za sekundu ten istý kód alebo sa UI trhá, pretože dekódovanie beží v UI vlákne.

Typické problémy nie sú tak veľmi „ZXing kann nicht lesen“, ale životný cyklus a architektúra: uvoľňovanie zdrojov kamery, riadenie frekvencie snímok, bezpečný prístup pri práci s TBitmap v multi-vlákne (GPU/CPU) a jasné Stop/Start, ktoré je robustné aj keď používatelia rýchlo navigujú alebo OS kameru krátkodobo odoberie.

Architekturüberblick: Pipeline statt „OnSampleBufferReady macht alles“

V praxi sa osvedčila malá pipeline s jasnými zodpovednosťami:

  • Kamera-Adapter: dodáva snímky (alebo ich kópie) v definovanom formáte.
  • Decoder: pracuje na pozadí (vlákno) a vracia výsledky cez callback.
  • Gate/Debounce: zabraňuje duplicitným skenom a reguluje záťaž (throttle).
  • UI-Schicht: zobrazuje náhľad, voliteľný fokusový rámček (ROI, „Region of InteREST“) a reaguje na výsledky.

Týmto zabránite, aby sa UI, kamera a dekodér navzájom blokovali. „ROI“ tu znamená orezané vyhľadávacie okno (napr. uprostred 60 %), ktoré odľahčí dekodér a zníži falošne pozitívne výsledky. Dôležité: ROI je nástroj pre výkon a použiteľnosť, nie bezpečnostný mechanizmus.

Source-Schnipsel: Robuster QR Code Scanner (FMX + ZXing) mit Debounce und sauberem Stop

Nasledujúci kód je myslený ako kompaktný, no do projektu použiteľný modul. Využíva ZXing (Delphi-Port) cez ZXing.ScanManager a pripája sa k TCameraComponent.OnSampleBufferReady. Kľúčové sú tri body:

  • Snímky sú obmedzované (nekaždý sample sa dekóduje).
  • Dekódovanie nebeží v UI vlákne.
  • Stop/Start je idempotentný (možno volať viackrát bez chaosu so zdrojmi).

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>
/// Kontrolér QR skenera pre FMX (Android/iOS).
/// Stará sa o riadenie rámcov kamery, dekódovanie na pozadí a čisté zastavenie/spustenie.
/// </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: Anteil des Bildes, der gescannt wird (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; // napr. 120
property DebounceMs: Cardinal read FDebounceMs write FDebounceMs; // napr. 1200
property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
property RoiScale: Single read FRoiScale write FRoiScale; // napr. 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 initialisieren und auf QR beschränken (Performance + weniger False Positives)
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;

// Kamera aktivieren: V reálnych aplikáciách najprv skontrolovať povolenia (Android) a zohľadniť UI-Flow.
if Assigned(FCamera) then
FCamera.Active := True;
end;

procedure TQrScannerController.Stop;
begin
if not FIsRunning then
Exit;
FIsRunning := False;

// Riadne deaktivovať
if Assigned(FCamera) then
FCamera.Active := False;

// Decoder-Flag zurücksetzen, falls Stop in einer ungünstigen Phase kommt
TInterlocked.Exchange(FIsDecoding, 0);
end;

function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
// Throttle: nedekódovať každý snímok
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);

// rovnaký text v rámci debounce okna – ignorovať
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;

// Len jedno dekódovanie súčasne (inak preťaženie fronty na slabých zariadeniach)
if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
Exit;

// Skopírovať vzorku kamery do FBitmap. Zámok, pretože rovnaký bitmap buffer by nemal byť používaný paralelne.
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;

// Dekódovanie na pozadí
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
// Vystrihnúť ROI do stredu: znižuje výpočtovú záťaž a usmerňuje používateľa.
// Upozornenie: pri veľmi malých QR kódoch môže byť ROI príliš úzke.
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 vlákno: navigácia, pípnutie, naplnenie poľa atď.
TThread.Queue(nil,
procedure
begin
if FIsRunning and Assigned(FOnResult) then
FOnResult(AText);
end);
end;

end.

Čo kód rieši (a prečo je to potrebné)

Throttle (MinIntervalMs) znižuje zaťaženie CPU a tvorbu tepla. Bez obmedzenia sa niektoré zariadenia snažia dekódovať 30–60 snímok/s; v praxi stačí 5–10/s, často menej. Debounce (DebounceMs) zabraňuje tomu, aby sa stabilne držaný QR kód spúšťal viackrát (napr. dvojité zaúčtovanie v jednom kroku procesu).

Interlocked-Flag (FIsDecoding) zabezpečuje, že maximálne jeden Decode-Task beží súčasne. Je to architektonický trik proti „preťaženiu fronty“: ak dekódovanie trvá 200 ms, ale nový task sa spúšťa každých 120 ms, fronta rastie a výsledky prichádzajú oneskorene, čo v prevádzke pôsobí ako „skener reaguje nesprávne“.

Okrajové podmienky a úskalia

  • TBitmap a viacvláknovosť: FMX-bitmapy môžu byť viazané na GPU. Prístup kopíruje frame do lokálnej bitmapy a dekóduje na pozadí. V závislosti od verzie/platformy Delphi môže byť napriek tomu potrebná opatrnosť: ak vidíte artefakty, vynúťte CPU-bitmapu (napr. cez pixel-Read/Write) alebo pracujte s ByteBufferom zo SampleBufferu (bližšie k platforme, ale stabilnejšie).
  • Stop/Start pri navigácii: V mobilných aplikáciách sa často pri zmene formulára alebo pri App-pauze zastavuje. Dôležité je, aby sa Stop smel volať viackrát a nespúšťal výnimky (idempotentné). Ďalej by výsledkový callback mal overiť, či skener ešte beží (robí to DoResultOnMainThread).
  • ROI príliš úzke: Stredové ROI zrýchľuje, ale môže zlyhať, ak používateľ drží kód mimo alebo je kód veľmi malý. Preto je EnableRoi konfigurovateľné a RoiScale obmedzené.
  • Zamknutie formátu na QR: Obmedzenie na QR_CODE je zvyčajne správne. Ak potrebujete aj Code128/EAN, rozšírte podporované formáty – počítajte však s viac false positive výsledkami a vyšším zaťažením CPU.

Delphi FMX cyklus života kamery: povolenia, pozadie, rotácia

Najčastejšie chyby nevznikajú pri dekódovaní, ale okolo kamery:

  • Android povolenia: Práva ku kamere treba získať za behu. Plánujte situáciu, že používateľ odmietne alebo vyberie „Iba tentoraz“. Technicky to znamená: oddeliť UI-state („Scanner pripravený?“) od stavu kamery, inak zostanete uviaznutí v polovičných stavoch.
  • Aplikácia ide do pozadia: Pri OnApplicationEvent (napr. EnteredBackground) by ste mali zavolať Stop. Pri návrate vedome zavolajte Start (a prípadne krátke oneskorenie), aby bolo preview stabilné.
  • Rotácia/zrkadlenie: Pre QR kódy je rotácia často nekritická, ale v niektorých kamerových pipeline môže byť bitmapa zrkadlená alebo otočená. Ak skeny fungujú „len v jednej orientácii“, je to indikátor. V takom prípade: pred skenovaním obrázok otočte/zrkadlite alebo použite decoder, ktorý využíva orientačné metadáta.

Ladenie v prevádzke: Takto nájdete skutočné príčiny

Ak skener „niekedy“ nečíta, reprodukovateľné ladenie má veľkú hodnotu. Tri opatrenia, ktoré sa osvedčili:

  1. Logovanie vzorkovania snímok: Logujte (len v Debug/Support režime) tick, veľkosť obrazu, veľkosť ROI, trvanie dekódovania. Tak okamžite uvidíte, či je problém v Throttle/Debounce alebo v zaťažení CPU.
  2. Ukladať testovacie obrázky: Ukladajte každých N sekúnd jedno ROI-obrázok (dočasne). Tým môžete bez kamery analyzovať, či je problém v kontraste/rozostrení.
  3. Oddeliť záťaž: Aktualizácie UI (Preview-Overlay, statusový text) neaktualizujte s vysokou frekvenciou. K efektu „trasenia UI“ často vedie príliš veľa Queue-udalostí.

Varianty: Ak potrebujete viac než „naskenovať a hotovo“

Viacero výsledkov, ale kontrolované

Pre dávkové procesy (napr. veľa štítkov za sebou) znížte DebounceMs a doplňte Whitelist/State-Machine: QR kód smie byť prijatý len vtedy, keď ho očakáva aktuálny krok procesu. To nie je UI logika, ale doménová logika – patrí do samostatnej vrstvy, aby boli skener a proces nezávisle testovateľné.

Offline validácia a bezpečné užitočné údaje

V podnikových procesoch QR kódy často obsahujú ID alebo tokeny. Nespoliehajte sa na to, že „QR = správny“. Validujte lokálne (formát, kontrolný súčet, očakávané prefixy) a serverovo (REST-API). Ak používate tokeny: doby platnosti, ochrana proti opakovaniu (replay) a logovanie s opatrnosťou (žiadne tokeny v čitateľnom tvare v logoch podpory).

Legacy-situácie: FMX-Scanner ako modul v zmiešaných kódbázach

Ak máte etablovaný VCL svet, FMX ako mobilný klient je často samostatná vetva. Držte skener ako controller triedu bez závislostí na Formách (ako vyššie), potom ho môžete integrovať do rôznych obrazoviek. To sa oplatí aj pri modernizácii: business-logika zostáva testovateľná, kamera je len vstupný kanál. Práve v legacy situáciách sa tiež oplatí jasné oddelenie pre logovanie, feature-flagy a vzdialenú konfiguráciu.

Záver: Robustné FMX-QR-skenovanie je problém životného cyklu – nie len volanie ZXing

QR kód skener v Delphi FMX bude stabilný, ak ho budete riešiť ako malý pipeline: kamera dodáva snímky, pozadový dekóder pracuje kontrolovane, a Debounce/Throttle zabránia duplicitným a oneskoreným udalostiam. Zdrojový útržok vyššie cieli presne na miesta, kde v skutočných mobilných business-procesoch dochádza k zlyhaniu: príliš veľa decode-úloh, nečisté zastavenie, blokovanie UI vlákna a zbytočná záťaž.

Obmedzenia nasadenia: Ak potrebujete extrémne vysoké rýchlosti skenovania (napr. pri priemyselnom skenovaní na páse) alebo máte tvrdé požiadavky na spracovanie obrazu, FMX štandardná kamera + bitmap-pipeline je často príliš nákladná. Potom sa oplatí platformovo-blízky prístup (Native Camera API, YUV-Buffer priamo, SIMD/NEON) alebo špecializované Scanner-SDK. Pre väčšinu procesne orientovaných mobilných aplikácií však uvedený prístup postačuje, ak sú životný cyklus, práva a threading správne integrované – a procesy za nimi jednoznačné.

Ak potrebujete prispôsobiť QR-scan do existujúcej Delphi-architektúry (vrátane okrajových prípadov ako navigácia, backgrounding, logging a validácia procesov), radi to prediskutujeme štruktúrovane:

V odbornom kontexte zohrávajú dôležitú úlohu aj Zxing Delphi a Fmx Tcameracomponent, ak musia integrácie, dátové toky a ďalší vývoj hladko spolupracovať.

Prediskutovať projekt alebo modernizačný zámer s Net-Base.

Ďalší krok

Keď sa téma stane reálnym projektom, architektúru, existujúci stav a prevádzku treba včas posudzovať spoločne.

Podporujeme nielen pri jednotlivých otázkach, ale aj vtedy, keď sa z fragmentov zdrojového kódu, tém súvisiacich s legacy systémami alebo nápadov na portál má stať robustný podnikový projekt.

  • Stav, cieľový obraz a technické riziká sa hodnotia spoločne.
  • REST, prístup k dátam, portály a Rollout nebudú odložené na neskôr.
  • Včas zistíte, ktorá cesta je ekonomicky a prevádzkovo životaschopná.

Zdieľať príspevok

Tento príspevok priamo zdieľať

LinkedIn, X, XING, Facebook, WhatsApp a e-mail sú ihneď k dispozícii. Pre Instagram pripravujeme priamo odkaz a krátky text.

E-mail

Instagram sa otvorí v novej karte. Odkaz a krátky text sa predtým skopírujú do schránky.