Net-Base Revistë

03.06.2026

Skanner i kodit QR në Delphi FMX: skanim me kamerë i qëndrueshëm, i sigurt për shumëfije dhe pa dridhje në UI

Një skaner QR praktik Delphi FMX varet nga lifecycle-i i kamerës, menaxhimi i thread-eve dhe një ndalim/nisje i pastër. Artikulli paraqet një qasje të fortë me ZXing, Debounce, Frame-Throttling, prerjen e ROI-së si dhe detaje për debug dhe operim për Android dhe iOS.

03.06.2026

Nga tema e revistës në praktikën e projektit

Faqe shërbimi dhe teknike të përshtatshme për artikullin

Skener QR Code Delphi FMX në praktikë

Një QR Code Scanner Delphi FMX në një demo ndërtohet shpejt: shfaqet pamja paraprake e kamerës, nxirret Bitmap, dhe ZXing kalon mbi të. Në softuerin real të biznesit (p.sh. pranimi i mallrave, caktimi i pajisjeve, ticketing, proceset e hyrjes) shfaqen kushte të jashtme: aplikacioni shkon në sfond, kamera humbet fokusin, përdoruesi mban pajisjen në kënd, formati i imazhit ndryshon – dhe papritmas skanoni të njëjtin kod dy herë në sekondë ose UI lëkundet, sepse dekodimi po kryhet në UI-Thread.

Problemet tipike nuk janë kaq shumë „ZXing nuk mund të lexojë”, por më shumë Lifecycle dhe arkitektura: lirimi i burimeve të kamerës, taktimi i frame-ve, siguria e thread-it kur aksesohet TBitmap (GPU/CPU) dhe një Stop/Start i qartë që funksionon pastër edhe kur përdoruesit navigojnë shpejt ose sistemi operativ i heq kamerës përkohësisht.

Përmbledhje e arkitekturës: Pipeline në vend të „OnSampleBufferReady macht alles”

Në praktikë është provuar të jetë efektive një pipeline e vogël me përgjegjësi të qarta:

  • Adapter i kamerës: furnizon Frames (ose kopje të tyre) në një format të përcaktuar.
  • Decoder: punon në thread-in e sfondit dhe kthen rezultatet përmes një callback.
  • Gate/Debounce: parandalon skanime të dyfishta dhe rregullon ngarkesën (throttle).
  • Shtresa UI: tregon pamjen paraprake, opsional kutinë e fokusit (ROI, „Region of InteREST”) dhe reagon ndaj rezultateve.

Me këtë evitoni që UI, kamera dhe decoder të bllokojnë njëri-tjetrin. „ROI” nënkupton këtu një dritare kërkimi të prerë (p.sh. në mes 60%), e cila lehtëson decoder-in dhe zvogëlon rezultatet false-positive. E rëndësishme: ROI është një mjet për performancë dhe përdorshmëri, jo një mekanizëm sigurie.

Shembull kodi: Skener QR Code robust (FMX + ZXing) me debounce dhe ndalim të pastër

