Net-Base Lehti

03.06.2026

QR-koodin skannaus Delphi FMX: kameraskannaus — luotettava, säikeiturvallinen ja ilman käyttöliittymän nykimistä

Käytännönmukainen QR-koodinlukija Delphi FMX on riippuvainen kameran elinkaaresta, säikeistyksestä ja siististä pysäytys-/käynnistyslogiikasta. Artikkeli esittelee vankan lähestymistavan ZXingillä, debounce-mekaniikalla, frame-throttlingilla, ROI-rajauksella sekä debug- ja käyttöön liittyvillä yksityiskohdilla Androidille ja iOS:lle.

03.06.2026

Lehden aiheesta projektikäytäntöön

Artikkeliin liittyvät palvelu- ja tekniikkasivut

QR-koodinlukija Delphi FMX käytännössä

Demoissa QR-koodinlukija Delphi FMX:ssä rakennetaan nopeasti: näytetään kameran esikatselu, otetaan Bitmap, ajetaan ZXing läpi. Todellisessa yritysohjelmistossa (esim. tavaran vastaanotto, laitteiden kohdistus, tikettijärjestelmät, kulunvalvontaprosessit) ilmenee kuitenkin reunaehtoja: sovellus siirtyy taustalle, kamera menettää fokuksen, käyttäjä pitää laitetta vinossa, kuvasuhde muuttuu – ja yhtäkkiä skannaatte samasta koodista kaksi kertaa sekunnissa tai käyttöliittymä tärisee, koska dekoodaus tapahtuu käyttöliittymäsäikeessä.

Tyypilliset ongelmat eivät niinkään ole „ZXing ei pysty lukemaan“, vaan elinkaari ja arkkitehtuuri: kameran resurssien vapautus, kehyksien rytmittäminen, säieturvallisuus TBitmap-käytössä (GPU/CPU) ja selkeä pysäytys/käynnistys, joka toimii puhtaasti myös silloin, kun käyttäjä navigoi nopeasti tai käyttöjärjestelmä ottaa kameran väliaikaisesti pois.

Arkkitehtuurin yleiskatsaus: putkisto sen sijaan, että „OnSampleBufferReady hoitaisi kaiken“

Käytännössä on osoittautunut toimivaksi pieni putkisto selkeillä vastuualueilla:

  • Kamera-adapteri: toimittaa kehyksiä (tai niiden kopioita) määritellyssä formaatissa.
  • Dekooderi: toimii taustasäikeessä ja palauttaa tulokset callbackin kautta.
  • Gate/Debounce: estää kaksoisskannaukset ja säätelee kuormitusta (throttle).
  • Käyttöliittymäkerros: näyttää esikatselun, valinnaisen tarkennuskehyksen (ROI, „Region of InteREST“) ja reagoi tuloksiin.

Tällä vältetään, että käyttöliittymä, kamera ja dekooderi estävät toisiaan. „ROI“ tarkoittaa tässä rajattua hakualuetta (esim. keskitetty 60 %), joka keventää dekooderin kuormitusta ja vähentää väärin positiivisia tuloksia. Tärkeää: ROI on suorituskyky- ja käytettävyystyökalu, ei turvallisuusmekanismi.

Source-Schnipsel: Robuster QR Code Scanner (FMX + ZXing) mit Debounce und sauberem Stop

