Nuo žurnalo temos iki projekto įgyvendinimo
Tinkami puslapiai apie paslaugas ir techninę informaciją šiam įrašui
QR kodo skaitytuvas Delphi FMX praktikoje
Vienas QR kodo skaitytuvas Delphi FMX demonstracijoje greitai surenkamas: parodyti kameros peržiūrą, paimti Bitmap, paleisti ZXing. Tačiau realioje verslo programinėje įrangoje (pvz., prekių priėmimas, įrenginių priskyrimas, bilietų sistema, prieigos procesai) atsiranda ribiniai atvejai: programa pereina į foną, kamera praranda fokusą, vartotojas laiko įrenginį pakreiptą, keičiasi vaizdo formatas – ir staiga skenuojate tą patį kodą du kartus per sekundę arba UI stringa, nes dekodavimas vyksta UI gijoje.
Tipinės problemos nėra tiek „ZXing negali skaityti“, kiek lifecycle ir architektūra: kameros išteklių atlaisvinimas, kadrų taktavimas, gijų saugumas prieigos prie TBitmap (GPU/CPU) metu, ir aiškus Stop/Start, kuris lieka tvarkingas net kai vartotojai greitai naršo arba OS laikinai atima kamerą.
Architektūros apžvalga: pipeline vietoje „OnSampleBufferReady atlieka viską“
Praktiškai pasiteisino maža pipeline su aiškiomis atsakomybėmis:
- Kamera-Adapter: teikia kadrus (ar jų kopijas) apibrėžtu formatu.
- Decoder: veikia foninėje gijoje ir grąžina rezultatus per callback.
- Gate/Debounce: užkerta kelią dvigubiems skenavimams ir reguliuoja apkrovą (throttle).
- UI-Schicht: rodo peržiūrą, pasirinktinį fokusavimo stačiakampį (ROI, „Region of InteREST“) ir reaguoja į rezultatus.
Tai padeda išvengti, kad UI, kamera ir dekoderis blokuotų vienas kitą. Čia „ROI“ reiškia apkarpytą paieškos langą (pvz., centre 60 %), kuris sumažina dekoderio apkrovą ir sumažina klaidingai teigiamus rezultatus. Svarbu: ROI yra našumo ir naudojimo patogumo priemonė, o ne saugumo mechanizmas.
Kodo fragmentas: stabilus QR kodo skaitytuvas (FMX + ZXing) su debounce ir tvarkingu Stop
Tolimesnis kodas skirtas kaip kompaktiškas, bet projektiškai tinkamas modulis. Jis naudoja ZXing (Delphi-portą) per ZXing.ScanManager ir prisijungia prie TCameraComponent.OnSampleBufferReady. Esminiai yra trys punktai:
- Kadrų srautas yra throttled (ne dekoduoti kiekvieno mėginio).
- Dekodavimas nevyksta UI gijoje.
- Stop/Start yra idempotent (gali būti kviečiamas kelis kartus, nepaliekant išteklių chaoso).
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-Scanner-Controller für FMX (Android/iOS).
/// Kümmert sich um Kamera-Frame-Gating, Hintergrund-Decoding und sauberes Stop/Start.
/// </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 gegen wiederholte gleiche Codes
FLastText: string;
FLastTextTick: Int64;
FDebounceMs: Cardinal;
// ROI: Anteil des Bildes, der gescannt wird (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; // z.B. 120
property DebounceMs: Cardinal read FDebounceMs write FDebounceMs; // z.B. 1200
property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
property RoiScale: Single read FRoiScale write FRoiScale; // z.B. 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 initialisieren und auf QR beschränken (Performance + weniger False Positives)
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;
// Kamera aktivieren: In echten Apps vorher Permissions prüfen (Android) und UI-Flow berücksichtigen.
if Assigned(FCamera) then
FCamera.Active := True;
end;
procedure TQrScannerController.Stop;
begin
if not FIsRunning then
Exit;
FIsRunning := False;
// Aktiv sauber abschalten
if Assigned(FCamera) then
FCamera.Active := False;
// Decoder-Flag zurücksetzen, falls Stop in einer ungünstigen Phase kommt
TInterlocked.Exchange(FIsDecoding, 0);
end;
function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
// Throttle: nicht jedes Frame dekodieren
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);
// gleicher Text innerhalb Debounce-Fenster - ignorieren
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;
// Nur ein Decode gleichzeitig (sonst Queue-Stau bei schwachen Geräten)
if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
Exit;
// Kamera-Sample in FBitmap kopieren. Lock, weil derselbe Bitmap-Buffer nicht parallel benutzt werden soll.
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;
// Hintergrund-Decoding
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 mittig ausschneiden: reduziert Rechenlast und lenkt den Nutzer.
// Achtung: bei sehr kleinen QR-Codes kann ROI zu eng sein.
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-Thread: Navigation, Beep, Feld füllen etc.
TThread.Queue(nil,
procedure
begin
if FIsRunning and Assigned(FOnResult) then
FOnResult(AText);
end);
end;
end.
Ką išsprendžia kodas (ir kodėl tai būtina)
Throttle (MinIntervalMs) sumažina CPU apkrovą ir šilumos išsiskyrimą. Be ribos kai kurie įrenginiai bando dekoduoti 30–60 kadrų/s; praktiškai pakanka 5–10/s, dažnai mažiau. Debounce (DebounceMs) neleidžia stabiliai laikomam QR kodui sukelti kelių įvykių (pvz., dvigubas įrašymas į proceso žingsnį).
Interlocked-žymuo (FIsDecoding) užtikrina, kad vyksta ne daugiau nei vienas dekodavimo uždavinys. Tai architektūrinis sprendimas prieš „eilės užsikimšimą“: jei dekodavimas trunka 200 ms, bet uždavinys pradedamas kas 120 ms, eilė auga ir rezultatai atkeliauja su vėlavimu, o tai eksploatacijoje atrodo kaip „skaitytuvas reaguoja neteisingai“.
Apribojimai ir spąstai
- TBitmap ir siūlų naudojimas: FMX-Bitmaps gali būti GPU-backed. Sprendimas nukopijuoja kadrą į vietinį Bitmap ir dekoduoja fone. Priklausomai nuo Delphi versijos/platformos vis tiek gali prireikti atsargumo: jei matote artefaktus, priverstinai naudokite CPU-Bitmap (pvz., per pikselių skaitymą/rašymą) arba dirbkite su ByteBuffer iš SampleBuffer (artimiau platformai, bet stabilesnis).
- Stop/Start navigacijos metu: Mobiliose programėlėse dažnai sustabdoma keičiant Form arba App-Pause įvykyje. Svarbu, kad
Stopbūtų galima kviesti kelis kartus ir kad jis negeneruotų išimčių (idempotentiškas). Be to, rezultatų atgalinis kvietimas turi patikrinti, ar skaitytuvas vis dar veikia (tai atliekaDoResultOnMainThread). - Per siauras ROI: Centrinis ROI pagreitina veikimą, bet gali nepasisekti, jei vartotojas laiko kodą už ribų arba kodas labai mažas. Todėl
EnableRoiyra konfigūruojamas, oRoiScaleribojamas. - Formatų užrakinimas ant QR: Ribojimas iki
QR_CODEdažniausiai yra teisingas. Jei jums reikia taip pat Code128/EAN, pridėkite formatus – skaičiuokite su didesniu klaidingų aptikimų skaičiumi ir didesne CPU apkrova.
Delphi FMX kameros gyvavimo ciklas: leidimai, fonas, rotacija
Dažniausios klaidos kyla ne dekodavimo metu, o aplink kamerą:
- Android leidimai: Kameros teisės turi būti prašomos vykdymo metu. Apsvarstykite atvejį, kai vartotojas atsisako arba pasirenka „Tik šį kartą“. Techniniu požiūriu tai reiškia: UI būseną („Skaitytuvas pasiruošęs?“) laikykite atskirai nuo kameros būsenos, kitaip galite užstrigti pusiau baigtose būsenose.
- Programa pereina į foną: OnApplicationEvent (pvz.,
EnteredBackground) metu turėtumėte kviestiStop. Grįžtant sąmoningai kviestiStart(ir, jei reikia, trumpą vėlinimą), kad peržiūra būtų stabili. - Sukimas / atspindėjimas: QR kodams sukimas dažnai nėra kritinis, tačiau kai kuriose kameros grandinėse Bitmap gali būti atspindėtas arba pasuktas. Jei skanavimas veikia „tik tam tikroje padėtyje“, tai yra signalas. Tokiu atveju: prieš skanavimą pasukite/atspindėkite vaizdą arba naudokite dekoderį, kuris naudoja orientacijos metaduomenis.
Klaidų paieška veikimo metu: kaip rasti tikrąsias priežastis
Jei skaitytuvas „kartais“ neskaito, galimybė pakartotinai atkurti klaidą derinimui yra itin vertinga. Trys priemonės, kurios dažnai pasiteisina:
- Frame-Sampling loggin: Loguokite (tik Debug/Support režime) Tick, vaizdo dydį, ROI dydį, dekodavimo trukmę. Taip iškart matysite, ar problema susijusi su Throttle/Debounce ar CPU apkrova.
- Išsaugoti testinius vaizdus: Išsaugokite kas N sekundžių vieną ROI vaizdą (laikinai). Tai leis be kameros aparatūros analizuoti, ar problema yra kontrastas ar neryškumas.
Queue-įvykių.Variantai: Jei reikia daugiau nei „nuskaityti ir paruošta“
Keli rezultatai, bet valdomai
Partijos procesams (pvz., daug etikečių iš eilės) sumažinkite DebounceMs ir papildykite baltojo sąrašo / būsenių mašina: QR kodas turi būti priimamas tik tada, kai dabartinis proceso žingsnis jo laukia. Tai nėra UI logika, o domeno logika – ji turi būti atskiro sluoksnio dalis, kad skaitytuvas ir procesas būtų nepriklausomai testuojami.
Offline patikra ir saugūs naudingieji duomenys
Įmonių procesuose QR kodai dažnai talpina ID arba token’us. Nesikliaukite, kad „QR = teisingas“. Vykdykite lokalią validaciją (formatas, kontrolinė suma, laukiamų prefiksų tikrinimas) ir serverinę patikrą (REST-API). Jei naudojate token’us: numatykite galiojimo laikus, apsaugą nuo pakartotinio panaudojimo (replay protection) ir elkitės atsargiai su žurnalais (logging) — nenaudokite token’ų nešifruoto teksto palaikymo žurnaluose.
Paveldėtos situacijos: FMX skaitytuvas kaip modulis mišriose kodo bazėse
Jei turite brandžią VCL aplinką, FMX kaip mobilus klientas dažnai yra atskiras srautas. Laikykite skaitytuvą kaip Controller klasę be formų priklausomybių (kaip aukščiau), taip galėsite jį integruoti į skirtingus ekranus. Tai atsiperka ir modernizuojant: verslo logika lieka testuojama, o kamera yra tik įvesties kanalas. Ypač paveldėtose situacijose verta aiškus atskyrimas dėl žurnalavimo, feature-flag’ų ir nuotolinės konfigūracijos.
Išvada: Patikimas FMX QR nuskaitymas yra gyvavimo ciklo problema – ne vien ZXing kvietimas
QR kodo skaitytuvas in Delphi FMX bus stabilus, jei jį traktuosite kaip mažą duomenų srautą: kamera tiekia kadrus, foninis dekoderis dirba kontroliuojamai, o Debounce/Throttle užkerta kelią dvigubiems ir vėlyviems įvykiams. Aukščiau pateiktas kodo fragmentas adresuoja būtent tas vietas, kuriose tikri mobilių verslo procesų scenarijai linkę žlugti: per daug decode-užduočių, netinkamas sustabdymas, UI gijos blokavimai ir nereikalinga apkrova.
Pritaikymo ribos: Jei jums reikia itin didelių nuskaitymo spartų (pvz., pramoninis nuskaitymas ant konvejerio) arba griežtų vaizdo apdorojimo reikalavimų, FMX standartinė kamera + bitmap pipeline dažnai yra per daug resursus naudojanti. Tokiu atveju verta platformai artimas sprendimas (Native Camera API, tiesioginis YUV buferis, SIMD/NEON) arba specializuotas Scanner-SDK. Tačiau daugumai procesų artimų mobiliųjų taikomųjų programų parodytas požiūris yra pakankamas, jei Lifecycle, leidimai ir gijų valdymas yra aiškiai integruoti – ir procesai už to yra aiškūs.
Jei turite pritaikyti QR nuskaitymą į esamą Delphi architektūrą (įskaitant kraštinius atvejus, pvz., navigaciją, backgrounding, žurnalavimą ir proceso validaciją), mes tai mielai aptartume struktūruotai:
Profesiniame kontekste taip pat svarbų vaidmenį atlieka Zxing Delphi ir Fmx Tcameracomponent, kai integracijos, duomenų srautai ir tolesnė plėtra turi veikti sklandžiai kartu.
Kitas žingsnis
Wenn aus dem Thema ein reales Projekt wird, sollten Architektur, Bestand und Betrieb frueh zusammen betrachtet werden.
Wir unterstuetzen nicht nur bei Einzelfragen, sondern auch dann, wenn aus Source-Schnipseln, Legacy-Themen oder Portalideen ein belastbares Unternehmensprojekt werden soll.
- Esama padėtis, tikslinis vaizdas ir techninės rizikos vertinami kartu.
- REST, duomenų prieiga, portalai ir rollout nebus perkelti į vėlesnį etapą kaip vėlyvos pasekmės.
- Jūs anksti matote, kuris kelias yra ekonomiškai ir operaciniškai tvarus.