Kodi i mëposhtëm është menduar si një bllok i kompakët, por i përshtatshëm për projekt. Ai përdor ZXing (Delphi-Port) përmes ZXing.ScanManager dhe lidhet me TCameraComponent.OnSampleBufferReady. Vendimtare janë tre pika:

  • Frames bëhen throttled (të mos dekodohen çdo sample).
  • Dekodimi nuk kryhet në UI-Thread.
  • Stop/Start është idempotent (mund të thirret shumëherë, pa shkaktuar kaos me burimet).
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>
  /// Kontrollues i skanerit QR për FMX (Android/iOS).
  /// Përkujdeset për kontrollin e frame-ve të kamerës, dekodimin në prapavijë dhe ndalimin/nisjen e pastër.
  /// </summary>
  TQrScannerController = class
  private
    FCamera: TCameraComponent;
    FScanManager: TScanManager;
    FBitmap: TBitmap;
    FLock: TObject;

    FOnResult: TQrScanResultEvent;

    // Rregullim i ritmit (throttle)
    FIsRunning: Boolean;
    FIsDecoding: Integer; // 0/1 si Interlocked-Flag
    FLastDecodeTick: Int64;
    FMinIntervalMs: Cardinal;

    // Debounce për kodet e njëjta të përsëritura
    FLastText: string;
    FLastTextTick: Int64;
    FDebounceMs: Cardinal;

    // ROI: përqindja e imazhit që skanohet (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.sh. 120
    property DebounceMs: Cardinal read FDebounceMs write FDebounceMs;         // p.sh. 1200
    property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
    property RoiScale: Single read FRoiScale write FRoiScale;                 // p.sh. 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;

  // Inicializoni ScanManager dhe kufizoni në QR (performancë + më pak pozitive të rreme)
  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;

  // Aktivizoni kamerën: në aplikacione reale verifikoni më parë lejet (Android) dhe merrni parasysh rrjedhën e ndërfaqes.
  if Assigned(FCamera) then
    FCamera.Active := True;
end;

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

  // Çaktivizim i pastër
  if Assigned(FCamera) then
    FCamera.Active := False;

  // Rivendos flag-un e dekoderit, nëse Stop vjen në një fazë të pafavorshme
  TInterlocked.Exchange(FIsDecoding, 0);
end;

function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
  // Rregullim i ritmit: mos dekodoni çdo 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);

  // i njëjti tekst brenda dritares së debounce - injoro
  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;

  // Vetëm një dekodim në të njëjtën kohë (përndryshe ngecje e kuyrës në pajisje të dobëta)
  if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
    Exit;

  // Kopjoni sample-in e kamerës në FBitmap. Bllokim, sepse i njëjti buffer i bitmap nuk duhet përdorur paralelisht.
  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;

  // Dekodim në prapavijë
  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
  // Nxjerrja e ROI në mes: redukton ngarkesën e përpunimit dhe orienton përdoruesin.
  // Kujdes: për kode QR shumë të vegjël ROI mund të jetë tepër i ngushtë.
  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: navigim, tingull sinjalizimi, mbushje fushe etj.
  TThread.Queue(nil,
    procedure
    begin
      if FIsRunning and Assigned(FOnResult) then
        FOnResult(AText);
    end);
end;

end.

Çfarë zgjidh ky kod (dhe pse është i nevojshëm)

Throttle (MinIntervalMs) ul ngarkesën e CPU-së dhe ngrohjen. Pa kufizim, disa pajisje përpiqen të dekodojnë 30–60 frame/s; në praktikë mjaftojnë 5–10/s, shpesh edhe më pak. Debounce (DebounceMs) parandalon që një kod QR i mbajtur i qetë të shkaktojë ngjarje më shumë se një herë (p.sh. regjistrim i dyfishtë në një hap procesi).

Interlocked-Flag (FIsDecoding) siguron që maksimalisht një Decode-Task të jetë duke u ekzekutuar. Kjo është një hile arkitekturore kundër „bllokimit të radhës“: nëse dekodimi zgjat 200 ms, por çdo 120 ms fillohet një task, rritet radhë pritjeje dhe rezultatet vijnë të vonuara, çka në operim duket si „skaneri reagon gabim“.

