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, befintligt bestånd och drift tidigt ses över gemensamt.
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 driftmässigt hållbar.