Net-Base Revija

03.06.2026

Skenirnik QR-kod v Delphi FMX: robustno skeniranje kamere, večnitno varno in brez tresenja uporabniškega vmesnika

Uporaben skener QR-kod Delphi FMX je odvisen od življenjskega cikla kamere, upravljanja niti in čistega Stop/Start. Prispevek predstavi robusten pristop z ZXing, Debounce, Frame-Throttling, izrezom ROI ter razhroščevalnimi in obratovalnimi podrobnostmi za Android in iOS.

03.06.2026

Od teme v reviji do projektne prakse

Ustrezne strani storitev in tehnični opisi k prispevku

QR Code Scanner Delphi FMX v praksi

En QR Code Scanner Delphi FMX je v demonstraciji hitro sestavljen: prikaže se predogled kamere, vzame bitmapo, zažene se ZXing. V resnični poslovni programski opremi (npr. prevzem blaga, dodeljevanje naprav, ticketing, postopki dostopa) pa se pojavijo dodatne robne zahteve: aplikacija gre v ozadje, kamera izgubi fokus, uporabnik drži napravo poševno, format slike se spremeni – in nenadoma skenirate isto kodo dvakrat na sekundo ali se uporabniški vmesnik zatika, ker se dekodiranje izvaja v UI-niti.

Tipične težave niso toliko „ZXing kann nicht lesen“, temveč življenjski cikel in arhitektura: sproščanje virov kamere, ritmizacija okvirjev, varnost niti pri dostopu do TBitmap (GPU/CPU), in jasen Stop/Start, ki ostane čist tudi, ko uporabniki hitro navigirajo ali ko OS začasno odvzame kamero.

Architekturüberblick: Pipeline statt „OnSampleBufferReady macht alles“

V praksi se je obnesla majhna pipeline s jasnimi odgovornostmi:

  • Adapter kamere: dostavlja okvirje (ali njihove kopije) v določenem formatu.
  • Decoder: deluje v ozadni niti in rezultate vrača preko callbacka.
  • Gate/Debounce: preprečuje dvojno skeniranje in regulira obremenitev (Throttle).
  • UI-sloj: prikazuje predogled, opcijsko fokusno pravokotno območje (ROI, „Region of InteREST“) in reagira na rezultate.

Tukaj ‚ROI‘ pomeni izrezano iskalno okno (npr. osrednje 60 %), ki razbremeni decoder in zmanjša napačno pozitivne rezultate. Pomembno: ROI je orodje za zmogljivost in uporabniško izkušnjo, ne varnostni mehanizem.

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

Naslednja koda je mišljena kot kompakten, a projektno uporaben gradnik. Uporablja ZXing (Delphi-Port) preko ZXing.ScanManager in se poveže na TCameraComponent.OnSampleBufferReady. Ključni so trije vidiki:

  • Okvirji so throttled (ne dekodiramo vsakega vzorca).
  • Dekodiranje poteka ne v UI-niti.
  • Stop/Start je idempotent (večkrat klicljiv, brez kaosa z viri).
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>
  /// Krmilnik QR-skeniranja za FMX (Android/iOS).
  /// Skrbi za upravljanje frekvence okvirjev iz kamere, dekodiranje v ozadju in urejeno zaustavljanje/zagon.
  /// </summary>
  TQrScannerController = class
  private
    FCamera: TCameraComponent;
    FScanManager: TScanManager;
    FBitmap: TBitmap;
    FLock: TObject;

    FOnResult: TQrScanResultEvent;

    // Gating/Throttle
    FIsRunning: Boolean;
    FIsDecoding: Integer; // 0/1 kot Interlocked-Flag
    FLastDecodeTick: Int64;
    FMinIntervalMs: Cardinal;

    // Debounce proti ponavljajočim se enakim kodam
    FLastText: string;
    FLastTextTick: Int64;
    FDebounceMs: Cardinal;

    // ROI: delež slike, ki 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;

  // ScanManager inicializirati in omejiti na QR (zmogljivost + manj 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;

  // Vklop kamere: v dejanskih aplikacijah prej preverite dovoljenja (Android) in upoštevajte UI-potok.
  if Assigned(FCamera) then
    FCamera.Active := True;
end;

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

  // Pravilno izklopiti
  if Assigned(FCamera) then
    FCamera.Active := False;

  // Ponastavi zastavico dekoderja, če Stop pride v neugodnem trenutku
  TInterlocked.Exchange(FIsDecoding, 0);
end;

