Net-Base Magazine

03.06.2026

Scanner de QR Code dans Delphi FMX : lecture par caméra robuste, sécurisée vis‑à‑vis des threads et sans scintillement de l'interface utilisateur

Un scanner de codes QR Delphi FMX adapté à la production dépend du cycle de vie de la caméra, de la gestion des threads et d'un arrêt et d'un démarrage propres. L'article présente une approche robuste basée sur ZXing, Debounce, Frame-Throttling, recadrage ROI ainsi que des détails de débogage et d'exploitation pour Android et iOS.

03.06.2026

Du thème du magazine à la pratique des projets

Pages de services et techniques pertinentes pour l'article

Scanner de codes QR Delphi FMX en pratique

Un Scanner de codes QR Delphi FMX se compose rapidement dans une démo : afficher la prévisualisation de la caméra, extraire le Bitmap, lancer ZXing dessus. Dans un vrai logiciel métier (p. ex. réception de marchandises, attribution d’appareils, billetterie, processus d’accès), des contraintes supplémentaires apparaissent : l’application passe en arrière-plan, la caméra perd le focus, l’utilisateur tient l’appareil de travers, le format d’image change — et soudain vous scannez deux fois par seconde le même code ou l’interface utilisateur saccade, parce que le décodage s’exécute dans le thread de l’interface utilisateur.

Les problèmes typiques relèvent moins de « ZXing ne peut pas lire » que du cycle de vie et de l’architecture : libération des ressources de la caméra, cadence des frames, sécurité des threads lors de l’accès à TBitmap (GPU/CPU), et un arrêt/démarrage clair qui RESTe propre même si les utilisateurs naviguent rapidement ou si l’OS retire temporairement la caméra.

Vue d’ensemble de l’architecture : pipeline plutôt que „OnSampleBufferReady macht alles“

Une petite pipeline aux responsabilités clairement définies a fait ses preuves :

  • Adaptateur caméra : fournit des Frames (ou des copies) dans un format défini.
  • Décodeur : s’exécute dans un thread d’arrière-plan et renvoie les résultats via un callback.
  • Gate/Debounce : évite les double-scans et régule la charge (Throttle).
  • Couche UI : affiche la prévisualisation, éventuellement un cadre de focus (ROI, „Region of InteREST“) et réagit aux résultats.

Cela évite que UI, caméra et décodeur se bloquent mutuellement. « ROI » désigne ici une fenêtre de recherche découpée (p. ex. centrée à 60 %), qui décharge le décodeur et réduit les faux positifs. Important : le ROI est un outil de performance et d’ergonomie, pas un mécanisme de sécurité.

Extrait de code : Scanner de codes QR robuste (FMX + ZXing) avec debounce et arrêt propre

Le code suivant est conçu comme un composant compact mais apte au projet. Il utilise ZXing (Delphi-port) via ZXing.ScanManager et se rattache à TCameraComponent.OnSampleBufferReady. Trois points sont décisifs :

  • Les Frames sont throttled (ne pas décoder chaque échantillon).
  • Le décodage ne s’exécute pas dans le thread UI.
  • Stop/Start est idempotent (appelable plusieurs fois sans chaos de ressources).
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>
  /// Contrôleur de scanner QR pour FMX (Android/iOS).
  /// Gère le gating des frames caméra, le décodage en arrière-plan et un arrêt/démarrage propre.
  /// </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; // 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;

  // 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;

  // Désactivation propre
  if Assigned(FCamera) then
    FCamera.Active := False;

  // Réinitialiser le flag du décodeur si Stop survient dans une phase inopportune
  TInterlocked.Exchange(FIsDecoding, 0);
end;

function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
  // Throttle : ne pas décoder chaque 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);

  // 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;

  // Décodage en arrière-plan
  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;

  // Thread principal (UI) : navigation, bip, remplir un champ, etc.
  TThread.Queue(nil,
    procedure
    begin
      if FIsRunning and Assigned(FOnResult) then
        FOnResult(AText);
    end);
end;

end.

Ce que le code résout (et pourquoi c’est nécessaire)

