Net-Base Revista

03.06.2026

Escàner de codis QR a Delphi FMX: escaneig per càmera robust, segur en entorns multifil i sense tremolor a la interfície d'usuari

Un escàner de codis QR pràctic i apte per a producció Delphi FMX depèn del cicle de vida de la càmera, del threading i d'una aturada/inici nets. L'article mostra un enfocament robust amb ZXing, Debounce, Frame-Throttling, retallat de ROI, així com detalls de depuració i d'operació per a Android i iOS.

03.06.2026

Del tema de la revista a la pràctica del projecte

Pàgines de serveis i tècniques pertinents per a l'article

QR Code Scanner Delphi FMX en la pràctica

Un QR Code Scanner Delphi FMX es munta ràpid en una demo: mostrar la previsualització de la càmera, obtenir un Bitmap, fer passar ZXing per sobre. Però en programari empresarial real (p. ex. recepció de mercaderies, assignació de dispositius, ticketing, processos d’accés) s’afegeixen RESTriccions operatives: l’aplicació passa a segon pla, la càmera perd el focus, l’usuari sosté el dispositiu tort, el format d’imatge canvia — i de cop escaneja dues vegades per segon el mateix codi o la UI s’entrebanca perquè la decodificació s’executa en el UI-Thread.

Els problemes típics no són tant un «ZXing no pot llegir», sinó el cicle de vida i l’arquitectura: alliberament de recursos de la càmera, regulació del ritme dels Frames, seguretat de fils en l’accés a TBitmap (GPU/CPU), i un Stop/Start clar que sigui net fins i tot quan els usuaris naveguen ràpidament o el SO retira la càmera temporalment.

Visió general de l’arquitectura: pipeline en lloc de «OnSampleBufferReady ho fa tot»

A la pràctica ha demostrat la seva validesa una petita pipeline amb responsabilitats clares:

  • Adaptador de càmera: lliura Frames (o còpies d’aquests) en un format definit.
  • Decodificador: treballa en un fil en segon pla i retorna resultats via callback.
  • Gate/Debounce: evita escanejats dobles i regula la càrrega (Throttle).
  • Capa UI: mostra la previsualització, opcionalment el rectangle de focus (ROI, „Region of InteREST“) i reacciona als resultats.

Això evita que UI, càmera i decodificador es bloquegin mútuament. „ROI“ aquí vol dir una finestra de cerca retallada (p. ex. central al 60 %) que alleuja el decodificador i redueix resultats falsament positius. Important: ROI és una eina de rendiment i usabilitat, no un mecanisme de seguretat.

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

El codi següent està pensat com un component compacte però apte per a projectes. Fa servir ZXing (Delphi-Port) a través de ZXing.ScanManager i s’enllaça a TCameraComponent.OnSampleBufferReady. Decisives són tres qüestions:

  • Els Frames es limiten (no decodificar cada mostra).
  • La decodificació no s’executa en el UI-Thread.
  • Stop/Start és idempotent (es pot invocar diverses vegades sense caos de recursos).

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>
/// Controlador de lector QR per FMX (Android/iOS).
/// S’encarrega del frame-gating de la càmera, la decodificació en segon pla i un Stop/Start net.
/// </summary>
TQrScannerController = class
private
FCamera: TCameraComponent;
FScanManager: TScanManager;
FBitmap: TBitmap;
FLock: TObject;

FOnResult: TQrScanResultEvent;

// Gating/Throttle
FIsRunning: Boolean;
FIsDecoding: Integer; // 0/1 com a flag interlocked
FLastDecodeTick: Int64;
FMinIntervalMs: Cardinal;

// Debounce contra codis idèntics repetits
FLastText: string;
FLastTextTick: Int64;
FDebounceMs: Cardinal;

// ROI: proporció de la imatge que s’escaneja (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; // p. ex. 120
property DebounceMs: Cardinal read FDebounceMs write FDebounceMs; // p. ex. 1200
property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
property RoiScale: Single read FRoiScale write FRoiScale; // p. 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;

// Inicialitzar el ScanManager i limitar-lo a QR (rendiment + menys falsos positius)
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;

// Activar la càmera: en aplicacions reals, comprovar permisos prèviament (Android) i tenir en compte el flux de la UI.
if Assigned(FCamera) then
FCamera.Active := True;
end;

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

// Desactivar d’una forma neta
if Assigned(FCamera) then
FCamera.Active := False;

