Net-Base Magasin

03.06.2026

QR Code Scanner i Delphi FMX: robust kameraskanning, trådsikker og utan UI-risting

Ein praktisk eigna QR-kode-skannar Delphi FMX står og fell med kameralivssyklus, Threading og ryddig Stop/Start. Artikkelen syner ein robust tilnærming med ZXing, Debounce, Frame-Throttling, ROI-beskjæring samt debug- og driftsdetaljar for Android og iOS.

03.06.2026

Frå magasinetema til prosjektpraksis

Passande teneste- og tekniske sider til innlegget

QR Code Scanner Delphi FMX in der Praxis

Ein QR Code Scanner Delphi FMX ist in der Demo schnell zusammengesetzt: Kamera-Preview anzeigen, Bitmap ziehen, ZXing drüberlaufen lassen. In echter Business-Software (z. B. Wareneingang, Gerätezuordnung, Ticketing, Zutrittsprozesse) kommen aber Randbedingungen dazu: App geht in den Hintergrund, Kamera verliert den Fokus, Benutzer hält das Gerät schräg, das Bildformat wechselt – und plötzlich scannen Sie zweimal pro Sekunde denselben Code oder die UI ruckelt, weil die Decodierung im UI-Thread läuft.

Die typischen Probleme sind weniger „ZXing kann nicht lesen“, sondern Lifecycle und Architektur: Ressourcenfreigabe der Kamera, Taktung der Frames, Thread-Sicherheit beim Zugriff auf TBitmap (GPU/CPU), und ein klarer Stop/Start, der auch dann sauber ist, wenn Nutzer schnell navigieren oder das OS die Kamera kurzfristig entzieht.

Architekturüberblick: Pipeline statt „OnSampleBufferReady macht alles“

Praktisch bewährt hat sich eine kleine Pipeline mit klaren Zuständigkeiten:

  • Kamera-Adapter: liefert Frames (oder Kopien davon) in einem definierten Format.
  • Decoder: arbeitet im Hintergrund-Thread und gibt Ergebnisse über ein Callback zurück.
  • Gate/Debounce: verhindert Doppel-Scans und regelt Last (Throttle).
  • UI-Schicht: zeigt Preview, optional Fokus-Rechteck (ROI, „Region of InteREST“) und reagiert auf Ergebnisse.

Damit vermeiden Sie, dass UI, Kamera und Decoder sich gegenseitig blockieren. „ROI“ meint hier ein zugeschnittenes Suchfenster (z. B. mittig 60 %), das den Decoder entlastet und falsch-positive Ergebnisse reduziert. Wichtig: ROI ist ein Performance- und Usability-Werkzeug, kein Sicherheitsmechanismus.

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

Der folgende Code ist als kompakter, aber projekttauglicher Baustein gedacht. Er nutzt ZXing (Delphi-Port) über ZXing.ScanManager und hängt sich an TCameraComponent.OnSampleBufferReady. Entscheidend sind drei Punkte:

  • Frames werden throttled (nicht jedes Sample decodieren).
  • Decoding läuft nicht im UI-Thread.
  • Stop/Start ist idempotent (mehrfach aufrufbar, ohne Ressourcenchaos).
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>
  /// QR-Scanner-Controller für FMX (Android/iOS).
  /// Kümmert sich um Kamera-Frame-Gating, Hintergrund-Decoding und sauberes Stop/Start.
  /// </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; // f.eks. 120
    property DebounceMs: Cardinal read FDebounceMs write FDebounceMs;         // f.eks. 1200
    property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
    property RoiScale: Single read FRoiScale write FRoiScale;                 // f.eks. 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: In echten Apps vorher Permissions prüfen (Android) und UI-Flow berücksichtigen.
  if Assigned(FCamera) then
    FCamera.Active := True;
end;

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

  // Aktiv sauber abschalten
  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: nicht jedes Frame dekodieren
  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);

  // gleicher Text innerhalb Debounce-Fenster - ignorieren
  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;

  // Hintergrund-Decoding
  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 mittig ausschneiden: reduziert Rechenlast und lenkt den Nutzer.
  // Achtung: bei sehr kleinen QR-Codes kann ROI zu eng sein.
  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: Navigation, Beep, Feld füllen etc.
  TThread.Queue(nil,
    procedure
    begin
      if FIsRunning and Assigned(FOnResult) then
        FOnResult(AText);
    end);
