Net-Base Rivista

03.06.2026

Scanner di codici QR in Delphi FMX: scansione con la fotocamera robusta, thread-safe e senza tremolio dell'interfaccia utente

Uno scanner QR Code Delphi FMX adatto all'uso pratico dipende dal ciclo di vita della fotocamera, dalla gestione dei thread e da un arresto/avvio pulito. Il contributo mostra un approccio robusto con ZXing, Debounce, Frame-Throttling, ritaglio ROI, oltre a dettagli di debug e operativi per Android e iOS.

03.06.2026

Dal tema della rivista alla pratica di progetto

Pagine di servizi e tecniche correlate all'articolo

Scanner di codici QR Delphi FMX nella pratica

Un QR Code Scanner Delphi FMX è in demo rapidamente assemblato: visualizzare l’anteprima della fotocamera, estrarre la Bitmap, eseguire ZXing sulla bitmap. Nelle applicazioni business reali (p.es. ricezione merci, assegnazione dispositivi, ticketing, processi di accesso) però si aggiungono condizioni al contorno: l’app passa in background, la fotocamera perde il focus, l’utente tiene il dispositivo inclinato, il formato dell’immagine cambia — e improvvisamente ci si trova a scansionare due volte al secondo lo stesso codice o l’interfaccia utente va a scatti, perché la decodifica avviene nel thread dell’interfaccia utente.

I problemi tipici non sono tanto „ZXing kann nicht lesen“, ma il lifecycle e l’architettura: rilascio delle risorse della fotocamera, temporizzazione dei frame, sicurezza dei thread nell’accesso a TBitmap (GPU/CPU), e un chiaro stop/start che sia pulito anche quando gli utenti navigano velocemente o il sistema operativo revoca temporaneamente la fotocamera.

Panoramica architetturale: pipeline invece di „OnSampleBufferReady macht alles”

Nella pratica si è dimostrata valida una piccola pipeline con responsabilità ben definite:

  • Adapter per la fotocamera: fornisce frame (o copie degli stessi) in un formato definito.
  • Decoder: lavora su un thread di background e RESTituisce i risultati tramite callback.
  • Gate/Debounce: evita scansioni duplicate e regola il carico (Throttle).
  • Strato UI: mostra l’anteprima, opzionalmente il riquadro di messa a fuoco (ROI, „Region of InteREST”) e reagisce ai risultati.

In questo modo si evita che UI, fotocamera e decoder si blocchino a vicenda. „ROI” indica qui una finestra di ricerca ritagliata (es. centrale al 60%), che alleggerisce il decoder e riduce i falsi positivi. Importante: la ROI è uno strumento di performance e usabilità, non un meccanismo di sicurezza.

Frammento di codice: Scanner QR robusto (FMX + ZXing) con debounce e stop pulito

Il codice seguente è pensato come un componente compatto ma adatto al progetto. Usa ZXing (Delphi-port) tramite ZXing.ScanManager e si aggancia a TCameraComponent.OnSampleBufferReady. Fondamentali sono tre punti:

  • I frame sono limitati (non decodificare ogni sample).
  • La decodifica non avviene nel thread dell’interfaccia utente.
  • Stop/Start è idempotente (invocabile ripetutamente senza causare caos nelle risorse).

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>
/// Controller scanner QR per FMX (Android/iOS).
/// Gestisce il gating dei frame della fotocamera, il decoding in background e uno stop/start pulito.
/// </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; // es. 120
property DebounceMs: Cardinal read FDebounceMs write FDebounceMs; // es. 1200
property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
property RoiScale: Single read FRoiScale write FRoiScale; // es. 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;

// Inizializzare ScanManager e limitare a QR (prestazioni + meno falsi positivi)
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;

// Attivare la fotocamera: nelle app reali verificare prima i permessi (Android) e considerare il flusso UI.
if Assigned(FCamera) then
FCamera.Active := True;
end;

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

// Disattivare in modo pulito
if Assigned(FCamera) then
FCamera.Active := False;

// Reimpostare il flag del decoder, se Stop arriva in una fase indesiderata
TInterlocked.Exchange(FIsDecoding, 0);
end;

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

// stesso testo entro la finestra di debounce – ignorare
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;