// Restablir el flag del decodificador en cas que Stop es cridi en una fase desfavorable
TInterlocked.Exchange(FIsDecoding, 0);
end;

function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
// Throttle: no decodificar cada 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);

// mateix text dins la finestra de debounce — ignorar
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;

// Només una decodificació alhora (si no, embús de la cua en dispositius poc potents)
if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
Exit;

// Copiar la mostra de la càmera a FBitmap. Lock, perquè no s’ha d’utilitzar en paral·lel el mateix buffer de bitmap.
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;

// Decodificació en segon pla
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
// Retallar l’ROI centrada: redueix la càrrega de càlcul i guia l’usuari.
// Atenció: amb codis QR molt petits l’ROI pot ser massa estret.
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;

// Fil de UI: navegació, beep, omplir camps, etc.
TThread.Queue(nil,
procedure
begin
if FIsRunning and Assigned(FOnResult) then
FOnResult(AText);
end);
end;

end.

Què resol el codi (i per què és necessari)

Throttle (MinIntervalMs) redueix la càrrega de CPU i la dissipació de calor. Sense limitació, alguns dispositius intenten decodificar 30–60 frames/s; a la pràctica n’hi ha prou amb 5–10/s, sovint menys. Debounce (DebounceMs) evita que un codi QR mantingut estable sigui activat diverses vegades (p. ex. registre doble en un pas de procés).

La bandera Interlocked-Flag (FIsDecoding) garanteix que com a màxim s’executi una tasca de decodificació. És un recurs d’arquitectura contra l’«atascament de la cua»: si la decodificació triga 200 ms però s’inicia una tasca cada 120 ms, la cua creix i els resultats arriben amb retard, cosa que en l’operació sembla «l’escàner respon incorrectament».

Condicions límit i punts de risc

  • TBitmap und Threading: FMX-Bitmaps poden ser GPU-backed. L’enfoc copia el frame a un bitmap local i el decodifica en segon pla. Depenent de la versió/plataforma de Delphi pot caldre precaució: si observeu artefactes, forceu un bitmap a CPU (p. ex. via lectura/escriptura de píxels) o treballeu amb un ByteBuffer des del SampleBuffer (més proper a la plataforma però més estable).
  • Stop/Start bei Navigation: En aplicacions mòbils sovint es fa Stop en canviar de formulari o en l’event de pausa de l’app. És important que Stop es pugui cridar diverses vegades i no generi excepcions (idempotent). A més, el callback de resultat hauria de comprovar si l’escàner encara està actiu (ho fa DoResultOnMainThread).
  • ROI zu eng: Un ROI centrat accelera, però pot fallar si l’usuari manté el codi fora del ROI o el codi és molt petit. Per això EnableRoi és configurable i RoiScale està limitat.
  • Format-Lock auf QR: Limitar a QR_CODE acostuma a ser correcte. Si també necessiteu Code128/EAN, amplieu els formats – però compteu amb més falsos positius i més ús de CPU.

Delphi FMX cicle de vida de la càmera: permisos, fons, rotació

Els errors més freqüents no provenen de la decodificació, sinó de l’entorn de la càmera:

  • Android Permissions: Els permisos de càmera s’han d’obtenir en temps d’execució. Prevegeu el cas que l’usuari denegi o seleccioni «Només aquesta vegada». Tècnicament això significa: mantenir l’estat de la UI («Scanner llest?») separat de l’estat de la càmera, sinó us exposeu a estats incomplets.
  • App geht in den Hintergrund: En el OnApplicationEvent (p. ex. EnteredBackground) s’ha de cridar Stop. En tornar, cridar explícitament Start (i potser una breu demora) perquè el preview sigui estable.
  • Rotation/Mirroring: Per als codis QR la rotació sovint no és crítica, però en algunes canalitzacions de càmera el bitmap pot estar reflectit o girat. Si els escanejos només funcionen en una orientació, això és un símptoma. En aquest cas: girar/reflectir abans del scan o utilitzar un decoder que faci servir metadades d’orientació.

Depuració en funcionament: com trobar les causes reals

