Net-Base Revistă

03.06.2026

Scanner QR în Delphi FMX: scanare cu cameră robustă, sigură pentru fire de execuție și fără tremurat al interfeței

Un scanner de coduri QR practic Delphi FMX se bazează pe ciclul de viață al camerei, threading și pe oprire/pornire curate. Articolul prezintă o abordare robustă cu ZXing, Debounce, Frame-Throttling, decupare ROI, precum și detalii de depanare și de operare pentru Android și iOS.

03.06.2026

De la tema din revistă la practica în proiecte

Pagini relevante de servicii și pagini tehnice pentru articol

Scanner de coduri QR Delphi FMX în practică

Un QR Code Scanner Delphi FMX poate fi montat rapid într-o demonstrație: afișezi previzualizarea camerei, preiei un Bitmap, rulezi ZXing peste el. În software business real (de ex. recepție marfă, alocare echipamente, ticketing, procese de acces) apar însă condiții-limită: aplicația intră în fundal, camera își pierde focusul, utilizatorul ține dispozitivul înclinat, formatul imaginii se schimbă – și dintr-o dată scanați de două ori pe secundă același cod sau interfața sacadează, deoarece decodificarea rulează în firul UI.

Problemele tipice sunt mai puțin „ZXing kann nicht lesen“, și mai mult legate de ciclul de viață și arhitectură: eliberarea resurselor camerei, sincronizarea cadrelor, siguranța la accesul pe thread pentru TBitmap (GPU/CPU), și un mecanism clar de Stop/Start, care rămâne curat chiar și atunci când utilizatorii navighează rapid sau OS-ul retrage camera temporar.

Privire de ansamblu a arhitecturii: Pipeline în loc de „OnSampleBufferReady macht alles“

În practică s-a dovedit eficientă o mică pipeline cu responsabilități clare:

  • Adaptor de cameră: furnizează cadre (sau copii ale acestora) într-un format definit.
  • Decoder: rulează pe un fir de lucru în fundal și returnează rezultatele printr-un callback.
  • Gate/Debounce: previne scanările duble și reglează încărcarea (Throttle).
  • Strat UI: afișează previzualizarea, opțional un dreptunghi de focus (ROI, „Regiunea de interes“) și reacționează la rezultate.

Astfel evitați ca UI, camera și decoderul să se blocheze reciproc. „ROI” înseamnă aici o fereastră de căutare decupată (de ex. 60% centrat), care descarcă sarcina decoderului și reduce rezultatele fals pozitive. Important: ROI este un instrument de performanță și uzabilitate, nu un mecanism de securitate.

Fragment de cod: Scanner QR robust (FMX + ZXing) cu Debounce și oprire curată

Codul următor este gândit ca un bloc compact, dar potrivit pentru proiect. Folosește ZXing (Delphi-port) prin ZXing.ScanManager și se leagă de TCameraComponent.OnSampleBufferReady. Esențiale sunt trei aspecte:

  • Cadrele sunt throttled (nu se decodează fiecare Sample).
  • Decodarea nu rulează în firul UI.
  • Stop/Start este idempotent (apelabil de multiple ori, fără haos de resurse).
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>
  /// Controler pentru scaner QR pentru FMX (Android/iOS).
  /// Se ocupă de gestionarea fluxului de cadre ale camerei, decodare în fundal și oprire/pornire ordonată.
  /// </summary>
  TQrScannerController = class
  private
    FCamera: TCameraComponent;
    FScanManager: TScanManager;
    FBitmap: TBitmap;
    FLock: TObject;

    FOnResult: TQrScanResultEvent;

    // Gating/Throttle
    FIsRunning: Boolean;
    FIsDecoding: Integer; // 0/1 ca flag Interlocked
    FLastDecodeTick: Int64;
    FMinIntervalMs: Cardinal;

    // Debounce împotriva codurilor identice repetate
    FLastText: string;
    FLastTextTick: Int64;
    FDebounceMs: Cardinal;

    // ROI: porțiunea imaginii care va fi scanată (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;

  // Inițializează ScanManager și limitează la QR (performanță + mai puține pozitive false)
  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;

  // Activare cameră: în aplicații reale verificați mai întâi permisiunile (Android) și luați în considerare fluxul UI.
  if Assigned(FCamera) then
    FCamera.Active := True;
end;

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

  // Dezactivare ordonată
  if Assigned(FCamera) then
    FCamera.Active := False;

  // Resetați flag-ul decoder dacă Stop survine într-o fază nepotrivită
  TInterlocked.Exchange(FIsDecoding, 0);
end;

function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
  // Limitare: nu decodați fiecare cadru
  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);

  // același text în fereastra de debounce - ignorați
  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;

  // Un singur decodificare simultan (altfel blocaj în coadă pe dispozitive slabe)
  if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
    Exit;

  // Copiați sample-ul camerei în FBitmap. Blocare, deoarece același buffer bitmap nu trebuie folosit în paralel.
  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;

  // Decodare în fundal
  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
  // Tăiați ROI centrat: reduce sarcina de procesare și direcționează utilizatorul.
  // Atenție: pentru coduri QR foarte mici, ROI poate fi prea strâmt.
  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 UI: navigare, semnal sonor (beep), completare câmp etc.
  TThread.Queue(nil,
    procedure
    begin
      if FIsRunning and Assigned(FOnResult) then
        FOnResult(AText);
    end);