// Solo una decodifica alla volta (altrimenti accumulo nella coda su dispositivi deboli)
if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
Exit;

// Copiare il sample della fotocamera in FBitmap. Lock, perché lo stesso buffer bitmap non deve essere utilizzato in parallelo.
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;

// Decoding in background
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
// Ritagliare la ROI al centro: riduce il carico computazionale e guida l’utente.
// Attenzione: per QR molto piccoli la ROI può risultare troppo stretta.
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;

// Thread UI: navigazione, beep, compilazione campi ecc.
TThread.Queue(nil,
procedure
begin
if FIsRunning and Assigned(FOnResult) then
FOnResult(AText);
end);
end;

end.

Cosa risolve il codice (e perché è necessario)

Throttle (MinIntervalMs) riduce il carico CPU e il riscaldamento. Senza limitazione alcuni dispositivi tentano di decodificare 30–60 frame/s; nella pratica bastano 5–10/s, spesso meno. Debounce (DebounceMs) impedisce che un QR code tenuto stabile venga attivato più volte (es. doppia registrazione in un passaggio di processo).

Il flag interlocked (FIsDecoding) assicura che giri al massimo un decode-task contemporaneamente. Si tratta di un accorgimento architetturale contro l’“ingorgo della coda”: se il decoding richiede 200 ms, ma viene avviato un task ogni 120 ms, la coda cresce e i risultati arrivano in ritardo, con effetto operativo percepito come “lo scanner reagisce in modo errato”.

Vincoli e insidie

  • TBitmap e threading: Le FMX-Bitmap possono essere supportate dalla GPU. L’approccio copia il frame in una bitmap locale e decodifica in background. A seconda della versione/piattaforma di Delphi può però essere necessaria cautela: se osservate artefatti, forzate una bitmap in CPU (es. tramite lettura/scrittura di pixel) oppure lavorate con un ByteBuffer ricavato dal SampleBuffer (più vicino alla piattaforma, ma più stabile).
  • Stop/Start durante la navigazione: Nelle app mobili spesso si ferma la camera al cambio di form o all’evento di pausa dell’app. È importante che Stop possa essere chiamato più volte senza lanciare eccezioni (idempotente). Inoltre il callback dei risultati dovrebbe verificare se lo scanner è ancora attivo (come fa DoResultOnMainThread).
  • ROI troppo stretta: Un ROI centrato accelera il processo, ma può fallire se l’utente tiene il codice fuori centro o il codice è molto piccolo. Per questo EnableRoi è configurabile e RoiScale ha limiti.
  • Blocco formato su QR: Limitare a QR_CODE è di solito corretto. Se servono anche Code128/EAN, estendete i formati—tenete però conto di più falsi positivi e di un maggior uso di CPU.

Delphi FMX Camera-Lifecycle: permessi, background, rotazione

I bug più frequenti non nascono nel decoding, ma intorno alla camera:

  • Permessi Android: I permessi camera vanno richiesti a runtime. Prevedete il caso in cui un utente rifiuti o scelga “Solo questa volta”. Tecnically questo significa: mantenere lo stato UI („Scanner pronto?“) separato dallo stato camera, altrimenti si rimane in stati incoerenti.
  • L’app va in background: All’evento OnApplicationEvent (es. EnteredBackground) chiamate Stop. Al ritorno chiamate consapevolmente Start (e eventualmente una breve attesa) in modo che il preview sia stabile.
  • Rotazione/Specchiamento: Per i QR code la rotazione spesso non è critica, ma in alcune pipeline la bitmap può risultare specchiata o ruotata. Se gli scan funzionano “solo in una posizione”, è un indicatore. In tal caso ruotate/specchiate l’immagine prima del decode o utilizzate un decoder che sfrutti i metadati di orientation.

Debugging durante il funzionamento: come individuare le cause reali

Se lo scanner “a volte” non legge, il debugging riproducibile è prezioso. Tre misure che danno risultati:

  1. Log del frame-sampling: Registrate (solo in modalità Debug/Supporto) tick, dimensione immagine, dimensione ROI, durata del decode. Così si vede subito se il problema è throttle/debounce o carico CPU.
  2. Conservare immagini di test: Salvate ogni N secondi un’immagine dell’ROI (temporanea). Questo permette di analizzare senza l’hardware della camera se contrasto/sfuocatura sono la causa.
  3. Separare il carico di lavoro: non aggiornare gli UI-Update (Preview-Overlay, Status-Text) ad alta frequenza. Il «tremolio» dell’interfaccia spesso è causato da troppi eventi Queue.

