Od teme v reviji do projektne prakse
Ustrezne strani storitev in tehnični opisi k prispevku
QR Code Scanner Delphi FMX v praksi
En QR Code Scanner Delphi FMX je v demonstraciji hitro sestavljen: prikaže se predogled kamere, vzame bitmapo, zažene se ZXing. V resnični poslovni programski opremi (npr. prevzem blaga, dodeljevanje naprav, ticketing, postopki dostopa) pa se pojavijo dodatne robne zahteve: aplikacija gre v ozadje, kamera izgubi fokus, uporabnik drži napravo poševno, format slike se spremeni – in nenadoma skenirate isto kodo dvakrat na sekundo ali se uporabniški vmesnik zatika, ker se dekodiranje izvaja v UI-niti.
Tipične težave niso toliko „ZXing kann nicht lesen“, temveč življenjski cikel in arhitektura: sproščanje virov kamere, ritmizacija okvirjev, varnost niti pri dostopu do TBitmap (GPU/CPU), in jasen Stop/Start, ki ostane čist tudi, ko uporabniki hitro navigirajo ali ko OS začasno odvzame kamero.
Architekturüberblick: Pipeline statt „OnSampleBufferReady macht alles“
V praksi se je obnesla majhna pipeline s jasnimi odgovornostmi:
- Adapter kamere: dostavlja okvirje (ali njihove kopije) v določenem formatu.
- Decoder: deluje v ozadni niti in rezultate vrača preko callbacka.
- Gate/Debounce: preprečuje dvojno skeniranje in regulira obremenitev (Throttle).
- UI-sloj: prikazuje predogled, opcijsko fokusno pravokotno območje (ROI, „Region of InteREST“) in reagira na rezultate.
Tukaj ‚ROI‘ pomeni izrezano iskalno okno (npr. osrednje 60 %), ki razbremeni decoder in zmanjša napačno pozitivne rezultate. Pomembno: ROI je orodje za zmogljivost in uporabniško izkušnjo, ne varnostni mehanizem.
Source-Schnipsel: Robuster QR Code Scanner (FMX + ZXing) mit Debounce und sauberem Stop
Naslednja koda je mišljena kot kompakten, a projektno uporaben gradnik. Uporablja ZXing (Delphi-Port) preko ZXing.ScanManager in se poveže na TCameraComponent.OnSampleBufferReady. Ključni so trije vidiki:
- Okvirji so throttled (ne dekodiramo vsakega vzorca).
- Dekodiranje poteka ne v UI-niti.
- Stop/Start je idempotent (večkrat klicljiv, brez kaosa z viri).
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>
/// Krmilnik QR-skeniranja za FMX (Android/iOS).
/// Skrbi za upravljanje frekvence okvirjev iz kamere, dekodiranje v ozadju in urejeno zaustavljanje/zagon.
/// </summary>
TQrScannerController = class
private
FCamera: TCameraComponent;
FScanManager: TScanManager;
FBitmap: TBitmap;
FLock: TObject;
FOnResult: TQrScanResultEvent;
// Gating/Throttle
FIsRunning: Boolean;
FIsDecoding: Integer; // 0/1 kot Interlocked-Flag
FLastDecodeTick: Int64;
FMinIntervalMs: Cardinal;
// Debounce proti ponavljajočim se enakim kodam
FLastText: string;
FLastTextTick: Int64;
FDebounceMs: Cardinal;
// ROI: delež slike, ki se skenira (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; // npr. 120
property DebounceMs: Cardinal read FDebounceMs write FDebounceMs; // npr. 1200
property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
property RoiScale: Single read FRoiScale write FRoiScale; // npr. 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 inicializirati in omejiti na QR (zmogljivost + manj lažno pozitivnih)
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;
// Vklop kamere: v dejanskih aplikacijah prej preverite dovoljenja (Android) in upoštevajte UI-potok.
if Assigned(FCamera) then
FCamera.Active := True;
end;
procedure TQrScannerController.Stop;
begin
if not FIsRunning then
Exit;
FIsRunning := False;
// Pravilno izklopiti
if Assigned(FCamera) then
FCamera.Active := False;
// Ponastavi zastavico dekoderja, če Stop pride v neugodnem trenutku
TInterlocked.Exchange(FIsDecoding, 0);
end;
function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
// Omejevanje: ne dekodiraj vsakega okvirja
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);
// enako besedilo znotraj Debounce-okna - prezri
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;
// Samo en dekod hkrati (sicer zastoj v vrsti na šibkih napravah)
if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
Exit;
// Kopiraj vzorec kamere v FBitmap. Zaklep, ker isti bitmap-buffer ne sme biti uporabljen vzporedno.
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;
// Dekodiranje v ozadju
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
// Izreži ROI na sredini: zmanjša obdelovalno obremenitev in usmeri uporabnika.
// Pozor: pri zelo majhnih QR-kodah je lahko ROI preozek.
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-nit: navigacija, zvočni signal, izpolnitev polja itd.
TThread.Queue(nil,
procedure
begin
if FIsRunning and Assigned(FOnResult) then
FOnResult(AText);
end);
end;
end.
Kaj koda rešuje (in zakaj je to potrebno)
Throttle (MinIntervalMs) zmanjša obremenitev CPU in razvoj toplote. Brez omejitve nekateri napravi poskušajo dekodirati 30–60 okvirjev/s; v praksi zadostuje 5–10/s, pogosto manj. Debounce (DebounceMs) prepreči, da bi se enakomerno držana QR-koda sprožila večkrat (npr. dvojno knjiženje v enem koraku procesa).
Interlocked-Flag (FIsDecoding) zagotavlja, da teče največ en Decode-Task. To je arhitekturni prijem proti „zastoju v čakalni vrsti“: če dekodiranje traja 200 ms, vendar se vsakih 120 ms zažene nov task, se čakalna vrsta poveča in rezultati prispejo z zamikom, kar v obratovanju deluje kot »skener se odziva napačno«.
Omejitve in pasti
- TBitmap in niti: FMX-Bitmaps so lahko podprte s GPU. Pristop kopira okvir v lokalno bitmapo in dekodira v ozadju. Glede na Delphi-verzijo/platformo je lahko vseeno potrebna previdnost: če opazite artefakte, prisilite CPU-bitmapo (npr. preko branja/pisanja pikslov) ali delajte z ByteBufferjem iz SampleBufferja (platformno bližje, a stabilneje).
- Stop/Start pri navigaciji: V mobilnih aplikacijah se pogosto ob menjavi forma ali ob dogodku pavze aplikacije ustavi zajem. Pomembno je, da je
Stopmogoče poklicati večkrat brez sproženja izjem (idempotentno). Rezultatni callback naj tudi preveri, ali je skener še vedno aktiven (to storiDoResultOnMainThread). - Preozko ROI: Središčni ROI pospeši dekodiranje, lahko pa zataji, če uporabnik drži kodo izven območja ali je koda zelo majhna. Zato je
EnableRoinastavljiv inRoiScaleomejen. - Zaklep formata na QR: Omejitev na
QR_CODEje v večini primerov pravilna. Če potrebujete tudi Code128/EAN, razširite nabor formatov — pričakujte pa več lažno pozitivnih zadetkov in večjo obremenitev CPU.
Delphi FMX življenjski cikel kamere: dovoljenja, ozadje, rotacija
Najpogostejše napake ne nastanejo pri dekodiranju, temveč okoli kamere:
- Android dovoljenja: Pravice za kamero je treba pridobiti med izvajanjem. Predvidite primer, ko uporabnik zavrne ali izbere »Samo tokrat«. Tehnično to pomeni: ločite UI-state („Skener pripravljen?“) od stanja kamere, sicer lahko obtičite v polovičnih stanjih.
- Aplikacija gre v ozadje: Ob
OnApplicationEvent(npr.EnteredBackground) pokličiteStop. Ob vračanju namerno pokličiteStart(in po potrebi kratko počakajte), da bo predogled stabilen. - Rotacija/zrcaljenje: Za QR-kode je rotacija pogosto nebistvena, vendar je pri nekaterih kamernih pipelinah bitmapa lahko zrcaljena ali zavrtena. Če skeniranje deluje »le v eni legi«, je to indikator. V tem primeru pred skeniranjem obrnite/zrcalite sliko ali uporabite dekoder, ki uporablja orientacijske metapodatke.
Razhroščevanje v obratovanju: tako najdete prave vzroke
Če skener „včasih“ ne bere, je reproducibilno razhroščevanje zelo dragoceno. Trije ukrepi, ki se izkažejo:
- Frame-sampling v log: Zabeležite (samo v načinu Debug/Support) tick, velikost slike, velikost ROI in trajanje dekodiranja. Tako takoj vidite, ali sta Throttle/Debounce ali obremenitev CPU vzrok problema.
- Shranite testne slike: Shrani vsakih N sekund sliko ROI (začasno). S tem lahko brez kamerne strojne opreme analizirate, ali sta kontrast ali zamegljenost vzrok težave.
- Ločite obremenitev: ne posodabljajte UI-posodobitev (preview-overlay, statusni tekst) z visoko frekvenco. „Tresenje UI“ pogosto izvira iz prevelikega števila
Queue-dogodkov.
Varianten: Wenn Sie mehr brauchen als „Scan und fertig“
Več rezultatov, vendar nadzorovano
Za serijske procese (npr. veliko nalepk zaporedoma) zmanjšajte DebounceMs in dodajte Whitelist/State-Machine: QR-koda naj bo sprejeta le, če jo pričakuje trenutni korak procesa. To ni UI-logika, temveč domenska logika – pripada svoji plasti, da sta skener in proces neodvisno testabilna.
Offline-validacija in varni uporabniški podatki
V poslovnih procesih QR-kode pogosto vsebujejo ID-je ali tokene. Ne zanašajte se na to, da „QR = pravilen“. Validirajte lokalno (format, kontrolna vsota, pričakovani prefiksi) in na strežniku (REST-API). Če uporabljate tokene: določite čas veljavnosti, zaščito pred ponovnim predvajanjem in previdno beleženje (brez tokenov v nešifriranem besedilu v support-logih).
Legacy-situacije: FMX-Scanner kot modul v mešanih kodebasah
Če imate razraščeno VCL-okolje, je FMX kot mobilni klient pogosto ločen razvojni tok. Ohranjajte skener kot Controller-klaso brez odvisnosti od Form (kot zgoraj), tako ga lahko integrirate v različne zaslone. To se obrestuje tudi pri modernizaciji: poslovna logika ostane testabilna, kamera je zgolj vhodni kanal. Ravno v legacy-situacijah se izplača jasna ločnica za beleženje, feature-flag-e in oddaljeno konfiguracijo.
Fazit: Solider FMX-QR-Scan ist ein Lifecycle-Problem – nicht nur ein ZXing-Aufruf
QR Code Scanner v Delphi FMX postane stabilen, če ga obravnavate kot majhno cevovod: kamera dovaja frames, ozadinski dekoder dela kontrolirano, in Debounce/Throttle preprečujeta dvojne ter zamujene dogodke. Zgornji izvleček izvorne kode naslavlja natanko tiste točke, kjer v resničnih mobilnih poslovnih procesih prihaja do težav: preveč decode-taskov, neurejen stop, blokade UI-niti in nepotrebna obremenitev.
Meje uporabe: Če potrebujete izjemno visoke hitrosti skeniranja (npr. industrijsko skeniranje na tekočem traku) ali stroge zahteve po obdelavi slik, je FMX-standardna kamera + bitmap-pipeline pogosto predraga. Tedaj se izplača pristop bližje platformi (Native Camera API, neposredni YUV-Buffer, SIMD/NEON) ali specializiran Scanner-SDK. Za večino procesno povezanih mobilnih aplikacij pa je prikazani pristop zadosten, če so lifecycle, pravice in threading čisto integrirani – in so postopki v ozadju jasno definirani.
Če morate QR-sken vključiti v obstoječo Delphi arhitekturo (vključno z robnimi primeri kot so navigacija, backgrounding, beleženje in validacija procesov), to z veseljem strukturirano razčlenimo:
V strokovnem okolju imata tudi Zxing Delphi in Fmx Tcameracomponent pomembno vlogo, kadar morajo integracije, tokovi podatkov in nadaljnji razvoj delovati usklajeno.
O projektu ali modernizacijskem načrtu se pogovorite z Net-Base.
Naslednji korak
Ko se tema spremeni v dejanski projekt, je treba arhitekturo, obstoječi sistem in obratovanje zgodaj obravnavati skupaj.
Ne podpiramo le pri posameznih vprašanjih, ampak tudi takrat, ko iz izrezkov izvorne kode, legacy-tem ali idej za portale nastane zanesljiv podjetniški projekt.
- Obstoječe stanje, ciljno stanje in tehnična tveganja se ocenjujejo skupaj.
- REST, dostop do podatkov, portali in uvedba niso prestavljeni kot poznejše posledice.
- Zgodaj prepoznate, katera pot je ekonomsko in obratovalno vzdržna.