Kushtet kufizuese dhe kurthet

  • TBitmap dhe Threading: FMX-Bitmaps mund të jenë të mbështetura nga GPU. Qasja kopjon frame-in në një bitmap lokale dhe dekodon në sfond. Në varësi të versionit/platformës së Delphi mund të kërkohet kujdes: nëse shihni artefakte, detyroni një CPU-Bitmap (p.sh. përmes lexim/shkrim të pixelëve) ose punoni me një ByteBuffer nga SampleBuffer (më afër platformës, por më i qëndrueshëm).
  • Stop/Start gjatë navigimit: Në aplikacionet mobile shpesh ndalohet kur ndryshohet forma ose në event-in e pauzës së app. E rëndësishme është që Stop të mund të thirret disa herë pa shkaktuar përjashtime (idempotent). Gjithashtu callback-u i rezultatit duhet të verifikojë nëse skaneri ende po funksionon (kryen DoResultOnMainThread).
  • ROI shumë i ngushtë: Një ROI i centruar përshpejton, por mund të dështojë nëse përdoruesi mban kodin jashtë zonës ose kodi është shumë i vogël. Prandaj EnableRoi është i konfigurueshëm dhe RoiScale i kufizuar.
  • Fiksimi i formatit në QR: Kufizimi tek QR_CODE zakonisht është i drejtë. Nëse keni nevojë edhe për Code128/EAN, zgjeroni formatet – por pritni më shumë false positives dhe më shumë ngarkesë CPU.

Delphi FMX cikli i jetës së kamerës: Lejet, sfondi, rotacioni

Gabimet më të zakonshme nuk vijnë nga dekodimi, por nga menaxhimi i kamerës:

  • Android Permissions: Të drejtat e kamerës duhen kërkuar në kohërzbatuese. Planifikoni rastin kur një përdorues refuzon ose zgjedh „Vetëm këtë herë“. Teknikisht do të thotë: mbani UI-State („Scanner gati?“) të ndarë nga Kamera-State, përndryshe do të ngecni në shtete të papërfunduara.
  • App shkon në sfond:OnApplicationEvent (p.sh. EnteredBackground) duhet të thërrisni Stop. Kur ktheheni, thërrisni me qëllim Start (dhe me një vonesë të shkurtër nëse duhet), në mënyrë që preview të jetë i qëndrueshëm.
  • Rotacion/Pasqyrim: Për kodet QR rotacioni shpesh nuk është kritik, por në disa pipeline të kamerës bitmap-i mund të jetë i pasqyrueshëm ose i rrotulluar. Nëse skanimet funksionojnë „vetëm në një pozicion“, kjo është një tregues. Në atë rast: rrotulloni/pasqyroni para skanimit ose përdorni një decoder që shfrytëzon metadata të orientimit.

Debugging në operim: Si të gjeni shkaktarët e vërtetë

Nëse skaneri „ndonjëherë“ nuk lexon, debugimi i riprodhueshëm vlen shumë. Tre masa që japin rezultat:

  1. Regjistroni frame-sampling: Logoni (vetëm në modalitetin Debug/Support) tick, madhësinë e imazhit, madhësinë e ROI, kohën e dekodimit. Kështu shihni menjëherë nëse problemi është Throttle/Debounce ose ngarkesa e CPU-së.
  2. Ruani imazhe testuese: Ruani çdo N sekonda një imazh ROI (përkohësisht). Me to mund të analizoni pa harduer kamere nëse kontrasti/paqartësia (blur) janë shkaku.
  3. Ndani ngarkesën e punës: Mos përditësoni përditësimet e UI-së (Preview-Overlay, Status-Text) me frekuencë të lartë. Dridhja e ndërfaqes vjen shpesh nga shumë Queue-ngjarje.

Varianta: Nëse ju duhet më shumë se „Skano dhe mbaro“

Rezultate të shumta, por të kontrolluara

Për procese grumbulli (p.sh. shumë etiketa njëra pas tjetrës) ulni DebounceMs dhe shtoni një Whitelist/State-Machine: Një QR-Code duhet të pranohet vetëm kur hapi aktual i procesit e pret atë. Kjo nuk është logjikë e UI-së, por logjikë domene – duhet të vendoset në një shtresë të veçantë, në mënyrë që skaneri dhe procesi të mbeten të testueshëm në mënyrë të pavarur.

Validimi offline dhe të dhëna të përdorshme të sigurta