Throttle (MinIntervalMs) réduit la charge CPU et la dissipation thermique. Sans limitation, certains appareils tentent de décoder 30–60 ips ; en pratique 5–10/s suffisent, souvent moins. Debounce (DebounceMs) évite qu’un code QR maintenu stable ne déclenche plusieurs fois (p. ex. double validation dans une étape de processus).

Le flag Interlocked (FIsDecoding) garantit qu’au plus une tâche de décodage s’exécute. C’est un artifice d’architecture contre le « bouchon de file » : si le décodage prend 200 ms mais qu’une tâche est lancée toutes les 120 ms, la file croît et les résultats arrivent décalés, ce qui en exploitation donne l’impression que « le scanner réagit mal ».

Conditions et pièges

  • TBitmap et threading : Les bitmaps FMX peuvent être basées sur le GPU. L’approche consiste à copier la frame dans une Bitmap locale et à décoder en arrière-plan. Selon la version/plateforme de Delphi, une vigilance reste nécessaire : si vous observez des artefacts, forcez une bitmap CPU (p. ex. via lecture/écriture des pixels) ou travaillez avec un ByteBuffer issu du SampleBuffer (plus proche de la plateforme, mais plus stable).
  • Stop/Start lors de la navigation : Dans les apps mobiles, on stoppe souvent lors du changement de form ou de l’événement de pause de l’app. Il est important que Stop puisse être appelé plusieurs fois sans lever d’exception (idempotent). De plus, le callback de résultat doit vérifier si le scanner est toujours actif (comme le fait DoResultOnMainThread).
  • ROI trop étroit : Un ROI centré accélère, mais peut échouer si l’utilisateur tient le code en dehors ou si le code est très petit. C’est pourquoi EnableRoi est configurable et RoiScale est limité.
  • Verrouillage de format sur QR : Restreindre aux QR_CODE est généralement correct. Si vous avez aussi besoin de Code128/EAN, étendez les formats – attendez-vous toutefois à plus de faux positifs et à une plus grande charge CPU.

Delphi FMX cycle de vie de la caméra : autorisations, arrière-plan, rotation

Les bugs les plus fréquents n’apparaissent pas au décodage mais autour de la caméra :

  • Autorisations Android : les droits caméra doivent être demandés à l’exécution. Préparez le cas où un utilisateur refuse ou choisit « seulement cette fois ». Techniquement cela signifie : séparer l’état UI (« Scanner prêt ? ») de l’état caméra, sinon vous risquez des états partiels.
  • L’app passe en arrière-plan : lors de OnApplicationEvent (p. ex. EnteredBackground), appelez Stop. Au retour, appelez explicitement Start (et éventuellement attendez un court délai) pour garantir la stabilité du preview.
  • Rotation / miroir : Pour les QR-codes, la rotation est souvent non critique, mais certaines pipelines caméra peuvent renvoyer une bitmap miroir ou tournée. Si les scans ne fonctionnent « que dans une seule orientation », c’est un indice. Dans ce cas : pivoter/miroiter avant le scan ou utiliser un décodeur qui exploite les métadonnées d’orientation.

Debugging en exploitation : comment trouver les causes réelles

Si le scanner ne lit « parfois » pas, pouvoir reproduire le problème est précieux. Trois mesures éprouvées :

  1. Logger le frame-sampling : consignez (uniquement en mode Debug/Support) le tick, la taille de l’image, la taille du ROI, la durée du décodage. Vous verrez immédiatement si Throttle/Debounce ou la charge CPU sont en cause.
  2. Sauvegarder des images de test : enregistrez toutes les N secondes une image ROI (temporaire). Cela permet d’analyser sans matériel caméra si le contraste/la netteté posent problème.
  3. Séparer la charge de travail : ne mettez pas à jour les éléments UI (Preview-Overlay, Status-Text) à haute fréquence. Le « tremblement » de l’UI provient souvent d’un trop grand nombre d’événements Queue.

Variantes : Wenn Sie mehr brauchen als „Scan und fertig“

Mehrere Ergebnisse, aber kontrolliert