function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
  // Omejevanje: ne dekodiraj vsakega okvirja
  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);

  // enako besedilo znotraj Debounce-okna - prezri
  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 en dekod hkrati (sicer zastoj v vrsti na šibkih napravah)
  if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
    Exit;

  // Kopiraj vzorec kamere v FBitmap. Zaklep, ker isti bitmap-buffer ne sme biti uporabljen vzporedno.
  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 v ozadju
  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
  // Izreži ROI na sredini: zmanjša obdelovalno obremenitev in usmeri uporabnika.
  // Pozor: pri zelo majhnih QR-kodah je lahko ROI preozek.
  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-nit: navigacija, zvočni signal, izpolnitev polja itd.
  TThread.Queue(nil,
    procedure
    begin
      if FIsRunning and Assigned(FOnResult) then
        FOnResult(AText);
    end);
end;

end.

Kaj koda rešuje (in zakaj je to potrebno)

Throttle (MinIntervalMs) zmanjša obremenitev CPU in razvoj toplote. Brez omejitve nekateri napravi poskušajo dekodirati 30–60 okvirjev/s; v praksi zadostuje 5–10/s, pogosto manj. Debounce (DebounceMs) prepreči, da bi se enakomerno držana QR-koda sprožila večkrat (npr. dvojno knjiženje v enem koraku procesa).

Interlocked-Flag (FIsDecoding) zagotavlja, da teče največ en Decode-Task. To je arhitekturni prijem proti „zastoju v čakalni vrsti“: če dekodiranje traja 200 ms, vendar se vsakih 120 ms zažene nov task, se čakalna vrsta poveča in rezultati prispejo z zamikom, kar v obratovanju deluje kot »skener se odziva napačno«.

Omejitve in pasti

  • TBitmap in niti: FMX-Bitmaps so lahko podprte s GPU. Pristop kopira okvir v lokalno bitmapo in dekodira v ozadju. Glede na Delphi-verzijo/platformo je lahko vseeno potrebna previdnost: če opazite artefakte, prisilite CPU-bitmapo (npr. preko branja/pisanja pikslov) ali delajte z ByteBufferjem iz SampleBufferja (platformno bližje, a stabilneje).
  • Stop/Start pri navigaciji: V mobilnih aplikacijah se pogosto ob menjavi forma ali ob dogodku pavze aplikacije ustavi zajem. Pomembno je, da je Stop mogoče poklicati večkrat brez sproženja izjem (idempotentno). Rezultatni callback naj tudi preveri, ali je skener še vedno aktiven (to stori DoResultOnMainThread).
  • Preozko ROI: Središčni ROI pospeši dekodiranje, lahko pa zataji, če uporabnik drži kodo izven območja ali je koda zelo majhna. Zato je EnableRoi nastavljiv in RoiScale omejen.
  • Zaklep formata na QR: Omejitev na QR_CODE je v večini primerov pravilna. Če potrebujete tudi Code128/EAN, razširite nabor formatov — pričakujte pa več lažno pozitivnih zadetkov in večjo obremenitev CPU.

Delphi FMX življenjski cikel kamere: dovoljenja, ozadje, rotacija

Najpogostejše napake ne nastanejo pri dekodiranju, temveč okoli kamere:

  • Android dovoljenja: Pravice za kamero je treba pridobiti med izvajanjem. Predvidite primer, ko uporabnik zavrne ali izbere »Samo tokrat«. Tehnično to pomeni: ločite UI-state („Skener pripravljen?“) od stanja kamere, sicer lahko obtičite v polovičnih stanjih.
  • Aplikacija gre v ozadje: Ob OnApplicationEvent (npr. EnteredBackground) pokličite Stop. Ob vračanju namerno pokličite Start (in po potrebi kratko počakajte), da bo predogled stabilen.
  • Rotacija/zrcaljenje: Za QR-kode je rotacija pogosto nebistvena, vendar je pri nekaterih kamernih pipelinah bitmapa lahko zrcaljena ali zavrtena. Če skeniranje deluje »le v eni legi«, je to indikator. V tem primeru pred skeniranjem obrnite/zrcalite sliko ali uporabite dekoder, ki uporablja orientacijske metapodatke.

Razhroščevanje v obratovanju: tako najdete prave vzroke

Če skener „včasih“ ne bere, je reproducibilno razhroščevanje zelo dragoceno. Trije ukrepi, ki se izkažejo:

  1. Frame-sampling v log: Zabeležite (samo v načinu Debug/Support) tick, velikost slike, velikost ROI in trajanje dekodiranja. Tako takoj vidite, ali sta Throttle/Debounce ali obremenitev CPU vzrok problema.
  2. Shranite testne slike: Shrani vsakih N sekund sliko ROI (začasno). S tem lahko brez kamerne strojne opreme analizirate, ali sta kontrast ali zamegljenost vzrok težave.
  3. Ločite obremenitev: ne posodabljajte UI-posodobitev (preview-overlay, statusni tekst) z visoko frekvenco. „Tresenje UI“ pogosto izvira iz prevelikega števila Queue-dogodkov.

