Net-Base Magasin

03.06.2026

QR-kodeskanner i Delphi FMX: robust kameraskanning, trådsikker og uten UI-hakk

En praktisk anvendbar QR-kode-skanner Delphi FMX står og faller med kameralivssyklus, threading og korrekt stop/start. Artikkelen viser en robust tilnærming med ZXing, Debounce, Frame-Throttling, ROI-beskjæring samt debug- og driftsdetaljer for Android og iOS.

03.06.2026

Fra magasinetema til prosjektpraksis

Egnede tjeneste- og tekniske sider for innlegget

QR-kode-skanner Delphi FMX i praksis

En QR Code Scanner Delphi FMX settes raskt sammen i en demo: vis kamera-preview, ta ut en Bitmap, la ZXing kjøre over den. I ekte business-programvare (f.eks. vareinnkomst, enhetstildeling, ticketing, adgangsprosesser) kommer det imidlertid tilleggsvilkår: appen går i bakgrunnen, kameraet mister fokus, brukeren holder enheten skrått, bildeforlaget endres – og plutselig scanner du samme kode to ganger i sekundet eller UI-en hakker fordi dekodingen kjører i UI-tråden.

De typiske problemene er mindre «ZXing kann nicht lesen», og mer lifecycle og arkitektur: frigjøring av kameraressurser, timing av frames, trådsikkerhet ved tilgang til TBitmap (GPU/CPU), og en tydelig Stop/Start som også er ren når brukere navigerer raskt eller OS midlertidig tar fra seg kameraet.

Arkitekturoversikt: Pipeline i stedet for „OnSampleBufferReady macht alles“

I praksis har en liten pipeline med klare ansvarsområder vist seg å fungere:

  • Kamera-adapter: leverer frames (eller kopier av dem) i et definert format.
  • Decoder: kjører i bakgrunnstråd og returnerer resultater via et callback.
  • Gate/Debounce: hindrer dobbeltskanninger og regulerer belastning (Throttle).
  • UI-lag: viser forhåndsvisning, valgfritt fokus-rektangel (ROI, „Region of InteREST“) og reagerer på resultater.

Dette forhindrer at UI, kamera og decoder blokkerer hverandre. «ROI» betyr her et utsnittet søkevindu (f.eks. midtstilt 60 %), som avlaster decoder og reduserer falsk-positive treff. Viktig: ROI er et ytelses- og brukbarhetsverktøy, ikke en sikkerhetsmekanisme.

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

Følgende kode er ment som en kompakt, men prosjektklar komponent. Den bruker ZXing (Delphi-port) via ZXing.ScanManager og kobler seg til TCameraComponent.OnSampleBufferReady. Avgørende er tre punkter:

  • Frames blir throttled (ikke dekode hvert sample).
  • Dekoding kjører ikke i UI-tråden.
  • Stop/Start er idempotent (kan kalles flere ganger uten ressurskaos).

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-skannerkontroller for FMX (Android/iOS).
/// Håndterer kamera-frame-gating, bakgrunnsdekoding og korrekt stopp/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 mot gjentatte like koder
FLastText: string;
FLastTextTick: Int64;
FDebounceMs: Cardinal;

// ROI: andel av bildet som skannes (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;

// Initialiserer ScanManager og begrenser til QR (ytelse + færre falske positiver)
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;

// Aktiverer kamera: I ekte apper må man sjekke tillatelser (Android) og ta hensyn til UI-flow.
if Assigned(FCamera) then
FCamera.Active := True;
end;

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

// Slå av på en ryddig måte
if Assigned(FCamera) then
FCamera.Active := False;

// Tilbakestill dekoder-flagget hvis Stop kommer i en ugunstig fase
TInterlocked.Exchange(FIsDecoding, 0);
end;

function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
// Throttle: ikke dekod hvert bilde
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 innenfor debounce-vindu – ignorer
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;

// Bare én dekoding om gangen (ellers kø-stans på svake enheter)
if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
Exit;

// Kopierer kamera-sample til FBitmap. Lås, fordi samme bitmap-buffer ikke skal brukes 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;

