Net-Base Magasin

03.06.2026

QR-kodsskanner i Delphi FMX: robust kameraskanning, trådsäker och utan UI-ryckningar

En praktiskt användbar QR-kodscanner Delphi FMX står och faller med kamerans livscykel, trådhantering och ordnad stopp/start. Artikeln visar ett robust tillvägagångssätt med ZXing, debounce, frame-throttling, ROI-beskärning samt felsöknings- och driftsdetaljer för Android och iOS.

03.06.2026

Från magasinets tema till projektpraxis

Passande tjänste- och tekniksidor för inlägget

QR-kodsläsare Delphi FMX i praktiken

En QR Code Scanner Delphi FMX är i demon snabbt ihopsatt: visa kameraförhandsvisning, ta en bitmap, köra ZXing över den. I verklig affärsprogramvara (t.ex. mottagning av varor, enhetsallokering, ticketing, åtkomstprocesser) tillkommer dock randvillkor: appen går i bakgrunden, kameran tappar fokus, användaren håller enheten snett, bildformatet ändras – och plötsligt skannas samma kod två gånger per sekund eller UI:t hakar eftersom dekodningen körs i UI-tråden.

De typiska problemen är mindre „ZXing kan inte läsa“, och mer livscykel och arkitektur: frigöring av kamerans resurser, taktning av frames, trådsäker åtkomst till TBitmap (GPU/CPU), och en tydlig stop/start som förblir ren även när användare navigerar snabbt eller operativsystemet tillfälligt tar kameran.

Arkitekturöversikt: Pipeline istället för „OnSampleBufferReady gör allt”

I praktiken har en liten pipeline med tydliga ansvarsområden visat sig fungera:

  • Kameraadapter: levererar frames (eller kopior av dem) i ett definierat format.
  • Decoder: arbetar i en bakgrundstråd och returnerar resultat via en callback.
  • Gate/Debounce: förhindrar dubbelskanningar och reglerar belastningen (Throttle).
  • UI-lagret: visar förhandsvisning, valfritt fokusrektangel (ROI, „Region of InteREST“) och reagerar på resultat.

Detta förhindrar att UI, kamera och decoder blockerar varandra. „ROI“ avser här ett beskuret sökfönster (t.ex. centrerat 60 %) som avlastar decodern och minskar falsk-positiva resultat. Viktigt: ROI är ett pRESTanda- och användbarhetsverktyg, inte en säkerhetsmekanism.

Kodexempel: Robust QR-kodsläsare (FMX + ZXing) med debounce och korrekt stop

Följande kod är avsedd som en kompakt, men projekttålig, byggsten. Den använder ZXing (Delphi-port) via ZXing.ScanManager och knyter an till TCameraComponent.OnSampleBufferReady. Avgörande är tre punkter:

  • Frames throttlas (throttled) (inte varje sample dekodas).
  • Dekodning körs inte i UI-tråden.
  • Stop/Start är idempotent (kan anropas flera gånger utan resurskaos).
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-skannercontroller för FMX (Android/iOS).
  /// Ansvarar för kamera-frame-gating, bakgrundsavkodning och ordnad stop/start.
  /// </summary>
  TQrScannerController = class
  private
    FCamera: TCameraComponent;
    FScanManager: TScanManager;
    FBitmap: TBitmap;
    FLock: TObject;

    FOnResult: TQrScanResultEvent;

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

    // Debounce mot upprepade samma koder
    FLastText: string;
    FLastTextTick: Int64;
    FDebounceMs: Cardinal;

    // ROI: del av bilden som skannas (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; // t.ex. 120
    property DebounceMs: Cardinal read FDebounceMs write FDebounceMs;         // t.ex. 1200
    property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
    property RoiScale: Single read FRoiScale write FRoiScale;                 // t.ex. 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;

  // Initiera ScanManager och begränsa till QR (prestanda + färre falsk positiva)
  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 aktivera: I riktiga appar kontrollera först behörigheter (Android) och ta hänsyn till UI-flödet.
  if Assigned(FCamera) then
    FCamera.Active := True;
end;

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

  // Stäng av aktivt och ordnat
  if Assigned(FCamera) then
    FCamera.Active := False;

  // Återställ decoder-flaggan om Stop anropas i en ogynnsam fas
  TInterlocked.Exchange(FIsDecoding, 0);
end;

function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
  // Throttle: avkoda inte varje bildruta
  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);

  // samma text inom debounce-fönstret - ignorera
  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;

  // Endast en avkodning åt gången (annars köuppbyggnad på svaga enheter)
  if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
    Exit;

  // Kopiera kamerasample till FBitmap. Lås, eftersom samma bitmap‑buffer inte ska användas parallellt.
  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;

  // Bakgrundsavkodning
  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
  // Klipp ut ROI centrerat: minskar beräkningsbörda och styr användaren.
  // Observera: vid mycket små QR-koder kan ROI bli för snäv.
  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-tråd: navigation, pip, fylla i fält osv.
  TThread.Queue(nil,
    procedure
    begin
      if FIsRunning and Assigned(FOnResult) then
        FOnResult(AText);
    end);
