Net-Base Magazín

03.06.2026

QR Code Scanner v Delphi FMX: robustní kamerové skenování, vláknově bezpečné a bez trhání uživatelského rozhraní

Praxí ověřený skener QR kódů Delphi FMX závisí na životním cyklu kamery, vícevláknovosti a pečlivém zastavení/spuštění. Příspěvek ukazuje robustní přístup s ZXing, Debounce, Frame‑Throttling, ROI‑ořezem a s ladicími a provozními detaily pro Android a iOS.

03.06.2026

Od tématu magazínu k projektové praxi

Vhodné stránky služeb a technické stránky k příspěvku

QR Code Scanner Delphi FMX v praxi

V demo verzi je QR Code Scanner Delphi FMX rychle složen: zobrazit náhled kamery, získat Bitmapu, pustit přes ni ZXing. V reálném byznysovém softwaru (např. příjem zboží, přiřazování zařízení, ticketing, procesy přístupu) se ale objeví další okrajové podmínky: aplikace přejde na pozadí, kamera ztratí fokus, uživatel drží zařízení nakřivo, změní se poměr stran obrazu – a najednou skenujete dvakrát za sekundu stejný kód nebo se UI trhá, protože dekódování běží v UI-Thread.

Typické problémy nejsou tolik „ZXing kann nicht lesen“, ale lifecycle a architektura: uvolňování zdrojů kamery, taktování snímků, thread-bezpečnost při přístupu na TBitmap (GPU/CPU) a jasný Stop/Start, který je spolehlivý i když uživatel rychle přepíná nebo mu OS dočasně odebere kameru.

Architekturüberblick: Pipeline statt „OnSampleBufferReady macht alles“

V praxi se osvědčila malá pipeline s jasnými odpovědnostmi:

  • Adaptér kamery: dodává snímky (nebo jejich kopie) ve definovaném formátu.
  • Decoder: pracuje na pozadí ve vlákně a vrací výsledky přes Callback.
  • Gate/Debounce: zabraňuje duplicitním skenům a reguluje zátěž (throttle).
  • UI vrstva: zobrazuje náhled, volitelně fokusní obdélník (ROI, „Region of InteREST“) a reaguje na výsledky.

Tím zabráníte, aby si UI, kamera a decoder navzájem blokovaly zdroje. „ROI“ zde znamená oříznuté vyhledávací okno (např. středních 60 %), které odlehčí decoderu a sníží falešně pozitivní výsledky. Důležité: ROI je nástroj pro výkon a použitelnost, nikoli bezpečnostní mechanismus.

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

Následující kód je myšlen jako kompaktní, ale projektně použitelný blok. Využívá ZXing (Delphi-Port) přes ZXing.ScanManager a připojuje se na TCameraComponent.OnSampleBufferReady. Rozhodující jsou tři body:

  • Snímky jsou throttled (není dekódován každý sample).
  • Dekódování neběží v UI-Thread.
  • Stop/Start je idempotentní (lze volat opakovaně bez rozhození zdrojů).
Delphi
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>
  /// Řadič QR skeneru pro FMX (Android/iOS).
  /// Zajišťuje řízení snímků kamery (frame gating), dekódování na pozadí a korektní spuštění/ukončení.
  /// </summary>
  TQrScannerController = class
  private
    FCamera: TCameraComponent;
    FScanManager: TScanManager;
    FBitmap: TBitmap;
    FLock: TObject;

    FOnResult: TQrScanResultEvent;

    // Řízení průtoku (gating/throttle)
    FIsRunning: Boolean;
    FIsDecoding: Integer; // 0/1 jako Interlocked-flag
    FLastDecodeTick: Int64;
    FMinIntervalMs: Cardinal;

    // Debounce proti opakujícím se stejným kódům
    FLastText: string;
    FLastTextTick: Int64;
    FDebounceMs: Cardinal;

    // ROI: část obrazu, která se skenuje (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; // např. 120
    property DebounceMs: Cardinal read FDebounceMs write FDebounceMs;         // např. 1200
    property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
    property RoiScale: Single read FRoiScale write FRoiScale;                 // např. 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;

  // Inicializovat ScanManager a omezit na QR (pro výkon a méně falešných pozitivních nálezů)
  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;

  // Aktivovat kameru: v reálných aplikacích předtím zkontrolovat oprávnění (Android) a zvážit tok UI.
  if Assigned(FCamera) then
    FCamera.Active := True;
