Net-Base Ajakiri

03.06.2026

QR-koodi skanner Delphi FMX-is: robustne, lõimeohutu kaameraskannimine ilma kasutajaliidese värisemiseta

Praktiline QR-koodi skanner Delphi FMX sõltub kaamera elutsükli, lõimimise ja korrektselt teostatud peatamise/käivitamise haldusest. Artikkel tutvustab robustset lahendust, mis kasutab ZXing-i, debounce-i, kaadrite piiramist, ROI-kärpimist ning annab silumise ja käituse üksikasjad Androidi ja iOS-i jaoks.

03.06.2026

Ajakirjateemast projektipraktikasse

Sobivad teenuse- ja tehnilised lehed postituse jaoks

QR-koodi skanner Delphi FMX praktikas

Demos saab QR-koodi skanneri Delphi FMX kiiRESTi kokku panna: kuvada kaamera eelvaade, võtta Bitmap, lasta ZXingil see dekodeerida. Tõsises ärirakenduses (nt kauba vastuvõtt, seadmete sidumine, piletisüsteemid, juurdepääsuprotsessid) tekivad aga täiendavad tingimused: rakendus läheb taustale, kaameral kaob fookus, kasutaja hoiab seadet kaldu, pildiformaat muutub – ja äkki loete sama koodi kaks korda sekundis või kasutajaliides jääb kinni, sest dekodeerimine jookseb UI-Thread’is.

Kõige tüüpilisemad probleemid ei ole niivõrd „ZXing ei suuda lugeda“, vaid elutsükkel ja arhitektuur: kaamera ressursside vabastamine, kaadrite sageduse juhtimine, lõimeohutus TBitmap (GPU/CPU) kasutamisel ning selge Stop/Start-mehhanism, mis jääb puhtaks ka siis, kui kasutaja kiiRESTi navigeerib või opsüsteem kaamera ajutiselt ära võtab.

Arhitektuuri ülevaade: Pipeline, nicht „OnSampleBufferReady macht alles“

Praktiliselt on hästi toiminud väike Pipeline selgete vastutuspiiridega:

  • Kaamera-adapter: tarnib kaadreid (või nende koopiaid) määratletud formaadis.
  • Dekooder: töötab taustalõimus ja tagastab tulemused callbacki kaudu.
  • Gate/Debounce: väldib topeltskanne ja reguleerib koormust (throttle).
  • UI-kiht: kuvab eelvaate, valikuliselt fookusruudu (ROI, „Region of InteREST“) ja reageerib tulemustele.

Sellega väldite, et UI, kaamera ja dekooder üksteist blokeerivad. „ROI“ tähendab siin kärbitud otsingavaadet (nt keskelt 60 %), mis vähendab dekooderi koormust ja vähendab valepositiivseid tulemusi. Oluline: ROI on jõudluse ja kasutatavuse tööriist, mitte turvamehhanism.

Koodinäide: vastupidav QR-koodi skanner (FMX + ZXing) koos debounce’i ja puhta peatamisega

Järgmine kood on mõeldud kompaktse, kuid projekti sobiva moodulina. See kasutab ZXing (Delphi-porti) läbi ZXing.ScanManager ja kasutab TCameraComponent.OnSampleBufferReady-signaali. Otsustavad on kolm punkti:

  • Kaadrite töötlemist throttled (mitte iga näidist dekodeeritakse).
  • Dekodeerimine ei toimu UI-Thread-is.
  • Stop/Start on idempotent (korratavalt kutsutav ilma ressursside kaosest).
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-skanneri kontroller FMX-ile (Android/iOS).
  /// Haldab kaamera kaadripäringut (frame-gating), taustdekodeerimist ja korrektselt peatamist/taaskäivitamist.
  /// </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 korduvate samade koodide vältimiseks
    FLastText: string;
    FLastTextTick: Int64;
    FDebounceMs: Cardinal;

    // ROI: pildi osa, mida skannitakse (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; // nt. 120
    property DebounceMs: Cardinal read FDebounceMs write FDebounceMs;         // nt. 1200
    property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
    property RoiScale: Single read FRoiScale write FRoiScale;                 // nt. 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 initsialiseerimine ja piiramine QR-koodidele (parem jõudlus ja vähem valepositiivseid)
  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;

  // Kaamera aktiveerimine: tootmiskeskkonnas eelnevalt kontrolli õigusi (Permissions) (Android) ja arvesta UI-voogu.
  if Assigned(FCamera) then
    FCamera.Active := True;
end;

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

  // Aktiivne korrektne väljalülitamine
  if Assigned(FCamera) then
    FCamera.Active := False;

  // Dekooderi lipu lähtestamine, kui Stop satub ebasobivasse faasi
  TInterlocked.Exchange(FIsDecoding, 0);
end;