Varianti: quando serve più di „scansione e fatto“

Più risultati, ma controllati

Per processi batch (p. es. molte etichette una dopo l’altra) riducete DebounceMs e aggiungete una Whitelist/State-Machine: un QR-Code deve essere accettato solo se lo prevede il passo di processo corrente. Questa non è logica UI, ma logica di dominio — appartiene a uno strato separato, in modo che scanner e processo rimangano testabili indipendentemente.

Validazione offline e protezione del payload

Nei processi aziendali i QR-Code spesso contengono ID o token. Non affidatevi al principio «QR = corretto». Validateli localmente (formato, checksum, prefissi attesi) e lato server (API REST). Se utilizzate token: prevedete scadenze, protezione da replay e cautela nel logging (mai token in chiaro nei log di supporto).

Situazioni legacy: FMX-Scanner come modulo in codebase miste

Se avete un mondo VCL consolidato, FMX come client mobile è spesso un ramo separato. Mantenete lo scanner come classe controller senza dipendenze da form (come sopra), così potete integrarlo in schermate differenti. Questo ripaga anche nelle modernizzazioni: la business logic resta testabile, la camera è solo un canale di input. In particolare in scenari legacy conviene inoltre un taglio netto per logging, feature-flags e configurazione remota.

Conclusione: un FMX-QR-Scan solido è un problema di ciclo di vita — non solo una chiamata a ZXing

Un QR Code Scanner in Delphi FMX diventa stabile se lo trattate come una piccola pipeline: la camera fornisce frame, un decoder in background lavora in modo controllato e Debounce/Throttle prevengono eventi doppi o tardivi. Lo snippet di codice sopra affronta esattamente i punti che nei processi mobili business reali collassano: troppe decode-task, stop non pulito, blocchi del thread UI e carico inutile.

Limiti d’impiego: se avete bisogno di tassi di scansione estremamente alti (p. es. scansione industriale su catena di montaggio) o requisiti stringenti per l’elaborazione immagini, la camera standard FMX + pipeline bitmap è spesso troppo onerosa. In tal caso conviene un approccio a basso livello sulla piattaforma (Native Camera API, buffer YUV diretto, SIMD/NEON) o uno SDK scanner specializzato. Per la maggior parte delle applicazioni mobile orientate al processo, tuttavia, l’approccio mostrato è sufficiente, purché ciclo di vita, permessi e threading siano integrati in modo pulito — e i processi sottostanti siano chiari.

Se dovete adattare una scansione QR a un’architettura Delphi esistente (inclusi casi limite come navigazione, backgrounding, logging e validazione di processo), possiamo definirlo insieme in modo strutturato:

Nel contesto tecnico anche Zxing Delphi e Fmx Tcameracomponent svolgono un ruolo importante quando integrazioni, flussi di dati e sviluppo futuro devono funzionare in modo coerente.

Discutere un progetto o un intervento di modernizzazione con Net-Base.

Passo successivo

Quando un tema diventa un progetto reale, architettura, sistemi esistenti e gestione operativa dovrebbero essere considerati insieme fin dall'inizio.

Non forniamo solo supporto per questioni isolate, ma anche quando da frammenti di codice sorgente, tematiche legacy o idee di portale deve nascere un progetto aziendale solido.

  • Stato attuale, stato obiettivo e rischi tecnici vengono valutati insieme.
  • REST, l'accesso ai dati, i portali e il rollout non vengono rimandati a fasi successive.
  • Vede in anticipo quale percorso è economicamente ed operativamente sostenibile.

Condividi il post

Condividi direttamente questo articolo

LinkedIn, X, XING, Facebook, WhatsApp e e-mail sono immediatamente disponibili. Per Instagram prepariamo direttamente il link e un breve testo.

E-mail

Instagram si apre in una nuova scheda. Il link e il breve testo vengono copiati prima negli appunti.