end;

end.

Kva koden løyser (og kvifor det er nødvendig)

Throttle (MinIntervalMs) reduserer CPU-belastning og varmeutvikling. Utan avgrensing prøver nokre einingar å dekode 30–60 Frames/s; i praksis held 5–10/s, ofte mindre. Debounce (DebounceMs) forhindrar at ein stabilt halden QR-kode utløyser fleire gongar (t.d. dobbel bokføring i eit prosesssteg).

Det Interlocked-Flag (FIsDecoding) sørgjer for at maksimalt ein Decode-Task køyrer. Dette er eit arkitekturgrep mot «Queue-Stau»: Dersom dekoding tek 200 ms, men det startar ein task kvar 120 ms, veks køen og resultata kjem forseinka, noko som i drift oppfattast som «skannaren reagerer feil».

Rammevilkår og fallgruver

  • TBitmap und Threading: FMX-Bitmaps kan vere GPU-backed. Tilnærminga kopierar ramma til ei lokal Bitmap og dekodar i bakgrunnen. Avhengig av Delphi-versjon/plattform kan det likevel vere naudsynt med forsiktighet: Ser du artefaktar, tving fram ei CPU-Bitmap (t.d. via Pixel-Read/Write) eller arbeid med ein ByteBuffer frå SampleBuffer (plattformnært, men meir stabilt).
  • Stop/Start bei Navigation: I mobile app-ar stoppar ein ofte ved byte av form eller ved App-Pause-event. Viktig er at Stop kan kallast fleire gongar utan å kaste unntak (idempotent). I tillegg bør resultat-callbacken sjekke om skannaren framleis køyrer (gjer DoResultOnMainThread).
  • ROI zu eng: Eit sentrert ROI akselererer, men kan feile dersom brukaren held koden utanfor eller koden er veldig liten. Difor er EnableRoi konfigurerbart og RoiScale avgrensa.
  • Format-Lock auf QR: Å avgrense til QR_CODE er som regel riktig. Treng du også Code128/EAN, utvid formatlista – rekna då med fleire falske positive og høgare CPU-belastning.

Delphi FMX Kamera-Lifecycle: Tillatelser, bakgrunn, rotasjon

Dei vanlegaste feila oppstår ikkje ved dekoding, men rundt kameraet:

  • Android Permissions: Kameratilgang må innhentast ved køyringstid. Planlegg for at ein brukar kan avslå eller velje «Berre denne gongen». Teknisk betyr det at UI-tilstand («Scanner klar?») må haldast separat frå kamera-tilstand, elles sit du fast i halvferdige tilstandar.
  • App geht in den Hintergrund: Ved OnApplicationEvent (t.d. EnteredBackground) bør du kalle Stop. Ved tilbakekomst kall eksplisitt Start (og eventuelt ein kort forsinkelse), slik at forhandsvisinga blir stabil.
  • Rotation/Mirroring: For QR-kodar er rotasjon ofte uproblemisk, men i nokre camera-pipelines kan bitmappa vere spegla eller rotert. Dersom skann fungerer «berre i ei haldning», er det eit teikn. I så fall: roter/spegl før skann eller bruk ein decoder som nyttar orientation-metadata.

Feilsøking i drift: Slik finn du dei eigentlege årsakene