function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
  // Throttle: mitte iga kaadrit dekodeerida
  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);

  // sama tekst Debounce-ajalõigu jooksul - ignoreeri
  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;

  // Ainult üks dekodeerimine korraga (muul juhul järjekorra ummik nõrkadel seadmetel)
  if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
    Exit;

  // Kaamera näidise kopeerimine FBitmap'i. Lukustus, sest sama bitmap-puhvrit ei tohiks paralleelselt kasutada.
  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;

  // Taustdekodeerimine
  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 keskele lõikamine: vähendab arvutuskoormust ja suunab kasutajat.
  // Tähelepanu: väga väikeste QR-koodide puhul võib ROI liiga kitsaks jääda.
  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-lõim: navigeerimine, piiks, välja täitmine jms.
  TThread.Queue(nil,
    procedure
    begin
      if FIsRunning and Assigned(FOnResult) then
        FOnResult(AText);
    end);
end;

end.

Millist probleemi kood lahendab (ja miks see vajalik on)

Throttle (MinIntervalMs) vähendab CPU-koormust ja soojenemist. Ilma piiranguta proovivad mõned seadmed dekodeerida 30–60 kaadrit/s; praktikas piisab 5–10/s, tihti veel vähem. Debounce (DebounceMs) takistab, et stabiilselt hoitud QR-kood aktiveeruks mitu korda järjest (nt topeltsalvestus protsessis).

Interlocked-Flag (FIsDecoding) tagab, et korraga käib maksimaalselt üks dekodeerimistöö. See on arhitektuuriline võte „järjekorra ummistuse“ vastu: kui dekodeerimine võtab 200 ms, aga töid käivitatakse iga 120 ms, siis järjekord kasvab ja tulemused jõuavad hilinemisega, mis tööelus avaldub nagu „skänner reageerib valesti“.

Piirtingimused ja lõksud

  • TBitmap ja lõimed: FMX-bitmap’id võivad olla GPU-toega. Lähenemine kopeerib kaadri lokaalsesse bitmap’i ja dekodeerib taustal. Sõltuvalt Delphi-versioonist/platvormist võib siiski vaja minna ettevaatust: kui näete artefakte, sundige CPU-bitmap (nt pixelite lugemise/kirjutamise kaudu) või kasutage SampleBuffer’ist võetud ByteBuffer’it (platvormipõhise, kuid stabiilsema lahendusena).
  • Stop/Start navigeerimisel: Mobiilirakendustes peatatakse sageli vormi vahetusel või rakenduse pausisündmuse puhul. Oluline on, et Stop võib kutsuda välja korduvalt ilma erandeid tekitamata (idempotentne). Lisaks peaks tulemuse callback kontrollima, kas skänner ikka töötab (seda teeb DoResultOnMainThread).
  • Liiga kitsas ROI: Keskendatud ROI kiirendab skannimist, kuid võib ebaõnnestuda, kui kasutaja hoiab koodi väljaspool keskpunkti või kood on väga väike. Seetõttu on EnableRoi konfigureeritav ja RoiScale piiratud.
  • Formaatlukustus QR-ile: Piiramine QR_CODE‚ile on enamasti õige. Kui vajate ka Code128/EAN, lisage formaate – arvutage aga rohkem vale-positiivseid ja suuremat CPU-koormust.

Delphi FMX kaamera elutsükkel: õigused, taust, rotatsioon

Levinumad vead ei teki dekodeerimisel, vaid kaamera ümber toimuvates olukordades:

  • Androidi õigused: Kaameraõigused tuleb küsida käitamisajal. Planeerige juhud, kus kasutaja keeldub või valib „Ainult seekord“. Tehniliselt tähendab see UI-seisu („Skänner valmis?“) hoidmist eraldi kaamera-seisundist, vastasel juhul võite jääda poolikusse olekusse.
  • Rakendus läheb taustale: OnApplicationEvent (nt EnteredBackground) puhul peaksite kutsuma Stop. Tagasi tulles kutsuge teadlikult Start (ja vajadusel lisage lühike viivitus), et eelvaade oleks stabiilne.
  • Rotatsioon/peegeldus: QR-koodide puhul ei ole rotatsioon sageli kriitiline, kuid mõnedes kaamerapipelines võib bitmap olla peegeldatud või pööratud. Kui skannimine töötab „ainult ühes asendis“, on see vihje. Sel juhul pöörake/peegeldage enne skannimist või kasutage dekooderit, mis kasutab orientatsioonimetadate teavet.

Tööaegne silumine: kuidas leida tegelikke põhjuseid

Kui skänner mõnikord ei loe, on reprodutseeritav silumine väga väärtuslik. Kolm kasulikku meetet:

  1. Frame-samplingu logimine: Logige (ainult Debug/Support-režiimis) tick, pildi suurus, ROI-suurus, dekodeerimisaeg. Nii näete kohe, kas probleemiks on Throttle/Debounce või CPU-koormus.
  2. Testpiltide salvestamine: Salvestage iga N sekundi järel üks ROI-pilt (ajutiselt). Sel moel saate ilma kaamerariistvarata analüüsida, kas probleemiks on kontrast või hägusus.
  3. Eraldage töökoormus: UI-uuendusi (eelvaate-ülekate, olekutekst) mitte kõrgel sagedusel uuendada. „UI-värisemine“ tuleneb sageli liiga paljudest Queue-sündmustest.