Seuraava koodikatkelma on tarkoitettu kompaktiksi mutta projektiystävälliseksi komponentiksi. Se käyttää ZXing (Delphi-porttia) ZXing.ScanManager-rajapinnan kautta ja kytkeytyy TCameraComponent.OnSampleBufferReady-tapahtumaan. Kolme seikkaa ovat ratkaisevia:

  • Kehykset on throttled (ei dekoodata jokaista näytettä).
  • Dekoodaus ei suoritu käyttöliittymäsäikeessä.
  • Pysäytys/käynnistys on idempotentti (voi kutsua useaan kertaan ilman resurssien sekasortoa).
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>
  /// FMX:lle (Android/iOS) tarkoitettu QR-skannerin ohjain.
  /// Huolehtii kameran kehyksien rajaamisesta, taustalla tapahtuvasta dekoodauksesta ja siististä pysäytys/käynnistys-käsittelystä.
  /// </summary>
  TQrScannerController = class
  private
    FCamera: TCameraComponent;
    FScanManager: TScanManager;
    FBitmap: TBitmap;
    FLock: TObject;

    FOnResult: TQrScanResultEvent;

    // Kehysten rajoitus / kuristus
    FIsRunning: Boolean;
    FIsDecoding: Integer; // 0/1 als Interlocked-Flag
    FLastDecodeTick: Int64;
    FMinIntervalMs: Cardinal;

    // Debounce toistuvien samanlaisten koodien estämiseksi
    FLastText: string;
    FLastTextTick: Int64;
    FDebounceMs: Cardinal;

    // ROI: skannattavan kuvan osuus (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; // esim. 120
    property DebounceMs: Cardinal read FDebounceMs write FDebounceMs;         // esim. 1200
    property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
    property RoiScale: Single read FRoiScale write FRoiScale;                 // esim. 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;

  // Alusta ScanManager ja rajoita QR:ään (suorituskyky + vähemmän väärät positiiviset)
  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;

  // Aktivoi kamera: oikeissa sovelluksissa tarkista ensin oikeudet (Android) ja huomioi käyttöliittymän kulku.
  if Assigned(FCamera) then
    FCamera.Active := True;
end;

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

  // Poista toiminto siististi käytöstä
  if Assigned(FCamera) then
    FCamera.Active := False;

  // Palauta dekooderilippu, jos Stop tulee epäedullisessa vaiheessa
  TInterlocked.Exchange(FIsDecoding, 0);
end;

function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
  // Kuristus: älä dekoodaa jokaista kehystä
  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 teksti debounce-ikkunan sisällä – ohita
  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;

  // Vain yksi dekoodaus kerrallaan (muuten jonoutuminen heikoilla laitteilla)
  if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
    Exit;

  // Kopioi kameran näyte FBitmapiin. Lukitus, koska samaa bitmap-puskuria ei saa käyttää rinnakkain.
  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;

  // Taustalla tapahtuva dekoodaus
  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
  // Leikkaa ROI keskeltä: vähentää laskentakuormaa ja ohjaa käyttäjää.
  // Huom: hyvin pienten QR-koodien tapauksessa ROI voi olla liian kapea.
  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-lanka: navigointi, piippaus, kentän täyttö jne.
  TThread.Queue(nil,
    procedure
    begin
      if FIsRunning and Assigned(FOnResult) then
        FOnResult(AText);
    end);
end;

end.

Mitä koodi ratkaisee (ja miksi se on tarpeen)

Throttle (MinIntervalMs) vähentää CPU-kuormaa ja lämmöntuotantoa. Ilman rajoitusta jotkin laitteet yrittävät dekoodata 30–60 kuvaa/s; käytännössä 5–10/s riittää, usein vähemmän. Debounce (DebounceMs) estää, että vakaasti pidetty QR-koodi laukaisee toistuvasti (esim. kaksoisvaraus prosessivaiheessa).

Interlocked-Flag (FIsDecoding) varmistaa, että korkeintaan yksi dekoodaus-tehtävä on käynnissä. Tämä on arkkitehtoninen keino „jonon tukkeutumista“ vastaan: jos dekoodaus kestää 200 ms, mutta uusia tehtäviä käynnistetään 120 ms välein, jono kasvaa ja tulokset saapuvat viiveellä, mikä käytössä tuntuu siltä kuin „skanneri reagoi väärin“.