Në proceset e kompanive QR-Code shpesh përmbajnë ID ose token. Mos u mbështetni në përfundimin që „QR = korrekt“. Validoni lokal (formatin, kontroll-shifrën, prefikset e pritura) dhe nga ana e serverit (REST-API). Nëse përdorni token: përcaktoni kohëzgjatjen e skadencës, mbrojtjen ndaj replay dhe bëni logging me kujdes (mos i evidentoni token-et në tekst të qartë në log-et e suportit).

Situata legacy: FMX-Scanner si modul në baza kodesh të përziera

Nëse keni një botë VCL të zhvilluar, FMX si klient mobil shpesh është një rrjedhë e ndarë. Mbani skanerin si Klasë kontrolluese pa varësi nga forma (si më lart), kështu mund ta integroni në ekrane të ndryshme. Kjo paguhet edhe gjatë modernizimit: logjika e biznesit mbetet e testueshme, kamera është vetëm një kanal inputi. Veçanërisht në situata legacy vlen një ndarje e qartë për logging, Feature-Flags dhe konfigurim në distancë.

Përfundim: Një FMX-QR-skanim i qëndrueshëm është një problem i ciklit të jetës – jo vetëm një thirrje ZXing

Një QR Code Scanner në Delphi FMX bëhet i qëndrueshëm kur e trajtoni si një pipeline të vogël: kamera jep frame, një dekoder në sfond punon i kontrolluar, dhe Debounce/Throttle parandalojnë ngjarjet e dyfishta dhe të vona. Fragmeti i burimit më sipër adreson saktësisht vendet që shkojnë në krizë në procese reale mobile të biznesit: shumë Decode-Tasks, ndalime të pasakta, bllokime të thread-it të UI-së dhe ngarkesë e panevojshme.

Kufizimet e përdorimit: Nëse keni nevojë për shkalla skanimi ekstremisht të larta (p.sh. skanime industriale në rrjedhën e prodhimit) ose kërkesa të ashpra për përpunimin e imazheve, FMX-kamera standarde + pipeline me Bitmap shpesh del shumë e shtrenjtë. Atëherë ia vlen një qasje afër platformës (Native Camera API, YUV-Buffer direkt, SIMD/NEON) ose një Scanner-SDK i specializuar. Për shumicën e aplikacioneve mobile afër procesit qasja e treguar mjafton, për sa kohë Lifecycle, të drejtat dhe Threading janë integruar qartë – dhe proceset pas tyre janë të qarta.

Nëse duhet të përshtatni një QR-Scan në një arkitekturë ekzistuese Delphi (përfshirë raste kufitare si Navigation, Backgrounding, Logging dhe validimi i procesit), ne e sqarojmë këtë me qejf në mënyrë të strukturuar:

Në kontekstin profesional Zxing Delphi dhe Fmx Tcameracomponent luajnë gjithashtu një rol të rëndësishëm, kur integrimet, rrjedhat e të dhënave dhe zhvillimi i mëtejshëm duhet të funksionojnë së bashku në mënyrë të pastër.

Diskutoni projektin ose iniciativën e modernizimit me Net-Base.

Hapi tjetër

Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb frueh zusammen betrachtet werden.

Wir unterstuetzen nicht nur bei Einzelfragen, sondern auch dann, wenn aus Source-Schnipseln, Legacy-Themen oder Portalideen ein belastbares Unternehmensprojekt werden soll.

  • Gjendja ekzistuese, imazhi i synuar dhe rreziqet teknike vlerësohen së bashku.
  • REST, akses në të dhëna, portalet dhe Rollout nuk shtyhen si pasoja të mëvonshme.
  • Ju e shihni herët se cila rrugë është e qëndrueshme ekonomikisht dhe operativisht.

Ndaje postimin

Shpërndaj këtë postim drejtpërdrejt

LinkedIn, X, XING, Facebook, WhatsApp dhe E‑Mail janë menjëherë të disponueshme. Për Instagram po përgatitim menjëherë lidhjen dhe tekstin e shkurtër.

Postë elektronike

Instagram hapet në një skedë të re. Linku dhe teksti i shkurtër kopjohen më parë në memorjen e kopjimit.