Frå magasinetema til prosjektpraksis
Passande teneste- og tekniske sider til innlegget
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).
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; // 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 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.
Kva koden løyser (og kvifor det er nødvendig)
Throttle (MinIntervalMs) reduserer CPU-belastning og varmeutvikling. Utan avgrensing prøver nokre einingar å dekode 30–60 Frames/s; i praksis held 5–10/s, ofte mindre. Debounce (DebounceMs) forhindrar at ein stabilt halden QR-kode utløyser fleire gongar (t.d. dobbel bokføring i eit prosesssteg).
Det Interlocked-Flag (FIsDecoding) sørgjer for at maksimalt ein Decode-Task køyrer. Dette er eit arkitekturgrep mot «Queue-Stau»: Dersom dekoding tek 200 ms, men det startar ein task kvar 120 ms, veks køen og resultata kjem forseinka, noko som i drift oppfattast som «skannaren reagerer feil».
Rammevilkår og fallgruver
- TBitmap und Threading: FMX-Bitmaps kan vere GPU-backed. Tilnærminga kopierar ramma til ei lokal Bitmap og dekodar i bakgrunnen. Avhengig av Delphi-versjon/plattform kan det likevel vere naudsynt med forsiktighet: Ser du artefaktar, tving fram ei CPU-Bitmap (t.d. via Pixel-Read/Write) eller arbeid med ein ByteBuffer frå SampleBuffer (plattformnært, men meir stabilt).
- Stop/Start bei Navigation: I mobile app-ar stoppar ein ofte ved byte av form eller ved App-Pause-event. Viktig er at
Stopkan kallast fleire gongar utan å kaste unntak (idempotent). I tillegg bør resultat-callbacken sjekke om skannaren framleis køyrer (gjerDoResultOnMainThread). - ROI zu eng: Eit sentrert ROI akselererer, men kan feile dersom brukaren held koden utanfor eller koden er veldig liten. Difor er
EnableRoikonfigurerbart ogRoiScaleavgrensa. - Format-Lock auf QR: Å avgrense til
QR_CODEer som regel riktig. Treng du også Code128/EAN, utvid formatlista – rekna då med fleire falske positive og høgare CPU-belastning.
Delphi FMX Kamera-Lifecycle: Tillatelser, bakgrunn, rotasjon
Dei vanlegaste feila oppstår ikkje ved dekoding, men rundt kameraet:
- Android Permissions: Kameratilgang må innhentast ved køyringstid. Planlegg for at ein brukar kan avslå eller velje «Berre denne gongen». Teknisk betyr det at UI-tilstand («Scanner klar?») må haldast separat frå kamera-tilstand, elles sit du fast i halvferdige tilstandar.
- App geht in den Hintergrund: Ved
OnApplicationEvent(t.d.EnteredBackground) bør du kalleStop. Ved tilbakekomst kall eksplisittStart(og eventuelt ein kort forsinkelse), slik at forhandsvisinga blir stabil. - Rotation/Mirroring: For QR-kodar er rotasjon ofte uproblemisk, men i nokre camera-pipelines kan bitmappa vere spegla eller rotert. Dersom skann fungerer «berre i ei haldning», er det eit teikn. I så fall: roter/spegl før skann eller bruk ein decoder som nyttar orientation-metadata.
Feilsøking i drift: Slik finn du dei eigentlege årsakene
Dersom skannaren «av og til» ikkje les, er reproducerbar feilsøking gull verdt. Tre tiltak som har vist seg nyttige:
- Frame-Sampling loggen: Logg (berre i Debug/Support-modus) tick, biletstorleik, ROI-storleik, dekodetid. Då ser du med ein gong om Throttle/Debounce eller CPU-belastning er problemet.
- Testbilder sichern: Lagre kvart N sekund eit ROI-bilete (midlertidig). Då kan du utan kamerahardware analysere om kontrast/uskarpheit er årsaka.
Queue-events.Variantar: Når De treng meir enn «Skann og ferdig»
Fleire resultat, men kontrollert
For stabelprosessar (t.d. mange etikettar etter kvarandre) reduserar De DebounceMs og legg til ei Whitelist/State-Machine: Ein QR-kode bør berre godkjennast når det aktuelle prosesssteget ventar han. Dette er ikkje UI-logikk, men domenelogikk – han høyrer til i eit eige lag, slik at skannar og prosess kan testast uavhengig.
Offline-validering og sikre nyttedata
I bedriftsprosessar inneheld QR-kodar ofte ID-ar eller token. Stol ikkje på at «QR = korrekt». Valider lokalt (format, kontrollsiffer, forventa prefiks) og på serveren (REST-API). Dersom De brukar token: sett utløpstid, replay-skydd, og handter logging med varsemd (ingen token i klartekst i support-loggar).
Legacy-situasjonar: FMX-skannar som modul i blanda kodebasar
Dersom De har ei etablert VCL-verda, er FMX som mobilklient ofte ein separat grein. Halde skannaren som ei Controller-klasse utan form-avhengnader (som ovanfor), då kan De integrere han i forskjellige skjermbilete. Dette lønar seg òg ved modernisering: forretningslogikken held seg testbar, kameraet er berre ein inndatakanal. Særleg i legacy-situasjonar er det verdt å ha eit klart snitt for logging, feature-flags og fjernkonfigurasjon.
Konklusjon: Ein robust FMX-QR-skann er eit livssyklusproblem – ikkje berre eit ZXing-kall
Ein QR Code Scanner i Delphi FMX blir stabil når De behandlar han som ei lita pipeline: kameraet leverer frames, ein bakgrunnsdekodar arbeider kontrollert, og debounce/throttle forhindrar doble og forseinka events. Kodeeksemplet ovanfor adresserer nøyaktig dei punkta som sviktar i reelle mobile forretningsprosessar: for mange dekodingsoppgåver, uregelmessig stop, UI-tråd-blokkeringar og unødig belastning.
Bruksgrenser: Dersom De treng ekstremt høge skannehastigheiter (t.d. industriskanning på transportband) eller strenge krav til bilehandtering, er FMX-standardkamera + bitmap-pipeline ofte for kostbar. Då lyt ein plattformsnær tilnærming (Native Camera API, YUV-Buffer direkte, SIMD/NEON) eller eit spesialisert skannar-SDK vurderast. For dei fleste prosessnære mobile applikasjonar er den viste løysinga likevel tilstrekkeleg, så lenge livssyklus, rettar og threading er integrerte – og prosessane bak er entydige.
Dersom De må tilpasse ein QR-skann til ei eksisterande Delphi-arkitektur (inkludert grensetilfelle som navigasjon, backgrounding, logging og prosessvalidering), avklarer vi dette gjerne strukturelt:
I fagmiljøet spelar òg Zxing Delphi og Fmx Tcameracomponent ei viktig rolle når integrasjonar, dataflyt og vidareutvikling må fungere ryddig saman.
Neste steg
Når temaet blir eit reelt prosjekt, bør arkitektur, eksisterande system og drift vurderast tidleg saman.
Vi støttar ikkje berre ved enkeltspørsmål, men òg når korte kildekodesnuttar, legacy-tema eller portalidéar skal utviklast til eit robust bedriftsprosjekt.
- Eksisterande tilstand, målbiletet og tekniske risikoar blir vurderast samla.
- REST, datatilgang, portalar og utrulling blir ikkje utsette til seinare som etterverknader.
- De ser tidleg kva veg som er økonomisk og driftsmessig berekraftig.