Reunaehdot ja sudenkuopat

  • TBitmap ja säikeet: FMX-Bitmapit voivat olla GPU-vetämät. Lähestymistapa kopioi kehyksen paikalliseen bitmaptiin ja dekoodaa taustalla. Riippuen Delphi-versiosta/ympäristöstä voi silti tarvita varovaisuutta: jos näet artefakteja, pakota CPU-bitmap (esim. Pixel-Read/Write) tai työskentele SampleBufferista saadun ByteBufferin kanssa (alustaläheisempi, mutta vakaampi).
  • Stop/Start navigoinnissa: Mobiilisovelluksissa pysäytetään usein lomaketta vaihdettaessa tai sovelluksen tauottaessa. On tärkeää, että Stop voidaan kutsua monta kertaa ilman poikkeuksia (idempotentti). Lisäksi tulos-callbackin tulisi tarkistaa, onko skanneri edelleen käynnissä (kuten DoResultOnMainThread tekee).
  • ROI liian tiukka: Keskitetty ROI nopeuttaa, mutta voi epäonnistua, jos käyttäjä pitää koodia ROI:n ulkopuolella tai koodi on hyvin pieni. Siksi EnableRoi on konfiguroitavissa ja RoiScale on rajoitettu.
  • Formaattilukitus QR:lle: Formaattirajaus QR_CODE-koodiin on useimmiten oikea. Jos tarvitset myös Code128/EAN, laajenna formaatteja – varaudu kuitenkin enemmän false positive -tuloksiin ja suurempaan CPU-kuormaan.

Delphi FMX kamera-lifecycle: käyttöoikeudet, tausta, rotaatio

Useimmat bugit eivät synny dekoodauksessa vaan kameran käsittelyssä:

  • Android-oikeudet: Kameraoikeudet on haettava ajossa. Varaudu tapaukseen, jossa käyttäjä kieltäytyy tai valitsee „Vain tällä kertaa“. Tekninen seuraus: pidä UI-tila („Skanneri valmis?“) erillään kameran tilasta, muuten alat helposti jäädä puolivalmiisiin tiloihin.
  • Sovellus siirtyy taustalle: OnApplicationEvent-käsittelijässä (esim. EnteredBackground) tulisi kutsua Stop. Palatessa kutsu tietoisesti Start (ja tarvittaessa pieni viive), jotta preview pysyy vakaana.
  • Rotaatiot/peilaus: QR-koodeille rotaatio usein ei ole kriittinen, mutta joissain kameraputkistoissa bitmap voi olla peilattu tai kierretty. Jos skannaukset toimivat „vain yhdessä asennossa“, se on merkki tästä. Tällöin: käännä/peilaa ennen skannausta tai käytä dekooderia, joka hyödyntää orientaatiometatietoja.

Vianmääritys käytössä: näin löydät todelliset syyt

