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
Stopkan kalles flere ganger uten å kaste exceptions (idempotent). I tillegg bør resultat-callbacken sjekke om skanneren fortsatt kjører (gjørDoResultOnMainThread). - ROI for trangt: Et sentrert ROI øker hastigheten, men kan feile hvis brukeren holder koden utenfor eller koden er veldig liten. Derfor er
EnableRoikonfigurerbart ogRoiScalebegrenset. - Format-lås på QR: Å begrense til
QR_CODEer 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 kalleStop. Ved retur kall eksplisittStart(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:
- 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.
- Sikre testbilder: Lagre hvert N. sekund et ROI-bilde (temporært). Da kan du analysere uten kamerahardware om kontrast/Uschärfe er problemet.
- 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.