No žurnāla tēmas līdz projektu praksei
Atbilstošas pakalpojumu un tehniskās lapas rakstam
QR koda skeneris Delphi FMX praksē
Viens QR koda skeneris Delphi FMX demonstrācijā ir ātri salikts: parādīt kameras priekšskatījumu, paņemt Bitmap, palaist ZXing pār to. Tomēr reālā biznesa programmatūrā (piem., ienākošā prece, ierīču piešķiršana, biļešu sistēma, piekļuves procesi) parādās papildu ierobežojumi: lietotne pāriet fonā, kamera zaudē fokusu, lietotājs tur ierīci slīpi, attēla formāts mainās — un pēkšņi jūs skenējat to pašu kodu divas reizes sekundē vai UI raustās, jo dekodēšana notiek UI pavedienā.
Tipiskās problēmas nav tik daudz „ZXing kann nicht lesen“, cik lifecycle un arhitektūra: kameras resursu atbrīvošana, kadru taktēšana, pavediena drošība piekļūstot TBitmap (GPU/CPU), un skaidrs Stop/Start, kas darbojas pareizi arī tad, ja lietotāji ātri navigē vai OS īslaicīgi atņem kameru.
Arhitektūras pārskats: Pipeline statt „OnSampleBufferReady macht alles“
Praktiski pārbaudījās maza pipeline ar skaidrām atbildībām:
- Kamera-Adapter: piegādā rāmjus (vai to kopijas) definētā formātā.
- Decoder: darbojas fonā pavedienā un atgriež rezultātus caur callback.
- Gate/Debounce: novērš dubultskanēšanu un regulē slodzi (Throttle).
- UI-Schicht: parāda priekšskatījumu, pēc izvēles fokusa taisnstūri (ROI, „Region of InteREST“) un reaģē uz rezultātiem.
Tādējādi jūs izvairāties no UI, kameras un dekodera savstarpējas bloķēšanas. „ROI“ šeit nozīmē iegrieztu meklēšanas logu (piem., centrā 60 %), kas atvieglo dekodera darbu un samazina nepatiesi pozitīvus rezultātus. Svarīgi: ROI ir veiktspējas un lietojamības rīks, nevis drošības mehānisms.
Source-Schnipsel: Robuster QR Code Scanner (FMX + ZXing) mit Debounce und sauberem Stop
Nākamais kods ir domāts kā kompakts, bet projektos izmantojams modulis. Tas izmanto ZXing (Delphi-Port) über ZXing.ScanManager un pieslēdzas TCameraComponent.OnSampleBufferReady. Būtiski ir trīs punkti:
- Rāmji tiek ierobežoti (netiek dekodēts katrs paraugs).
- Dekodēšana nenotiek UI pavedienā.
- Stop/Start ir idempotents (var izsaukt vairākas reizes, bez resursu haosa).
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 skenera kontrolieris FMX (Android/iOS).
/// Pārvalda kameras kadru vadību (frame gating), fonā veic dekodēšanu un nodrošina korektu Start/Stop.
/// </summary>
TQrScannerController = class
private
FCamera: TCameraComponent;
FScanManager: TScanManager;
FBitmap: TBitmap;
FLock: TObject;
FOnResult: TQrScanResultEvent;
// Gating/Throttle
FIsRunning: Boolean;
FIsDecoding: Integer; // 0/1 kā Interlocked karogs
FLastDecodeTick: Int64;
FMinIntervalMs: Cardinal;
// Debounce pret atkārtotiem vienādiem kodiem
FLastText: string;
FLastTextTick: Int64;
FDebounceMs: Cardinal;
// ROI: attēla daļa, kas tiek skenēta (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; // piem. 120
property DebounceMs: Cardinal read FDebounceMs write FDebounceMs; // piem. 1200
property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
property RoiScale: Single read FRoiScale write FRoiScale; // piem. 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;
// Inicializē ScanManager un ierobežo uz QR (veiktspēja + mazāk viltus pozitīvu)
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;
// Aktivēt kameru: reālās lietotnēs pirms tam pārbaudīt atļaujas (Android) un ņemt vērā UI plūsmu.
if Assigned(FCamera) then
FCamera.Active := True;
end;
procedure TQrScannerController.Stop;
begin
if not FIsRunning then
Exit;
FIsRunning := False;
// Aktivīgi un tīri izslēgt
if Assigned(FCamera) then
FCamera.Active := False;
// Atnullēt dekodera karogu, ja Stop tiek izsaukts nepiemērotā fāzē
TInterlocked.Exchange(FIsDecoding, 0);
end;
function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
// Throttle: ne dekodēt katru kadru
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);
// tas pats teksts debounce-logā – ignorēt
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;
// Tikai viena dekodēšana vienlaikus (citādi rindas sastrēgums vājās ierīcēs)
if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
Exit;
// Kopēt kameras paraugu uz FBitmap. Bloķēšana, jo tas pats bitmap buferis nedrīkst tikt izmantots paralēli.
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;
// Fonā veic dekodēšanu
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
// Izgriež ROI centrā: samazina aprēķinu slodzi un fokusē lietotāju.
// Uzmanību: ļoti maziem QR kodiem ROI var būt pārāk šaurs.
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-pavedienā: navigācija, pīkstiens, lauka aizpildīšana utt.
TThread.Queue(nil,
procedure
begin
if FIsRunning and Assigned(FOnResult) then
FOnResult(AText);
end);
end;
end.
Ko kods atrisina (un kāpēc tas ir nepieciešams)
Throttle (MinIntervalMs) samazina CPU noslodzi un siltuma izdalīšanos. Bez ierobežojuma dažas ierīces mēģina dekodēt 30–60 kadri/s; praksē pietiek ar 5–10/s, bieži pat mazāk. Debounce (DebounceMs) novērš, ka vienmēr turēts QR kods tiktu izraisīts vairākas reizes (piem., dubultrezervācija procesa solī).
Interlocked-Flag (FIsDecoding) nodrošina, ka vienlaikus darbojas maks. viens dekodēšanas uzdevums. Tas ir arhitektūras paņēmiens pret rindas sastrēgumu: ja dekodēšana aizņem 200 ms, bet jaunus uzdevumus palaiž ik pēc 120 ms, rinda pieaug un rezultāti pienāk ar aizkavi, kas darbībā izpaužas kā “skeneris reaģē nepareizi”.
Robežnosacījumi un kritiskās vietas
- TBitmap un daudzprocesu darbs: FMX-Bitmaps var tikt atbalstītas GPU pusē. Pieeja kopē kadru uz lokālu bitmap un dekodē fonā. Atkarībā no Delphi versijas/platfomas tomēr var būt nepieciešama piesardzība: ja redzat artefaktus, piespiediet CPU-bitmap (piem., caur pikseļu lasīšanu/rakstīšanu) vai strādājiet ar byteBuffer no SampleBuffer (platformai tuvāks, bet stabilāks risinājums).
- Stop/Start pie navigācijas: Mobilajās lietotnēs bieži aptur, pārejot formā vai saņemot app-pauzes notikumu. Svarīgi, lai
Stopdrīkst tikt izsaukts vairākas reizes un tas neizsauc izņēmumus (idempotents). Turklāt rezultāta callback jāpiemēro pārbaudei, vai skeneris joprojām darbojas (to nodrošinaDoResultOnMainThread). - ROI pārāk šaurs: Centrsēta ROI paātrina apstrādi, taču var neizdoties, ja lietotājs tur kodu ārpus laukuma vai ja kods ir ļoti mazs. Tāpēc
EnableRoiir konfigurējams unRoiScaleir ierobežots. - Formāta ierobežojums uz QR: Ierobežošana uz
QR_CODEparasti ir pareiza. Ja nepieciešami arī Code128/EAN, paplašiniet formātus — rēķinieties gan ar vairāk false positives, gan lielāku CPU slodzi.
Delphi FMX kameras dzīves cikls: atļaujas, fons, rotācija
Visbiežākās kļūdas rodas ne dekodēšanā, bet ap kameru:
- Android atļaujas: Kameras tiesības jāpieprasa izpildlaikā. Ieplānojiet scenāriju, ka lietotājs noraida vai izvēlas “Tikai šoreiz”. Tehniski tas nozīmē: UI stāvokli («Skeneris gatavs?») uzturēt atsevišķi no kameras stāvokļa, citādi var iestrēgt pusgatavos stāvokļos.
- Lietotne nonāk fonā: Pie
OnApplicationEvent(piem.,EnteredBackground) jāizsaucStop. Atgriežoties apzināti izsaucietStart(un, ja nepieciešams, nelielu aizkavi), lai priekšskats stabilizētos. - Rotācija/spoguļattēls: QR kodiem rotācija bieži nav kritiska, taču dažās kameras kanālu ķēdēs bitmap var būt apgriezts vai pagriezts. Ja skanējumi strādā tikai vienā orientācijā, tas ir indikators. Šādos gadījumos pirms skenēšanas pagrieziet/apgrieziet attēlu vai izmantojiet dekoderi, kas ņem vērā orientācijas metadatus.
Debugēšana darbībā: kā atrast īstos iemeslus
Ja skeneris “reizēm” nelasa, reproducējama debugēšana ir zelts. Trīs pārbaudītas darbības:
- Frame-sampling logēšana: Logējiet (tikai Debug/Support režīmā) tiku, attēla izmēru, ROI izmēru, dekodēšanas ilgumu. Tā uzreiz redzēsiet, vai problēma ir Throttle/Debounce vai CPU slodze.
- Testattēlu saglabāšana: Saglabājiet katru N sekundi ROI attēlu (pagaidu). Tas ļauj bez kameras aparatūras analizēt, vai problēmu rada kontrasts vai izplūdums.
- Atskirt darba slodzi: UI-Updates (Preview-Overlay, Status-Text) pārāk bieži neatjaunināt. Das „UI-Zittern“ bieži rodas no pārāk daudziem
Queue-Events.
Varianti: ja jums vajadzīgs vairāk nekā „skenēt un gatavs“
Vairāki rezultāti, taču kontrolēti
Stakelu procesos (piem., daudz etiķešu pēc kārtas) samaziniet DebounceMs un papildiniet ar Whitelist/State-Machine: QR-kodu drīkst pieņemt tikai tad, ja pašreizējais procesa solis to gaida. Tā nav UI loģika, bet domēna loģika — tā pieder atsevišķam slānim, lai skeneri un procesu varētu testēt neatkarīgi.
Offline validācija un droši lietojamie dati
Uzņēmuma procesos QR kodi bieži satur ID vai tokenus. Neuzticieties pieņēmumam „QR = korrekt“. Validējiet lokāli (formāts, pārbaudes summa, gaidītie prefixi) un servera pusē (REST-API). Ja izmantojat tokenus: derīguma termiņi, replay aizsardzība un žurnālošana ar piesardzību (neierakstīt tokenus skaidrā tekstā atbalsta žurnālos).
Legacy situācijas: FMX-Scanner kā modulis jauktās koda bāzēs
Ja jums ir izveidojusies VCL vide, FMX kā mobilais klients bieži ir atsevišķs pavedienu virziens. Saglabājiet skeneri kā Controller-klasi bez Form- atkarībām (kā augstāk), tā to varēs integrēt dažādos ekrānos. Tas atmaksājas arī modernizācijas laikā: biznesa loģika paliek testējama, kamera ir tikai ievades kanāls. Tieši legacy situācijās ir vērts noteikt skaidru saskarni žurnālošanai, feature-flag un attālai konfigurācijai.
Secinājums: stabils FMX-QR-skanēšanas risinājums ir dzīves cikla jautājums — ne tikai ZXing izsaukums
QR Code Scanner in Delphi FMX kļūst stabils, ja to traktē kā mazu pipeline: kamera piegādā kadrus, fona dekoders strādā kontrolēti, un Debounce/Throttle novērš dubultus un novēlotus notikumus. Iepriekš redzamais koda fragments tieši adresē tās vietas, kas reālos mobilajos biznesa procesos mēdz radīt problēmas: pārāk daudz Decode-uzdevumu, netīrs Stop, UI pavediena bloķēšana un nevajadzīga slodze.
Einsatzgrenzen: Ja jums nepieciešamas ļoti augstas skenēšanas frekvences (piem., rūpnieciskā skenēšana uz konveijera) vai stingras prasības attēlu apstrādei, FMX-standarta kamera + Bitmap-pipeline bieži ir pārāk resursu dārga. Tad ir vērts izvēlēties platformai tuvu pieeju (Native Camera API, YUV-Buffer tieši, SIMD/NEON) vai specializētu skenera SDK. Lielākajai daļai procesu tuvinošo mobilu lietojumprogrammu tomēr parādītais piegājiens ir pietiekams, ja dzīves cikls, atļaujas un pavedienu pārvaldība ir tīri integrēti — un procesi aiz tiem ir skaidri definēti.
Ja jums jāpielāgo QR-skanēšana esošai Delphi arhitektūrai (ieskaitot maliņu gadījumus kā navigācija, backgrounding, logging un procesa validācija), mēs to labprāt izrunāsim strukturēti:
Profesionālajā kontekstā arī Zxing Delphi un Fmx Tcameracomponent spēlē nozīmīgu lomu, ja integrācijām, datu plūsmām un turpmākai attīstībai jādarbojas kopā tīri.
Nākamais solis
Ja no tēmas rodas reāls projekts, arhitektūra, esošais stāvoklis un ekspluatācija būtu jāizskata kopā jau agri.
Mēs atbalstām ne tikai atsevišķu jautājumu risināšanā, bet arī tad, kad no avota koda fragmentiem, mantojuma sistēmu jautājumiem vai portāla idejām jāizveido stabils uzņēmuma līmeņa projekts.
- Esošais stāvoklis, mērķa stāvoklis un tehniskie riski tiek kopīgi vērtēti.
- REST, datu piekļuve, portāli un izvēršana netiek atlikti kā vēlākas sekas.
- Jūs savlaicīgi redzat, kurš ceļš ir ekonomiski un darbības ziņā dzīvotspējīgs.