Net-Base Magazine

03.06.2026

QR-codescanner in Delphi FMX: robuuste camerascanning, thread-safe en zonder UI-trillingen

Een praktijkbestendige QR-codescanner Delphi FMX staat of valt met de cameralevenscyclus, threading en een correcte stop-/startprocedure. Het artikel toont een robuuste aanpak met ZXing, Debounce, Frame-Throttling, ROI-bijsnijding en debug- en operationele details voor Android en iOS.

03.06.2026

Van magazinethema naar projectpraktijk

Relevante dienst- en technische pagina's bij het artikel

QR Code Scanner Delphi FMX in de praktijk

Een QR Code Scanner Delphi FMX is in de demo snel samengesteld: camera-preview tonen, bitmap trekken, ZXing eroverheen laten lopen. In echte business-software (bijv. goederenontvangst, toewijzing van apparaten, ticketing, toegangsprocessen) komen echter randvoorwaarden bij: de app gaat naar de achtergrond, de camera verliest de focus, de gebruiker houdt het apparaat schuin, het beeldformaat verandert – en plots scan je twee keer per seconde dezelfde code of hapert de UI omdat de decodering in de UI-thread draait.

De typische problemen zijn minder „ZXing kan niet lezen”, maar lifecycle en architectuur: het vrijgeven van cameramiddelen, de timing van frames, thread-veiligheid bij toegang tot TBitmap (GPU/CPU), en een duidelijk Stop/Start dat ook correct werkt wanneer gebruikers snel navigeren of het OS de camera tijdelijk intrekt.

Architectuuroverzicht: Pipeline in plaats van „OnSampleBufferReady macht alles”

In de praktijk heeft een kleine pipeline met duidelijke verantwoordelijkheden zich bewezen:

  • Kamera-Adapter: levert frames (of kopieën daarvan) in een gedefinieerd formaat.
  • Decoder: werkt in een achtergrondthread en geeft resultaten via een callback terug.
  • Gate/Debounce: voorkomt dubbele scans en regelt de belasting (throttle).
  • UI-Schicht: toont preview, optioneel focus-rechthoek (ROI, „Region of InteREST”) en reageert op resultaten.

Daardoor voorkomt u dat UI, camera en decoder elkaar blokkeren. „ROI” betekent hier een bijgesneden zoekvenster (bijv. centraal 60%) dat de decoder ontlast en fout-positieve resultaten reduceert. Belangrijk: ROI is een performance- en usability-gereedschap, geen beveiligingsmechanisme.

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

De volgende code is bedoeld als compact maar projectgeschikt bouwblok. Het gebruikt ZXing (Delphi-port) via ZXing.ScanManager en koppelt zich aan TCameraComponent.OnSampleBufferReady. Cruciaal zijn drie punten:

  • Frames worden gereguleerd (niet elk sample decoderen).
  • Decodering draait niet in de UI-thread.
  • Stop/Start is idempotent (meermalig aanroepbaar, zonder resourceschaos).

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-scannercontroller voor FMX (Android/iOS).
/// Regelt camera-frame-gating, achtergronddecodering en correct stoppen/starten.
/// </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 tegen herhaalde dezelfde codes
FLastText: string;
FLastTextTick: Int64;
FDebounceMs: Cardinal;

// ROI: aandeel van de afbeelding dat wordt gescand (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; // z.B. 120
property DebounceMs: Cardinal read FDebounceMs write FDebounceMs; // z.B. 1200
property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
property RoiScale: Single read FRoiScale write FRoiScale; // z.B. 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 initialiseren en beperken tot QR-codes (prestaties + minder 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;

// Camera inschakelen: in echte apps eerst permissies controleren (Android) en de UI-flow meenemen.
if Assigned(FCamera) then
FCamera.Active := True;
end;

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

// Actief netjes uitschakelen
if Assigned(FCamera) then
FCamera.Active := False;

// Decoder-flag terugzetten, voor het geval Stop in een ongunstige fase wordt aangeroepen
TInterlocked.Exchange(FIsDecoding, 0);
end;

function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
// Throttle: niet elk frame decoderen
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);

// dezelfde tekst binnen het debounce-venster – negeren
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;

// Slechts één decode tegelijk (anders wachtrij-achterstand op zwakke apparaten)
if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
Exit;