Varianten: Wenn Sie mehr brauchen als „Scan und fertig“

Več rezultatov, vendar nadzorovano

Za serijske procese (npr. veliko nalepk zaporedoma) zmanjšajte DebounceMs in dodajte Whitelist/State-Machine: QR-koda naj bo sprejeta le, če jo pričakuje trenutni korak procesa. To ni UI-logika, temveč domenska logika – pripada svoji plasti, da sta skener in proces neodvisno testabilna.

Offline-validacija in varni uporabniški podatki

V poslovnih procesih QR-kode pogosto vsebujejo ID-je ali tokene. Ne zanašajte se na to, da „QR = pravilen“. Validirajte lokalno (format, kontrolna vsota, pričakovani prefiksi) in na strežniku (REST-API). Če uporabljate tokene: določite čas veljavnosti, zaščito pred ponovnim predvajanjem in previdno beleženje (brez tokenov v nešifriranem besedilu v support-logih).

Legacy-situacije: FMX-Scanner kot modul v mešanih kodebasah

Če imate razraščeno VCL-okolje, je FMX kot mobilni klient pogosto ločen razvojni tok. Ohranjajte skener kot Controller-klaso brez odvisnosti od Form (kot zgoraj), tako ga lahko integrirate v različne zaslone. To se obrestuje tudi pri modernizaciji: poslovna logika ostane testabilna, kamera je zgolj vhodni kanal. Ravno v legacy-situacijah se izplača jasna ločnica za beleženje, feature-flag-e in oddaljeno konfiguracijo.

Fazit: Solider FMX-QR-Scan ist ein Lifecycle-Problem – nicht nur ein ZXing-Aufruf

QR Code Scanner v Delphi FMX postane stabilen, če ga obravnavate kot majhno cevovod: kamera dovaja frames, ozadinski dekoder dela kontrolirano, in Debounce/Throttle preprečujeta dvojne ter zamujene dogodke. Zgornji izvleček izvorne kode naslavlja natanko tiste točke, kjer v resničnih mobilnih poslovnih procesih prihaja do težav: preveč decode-taskov, neurejen stop, blokade UI-niti in nepotrebna obremenitev.

Meje uporabe: Če potrebujete izjemno visoke hitrosti skeniranja (npr. industrijsko skeniranje na tekočem traku) ali stroge zahteve po obdelavi slik, je FMX-standardna kamera + bitmap-pipeline pogosto predraga. Tedaj se izplača pristop bližje platformi (Native Camera API, neposredni YUV-Buffer, SIMD/NEON) ali specializiran Scanner-SDK. Za večino procesno povezanih mobilnih aplikacij pa je prikazani pristop zadosten, če so lifecycle, pravice in threading čisto integrirani – in so postopki v ozadju jasno definirani.

Če morate QR-sken vključiti v obstoječo Delphi arhitekturo (vključno z robnimi primeri kot so navigacija, backgrounding, beleženje in validacija procesov), to z veseljem strukturirano razčlenimo:

V strokovnem okolju imata tudi Zxing Delphi in Fmx Tcameracomponent pomembno vlogo, kadar morajo integracije, tokovi podatkov in nadaljnji razvoj delovati usklajeno.

O projektu ali modernizacijskem načrtu se pogovorite z Net-Base.

Naslednji korak

Ko se tema spremeni v dejanski projekt, je treba arhitekturo, obstoječi sistem in obratovanje zgodaj obravnavati skupaj.

Ne podpiramo le pri posameznih vprašanjih, ampak tudi takrat, ko iz izrezkov izvorne kode, legacy-tem ali idej za portale nastane zanesljiv podjetniški projekt.

  • Obstoječe stanje, ciljno stanje in tehnična tveganja se ocenjujejo skupaj.
  • REST, dostop do podatkov, portali in uvedba niso prestavljeni kot poznejše posledice.
  • Zgodaj prepoznate, katera pot je ekonomsko in obratovalno vzdržna.

Deli objavo

Deli ta prispevek neposredno

LinkedIn, X, XING, Facebook, WhatsApp in e-pošta so takoj na voljo. Za Instagram bomo neposredno pripravili povezavo in kratek opis.

E-pošta

Instagram se odpre v novem zavihku. Povezava in kratek opis se pred tem kopirata v odložišče.