Net-Base Magazin

03.06.2026

QR Code Scanner in Delphi FMX: Kamera-Scan robust, thread-sicher und ohne UI-Zittern

Ein praxistauglicher QR Code Scanner Delphi FMX steht und fällt mit Kameralifecycle, Threading und sauberem Stop/Start. Der Beitrag zeigt einen robusten Ansatz mit ZXing, Debounce, Frame-Throttling, ROI-Zuschnitt sowie Debug- und Betriebsdetails für Android und iOS.

03.06.2026

Vom Magazinthema zur Projektpraxis

Passende Leistungs- und Technikseiten zum Beitrag

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; // z.B. 120
    property DebounceMs: Cardinal read FDebounceMs write FDebounceMs;         // z.B. 1200
    property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
    property RoiScale: Single read FRoiScale write FRoiScale;                 // z.B. 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.

Was der Code löst (und warum es nötig ist)

Throttle (MinIntervalMs) reduziert CPU-Last und Wärmeentwicklung. Ohne Begrenzung versuchen manche Geräte 30–60 Frames/s zu decodieren; in der Praxis reicht 5–10/s, oft weniger. Debounce (DebounceMs) verhindert, dass ein stabil gehaltener QR-Code mehrfach ausgelöst wird (z. B. doppeltes Buchen in einem Prozessschritt).

Das Interlocked-Flag (FIsDecoding) sorgt dafür, dass maximal ein Decode-Task läuft. Das ist ein Architekturkniff gegen „Queue-Stau“: Wenn Decoding 200 ms dauert, aber alle 120 ms ein Task gestartet wird, wächst die Warteschlange und Ergebnisse kommen zeitversetzt, was im Betrieb wie „Scanner reagiert falsch“ wirkt.

Randbedingungen und Stolperfallen

  • TBitmap und Threading: FMX-Bitmaps können GPU-backed sein. Der Ansatz kopiert das Frame in eine lokale Bitmap und decodiert im Hintergrund. Je nach Delphi-Version/Plattform kann trotzdem Vorsicht nötig sein: Wenn Sie Artefakte sehen, erzwingen Sie eine CPU-Bitmap (z. B. über Pixel-Read/Write) oder arbeiten Sie mit einem ByteBuffer aus dem SampleBuffer (plattformnaher, aber stabiler).
  • Stop/Start bei Navigation: In mobilen Apps wird oft beim Wechseln des Forms oder beim App-Pause-Event gestoppt. Wichtig ist, dass Stop mehrfach aufgerufen werden darf und keine Exceptions erzeugt (idempotent). Außerdem sollte das Ergebnis-Callback prüfen, ob der Scanner noch läuft (macht DoResultOnMainThread).
  • ROI zu eng: Ein mittiges ROI beschleunigt, kann aber scheitern, wenn Nutzer den Code außerhalb halten oder der Code sehr klein ist. Deshalb ist EnableRoi konfigurierbar und RoiScale begrenzt.
  • Format-Lock auf QR: Das Beschränken auf QR_CODE ist meist richtig. Wenn Sie auch Code128/EAN brauchen, erweitern Sie die Formate – rechnen Sie aber mit mehr False Positives und mehr CPU.

Delphi FMX Kamera-Lifecycle: Berechtigungen, Hintergrund, Rotation

Die häufigsten Bugs entstehen nicht beim Decoding, sondern rund um die Kamera:

  • Android Permissions: Kamera-Rechte sind zur Laufzeit einzuholen. Planen Sie den Fall ein, dass ein Nutzer ablehnt oder „Nur diesmal“ wählt. Technisch heißt das: UI-State („Scanner bereit?“) getrennt vom Kamera-State halten, sonst hängen Sie in halbfertigen States.
  • App geht in den Hintergrund: Beim OnApplicationEvent (z. B. EnteredBackground) sollten Sie Stop aufrufen. Beim Zurückkehren bewusst Start (und ggf. kurze Verzögerung), damit das Preview stabil ist.
  • Rotation/Mirroring: Für QR-Codes ist Rotation oft unkritisch, aber bei manchen Kamera-Pipelines kann das Bitmap gespiegelt oder gedreht sein. Wenn Scans „nur in einer Haltung“ funktionieren, ist das ein Hinweis. In dem Fall: vor dem Scan drehen/spiegeln oder auf einen Decoder setzen, der Orientation-Metadaten nutzt.

Debugging im Betrieb: So finden Sie die echten Ursachen

Wenn der Scanner „manchmal“ nicht liest, ist reproduzierbares Debugging Gold wert. Drei Maßnahmen, die sich bewähren:

  1. Frame-Sampling loggen: Loggen Sie (nur im Debug/Support-Modus) Tick, Bildgröße, ROI-Größe, Decode-Dauer. So sehen Sie sofort, ob Throttle/Debounce oder CPU-Last das Problem ist.
  2. Testbilder sichern: Speichern Sie alle N Sekunden ein ROI-Bild (temporär). Damit können Sie ohne Kamera-Hardware analysieren, ob Kontrast/Unschärfe das Problem ist.
  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.

Nächster Schritt

Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb frueh zusammen betrachtet werden.

Wir unterstuetzen nicht nur bei Einzelfragen, sondern auch dann, wenn aus Source-Schnipseln, Legacy-Themen oder Portalideen ein belastbares Unternehmensprojekt werden soll.

  • Bestand, Zielbild und technische Risiken werden zusammen bewertet.
  • REST, Datenzugriff, Portale und Rollout werden nicht als Spaetfolgen verschoben.
  • Sie sehen frueh, welcher Weg wirtschaftlich und betrieblich tragfähig ist.

Beitrag teilen

Diesen Beitrag direkt weitergeben

LinkedIn, X, XING, Facebook, WhatsApp und E-Mail sind sofort verfügbar. Für Instagram bereiten wir Link und Kurztext direkt vor.

E-Mail

Instagram oeffnet in einem neuen Tab. Link und Kurztext werden vorher in die Zwischenablage kopiert.