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).
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
Stopkan anropas flera gånger utan att kasta exceptions (idempotent). Dessutom bör resultat-callbacken kontrollera om scannern fortfarande körs (det görDoResultOnMainThread). - 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
EnableRoikonfigurerbar ochRoiScalebegrä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 anropaStop. Vid återkomst anropa medvetetStart(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:
- 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.
- 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.
- 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.