Quan l’escàner no llegeix de tant en tant, el depurat reproducible és molt valuós. Tres mesures que donen bons resultats:

  1. Frame-Sampling loggen: Registreu (només en mode Debug/Support) tick, mida d’imatge, mida del ROI, durada de decodificació. Així veureu immediatament si Throttle/Debounce o la càrrega de CPU són el problema.
  2. Testbilder sichern: Desar cada N segons una imatge de l’ROI (temporal). Això us permet analitzar sense maquinari de càmera si el problema és de contrast/borrositat.
  3. Separar la càrrega de treball: no actualitzeu els UI-Updates (Preview-Overlay, Status-Text) amb alta freqüència. El «tremolor de la UI» sovint prové de massa esdeveniments Queue.

Variants: Si necessiteu més que «escannejar i llest»

Diversos resultats, però controlats

Per a processos per lots (p. ex. moltes labels una darrere l’altra) reduïu DebounceMs i afegiu una Whitelist/State-Machine: un QR-Code només s’ha d’acceptar quan l’etapa de procés actual l’estigui esperant. Això no és lògica d’interfície d’usuari, sinó lògica de domini — ha d’anar en una capa pròpia perquè l’escàner i el procés siguin testables de manera independent.

Validació offline i dades de càrrega segures

En processos empresarials els QR-Codes sovint contenen IDs o Token. No confieu que «QR = correcte». Valideu localment (format, suma de comprovació, prefixos esperats) i al servidor (REST-API). Si utilitzeu Token: temps d’expiració, protecció contra replay i logging amb precaució (no posis Tokens en text clar als logs de suport).

Situacions legacy: FMX-Scanner com a mòdul en bases de codi mixtes

Si disposeu d’un entorn VCL consolidat, FMX com a client mòbil sovint és un fil separat. Mantingueu l’escàner com a classe Controller sense dependències de Form (com abans), així el podreu integrar en pantalles diferents. Això també compensa en processos de modernització: la lògica de negoci continua sent testable, la càmera és només un canal d’entrada. Especialment en situacions legacy interessa una separació clara per a logging, Feature-Flags i configuració remota.

Conclusió: Un FMX-QR-Scan sòlid és un problema de cicle de vida — no només una crida a ZXing

Un QR Code Scanner en Delphi FMX serà estable si el trateu com una petita pipeline: la càmera subministra frames, un decodificador en background treballa de manera controlada, i Debounce/Throttle eviten esdeveniments duplicats i tardans. L’extracte de codi anterior aborda exactament els punts que fallen en processos mòbils de negoci reals: massa Decode-Tasks, un Stop poc net, bloquejos del UI-Thread i càrrega innecessària.

Límits d’ús: si necessiteu taxes d’escaneig extremadament altes (p. ex. escaneig industrial a la cadena) o requisits estrictes de processament d’imatge, la càmera estàndard FMX + pipeline de Bitmap sovint resulta massa costosa. En aquests casos convé un enfocament proper a la plataforma (Native Camera API, YUV-Buffer directe, SIMD/NEON) o un SDK d’escàner especialitzat. Per a la majoria d’aplicacions mòbils pròximes al procés, l’enfocament mostrat és suficient sempre que el cicle de vida, els permisos i el threading estiguin integrats correctament — i que els processos subjacents siguin clars.

Si cal integrar un QR-Scan en una arquitectura Delphi existent (incloent casos límit com navegació, backgrounding, logging i validació de processos), ho podem tractar de forma estructurada:

En l’àmbit tècnic també tenen un paper important Zxing Delphi i Fmx Tcameracomponent quan cal que les integracions, els fluxos de dades i l’evolució es coordinin de manera neta.

Parlar del projecte o del pla de modernització amb Net-Base.

Pas següent

Quan un tema esdevé un projecte real, l'arquitectura, l'entorn existent i les operacions s'haurien de considerar conjuntament des de bon començament.

No només donem suport en qüestions puntuals, sinó també quan, a partir de fragments de codi font, temes de sistemes heredats o idees de portal, ha de sorgir un projecte empresarial sòlid.

  • L'estat actual, la visió objectiu i els riscos tècnics s'avaluen conjuntament.
  • REST, l'accés a les dades, els portals i el desplegament no es releguen a fases posteriors.
  • Vostè veurà aviat quin camí és econòmicament i operativament viable.

Comparteix la publicació

Comparteix aquesta publicació directament

LinkedIn, X, XING, Facebook, WhatsApp i E-Mail estan disponibles de forma immediata. Per a Instagram preparem directament l’enllaç i un text breu.

Correu electrònic

Instagram s'obre en una pestanya nova. L'enllaç i el text curt es copien prèviament al porta-retalls.