// Kamera-sample naar FBitmap kopiëren. Lock, omdat dezelfde bitmap-buffer niet parallel gebruikt mag worden.
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;

// Achtergronddecodering
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 centraal uitsnijden: vermindert rekenbelasting en richt gebruiker.
// Let op: bij zeer kleine QR-codes kan de ROI te krap zijn.
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: navigatie, piep, veld vullen enz.
TThread.Queue(nil,
procedure
begin
if FIsRunning and Assigned(FOnResult) then
FOnResult(AText);
end);
end;

end.

Wat de code oplost (en waarom dat nodig is)

Throttle (MinIntervalMs) vermindert CPU-belasting en warmteontwikkeling. Zonder beperking proberen sommige apparaten 30–60 frames/s te decoderen; in de praktijk volstaat 5–10/s, vaak minder. Debounce (DebounceMs) voorkomt dat een stabiel gehouden QR-code meerdere keren wordt getriggerd (bijv. dubbele boeking in een processtap).

Het Interlocked-Flag (FIsDecoding) zorgt ervoor dat maximaal één decode-task tegelijk draait. Dit is een architectuurtruc tegen wachtrij-ophoping: als decoderen 200 ms duurt, maar elke 120 ms een task wordt gestart, groeit de wachtrij en komen resultaten vertraagd binnen, wat in de praktijk lijkt op „scanner reageert verkeerd“.

Randvoorwaarden und Stolperfallen

  • TBitmap und Threading: FMX-Bitmaps kunnen GPU-backed zijn. De aanpak kopieert het frame naar een lokale bitmap en decodeert op de achtergrond. Afhankelijk van de Delphi-versie/platform kan toch voorzichtigheid nodig zijn: als u artefacten ziet, forceer een CPU-bitmap (bijv. via Pixel-Read/Write) of werk met een ByteBuffer uit de SampleBuffer (platformnäher, maar stabieler).
  • Stop/Start bij Navigation: In mobiele apps wordt vaak bij het wisselen van het form of bij het App-Pause-event gestopt. Belangrijk is dat Stop meermalen kan worden aangeroepen zonder exceptions (idempotent). Bovendien moet de resultaat-callback controleren of de scanner nog draait (doet DoResultOnMainThread).
  • ROI zu eng: Een centraal ROI versnelt, maar kan falen als gebruikers de code buiten houden of de code zeer klein is. Daarom is EnableRoi configureerbaar en is RoiScale beperkt.
  • Format-Lock op QR: Het beperken tot QR_CODE is meestal correct. Als u ook Code128/EAN nodig heeft, breid dan de formaten uit – houd echter rekening met meer false positives en hogere CPU-belasting.

Delphi FMX Kamera-Lifecycle: Berechtigungen, Hintergrund, Rotation

De meest voorkomende bugs ontstaan niet tijdens het decoderen, maar rond de camera:

  • Android Permissions: Camera-rechten moeten tijdens runtime worden gevraagd. Plan voor het geval een gebruiker weigert of „Alleen deze keer“ kiest. Technisch betekent dit: UI-state („Scanner klaar?“) scheiden van camera-state, anders blijft u in half-afgewerkte toestanden hangen.
  • App gaat naar de achtergrond: Bij OnApplicationEvent (bijv. EnteredBackground) moet u Stop aanroepen. Bij terugkeer expliciet Start (en eventueel een korte vertraging) toepassen, zodat de preview stabiel is.
  • Rotation/Mirroring: Voor QR-codes is rotatie vaak onkritisch, maar in sommige camerapipelines kan het bitmap gespiegeld of gedraaid zijn. Als scans „alleen in één houding“ werken, is dat een aanwijzing. In dat geval: draai/spiegel het beeld vóór het scannen of gebruik een decoder die orientation-metadata ondersteunt.

Debugging im Betrieb: So finden Sie die echten Ursachen

Als de scanner „soms“ niet leest, is reproduceerbaar debuggen van grote waarde. Drie maatregelen die zich bewijzen:

  1. Frame-Sampling loggen: Log (alleen in Debug/Support-modus) tick, afbeeldingsgrootte, ROI-grootte, decode-duur. Zo ziet u direct of Throttle/Debounce of CPU-belasting het probleem veroorzaakt.
  2. Testbilder sichern: Sla om de N seconden een ROI-afbeelding op (tijdelijk). Daarmee kunt u zonder camera-hardware analyseren of contrast/onscherpte het probleem is.
  3. Workload scheiden: UI-Updates (Preview-Overlay, Status-Text) niet met hoge frequentie bijwerken. Het ‚UI-trillen‘ komt vaak door te veel Queue-events.

