Net-Base Časopis

03.06.2026

QR Code skener u Delphi FMX: robustno skeniranje kamerom, sigurno za više niti i bez treperenja korisničkog sučelja

Praktičan QR Code skener Delphi FMX zavisi od životnog ciklusa kamere, upravljanja dretvama i korektnog zaustavljanja/pokretanja. Članak prikazuje robustan pristup sa ZXing, debounce-om, frame-throttlingom, ROI-izrezom te detaljima za debug i operativni rad na Androidu i iOS-u.

03.06.2026

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).
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>
  /// 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 Stop može pozvati više puta i da ne baca iznimke (idempotentno). Također bi rezultat‑callback trebao provjeriti da li skener još radi (to radi DoResultOnMainThread).
  • 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 EnableRoi konfigurabilan i RoiScale ograničen.
  • Format‑lock na QR: Ograničavanje na QR_CODE je 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 pozvati Stop. Pri povratku svjesno pozovite Start (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:

  1. 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.
  2. 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.
  3. 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.

Podijeli objavu

Ovu objavu direktno proslijediti

LinkedIn, X, XING, Facebook, WhatsApp i e-pošta su odmah dostupni. Za Instagram pripremamo link i kratak tekst.

E-pošta

Instagram se otvara u novom tabu. Link i kratak tekst se prethodno kopiraju u međuspremnik.