Do tema da revista à prática do projeto
Páginas de serviços e técnicas correspondentes ao artigo
Scanner de QR Code Delphi FMX na prática
Um Scanner de QR Code Delphi FMX é rapidamente montado na demo: mostrar preview da câmera, capturar o Bitmap, executar o ZXing. Em software empresarial real (p.ex. entrada de mercadorias, atribuição de equipamentos, ticketing, processos de acesso) surgem RESTrições adicionais: o app vai para segundo plano, a câmera perde o foco, o usuário segura o dispositivo inclinado, o formato da imagem muda — e de repente você está escaneando o mesmo código duas vezes por segundo ou a UI engasga porque a decodificação roda no UI-Thread.
Os problemas típicos são menos um “ZXing não consegue ler” e mais relacionados ao lifecycle e à arquitetura: liberação de recursos da câmera, cadência dos frames, segurança de thread ao acessar TBitmap (GPU/CPU) e um Stop/Start claro que permaneça limpo mesmo quando o usuário navega rapidamente ou o OS retira a câmera temporariamente.
Visão geral da arquitetura: Pipeline em vez de „OnSampleBufferReady faz tudo“
Na prática, uma pequena pipeline com responsabilidades claras provou-se eficaz:
- Adaptador de câmera: fornece frames (ou cópias deles) em um formato definido.
- Decoder: trabalha em thread de background e devolve resultados via callback.
- Gate/Debounce: evita leituras duplicadas e regula a carga (throttle).
- Camada de UI: mostra o preview, opcionalmente um retângulo de foco (ROI, „Region of InteREST“) e reage aos resultados.
Assim você evita que UI, câmera e decoder se bloqueiem mutuamente. “ROI” aqui significa uma janela de busca recortada (p.ex. central 60 %), que alivia o decoder e reduz falsos positivos. Importante: ROI é uma ferramenta de performance e usabilidade, não um mecanismo de segurança.
Trecho de código: Scanner de QR Code robusto (FMX + ZXing) com debounce e stop limpo
O código a seguir foi pensado como um bloco compacto, mas apto para projetos. Ele usa ZXing (Delphi-Port) via ZXing.ScanManager e se conecta a TCameraComponent.OnSampleBufferReady. Três pontos são decisivos:
- Frames são throttled (não decodificar cada sample).
- A decodificação não roda no UI-Thread.
- Stop/Start é idempotente (pode ser chamado múltiplas vezes sem 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>
/// QR-Scanner-Controller für FMX (Android/iOS).
/// Kümmert sich um Kamera-Frame-Gating, Hintergrund-Decoding und sauberes Stop/Start.
/// </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; // 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 initialisieren und auf QR beschränken (Performance + weniger 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;
// Kamera aktivieren: In echten Apps vorher Permissions prüfen (Android) und UI-Flow berücksichtigen.
if Assigned(FCamera) then
FCamera.Active := True;
end;
procedure TQrScannerController.Stop;
begin
if not FIsRunning then
Exit;
FIsRunning := False;
// Aktiv sauber abschalten
if Assigned(FCamera) then
FCamera.Active := False;
// Decoder-Flag zurücksetzen, falls Stop in einer ungünstigen Phase kommt
TInterlocked.Exchange(FIsDecoding, 0);
end;
function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
// Throttle: nicht jedes Frame dekodieren
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);
// gleicher Text innerhalb Debounce-Fenster - ignorieren
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;
// Nur ein Decode gleichzeitig (sonst Queue-Stau bei schwachen Geräten)
if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
Exit;
// Kamera-Sample in FBitmap kopieren. Lock, weil derselbe Bitmap-Buffer nicht parallel benutzt werden soll.
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;
// Hintergrund-Decoding
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 mittig ausschneiden: reduziert Rechenlast und lenkt den Nutzer.
// Achtung: bei sehr kleinen QR-Codes kann ROI zu eng sein.
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: Navigation, Beep, Feld füllen etc.
TThread.Queue(nil,
procedure
begin
if FIsRunning and Assigned(FOnResult) then
FOnResult(AText);
end);
end;
end.
O que o código resolve (e por que é necessário)
Throttle (MinIntervalMs) reduz a carga da CPU e a geração de calor. Sem limitação, alguns dispositivos tentam decodificar 30–60 frames/s; na prática 5–10/s são suficientes, frequentemente menos. Debounce (DebounceMs) evita que um código QR mantido estável dispare várias vezes (por exemplo, registro duplicado em uma etapa do processo).
O Interlocked-Flag (FIsDecoding) garante que no máximo uma tarefa de decode esteja em execução. Isso é um artifício arquitetural contra „acúmulo na fila“: se o decode leva 200 ms, mas uma tarefa é iniciada a cada 120 ms, a fila cresce e os resultados chegam defasados, o que em operação parece que „o scanner reage errado“.
Condições e armadilhas
- TBitmap und Threading: FMX-Bitmaps podem ser suportadas por GPU. A abordagem copia o frame para uma Bitmap local e decodifica em segundo plano. Dependendo da versão/plataforma do Delphi pode ainda ser necessário cuidado: se você observar artefatos, force uma Bitmap em CPU (por exemplo via leitura/gravação de pixels) ou trabalhe com um ByteBuffer do SampleBuffer (mais próximo da plataforma, mas mais estável).
- Stop/Start bei Navigation: Em apps móveis costuma-se parar ao trocar de Forms ou no evento de pausa do app. É importante que
Stoppossa ser chamado múltiplas vezes sem gerar exceções (idempotente). Além disso, o callback de resultado deve verificar se o scanner ainda está ativo (fazDoResultOnMainThread). - ROI zu eng: Um ROI central acelera, mas pode falhar se os usuários segurarem o código fora do centro ou se o código for muito pequeno. Por isso
EnableRoié configurável eRoiScaleé limitado. - Format-Lock auf QR: Restringir para
QR_CODEgeralmente é correto. Se você também precisar de Code128/EAN, expanda os formatos — espere mais falsos positivos e maior uso de CPU.
Delphi FMX Ciclo de vida da câmera: Permissões, segundo plano, rotação
Os bugs mais comuns não surgem no decode, mas em torno da câmera:
- Android Permissions: As permissões de câmera devem ser solicitadas em tempo de execução. Planeje o caso em que um usuário recuse ou escolha „Somente desta vez“. Tecnicamente isso significa: mantenha o estado da UI („Scanner pronto?“) separado do estado da câmera, caso contrário você ficará preso em estados incompletos.
- App geht in den Hintergrund: No
OnApplicationEvent(por ex.EnteredBackground) você deve chamarStop. Ao retornar, chame conscientementeStart(e, se necessário, uma breve demora), para que o preview fique estável. - Rotation/Mirroring: Para códigos QR a rotação muitas vezes não é crítica, mas em algumas pipelines de câmera o Bitmap pode estar espelhado ou rotacionado. Se os scans „só funcionam em uma orientação“, isso é um indicativo. Nesse caso: rotacione/espelhe antes do scan ou use um decodificador que utilize metadados de orientação.
Depuração em operação: Como encontrar as causas reais
Se o scanner „às vezes“ não lê, depuração reproduzível vale ouro. Três medidas que se mostram eficazes:
- Frame-Sampling loggen: Registre (apenas em modo Debug/Suporte) tick, tamanho da imagem, tamanho do ROI, duração do decode. Assim você vê imediatamente se Throttle/Debounce ou a carga de CPU é o problema.
- Testbilder sichern: Salve a cada N segundos uma imagem do ROI (temporária). Com isso você pode, sem hardware de câmera, analisar se contraste/desfoque é o problema.
- Separar a carga de trabalho: não atualize os elementos da UI (Preview-Overlay, Status-Text) em alta frequência. O „tremor da UI“ vem frequentemente de muitos eventos
Queue.
Variantes: 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/Máquina de Estados: 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.
Próximo passo
Quando um tema se torna um projeto real, arquitetura, sistemas existentes e operação devem ser considerados em conjunto desde o início.
Não apenas apoiamos questões pontuais, mas também quando fragmentos de código-fonte, temas legados ou ideias de portais precisam evoluir para um projeto empresarial robusto.
- Estado atual, estado-alvo e riscos técnicos são avaliados em conjunto.
- REST, o acesso a dados, os portais e o Rollout não são adiados para uma fase posterior.
- Você vê cedo qual caminho é economicamente e operacionalmente viável.