Net-Base Magasin

03.06.2026

QR-kodescanner i Delphi FMX: Robust, trådsikker kameraskanning uden UI-rystelser

En praksisegnet QR-kode-scanner Delphi FMX står og falder med kameraets livscyklus, trådning og ordentlig stop/start. Artiklen viser en robust tilgang med ZXing, Debounce, Frame-Throttling, ROI-beskæring samt debug- og driftsdetaljer for Android og iOS.

03.06.2026

Fra magasinets tema til projektpraksis

Passende service- og tekniske sider til artiklen

QR Code Scanner Delphi FMX i praksis

En QR Code Scanner Delphi FMX er i en demo hurtigt sat sammen: vise kamera-preview, udtrække en Bitmap, lade ZXing køre over billedet. I ægte forretningssoftware (f.eks. varemodtagelse, enhedstildeling, ticketing, adgangsprocesser) optræder dog yderligere randbetingelser: appen går i baggrunden, kameraet mister fokus, brugeren holder enheden skævt, billedformatet skifter – og pludselig scannes samme kode to gange i sekundet, eller UI’en hakker, fordi dekodningen kører i UI-tråden.

De typiske problemer er mindre „ZXing kan ikke læse“, og mere livscyklus og arkitektur: frigivelse af kameraressourcer, styring af frame-takten, trådsikkerhed ved adgang til TBitmap (GPU/CPU), og en klar stop/start, der forbliver ren, også når brugere hurtigt navigerer eller OS midlertidigt frakobler kameraet.

Arkitekturoversigt: Pipeline i stedet for „OnSampleBufferReady macht alles“

I praksis har en lille pipeline med klare ansvarsområder vist sig hensigtsmæssig:

  • Kamera-adapter: leverer frames (eller kopier af dem) i et defineret format.
  • Decoder: arbejder i en baggrundstråd og returnerer resultater via et callback.
  • Gate/Debounce: forhindrer dobbeltscans og styrer belastning (Throttle).
  • UI-laget: viser preview, valgfrit fokus-rektangel (ROI, „Region of InteREST“) og reagerer på resultater.

Dermed undgår du, at UI, kamera og decoder blokerer hinanden. „ROI“ betyder her et beskåret søgefelt (f.eks. midtstillet 60 %), som aflaster decoderen og reducerer falsk-positive resultater. Vigtigt: ROI er et performance- og brugervenlighedsværktøj, ikke en sikkerhedsmekanisme.

Kodeudsnit: Robust QR Code Scanner (FMX + ZXing) med Debounce og ordentligt stop

Den følgende kode er tænkt som en kompakt, men produktionsklar byggeblok. Den bruger ZXing (Delphi-Port) via ZXing.ScanManager og kobler sig på TCameraComponent.OnSampleBufferReady. Afgørende er tre punkter:

  • Frames bliver throttled (ikke hvert Sample dekodes).
  • Dekodning kører ikke i UI-tråden.
  • Stop/Start er idempotent (kan kaldes flere gange uden ressourcekaos).

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 til FMX (Android/iOS).
/// Håndterer kamera-frame-gating, baggrundsdekodning og korrekt 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 initialiseres og begrænses til QR (ydelse + færre falsk-positive)
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;

// Aktivér kameraet: I rigtige apps skal tilladelser kontrolleres først (Android) og UI-flow tages i betragtning.
if Assigned(FCamera) then
FCamera.Active := True;
end;

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

// Deaktiver kameraet sikkert
if Assigned(FCamera) then
FCamera.Active := False;

// Nulstil decoder-flaget, hvis Stop indtræffer i en ugunstig fase
TInterlocked.Exchange(FIsDecoding, 0);
end;

function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
// Throttle: dekodér ikke hvert 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);

// samme tekst inden for debounce-vinduet — ignoreres
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;

// Kun én dekodning ad gangen (ellers kø-opbygning på svage enheder)
if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
Exit;

// Kopiér kameraprøve til FBitmap. Lås, fordi samme bitmap-buffer ikke må bruges parallelt.
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;

// Baggrundsdekodning
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
// Beskær ROI centreret: reducerer beregningsbelastning og guider brugeren.
// Bemærk: ved meget små QR-koder kan ROI være for snæver.
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, lydsignal, udfyldning af felt mv.
TThread.Queue(nil,
procedure
begin
if FIsRunning and Assigned(FOnResult) then
FOnResult(AText);
end);
end;

end.

Hvad koden løser (og hvorfor det er nødvendigt)

Throttle (MinIntervalMs) reducerer CPU-belastning og varmeudvikling. Uden begrænsning forsøger nogle enheder at dekode 30–60 frames/s; i praksis er 5–10/s tilstrækkeligt, ofte mindre. Debounce (DebounceMs) forhindrer, at en stabilt holdt QR-kode aflæses flere gange (f.eks. dobbeltskrivning i et procestrin).

Det Interlocked-Flag (FIsDecoding) sikrer, at højst ét Decode-Task kører samtidigt. Det er et arkitekturtrick mod „Queue-Stau“: Hvis dekodning tager 200 ms, men der starter et Task hver 120 ms, vokser køen og resultaterne kommer forsinket, hvilket i drift opleves som „Scanner reagiert falsch“.

