Net-Base Revista

03.06.2026

Scanner de QR Code em Delphi FMX: varredura pela câmera robusta, segura para threads e sem tremores na UI

Um scanner de QR Code pronto para uso prático Delphi FMX depende do ciclo de vida da câmera, do gerenciamento de threads e de uma parada e inicialização limpas. O artigo apresenta uma abordagem robusta com ZXing, debounce, throttling de frames, recorte de ROI, além de detalhes de depuração e operação para Android e iOS.

03.06.2026

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).
Delphi
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 Stop possa 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 (faz DoResultOnMainThread).
  • 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 e RoiScale é limitado.
  • Format-Lock auf QR: Restringir para QR_CODE geralmente é 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 chamar Stop. Ao retornar, chame conscientemente Start (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:

  1. 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.
  2. 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.
  3. 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.

Partilhar publicação

Compartilhar esta publicação diretamente

LinkedIn, X, XING, Facebook, WhatsApp e e‑mail estão imediatamente disponíveis. Para o Instagram, preparamos o link e um texto curto de imediato.

E-mail

O Instagram abre numa nova aba. O link e o texto curto são copiados previamente para a área de transferência.