Jos skanneri „joskus“ ei lue, toistettava vianmääritys on kullanarvoinen. Kolme toimenpidettä, jotka yleensä toimivat:

  1. Frame-Samplingin lokitus: Kirjaa lokiin (vain Debug/Support-tilassa) tick, kuvan koko, ROI-koko, dekoodauksen kesto. Näin näet välittömästi, ovatko ongelmat Throttle/Debounce-asetuksissa tai CPU-kuormassa.
  2. Testikuvien tallennus: Tallenna joka N sekunti yksi ROI-kuva (väliaikaisesti). Näin voit ilman kamera‑laitteistoa analysoida, aiheutuuko ongelma kontrastista tai epäterävyydestä.
  • Erottele työkuorma: Älä päivitä käyttöliittymäpäivityksiä (Preview-Overlay, Status-Text) liian usein. UI-värinä johtuu usein liian monista Queue-tapahtumista.
  • Vaihtoehdot: Wenn Sie mehr brauchen als „Scan und fertig“

    Useita tuloksia, mutta hallitusti

    Eräprosesseissa (esim. monet tarrat peräkkäin) pienennä DebounceMs-arvoa ja lisää eine Whitelist/State-Machine: QR-koodi hyväksytään vain, jos nykyinen prosessivaihe sitä odottaa. Tämä ei ole käyttöliittymälogiikkaa, vaan toimialalogiikkaa – sen tulee olla omassa kerroksessaan, jotta skanneri ja prosessi säilyvät riippumattomasti testattavina.

    Offline-validointi ja turvalliset hyötytiedot

    Yritysprosesseissa QR-koodit sisältävät usein ID-tunnisteita tai tokeneita. Älä luota siihen, että „QR = korrekt“. Vahvista paikallisesti (muoto, tarkistesumma, odotetut etuliitteet) ja palvelinpuolella (REST-API). Jos käytätte tokeneita: huomioikaa vanhenemisajat, replay-suoja ja kirjaus varoen (ei tokeneita selkokielellä tukilokeihin).

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

    Jos teillä on kasvanut VCL-ympäristö, FMX mobiiliklienttinä on usein oma erillinen haaransa. Pidä skanneri als Controller-Klasse ohne Form-Abhängigkeiten (kuten yllä), niin voit integroida sen eri näkymiin. Tämä kannattaa myös modernisoinnissa: liiketoimintalogiikka säilyy testattavana, kamera on vain syötekanava. Erityisesti legacy-tilanteissa selkeä rajapinta kirjausta, feature-lippuja ja etäkonfiguraatiota varten on hyödyllinen.

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

    QR-koodin skanneri in Delphi FMX vakautuu, jos käsittelette sitä pienenä putkena: kamera tuottaa kehyksiä, taustalla toimiva dekooderi työskentelee kontrolloidusti, ja Debounce/Throttle estävät kaksois- ja myöhästyneet tapahtumat. Yllä oleva lähdekoodikatkelma käsittelee juuri niitä kohtia, jotka oikeissa mobiileissa liiketoimintaprosesseissa aiheuttavat ongelmia: liian monta dekoodaus-tehtävää, epätäydellinen pysäytys, UI-threadin tukokset ja tarpeeton kuorma.

    Käyttörajat: Jos tarvitsette erittäin korkeita skannausnopeuksia (esim. teollinen skannaus liukuhihnalla) tai tiukkoja vaatimuksia kuvankäsittelylle, on FMX-Standardkamera + Bitmap-Pipeline usein liian raskas. Silloin kannattaa platformiläheinen lähestymistapa (Native Camera API, YUV-Buffer direkt, SIMD/NEON) tai erikoistunut Scanner-SDK. Useimpiin prosessiläheisiin mobiilisovelluksiin esitetty lähestymistapa kuitenkin riittää, edellyttäen että Lifecycle, Rechte und Threading on siististi integroidut – ja taustaprosessit ovat yksiselitteiset.

    Jos teidän täytyy sovittaa QR-skannaus olemassa olevaan Delphi-arkkitehtuuriin (mukaan lukien reunatapaukset kuten navigointi, Backgrounding, Logging und Prozessvalidierung), selvitämme sen mielellämme strukturoituna:

    Asiantuntijaympäristössä myös Zxing Delphi und Fmx Tcameracomponentilla on keskeinen rooli, kun Integrationen, Datenflüsse und Weiterentwicklung sauber zusammenspielen müssen.

    Keskustele projektista tai modernisointihankkeesta Net-Base kanssa.

    Seuraava vaihe

    Kun aiheesta tulee todellinen projekti, arkkitehtuuri, nykyinen järjestelmäkanta ja käyttö tulisi varhaisessa vaiheessa tarkastella yhdessä.

    Emme tue pelkästään yksittäiskysymyksissä, vaan myös silloin, kun lähdekoodipalasista, legacy-aiheista tai portaali-ideoista halutaan muodostaa luotettava yrityshanke.

    • Nykytila, tavoitetila ja tekniset riskit arvioidaan yhdessä.
    • REST, datan käyttö, portaalit ja käyttöönotto eivät jätetä myöhempien seurausten varaan.
    • Näette ajoissa, mikä ratkaisu on taloudellisesti ja toiminnallisesti kestävä.

    Jaa artikkeli

    Jaa tämä viesti suoraan

    LinkedIn, X, XING, Facebook, WhatsApp ja sähköposti ovat heti käytettävissä. Instagramia varten valmistelemme linkin ja lyhyen tekstin.

    Sähköposti

    Instagram avautuu uuteen välilehteen. Linkki ja lyhyt teksti kopioidaan ensin leikepöydälle.