end;

end.

Vad koden löser (och varför det är nödvändigt)

Throttle (MinIntervalMs) minskar CPU-belastning och värmeutveckling. Utan begränsning försöker vissa enheter avkoda 30–60 frames/s; i praktiken räcker 5–10/s, ofta mindre. Debounce (DebounceMs) förhindrar att en stabilt hållen QR-kod triggas flera gånger (t.ex. dubbel bokning i ett processsteg).

Interlocked-Flag (FIsDecoding) ser till att högst en decode-task körs samtidigt. Det är ett arkitekturknep mot „kö-stagnation“: Om avkodning tar 200 ms, men en task startas var 120 ms, växer kön och resultaten kommer fördröjt, vilket i drift upplevs som „scannern reagerar felaktigt“.

Randvillkor och fallgropar

  • TBitmap och trådning: FMX-bitmaps kan vara GPU-backed. Tillvägagångssättet kopierar frame till en lokal bitmap och avkodar i bakgrunden. Beroende på Delphi-version/plattform kan ändå försiktighet krävas: Om ni ser artefakter, tvinga fram en CPU-bitmap (t.ex. via pixel-read/write) eller arbeta med en ByteBuffer från SampleBuffer (mer plattformsnära, men stabilare).
  • Stop/Start vid navigation: I mobila appar stoppas ofta vid byte av form eller vid app-pause-event. Viktigt är att Stop kan anropas flera gånger utan att kasta exceptions (idempotent). Dessutom bör resultat-callbacken kontrollera om scannern fortfarande körs (det gör DoResultOnMainThread).
  • ROI för snävt: Ett centrerat ROI snabbar upp, men kan misslyckas om användaren håller koden utanför eller koden är mycket liten. Därför är EnableRoi konfigurerbar och RoiScale begränsad.
  • Format-lås på QR: Att begränsa till QR_CODE är oftast rätt. Om ni även behöver Code128/EAN, utöka formaten – räkna dock med fler false positives och högre CPU-användning.

Delphi FMX kamera-livscykel: behörigheter, bakgrund, rotation

De vanligaste buggarna uppstår inte vid dekodning utan runt kamerahanteringen:

  • Android-behörigheter: Kamerarättigheter måste begäras i runtime. Planera för att en användare nekar eller väljer „Endast denna gång“. Tekniskt innebär det att UI-state („Scanner redo?“) hålls separat från kamera-state, annars fastnar ni i ofärdiga tillstånd.
  • App går i bakgrunden: Vid OnApplicationEvent (t.ex. EnteredBackground) bör ni anropa Stop. Vid återkomst anropa medvetet Start (och eventuellt en kort fördröjning) så att preview blir stabil.
  • Rotation/Mirroring: För QR-koder är rotation ofta okritisk, men i vissa kamerapipelines kan bitmapen vara spegelvänd eller roterad. Om skanning fungerar „endast i en hållning“ är det en indikation. I så fall: rotera/spegla innan skanning eller använd en decoder som använder orienteringsmetadata.

Felsökning i drift: Så hittar ni de verkliga orsakerna

Om scannern „ibland“ inte läser är reproducerbar felsökning mycket värdefull. Tre åtgärder som visat sig effektiva:

  1. Logga frame-sampling: Logga (endast i debug/support-läge) tick, bildstorlek, ROI-storlek, decode-tid. Så ser ni omedelbart om Throttle/Debounce eller CPU-belastning är problemet.
  2. Spara testbilder: Spara var N:e sekund en ROI-bild (temporärt). Då kan ni, utan kamerahårdvara, analysera om kontrast/oskärpa är orsaken.
  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ästa steg

När ett ämne blir ett verkligt projekt bör arkitektur, befintliga system och drift behandlas gemensamt redan i ett tidigt skede.

Vi stöder inte bara vid enstaka frågor, utan även när kodsfragment, legacy-frågor eller portalidéer ska utvecklas till ett robust företagsprojekt.

  • Nuläge, målbild och tekniska risker bedöms tillsammans.
  • REST, dataåtkomst, portaler och utrullning skjuts inte upp som sena följder.
  • Ni ser tidigt vilken väg som är ekonomiskt och driftsmässigt bärkraftig.

Dela inlägg

Dela det här inlägget direkt

LinkedIn, X, XING, Facebook, WhatsApp och e‑post är omedelbart tillgängliga. För Instagram förbereder vi länken och en kort text direkt.

E-post

Instagram öppnas i en ny flik. Länken och korttexten kopieras till urklipp först.