// Bakgrunnsdekoding
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
// Skjærer ut ROI sentrert: reduserer beregningsbelastning og leder brukeren.
// Merk: ved veldig små QR-koder kan ROI bli for trang.
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: navigasjon, beep, fylle felt osv.
TThread.Queue(nil,
procedure
begin
if FIsRunning and Assigned(FOnResult) then
FOnResult(AText);
end);
end;

end.

Hva koden løser (og hvorfor det er nødvendig)

Throttle (MinIntervalMs) reduserer CPU-belastning og varmeutvikling. Uten begrensning prøver enkelte enheter å dekode 30–60 frames/s; i praksis er 5–10/s ofte nok, ofte mindre. Debounce (DebounceMs) forhindrer at en stabilt holdt QR-kode trigges flere ganger (f.eks. dobbelt bokføring i et prosesssteg).

Det Interlocked-Flag (FIsDecoding) sørger for at maksimalt én Decode-Task kjører. Dette er et arkitekturtriks mot «Queue-Stau»: Hvis dekoding tar 200 ms, men en Task startes hvert 120 ms, vokser køen og resultatene kommer forsinket, noe som i drift framstår som «Scanner reagiert falsch».

Rammebetingelser og fallgruver

  • TBitmap og threading: FMX-Bitmaps kan være GPU-backed. Tilnærmingen kopierer rammen til en lokal bitmap og dekoder i bakgrunnen. Avhengig av Delphi-versjon/plattform kan det likevel være nødvendig å være forsiktig: Hvis du ser artefakter, tving fram en CPU-basert bitmap (f.eks. via Pixel-Read/Write) eller arbeid med en ByteBuffer fra SampleBuffer (plattformnært, men mer stabilt).
  • Stop/Start ved navigasjon: I mobile apper stopper man ofte ved bytte av form eller ved app-pause-event. Viktig er at Stop kan kalles flere ganger uten å kaste exceptions (idempotent). I tillegg bør resultat-callbacken sjekke om skanneren fortsatt kjører (gjør DoResultOnMainThread).
  • ROI for trangt: Et sentrert ROI øker hastigheten, men kan feile hvis brukeren holder koden utenfor eller koden er veldig liten. Derfor er EnableRoi konfigurerbart og RoiScale begrenset.
  • Format-lås på QR: Å begrense til QR_CODE er som regel riktig. Hvis du også trenger Code128/EAN, utvid formatene — beregn derimot flere falske positiver og høyere CPU-bruk.

Delphi FMX kamera-livssyklus: tillatelser, bakgrunn, rotasjon

De vanligste feilene oppstår ikke ved dekoding, men rundt kameraet:

  • Android-tillatelser: Kamera-rettigheter må innhentes ved kjøretid. Planlegg for at en bruker nekter eller velger «Bare denne gangen». Teknisk betyr det: hold UI-state («Scanner bereit?») adskilt fra kamera-state, ellers ender du i halvferdige tilstander.
  • App går i bakgrunnen: Ved OnApplicationEvent (f.eks. EnteredBackground) bør du kalle Stop. Ved retur kall eksplisitt Start (og eventuelt en kort forsinkelse) slik at preview blir stabilt.
  • Rotasjon/speiling: For QR-koder er rotasjon ofte uproblematisk, men i enkelte kamera-pipelines kan bitmapen være speilet eller rotert. Hvis skanninger «nur in einer Haltung» fungerer, er det en indikasjon. I så fall: roter/speil bildet før skann eller bruk en decoder som benytter orienterings-metadata.

Feilsøking i drift: Slik finner du de reelle årsakene

Hvis skanneren «manchmal» ikke leser, er reproducerbar feilsøking svært verdifull. Tre tiltak som har vist seg effektive:

  1. Logg frame-sampling: Logg (kun i Debug/Support-Modus) Tick, bildestørrelse, ROI-størrelse, Decode-Dauer. På den måten ser du raskt om Throttle/Debounce eller CPU-last er problemet.
  2. Sikre testbilder: Lagre hvert N. sekund et ROI-bilde (temporært). Da kan du analysere uten kamerahardware om kontrast/Uschärfe er problemet.
  3. Workload trennen: UI-oppdateringer (preview-overlay, status-tekst) må ikke oppdateres med høy frekvens. Det „UI-flimmeret“ kommer ofte av for mange Queue-hendelser.