end;

end.

Ce rezolvă codul (și de ce este necesar)

Throttle (MinIntervalMs) reduce încărcarea CPU și disiparea de căldură. Fără o limitare, unele dispozitive încearcă să decodeze 30–60 cadre/s; în practică sunt suficiente 5–10/s, adesea mai puțin. Debounce (DebounceMs) împiedică ca un cod QR ținut stabil să fie declanșat de mai multe ori (de ex. dublă înregistrare într-un pas de proces).

Flag-ul Interlocked (FIsDecoding) asigură că rulează maximum un Decode-Task. Acesta este un truc arhitectural împotriva „blocajului de coadă”: dacă decodarea durează 200 ms, dar la fiecare 120 ms pornește un task, coada crește și rezultatele apar decalate în timp, ceea ce în exploatare se manifestă ca „scannerul reacționează greșit”.

Condiții și capcane

  • TBitmap und Threading: FMX-Bitmaps pot fi bazate pe GPU. Abordarea copiază frame-ul într-un Bitmap local și decodează în background. În funcție de versiunea/platforma Delphi poate fi totuși necesară prudență: dacă observați artefacte, forțați un CPU-Bitmap (de ex. prin Pixel-Read/Write) sau lucrați cu un ByteBuffer din SampleBuffer (mai apropiat de platformă, dar mai stabil).
  • Stop/Start bei Navigation: În aplicațiile mobile se oprește adesea la schimbarea Form-ului sau la evenimentul de pauză al aplicației. Important este ca Stop să poată fi apelat de mai multe ori fără a genera excepții (idempotent). În plus, callback-ul de rezultat ar trebui să verifice dacă scannerul mai rulează (face DoResultOnMainThread).
  • ROI zu eng: Un ROI centrat accelerează procesarea, dar poate eșua dacă utilizatorii țin codul în afara zonei sau codul este foarte mic. De aceea EnableRoi este configurabil și RoiScale este limitat.
  • Format-Lock auf QR: Limitarea la QR_CODE este de regulă corectă. Dacă aveți nevoie și de Code128/EAN, extindeți formatele – așteptați-vă însă la mai multe false positives și consum CPU mai mare.

Delphi FMX Kamera-Lifecycle: Berechtigungen, Hintergrund, Rotation

Cele mai frecvente bug-uri nu apar la decodare, ci în jurul camerei:

  • Android Permissions: Drepturile camerei trebuie obținute la runtime. Planificați scenariul în care un utilizator refuză sau alege „Nur diesmal”. Din punct de vedere tehnic înseamnă: păstrați separat starea UI („Scanner bereit?”) de starea camerei, altfel rămâneți în stări parțiale.
  • App geht in den Hintergrund: La OnApplicationEvent (de ex. EnteredBackground) ar trebui să apelați Stop. La revenire apelați conștient Start (și eventual o scurtă întârziere), pentru ca previzualizarea să fie stabilă.
  • Rotation/Mirroring: Pentru codurile QR rotația este adesea necritic, dar în unele pipeline-uri ale camerei bitmap-ul poate fi oglindit sau rotit. Dacă scanările funcționează „nur in einer Haltung”, acesta este un indiciu. În acest caz: înainte de scanare rotiți/oglindiți sau folosiți un decoder care utilizează metadatele de orientare.

Debugging im Betrieb: So finden Sie die echten Ursachen

Dacă scannerul „manchmal” nu citește, debugging-ul reproducibil este foarte valoros. Trei măsuri dovedite:

  1. Frame-Sampling loggen: Logați (doar în modul Debug/Support) tick-ul, dimensiunea imaginii, dimensiunea ROI, durata de decodare. Astfel vedeți imediat dacă Throttle/Debounce sau încărcarea CPU sunt problema.
  2. Testbilder sichern: Salvați la fiecare N secunde o imagine ROI (temporar). Cu acestea puteți analiza fără hardware de cameră dacă contrastul/neclaritatea sunt cauza problemei.
  3. Separarea sarcinii de lucru: Nu actualizați actualizările UI (Preview-Overlay, Status-Text) la frecvențe ridicate. Das „UI-Zittern“ kommt oft von zu vielen Queue-Events.

Varianten: 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/State-Machine: 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.

Discutați un proiect sau un demers de modernizare cu Net-Base.

Următorul pas

Când o temă devine un proiect real, arhitectura, infrastructura existentă și operarea trebuie analizate împreună de la început.

Nu oferim sprijin doar pentru întrebări punctuale, ci și atunci când fragmente de cod sursă, probleme legacy sau idei de portal trebuie transformate într-un proiect robust la nivel de companie.

  • Situația curentă, starea țintă și riscurile tehnice sunt evaluate împreună.
  • REST, accesul la date, portalurile și Rollout nu sunt amânate ca consecințe ulterioare.
  • Veți vedea din timp ce cale este viabilă din punct de vedere economic și operațional.

Partajează postarea

Distribuiți această postare direct

LinkedIn, X, XING, Facebook, WhatsApp și e-mail sunt disponibile imediat. Pentru Instagram pregătim linkul și textul scurt imediat.

E-mail

Instagram se deschide într-o filă nouă. Linkul și textul scurt se copiază în prealabil în clipboard.