end;

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

  // Korektně vypnout
  if Assigned(FCamera) then
    FCamera.Active := False;

  // Resetovat příznak dekodéru, pokud Stop přijde v nevhodné fázi
  TInterlocked.Exchange(FIsDecoding, 0);
end;

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

  // stejný text v rámci debounce okna - ignorovat
  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;

  // Jen jedno dekódování současně (jinak zácpa fronty na slabých zařízeních)
  if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
    Exit;

  // Kopírovat vzorek kamery do FBitmap. Zamykání, protože stejný bitmap buffer se nesmí používat paralelně.
  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ódování 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
  // Oříznout ROI uprostřed: snižuje výpočetní zatížení a zaměřuje uživatele.
  // Pozor: u velmi malých QR kódů může být ROI příliš úzké.
  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: navigace, pípnutí, vyplnění pole apod.
  TThread.Queue(nil,
    procedure
    begin
      if FIsRunning and Assigned(FOnResult) then
        FOnResult(AText);
    end);
end;

end.

Co kód řeší (a proč je to potřeba)

Throttle (MinIntervalMs) snižuje zatížení CPU a tvorbu tepla. Bez omezení se některá zařízení pokouší dekódovat 30–60 snímků/s; v praxi stačí 5–10/s, často méně. Debounce (DebounceMs) zabraňuje tomu, aby se stabilně držený QR kód spouštěl vícekrát (např. dvojité zaúčtování v jednom kroku procesu).

Interlocked-Flag (FIsDecoding) zajišťuje, že běží maximálně jeden Decode-Task. Je to architektonický trik proti „Queue-Stau“: pokud dekódování trvá 200 ms, ale úkol se spouští každých 120 ms, fronta poroste a výsledky přijdou se zpožděním, což v provozu působí jako „Scanner reagiert falsch“.

Okolnosti a úskalí

  • TBitmap a threading: FMX-Bitmapy mohou být GPU-backed. Přístup kopíruje frame do lokální bitmapy a dekóduje na pozadí. V závislosti na Delphi-verzi/platformě může být přesto potřeba opatrnosti: pokud vidíte artefakty, vynucujte CPU-bitmapu (např. přes Pixel-Read/Write) nebo pracujte s byte bufferem ze SampleBufferu (bližší platformě, ale stabilnější).
  • Stop/Start při navigaci: V mobilních aplikacích se často při přechodu formuláře nebo při App-Pause zastaví. Důležité je, aby Stop bylo možné volat opakovaně a aby nevyhazovalo výjimky (idempotentní). Navíc by callback výsledku měl ověřit, zda skener stále běží (dělá to DoResultOnMainThread).
  • ROI příliš úzké: Středové ROI urychlí zpracování, může ale selhat, pokud uživatel drží kód mimo střed nebo je kód velmi malý. Proto je EnableRoi konfigurovatelné a RoiScale omezené.
  • Formát-lock na QR: Omezení na QR_CODE je většinou správné. Pokud potřebujete také Code128/EAN, rozšiřte formáty – počítejte však s více falešnými pozitivy a vyšším zatížením CPU.

Delphi FMX Kamera-Lifecycle: Oprávnění, pozadí, rotace

Nejčastější chyby nevznikají při dekódování, ale okolo kamery:

  • Android Permissions: Práva ke kameře je nutné získat za běhu. Naplánujte případ, že uživatel odmítne nebo zvolí „Jenom tentokrát“. Technicky to znamená: držet UI-State („Scanner bereit?“) oddělený od Kamera-State, jinak uvíznete v polovičních stavech.
  • Aplikace jde do pozadí: Při OnApplicationEvent (např. EnteredBackground) byste měli zavolat Stop. Při návratu vědomě zavolejte Start (a případně krátké zpoždění), aby byl preview stabilní.
  • Rotace/Zrcadlení: Pro QR kódy je rotace často nekritická, ale v některých kamerových pipelinech může být bitmapa zrcadlená nebo otočená. Pokud skeny fungují „jen v jedné orientaci“, je to ukazatel. V takovém případě: před skenem otočte/zrcadlete nebo použijte decoder, který využívá metadata orientace.

Debugging v provozu: tak najdete skutečné příčiny