Varianter: Når du trenger mer enn „Scan und fertig“

Flere resultater, men kontrollert

For batchprosesser (f. eks. mange labels etter hverandre) reduserer du DebounceMs og kompletterer med en Whitelist/State-Machine: En QR-kode skal bare aksepteres når det aktuelle prosesssteget forventer den. Dette er ikke UI-logikk, men domenelogikk – den hører hjemme i et eget lag, slik at skanner og prosess forblir uavhengig testbare.

Offline-validering og sikre nyttedata

I virksomhetsprosesser inneholder QR-koder ofte ID-er eller token. Ikke stol på at „QR = korrekt“. Valider lokalt (format, kontrollsum, forventede prefikser) og serverside (REST-API). Hvis du bruker token: utløpstid, replay-beskyttelse, og logging med forsiktighet (ingen token i klartekst i support-logger).

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

Hvis du har en etablert VCL-verden, er FMX som mobilklient ofte en separat gren. Hold skanneren som en controller-klasse uten form-avhengigheter (som ovenfor), da kan du integrere den i forskjellige skjermer. Dette lønner seg også ved modernisering: forretningslogikken forblir testbar, kameraet er bare en input-kanal. Spesielt i legacy-situasjoner er det også verdt med et klart grensesnitt for logging, feature-flags og fjernkonfigurasjon.

Konklusjon: En solid FMX-QR-scan er et livssyklusproblem – ikke bare et ZXing-Aufruf

En QR-kodeskanner i Delphi FMX blir stabil hvis du behandler den som en liten pipeline: kameraet leverer frames, en bakgrunnsdekoder arbeider kontrollert, og debounce/throttle forhindrer doble og sene hendelser. Kodeutdraget ovenfor adresserer nettopp de punktene som svikter i reelle mobile forretningsprosesser: for mange decode-tasks, uryddig stopp, UI-thread-blokkeringer og unødig belastning.

Bruksgrenser: Hvis du trenger ekstremt høye skannehastigheter (f. eks. industriell skanning på løpende bånd) eller har strenge krav til bildebehandling, er FMX-standardkameraet + bitmap-pipelinen ofte for kostbar. Da lønner det seg med en plattformnær tilnærming (Native Camera API, YUV-Buffer direkte, SIMD/NEON) eller et spesialisert scanner-SDK. For de fleste prosessnære mobile applikasjoner er imidlertid den viste tilnærmingen tilstrekkelig, forutsatt at livssyklus, rettigheter og threading er pent integrert – og at prosessene bak er entydige.

Hvis du må tilpasse et QR-scan til en eksisterende Delphi-arkitektur (inklusive randtilfeller som navigasjon, backgrounding, logging og prosessvalidering), avklarer vi det gjerne strukturert:

I faglig sammenheng spiller også Zxing Delphi og Fmx Tcameracomponent en viktig rolle når integrasjoner, dataflyter og videreutvikling må samarbeide ryddig.

Diskuter prosjekt eller moderniseringsprosjekt med Net-Base.

Neste steg

Når et tema blir et reelt prosjekt, bør arkitektur, eksisterende systemer og drift tidlig vurderes samlet.

Vi bistår ikke bare med enkeltspørsmål, men også når kodesnutter, legacy-temaer eller portalideer skal utvikles til et robust virksomhetsprosjekt.

  • Eksisterende tilstand, målbildet og tekniske risikoer vurderes samlet.
  • REST, datatilgang, portaler og utrulling blir ikke utsatt som sene følger.
  • Dere ser tidlig hvilken vei som er økonomisk og driftsmessig levedyktig.

Del innlegg

Del dette innlegget direkte

LinkedIn, X, XING, Facebook, WhatsApp og e‑post er umiddelbart tilgjengelig. For Instagram forbereder vi lenke og kort tekst umiddelbart.

E-post

Instagram åpnes i en ny fane. Lenken og kortteksten kopieres først til utklippstavlen.