Pour les processus par lots (p. ex. de nombreuses étiquettes successives), réduisez DebounceMs et ajoutez une liste blanche / machine à états : un QR‑Code ne doit être accepté que si l’étape de processus courante l’attend. Ce n’est pas de la logique UI, mais de la logique de domaine — elle doit résider dans une couche dédiée afin que le scanner et le processus restent testables indépendamment.

Offline-Validierung und sichere Nutzdaten

Dans les processus d’entreprise, les QR‑Codes contiennent souvent des IDs ou des tokens. Ne vous fiez pas à l’idée que « QR = correct ». Validez localement (format, somme de contrôle, préfixes attendus) et côté serveur (API REST). Si vous utilisez des tokens : pensez aux durées de validité, à la protection contre le replay et au logging avec précaution (ne consignez pas les tokens en clair dans les logs de support).

Legacy-Situationen: FMX-Scanner als Modul in gemischten Codebasen

Si vous avez un parc VCL existant, FMX en tant que client mobile est souvent un embranchement séparé. Conservez le scanner comme classe controller sans dépendances aux Forms (comme ci‑dessus), ainsi vous pourrez l’intégrer dans différents écrans. Cela paie lors des modernisations : la logique métier reste testable, la caméra n’est qu’un canal d’entrée. Dans les situations legacy, une séparation claire pour le logging, les Feature‑Flags et la configuration distante est également utile.

Fazit: Solider FMX-QR-Scan ist ein Lifecycle-Problem – nicht nur ein ZXing-Aufruf

Un scanner de QR Code dans Delphi FMX devient stable si vous le traitez comme une petite pipeline : la caméra fournit des frames, un décodeur en arrière‑plan travaille de façon contrôlée, et Debounce/Throttle empêchent les événements doublons et tardifs. L’extrait de code ci‑dessus adresse précisément les points qui font défaut dans de vrais processus mobiles métier : trop de tâches de décodage, arrêt mal géré, blocages du thread UI et charge inutile.

Limites d’utilisation : si vous avez besoin de taux de scan extrêmement élevés (p. ex. scan industriel sur chaîne) ou d’exigences strictes en traitement d’image, la caméra standard FMX + pipeline bitmap est souvent trop coûteuse. Dans ce cas, un approche proche de la plateforme (Native Camera API, YUV‑Buffer direct, SIMD/NEON) ou un SDK de scanner spécialisé est recommandée. Pour la plupart des applications mobiles de terrain, l’approche présentée suffit toutefois, à condition que le lifecycle, les droits et le threading soient intégrés proprement — et que les processus en aval soient bien définis.

Si vous devez intégrer un scan QR dans une architecture Delphi existante (incluant les cas limites comme la navigation, le backgrounding, le logging et la validation de processus), nous clarifions cela volontiers de manière structurée :

Dans le contexte métier, Zxing Delphi et Fmx Tcameracomponent jouent également un rôle important lorsque les intégrations, les flux de données et l’évolution doivent s’articuler proprement.

Projet ou mission de modernisation à discuter avec Net-Base.

Étape suivante

Lorsque ce sujet devient un projet concret, l'architecture, l'existant et l'exploitation doivent être examinés ensemble dès le départ.

Nous n'intervenons pas seulement sur des questions ponctuelles, mais aussi lorsque des fragments de code source, des problématiques liées aux systèmes legacy ou des concepts de portail doivent se transformer en un projet d'entreprise robuste.

  • L'état des lieux, l'état cible et les risques techniques sont évalués conjointement.
  • REST, l'accès aux données, les portails et le déploiement ne sont pas repoussés en tant que conséquences ultérieures.
  • Vous identifiez tôt quelle voie est viable sur le plan économique et opérationnel.

Partager l'article

Partager directement cette publication

LinkedIn, X, XING, Facebook, WhatsApp et e-mail sont immédiatement disponibles. Pour Instagram, nous préparons directement le lien et un court texte.

Courriel

Instagram s'ouvre dans un nouvel onglet. Le lien et le court texte sont préalablement copiés dans le presse-papiers.