Randbetingelser og faldgruber

  • TBitmap og trådning: FMX-Bitmaps kan være GPU-backed. Tilgangen kopierer frame’et til en lokal Bitmap og dekoder i baggrunden. Afhængigt af Delphi-version/platform kan der alligevel være behov for forsigtighed: Ser du artefakter, fremtvung en CPU-Bitmap (f.eks. via Pixel-Read/Write) eller arbejd med en ByteBuffer fra SampleBuffer (platformnært, men mere stabilt).
  • Stop/Start ved navigation: I mobile apps stopper man ofte ved skift af Form eller ved App-Pause-eventet. Det er vigtigt, at Stop kan kaldes flere gange uden at kaste undtagelser (idempotent). Desuden bør resultat-callbacken tjekke, om scanneren stadig kører (det gør DoResultOnMainThread).
  • ROI for snævt: Et centreret ROI fremskynder, men kan fejle, hvis brugere holder koden udenfor eller koden er meget lille. Derfor er EnableRoi konfigurerbar og RoiScale begrænset.
  • Format-lock på QR: Begrænsning til QR_CODE er som regel korrekt. Hvis du også har brug for Code128/EAN, udvid formaterne – forbered dig dog på flere False Positives og højere CPU-forbrug.

Delphi FMX Kamera-Lifecycle: Tilladelser, baggrund, rotation

De hyppigste fejl opstår ikke under dekodning, men omkring kameraet:

  • Android-tilladelser: Kameraadgang skal indhentes ved runtime. Planlæg for tilfælde, hvor en bruger afviser eller vælger „Kun denne gang“. Teknisk betyder det: hold UI-state („Scanner klar?“) adskilt fra kamera-state, ellers står du i halvfærdige tilstande.
  • App går i baggrund: Ved OnApplicationEvent (f.eks. EnteredBackground) bør du kalde Stop. Ved tilbagevenden kald bevidst Start (og evt. en kort forsinkelse), så previewet bliver stabilt.
  • Rotation/mirroring: For QR-koder er rotation ofte uproblematisk, men i nogle kamerapipelines kan Bitmap’et være spejlet eller roteret. Hvis scannene kun virker „i én vinkel“, er det et tegn. I så fald: drej/spejl før scanningen eller brug en decoder, der udnytter Orientation-Metadaten.

Fejlsøgning i drift: Sådan finder du de reelle årsager

Hvis scanneren „nogle gange“ ikke aflæser, er reproducerbar fejlsøgning guld værd. Tre tiltag, der har vist sig effektive:

  1. Log frame-sampling: Log (kun i Debug/Support-tilstand) Tick, billedstørrelse, ROI-størrelse, Decode-Duration. Så ser du med det samme, om Throttle/Debounce eller CPU-belastning er problemet.
  2. Gem testbilleder: Gem hvert N. sekund et ROI-billede (midlertidigt). Dermed kan du uden kamerahardware analysere, om kontrast/ufokus er problemet.
  • Workload trennen: UI-Updates (Preview-Overlay, Status-Text) nicht in hoher Frequenz aktualisieren. Das „UI-Zittern“ kommt oft von zu vielen Queue-Events.
  • Varianter: Hvis I har brug for mere end „Scan und fertig“

    Flere resultater, men kontrolleret

    For batchprocesser (z. B. mange Labels nacheinander) sænk værdien af DebounceMs og tilføj en Whitelist/State-Machine: En QR-Code må kun accepteres, når det aktuelle procestrin forventer den. Det er ikke UI-logik, men domænelogik – den hører til i et separat lag, så Scanner og Prozess forbliver uafhængigt testbare.

    Offline-validering og sikre Nutzdaten

    I Unternehmensprozessen indeholder QR-Codes ofte ID’er eller Token. Stol ikke på, at „QR = korrekt“. Valider lokalt (Format, Prüfsumme, erwartete Prefixe) og serverseitig (REST-API). Hvis I bruger Token: Ablaufzeiten, Replay-Schutz, und Logging mit Vorsicht (keine Tokens im Klartext in Support-Logs).

    Legacy-situationer: FMX-Scanner als Modul in gemischten Codebasen

    Hvis I har et gewachsenes VCL-Umfeld, er FMX som Mobile-Client ofte en separat gren. Hold scanneren som en Controller-Klasse ohne Form-Abhängigkeiten (wie oben), så kan I integrere den i forskellige Screens. Det betaler sig også ved Modernisierung: Die Business-Logik bleibt testbar, die Kamera ist nur ein Input-Kanal. Især i Legacy-situationer er en klar adskillelse für Logging, Feature-Flags und Remote-Konfiguration nyttig.

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

    En QR Code Scanner i Delphi FMX bliver stabil, hvis I behandler den som en lille pipeline: Kamera leverer Frames, en baggrunds-Decoder arbejder kontrolleret, og Debounce/Throttle forhindrer Doppel- und Spät-Events. Der Source-Schnipsel oben adresserer genau die Stellen, die i echten mobilen Business-Prozessen kippen: zu viele Decode-Tasks, unsauberer Stop, UI-Thread-Blockaden und unnötige Last.

    Anvendelsesgrænser: Hvis I har 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æste trin

    Når et emne bliver til et reelt projekt, bør arkitektur, eksisterende systemer og drift tidligt vurderes samlet.

    Vi støtter ikke kun ved enkeltspørsmål, men også når kildekodeudsnit, legacy-komponenter eller portalidéer skal udvikles til et robust virksomhedsprojekt.

    • Eksisterende tilstand, målbillede og tekniske risici vurderes samlet.
    • REST, dataadgang, portaler og idrulning bliver ikke udskudt som eftertanker.
    • I ser tidligt, hvilken vej der er økonomisk og driftsmæssigt holdbar.

    Del indlæg

    Del dette indlæg direkte

    LinkedIn, X, XING, Facebook, WhatsApp og e-mail er straks tilgængelige. Til Instagram forbereder vi link og kort tekst med det samme.

    E-mail

    Instagram åbner i en ny fane. Linket og kortteksten kopieres på forhånd til udklipsholderen.