Valikud: Kui vajate enamat kui „skaneeri ja valmis“

Mitu tulemust, kuid kontrollitult

Hunnikprotsesside puhul (nt palju sildiseid järjest) vähendage DebounceMs ja lisage Whitelist/State-Machine: QR-kood tuleks aktsepteerida ainult siis, kui praegune protsessisamm seda ootab. See ei ole UI-loogika, vaid domeeniloogika — see kuulub eraldi kihile, et skanner ja protsess oleksid sõltumatult testitavad.

Ühenduseta valideerimine ja turvalised kasutusandmed

Ettevõtte protsessides sisaldavad QR-koodid sageli ID-sid või tokeneid. Ärge usaldage, et „QR = korrektne“. Valideerige lokaalselt (vorming, kontrollsumma, oodatud prefiksid) ja serveripoolselt (REST-API). Kui kasutate tokeneid: määrake aegumised, replay-kaitse ja logige ettevaatlikult (ärge salvestage tokeneid selges tekstis tugilogidesse).

Pärandolukorrad: FMX-skanner moodulina segatud koodibaasides

Kui teil on kasvanud VCL-maailm, on FMX mobiilikliendina sageli eraldi haru. Hoidke skanner kontrollerklassina ilma vormisõltuvusteta (nagu eespool), nii saate selle integreerida erinevatesse ekraanidesse. See tasub end ära ka moderniseerimisel: äriloogika jääb testitavaks, kaamera on ainult sisendkanal. Eriti pärandolukordades on mõistlik selge lõhe teha logimise, feature-flagide ja kaugkonfiguratsiooni jaoks.

Kokkuvõte: Tõhus FMX-QR-skanne on elutsükli probleem — mitte ainult ZXing-kutse

QR-koodi skanner Delphi FMX-is muutub stabiilseks, kui käsitlete seda nagu väikest torujuhet: kaamera annab kaadreid, taustal töötav dekooder töötab kontrollitud viisil ning Debounce/Throttle väldivad topelt- ja hilinenud sündmusi. Ülaltoodud koodilõik adresseerib täpselt need kohad, kus tõelistes mobiilsetes äriprotsessides probleemid tekivad: liiga palju Decode-töid, ebaõige stopp, UI-threadi blokeerimised ja tarbetu koormus.

Kasutuspiirid: Kui vajate äärmiselt kõrgeid skaneerimissagedusi (nt tööstuslik skaneerimine liinil) või on teil ranged nõuded pilditöötlusele, on FMX-i standardkaamera + bitmap-torustik sageli liiga kallis. Siis tasub platvormilähedane lähenemine (Native Camera API, YUV-Buffer otse, SIMD/NEON) või spetsialiseeritud Scanner-SDK. Enamiku protsessikesksete mobiilirakenduste jaoks on näidatud lähenemine siiski piisav, kui elutsükkel, õigused ja threadimine on puhtalt integreeritud — ning protsessid ise on selged.

Kui peate QR-skannimise olemasolevasse Delphi-arhitektuuri sobitama (ka kõrvaljuhtumid nagu navigeerimine, taustale viimine, logimine ja protsessi valideerimine), selgitame seda meelsasti struktureeritult:

Valdkondlikus kontekstis mängivad ka Zxing Delphi ja Fmx Tcameracomponent olulist rolli, kui integratsioonid, andmevood ja edasiarendus peavad puhtalt koos töötama.

Arutage projekti või moderniseerimishanket koos Net-Base.

Järgmine samm

Kui teema muutub reaalseks projektiks, tuleks arhitektuuri, olemasolevat süsteemi ja käitust varakult ühiselt vaadelda.

Me ei toeta ainult üksikute küsimuste lahendamist, vaid ka siis, kui lähtekoodilõikudest, pärandsüsteemidest või portaalikontseptsioonidest peab saama usaldusväärne ettevõtteprojekt.

  • Olemasolev olukord, sihtpilt ja tehnilised riskid hinnatakse üheskoos.
  • REST, andmete juurdepääs, portaalid ja juurutamine ei lükata hilisemaks.
  • Te näete varakult, milline tee on majanduslikult ja operatiivselt jätkusuutlik.

Jaga postitust

Jaga seda postitust otse

LinkedIn, X, XING, Facebook, WhatsApp ja e-post on kohe saadaval. Instagrami jaoks valmistame kohe lingi ja lühiteksti ette.

e-post

Instagram avatakse uues vahekaardis. Link ja lühitekst kopeeritakse eelnevalt lõikepuhvrisse.