Net-Base Revista

03.06.2026

Escáner de códigos QR en Delphi FMX: escaneo con cámara robusto, seguro para subprocesos y sin sacudidas en la interfaz de usuario

Un lector de códigos QR apto para producción Delphi FMX depende del ciclo de vida de la cámara, de la gestión de hilos y de un arranque/parada limpios. El artículo presenta un enfoque robusto con ZXing, debounce, frame-throttling, recorte de ROI, además de detalles de depuración y operación para Android e iOS.

03.06.2026

Del tema de la revista a la práctica del proyecto

Páginas de servicios y técnicas relacionadas

Escáner de códigos QR Delphi FMX en la práctica

Un escáner de códigos QR Delphi FMX se monta rápidamente en una demo: mostrar la vista previa de la cámara, extraer un Bitmap, pasar ZXing por encima. Pero en software empresarial real (p. ej. recepción de mercancías, asignación de dispositivos, ticketing, procesos de acceso) surgen condiciones límite: la app pasa a segundo plano, la cámara pierde el foco, el usuario sostiene el dispositivo ladeado, cambia el formato de imagen – y de repente escanea dos veces por segundo el mismo código o la interfaz se queda trabada porque la decodificación se ejecuta en el hilo de la interfaz de usuario.

Los problemas típicos no son tanto «ZXing no puede leer», sino el lifecycle y la arquitectura: liberación de recursos de la cámara, cadencia de los frames, seguridad de hilos al acceder a TBitmap (GPU/CPU), y un Stop/Start claro que también sea limpio cuando los usuarios navegan rápidamente o el SO priva temporalmente de la cámara.

Resumen de arquitectura: pipeline en lugar de „OnSampleBufferReady macht alles“

En la práctica ha demostrado su eficacia una pequeña pipeline con responsabilidades claras:

  • Adaptador de cámara: proporciona frames (o copias de estos) en un formato definido.
  • Decodificador: funciona en un hilo en segundo plano y devuelve resultados mediante un callback.
  • Gate/Debounce: evita escaneos dobles y regula la carga (Throttle).
  • Capa de UI: muestra la vista previa, opcionalmente un rectángulo de enfoque (ROI, «Region of InteREST») y responde a los resultados.

Con ello evita que la UI, la cámara y el decodificador se bloqueen entre sí. «ROI» se refiere aquí a una ventana de búsqueda recortada (p. ej. central al 60 %), que aligera la carga del decodificador y reduce falsos positivos. Importante: el ROI es una herramienta de rendimiento y usabilidad, no un mecanismo de seguridad.

Fragmento de código: Escáner de QR robusto (FMX + ZXing) con Debounce y parada limpia