Pokud skener „občas“ nečte, reprodukovatelné ladění má velkou hodnotu. Tři opatření, která se osvědčila:

  1. Logování frame-samplingu: Logujte (pouze v Debug/Support-režimu) tick, rozměr obrazu, velikost ROI, dobu dekódování. Tak hned uvidíte, zda je problém v Throttle/Debounce nebo v zatížení CPU.
  2. Ukládání testovacích snímků: Ukládejte každých N sekund obrázek ROI (dočasně). Díky tomu můžete bez kamerového hardware analyzovat, zda je problém v kontrastu/rozostření.
  • Oddělit zátěž: UI aktualizace (Preview-Overlay, stavový text) neaktualizovat s vysokou frekvencí. „chvění UI“ často způsobují příliš mnohá Queue-události.
  • Varianty: Když potřebujete víc než „naskenuj a hotovo“

    Více výsledků, ale kontrolovaně

    Pro dávkové procesy (např. mnoho štítků za sebou) snižte DebounceMs a doplňte Whitelist/State-Machine: QR kód smí být akceptován pouze tehdy, když jej aktuální krok procesu očekává. To není UI logika, ale doménová logika – patří do samostatné vrstvy, aby byl Scanner a proces nezávisle testovatelný.

    Offline-Validierung und sichere Nutzdaten

    V podnikovém provozu QR kódy často obsahují ID nebo tokeny. Nespoléhejte se na to, že „QR = správný“. Validujte lokálně (formát, kontrolní součet, očekávané prefixy) a na straně serveru (REST-API). Pokud používáte tokeny: nastavte doby platnosti, ochranu proti replay útokům a buďte opatrní při logování (žádné tokeny v prostém textu v podporových logách).

    Legacy-Situationen: FMX-Scanner als Modul in gemischten Codebasen

    Pokud máte vyrostlý VCL svět, je FMX jako mobilní klient často samostatný proud. Držte Scanner jako Controller-Klasse ohne Form-Abhängigkeiten (jak výše), pak jej můžete integrovat do různých obrazovek. To se vyplatí i při modernizaci: business logika zůstává testovatelná, kamera je jen vstupní kanál. Zejména v legacy situacích se vyplatí jasné oddělení pro logging, feature-flagy a vzdálenou konfiguraci.

    Závěr: Robustní FMX-QR-Scan je problém životního cyklu – ne jen volání ZXing

    QR kód Scanner v Delphi FMX bude stabilní, pokud s ním budete zacházet jako s malým pipeline: kamera dodává snímky, background-decoder pracuje kontrolovaně a Debounce/Throttle zabrání duplicitním a opožděným událostem. Zdrojový útržek výše přesně cílí na místa, která se v reálných mobilních podnikových procesech lámou: příliš mnoho decode-úloh, nečisté zastavení, blokace UI vlákna a zbytečná zátěž.

    Meze použití: Pokud potřebujete extrémně vysoké rychlosti skenování (např. průmyslové skenování na dopravním pásu) nebo tvrdé požadavky na zpracování obrazu, je standardní FMX-kamera + bitmap-pipeline často příliš nákladná. Pak se vyplatí přístup blíže k platformě (Native Camera API, YUV-Buffer přímo, SIMD/NEON) nebo specializované Scanner-SDK. Pro většinu procesně orientovaných mobilních aplikací však uvedený přístup postačí, pokud jsou lifecycle, práva a threading správně integrovány – a procesy za nimi jsou jednoznačné.

    Pokud musíte přizpůsobit QR-scan do existující Delphi-architektury (včetně okrajových případů jako navigace, backgrounding, logging a validace procesu), rádi to s vámi strukturovaně vyjasníme:

    V odborném kontextu hrají také Zxing Delphi a Fmx Tcameracomponent důležitou roli, pokud integrace, datové toky a další vývoj musí spolupracovat čistě.

    Projednat projekt nebo modernizační záměr s Net-Base.

    Další krok

    Když se z tématu stane reálný projekt, měly by být architektura, stávající systém a provoz včas posuzovány společně.

    Podporujeme nejen při jednotlivých otázkách, ale i v případě, že se z útržků zdrojového kódu, legacy témat nebo nápadů na portál má vyvinout robustní podnikový projekt.

    • Současný stav, cílový stav a technická rizika jsou hodnoceny společně.
    • REST, přístup k datům, portály a nasazení nebudou odkládány na později.
    • Vidíte včas, která cesta je ekonomicky i provozně životaschopná.

    Sdílet příspěvek

    Sdílet tento příspěvek přímo

    LinkedIn, X, XING, Facebook, WhatsApp a e-mail jsou ihned k dispozici. Pro Instagram připravíme odkaz a stručný text.

    E-mail

    Instagram se otevře v nové záložce. Odkaz a krátký text budou předtím zkopírovány do schránky.