Dersom skannaren «av og til» ikkje les, er reproducerbar feilsøking gull verdt. Tre tiltak som har vist seg nyttige:

  1. Frame-Sampling loggen: Logg (berre i Debug/Support-modus) tick, biletstorleik, ROI-storleik, dekodetid. Då ser du med ein gong om Throttle/Debounce eller CPU-belastning er problemet.
  2. Testbilder sichern: Lagre kvart N sekund eit ROI-bilete (midlertidig). Då kan du utan kamerahardware analysere om kontrast/uskarpheit er årsaka.
  • Skil arbeidsmengda: UI-oppdateringar (Preview-Overlay, status-tekst) ikkje oppdaterast i høg frekvens. UI-jitter kjem ofte av for mange Queue-events.
  • Variantar: Når De treng meir enn «Skann og ferdig»

    Fleire resultat, men kontrollert

    For stabelprosessar (t.d. mange etikettar etter kvarandre) reduserar De DebounceMs og legg til ei Whitelist/State-Machine: Ein QR-kode bør berre godkjennast når det aktuelle prosesssteget ventar han. Dette er ikkje UI-logikk, men domenelogikk – han høyrer til i eit eige lag, slik at skannar og prosess kan testast uavhengig.

    Offline-validering og sikre nyttedata

    I bedriftsprosessar inneheld QR-kodar ofte ID-ar eller token. Stol ikkje på at «QR = korrekt». Valider lokalt (format, kontrollsiffer, forventa prefiks) og på serveren (REST-API). Dersom De brukar token: sett utløpstid, replay-skydd, og handter logging med varsemd (ingen token i klartekst i support-loggar).

    Legacy-situasjonar: FMX-skannar som modul i blanda kodebasar

    Dersom De har ei etablert VCL-verda, er FMX som mobilklient ofte ein separat grein. Halde skannaren som ei Controller-klasse utan form-avhengnader (som ovanfor), då kan De integrere han i forskjellige skjermbilete. Dette lønar seg òg ved modernisering: forretningslogikken held seg testbar, kameraet er berre ein inndatakanal. Særleg i legacy-situasjonar er det verdt å ha eit klart snitt for logging, feature-flags og fjernkonfigurasjon.

    Konklusjon: Ein robust FMX-QR-skann er eit livssyklusproblem – ikkje berre eit ZXing-kall

    Ein QR Code Scanner i Delphi FMX blir stabil når De behandlar han som ei lita pipeline: kameraet leverer frames, ein bakgrunnsdekodar arbeider kontrollert, og debounce/throttle forhindrar doble og forseinka events. Kodeeksemplet ovanfor adresserer nøyaktig dei punkta som sviktar i reelle mobile forretningsprosessar: for mange dekodingsoppgåver, uregelmessig stop, UI-tråd-blokkeringar og unødig belastning.

    Bruksgrenser: Dersom De treng ekstremt høge skannehastigheiter (t.d. industriskanning på transportband) eller strenge krav til bilehandtering, er FMX-standardkamera + bitmap-pipeline ofte for kostbar. Då lyt ein plattformsnær tilnærming (Native Camera API, YUV-Buffer direkte, SIMD/NEON) eller eit spesialisert skannar-SDK vurderast. For dei fleste prosessnære mobile applikasjonar er den viste løysinga likevel tilstrekkeleg, så lenge livssyklus, rettar og threading er integrerte – og prosessane bak er entydige.

    Dersom De må tilpasse ein QR-skann til ei eksisterande Delphi-arkitektur (inkludert grensetilfelle som navigasjon, backgrounding, logging og prosessvalidering), avklarer vi dette gjerne strukturelt:

    I fagmiljøet spelar òg Zxing Delphi og Fmx Tcameracomponent ei viktig rolle når integrasjonar, dataflyt og vidareutvikling må fungere ryddig saman.

    Drøft prosjekt eller moderniseringsprosjekt med Net-Base.

    Neste steg

    Når temaet blir eit reelt prosjekt, bør arkitektur, eksisterande system og drift vurderast tidleg saman.

    Vi støttar ikkje berre ved enkeltspørsmål, men òg når korte kildekodesnuttar, legacy-tema eller portalidéar skal utviklast til eit robust bedriftsprosjekt.

    • Eksisterande tilstand, målbiletet og tekniske risikoar blir vurderast samla.
    • REST, datatilgang, portalar og utrulling blir ikkje utsette til seinare som etterverknader.
    • De ser tidleg kva veg som er økonomisk og driftsmessig berekraftig.

    Del innlegg

    Del dette innlegget direkte

    LinkedIn, X, XING, Facebook, WhatsApp og e-post er tilgjengelege med ein gong. For Instagram klargjer vi lenke og kort tekst med det same.

    E-post

    Instagram opnar i ein ny fane. Lenkje og kort tekst blir kopiert til utklippstavla på førehand.