El siguiente código está pensado como un módulo compacto pero apto para proyectos. Utiliza ZXing (Delphi-Port) a través de ZXing.ScanManager y se engancha a TCameraComponent.OnSampleBufferReady. Son decisivos tres puntos:

  • Los frames se throttlean (no decodificar cada muestra).
  • La decodificación no se ejecuta en el hilo de la interfaz de usuario.
  • El Stop/Start es idempotente (invocable varias veces sin provocar caos en los 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 escáner QR para FMX (Android/iOS).
/// Gestiona el control de fotogramas de la cámara, la decodificación en segundo plano y un inicio/parada ordenados.
/// </summary>
TQrScannerController = class
private
FCamera: TCameraComponent;
FScanManager: TScanManager;
FBitmap: TBitmap;
FLock: TObject;

FOnResult: TQrScanResultEvent;

// Control/Limitación
FIsRunning: Boolean;
FIsDecoding: Integer; // 0/1 als Interlocked-Flag
FLastDecodeTick: Int64;
FMinIntervalMs: Cardinal;

// Debounce para evitar códigos idénticos repetidos
FLastText: string;
FLastTextTick: Int64;
FDebounceMs: Cardinal;

// ROI: proporción de la imagen que se escanea (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. ej. 120
property DebounceMs: Cardinal read FDebounceMs write FDebounceMs; // p. ej. 1200
property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
property RoiScale: Single read FRoiScale write FRoiScale; // p. ej. 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;

// Inicializar ScanManager y limitar a QR (rendimiento + menos falsos positivos)
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 cámara: en aplicaciones reales comprobar permisos antes (Android) y considerar el flujo de la UI.
if Assigned(FCamera) then
FCamera.Active := True;
end;

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

// Desactivar de forma ordenada
if Assigned(FCamera) then
FCamera.Active := False;

// Restablecer la bandera del decodificador en caso de que Stop ocurra en una fase desfavorable
TInterlocked.Exchange(FIsDecoding, 0);
end;

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

// mismo texto dentro de la ventana 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;

// Solo un decodificado a la vez (si no, acumulación en la cola en dispositivos con pocos recursos)
if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
Exit;

// Copiar la muestra de la cámara a FBitmap. Bloqueo, porque el mismo buffer de bitmap no debe usarse en paralelo.
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ón en segundo plano
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
// Recortar ROI centrado: reduce la carga de cálculo y guía al usuario.
// Atención: con códigos QR muy pequeños, el ROI puede ser demasiado estrecho.
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;

// Hilo de UI: navegación, aviso sonoro, rellenar campo, etc.
TThread.Queue(nil,
procedure
begin
if FIsRunning and Assigned(FOnResult) then
FOnResult(AText);
end);
end;

end.

Qué resuelve el código (y por qué es necesario)

Throttle (MinIntervalMs) reduce la carga de CPU y la generación de calor. Sin limitación, algunos dispositivos intentan decodificar 30–60 fotogramas/s; en la práctica bastan 5–10/s, a menudo menos. Debounce (DebounceMs) evita que un código QR mantenido estable se dispare varias veces (p. ej., doble contabilización en un paso de proceso).

La bandera Interlocked (FIsDecoding) garantiza que como máximo se ejecute una tarea de Decode. Es un truco de arquitectura contra el «atasco de cola»: si la decodificación tarda 200 ms, pero se inicia una tarea cada 120 ms, la cola crece y los resultados llegan con retardo, lo que en explotación parece que «el escáner responde mal».

Condiciones límite y trampas

  • TBitmap und Threading: Las FMX-Bitmaps pueden estar respaldadas por la GPU. El enfoque copia el fotograma en un Bitmap local y decodifica en segundo plano. Dependiendo de la versión/plataforma de Delphi puede ser necesaria precaución: si observa artefactos, fuerce un Bitmap en CPU (p. ej. mediante Pixel-Read/Write) o trabaje con un ByteBuffer del SampleBuffer (más cercano a la plataforma, pero más estable).
  • Stop/Start bei Navigation: En apps móviles suele detenerse al cambiar de formulario o en el evento de pausa de la app. Es importante que Stop pueda llamarse varias veces y no genere excepciones (idempotente). Además, el callback de resultados debe comprobar si el escáner sigue en ejecución (lo hace DoResultOnMainThread).
  • ROI demasiado estrecho: Un ROI centrado acelera, pero puede fallar si el usuario mantiene el código fuera del área o el código es muy pequeño. Por eso EnableRoi es configurable y RoiScale está limitada.
  • Bloqueo de formato en QR: Restringir a QR_CODE suele ser correcto. Si también necesita Code128/EAN, amplíe los formatos, pero contemple más falsos positivos y mayor uso de CPU.

Delphi FMX ciclo de vida de la cámara: permisos, segundo plano, rotación

Los errores más frecuentes no ocurren durante la decodificación, sino en torno a la cámara:

  • Permisos de Android: Los permisos de cámara deben solicitarse en tiempo de ejecución. Tenga en cuenta que un usuario puede denegar o elegir «Solo esta vez». Técnicamente eso significa mantener el estado de la UI («¿Escáner listo?») separado del estado de la cámara, de lo contrario puede quedar en estados intermedios.
  • La app pasa a segundo plano: En OnApplicationEvent (p. ej. EnteredBackground) debe llamar a Stop. Al volver, llame explícitamente a Start (y, si procede, añada una breve demora) para que la vista previa sea estable.
  • Rotación/Espejado: Para códigos QR la rotación suele ser poco crítica, pero en algunas pipelines de cámara el Bitmap puede estar espejado o girado. Si los escaneos funcionan «solo en una orientación», es una pista. En ese caso: antes de escanear gire/espeje la imagen o utilice un decodificador que emplee metadatos de orientación.

Depuración en explotación: así encuentra las causas reales

Si el escáner «a veces» no lee, la depuración reproducible vale su peso en oro. Tres medidas que funcionan bien:

  1. Frame-Sampling loggen: Registre (solo en modo Debug/Support) tick, tamaño de imagen, tamaño del ROI, duración de la decodificación. Así verá de inmediato si Throttle/Debounce o la carga de CPU son el problema.
  2. Guardar imágenes de prueba: Guarde cada N segundos una imagen del ROI (temporal). Con ello podrá analizar sin hardware de cámara si el problema es contraste o desenfoque.
  • Separar la carga de trabajo: No actualizar las UI-Updates (Preview-Overlay, Status-Text) con alta frecuencia. El „temblor“ de la UI suele deberse a demasiados eventos Queue.
  • Variantes: Si necesita más que „escanear y listo“

    Varios resultados, pero controlados

    Para procesos por lotes (p. ej., muchas etiquetas consecutivas) reduzca DebounceMs y añada una Whitelist/State-Machine: un código QR solo debe aceptarse cuando el paso de proceso actual lo espera. Eso no es lógica de UI, sino lógica de dominio: pertenece a una capa propia, de modo que el escáner y el proceso sean comprobables mediante pruebas de forma independiente.

    Validación sin conexión y datos de carga seguros

    En procesos empresariales los códigos QR a menudo contienen IDs o tokens. No confíe en que „QR = correcto“. Valide localmente (formato, suma de comprobación, prefijos esperados) y en el servidor (REST-API). Si usa tokens: tiempos de expiración, protección contra replay y registro con precaución (no incluya tokens en texto claro en los logs de soporte).

    Situaciones heredadas: FMX-Scanner como módulo en bases de código mixtas

    Si dispone de un ecosistema VCL consolidado, FMX como cliente móvil suele ser una rama separada. Mantenga el escáner como clase Controller sin dependencias de formulario (como arriba), así podrá integrarlo en distintas pantallas. Esto también compensa en procesos de modernización: la lógica de negocio permanece testeable y la cámara es solo un canal de entrada. Precisamente en situaciones heredadas conviene además una separación clara para registro, Feature-Flags y configuración remota.

    Conclusión: Un escaneo QR sólido en FMX es un problema de ciclo de vida — no solo una llamada a ZXing

    Un escáner de códigos QR en Delphi FMX será estable si lo trata como una pequeña canalización: la cámara entrega frames, un decodificador en segundo plano trabaja de forma controlada, y Debounce/Throttle evitan eventos duplicados y tardíos. El fragmento de código anterior aborda exactamente los puntos donde fallan los procesos móviles empresariales reales: demasiadas tareas de decodificación, detenciones incorrectas, bloqueos del hilo de la UI y cargas innecesarias.

    Límites de uso: Si necesita tasas de escaneo extremadamente altas (p. ej., escaneado industrial en cinta) o requisitos exigentes de procesamiento de imágenes, la cámara estándar de FMX + la tubería de bitmaps suele ser demasiado costosa. Entonces conviene un enfoque cercano a la plataforma (Native Camera API, YUV-Buffer directo, SIMD/NEON) o un SDK de escaneo especializado. Para la mayoría de las aplicaciones móviles orientadas a procesos, el enfoque mostrado es suficiente, siempre que el ciclo de vida, los permisos y el manejo de hilos estén integrados correctamente — y que los procesos subyacentes sean claros.

    Si necesita adaptar un escaneo QR a una arquitectura existente Delphi (incluyendo casos límite como navegación, ejecución en segundo plano, registro y validación de procesos), podemos aclararlo de forma estructurada:

    En el ámbito técnico también juegan un papel importante Zxing Delphi y Fmx Tcameracomponent cuando la integración, los flujos de datos y la evolución deben encajar de forma limpia.

    Discutir un proyecto o iniciativa de modernización con Net-Base.

    Siguiente paso

    Cuando el tema se convierte en un proyecto real, la arquitectura, los sistemas existentes y la operación deben considerarse desde el principio.

    No solo apoyamos en consultas puntuales, sino también cuando, a partir de fragmentos de código fuente, temas heredados o ideas de portales, debe consolidarse un proyecto empresarial robusto.

    • La situación actual, el estado objetivo y los riesgos técnicos se evalúan conjuntamente.
    • REST, el acceso a datos, los portales y el rollout no se posponen como consecuencias tardías.
    • Detecta con antelación qué enfoque es viable desde el punto de vista económico y operativo.

    Compartir entrada

    Compartir esta publicación directamente

    LinkedIn, X, XING, Facebook, WhatsApp y correo electrónico están disponibles de inmediato. Para Instagram preparamos el enlace y un texto breve de inmediato.

    Correo electrónico

    Instagram se abre en una nueva pestaña. El enlace y el texto breve se copian previamente en el portapapeles.