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).
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ä
Stopvoidaan kutsua monta kertaa ilman poikkeuksia (idempotentti). Lisäksi tulos-callbackin tulisi tarkistaa, onko skanneri edelleen käynnissä (kutenDoResultOnMainThreadtekee). - 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
EnableRoion konfiguroitavissa jaRoiScaleon 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 kutsuaStop. Palatessa kutsu tietoisestiStart(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:
- 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.
- 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ä.
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ä.