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).
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
Stopvõib kutsuda välja korduvalt ilma erandeid tekitamata (idempotentne). Lisaks peaks tulemuse callback kontrollima, kas skänner ikka töötab (seda teebDoResultOnMainThread). - 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
EnableRoikonfigureeritav jaRoiScalepiiratud. - 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(ntEnteredBackground) puhul peaksite kutsumaStop. Tagasi tulles kutsuge teadlikultStart(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:
- 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.
- 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.
- 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.
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.