Varianten: Als u meer nodig heeft dan „Scan und fertig“

Meerdere resultaten, maar gecontroleerd

Voor batchprocessen (bijv. veel labels achter elkaar) verlaagt u DebounceMs en voegt u een Whitelist/State-Machine toe: een QR-code mag alleen geaccepteerd worden als de huidige processtap deze verwacht. Dit is geen UI-logica, maar domeinlogica – die hoort in een aparte laag, zodat scanner en proces onafhankelijk testbaar blijven.

Offline-validatie en veilige Nutzdaten

In bedrijfsprocessen bevatten QR-codes vaak IDs of token. Vertrouw niet op „QR = correct“. Valideer lokaal (formaat, controlegetal, verwachte prefixen) en aan de serverzijde (REST-API). Als u token gebruikt: vervaltijden, replay-bescherming, en logging met voorzichtigheid (geen tokens in platte tekst in support-logs).

Legacy-situaties: FMX-Scanner als module in gemengde codebasen

Als u een gegroeide VCL-omgeving heeft, is FMX als mobiele client vaak een aparte tak. Houd de scanner als een controller-klasse zonder Form-afhankelijkheden (zoals hierboven), dan kunt u deze in verschillende schermen integreren. Dat betaalt zich ook uit bij modernisering: de businesslogica blijft testbaar, de camera is slechts een inputkanaal. Juist in legacy-situaties is bovendien een duidelijke scheiding voor logging, feature-flags en remote-configuratie zinvol.

Conclusie: Een solide FMX-QR-scan is een Lifecycle-probleem – niet alleen een ZXing-aanroep

Een QR-codescanner in Delphi FMX wordt stabiel als u deze behandelt als een kleine pipeline: de camera levert frames, een achtergronddecoder werkt gecontroleerd, en Debounce/Throttle voorkomen dubbele en late events. Het bovenstaande bronfragment adresseert precies de punten die in echte mobiele businessprocessen problemen veroorzaken: te veel decode-taken, onzuivere stop, UI-thread-blokkades en onnodige belasting.

Beperkingen: Als u extreem hoge scanfrequenties nodig heeft (bijv. industrieel scannen aan een lopende band) of strikte eisen aan beeldverwerking, is de FMX-standaardcamera + bitmap-pipeline vaak te kostbaar. Dan is een platformnabije aanpak (Native Camera API, YUV-Buffer direct, SIMD/NEON) of een gespecialiseerd scanner-SDK zinvol. Voor de meeste procesgebonden mobiele toepassingen volstaat de getoonde aanpak echter, mits Lifecycle, rechten en threading netjes geïntegreerd zijn – en de achterliggende processen eenduidig zijn.

Als u een QR-scan in een bestaande Delphi-architectuur moet inpassen (inclusief randgevallen zoals navigatie, backgrounding, logging en procesvalidatie), bespreken wij dat graag gestructureerd:

In de vakinhoudelijke context spelen ook Zxing Delphi en Fmx Tcameracomponent een belangrijke rol, wanneer integraties, datastromen en doorontwikkeling netjes moeten samenspelen.

Project of moderniseringsproject met Net-Base bespreken.

Volgende stap

Wanneer het onderwerp een echt project wordt, zouden architectuur, bestaande omgeving en beheer in een vroeg stadium gezamenlijk moeten worden bekeken.

We ondersteunen niet alleen bij individuele vragen, maar ook wanneer uit broncodefragmenten, legacy-onderwerpen of portalideeën een robuust bedrijfsproject moet ontstaan.

  • Huidige situatie, doelbeeld en technische risico's worden gezamenlijk beoordeeld.
  • REST, gegevens‑toegang, portalen en uitrol worden niet als latere gevolgen uitgesteld.
  • U ziet vroeg welke weg economisch en operationeel houdbaar is.

Bericht delen

Dit bericht direct delen

LinkedIn, X, XING, Facebook, WhatsApp en e-mail zijn direct beschikbaar. Voor Instagram bereiden we de link en een korte tekst direct voor.

E-mail

Instagram opent in een nieuw tabblad. Link en korte tekst worden van tevoren naar het klembord gekopieerd.