De la tema din revistă la practica în proiecte
Pagini relevante de servicii și pagini tehnice pentru articol
Scanner de coduri QR Delphi FMX în practică
Un QR Code Scanner Delphi FMX poate fi montat rapid într-o demonstrație: afișezi previzualizarea camerei, preiei un Bitmap, rulezi ZXing peste el. În software business real (de ex. recepție marfă, alocare echipamente, ticketing, procese de acces) apar însă condiții-limită: aplicația intră în fundal, camera își pierde focusul, utilizatorul ține dispozitivul înclinat, formatul imaginii se schimbă – și dintr-o dată scanați de două ori pe secundă același cod sau interfața sacadează, deoarece decodificarea rulează în firul UI.
Problemele tipice sunt mai puțin „ZXing kann nicht lesen“, și mai mult legate de ciclul de viață și arhitectură: eliberarea resurselor camerei, sincronizarea cadrelor, siguranța la accesul pe thread pentru TBitmap (GPU/CPU), și un mecanism clar de Stop/Start, care rămâne curat chiar și atunci când utilizatorii navighează rapid sau OS-ul retrage camera temporar.
Privire de ansamblu a arhitecturii: Pipeline în loc de „OnSampleBufferReady macht alles“
În practică s-a dovedit eficientă o mică pipeline cu responsabilități clare:
- Adaptor de cameră: furnizează cadre (sau copii ale acestora) într-un format definit.
- Decoder: rulează pe un fir de lucru în fundal și returnează rezultatele printr-un callback.
- Gate/Debounce: previne scanările duble și reglează încărcarea (Throttle).
- Strat UI: afișează previzualizarea, opțional un dreptunghi de focus (ROI, „Regiunea de interes“) și reacționează la rezultate.
Astfel evitați ca UI, camera și decoderul să se blocheze reciproc. „ROI” înseamnă aici o fereastră de căutare decupată (de ex. 60% centrat), care descarcă sarcina decoderului și reduce rezultatele fals pozitive. Important: ROI este un instrument de performanță și uzabilitate, nu un mecanism de securitate.
Fragment de cod: Scanner QR robust (FMX + ZXing) cu Debounce și oprire curată
Codul următor este gândit ca un bloc compact, dar potrivit pentru proiect. Folosește ZXing (Delphi-port) prin ZXing.ScanManager și se leagă de TCameraComponent.OnSampleBufferReady. Esențiale sunt trei aspecte:
- Cadrele sunt throttled (nu se decodează fiecare Sample).
- Decodarea nu rulează în firul UI.
- Stop/Start este idempotent (apelabil de multiple ori, fără haos de resurse).
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>
/// Controler pentru scaner QR pentru FMX (Android/iOS).
/// Se ocupă de gestionarea fluxului de cadre ale camerei, decodare în fundal și oprire/pornire ordonată.
/// </summary>
TQrScannerController = class
private
FCamera: TCameraComponent;
FScanManager: TScanManager;
FBitmap: TBitmap;
FLock: TObject;
FOnResult: TQrScanResultEvent;
// Gating/Throttle
FIsRunning: Boolean;
FIsDecoding: Integer; // 0/1 ca flag Interlocked
FLastDecodeTick: Int64;
FMinIntervalMs: Cardinal;
// Debounce împotriva codurilor identice repetate
FLastText: string;
FLastTextTick: Int64;
FDebounceMs: Cardinal;
// ROI: porțiunea imaginii care va fi scanată (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; // p.ex. 120
property DebounceMs: Cardinal read FDebounceMs write FDebounceMs; // p.ex. 1200
property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
property RoiScale: Single read FRoiScale write FRoiScale; // p.ex. 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;
// Inițializează ScanManager și limitează la QR (performanță + mai puține pozitive false)
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;
// Activare cameră: în aplicații reale verificați mai întâi permisiunile (Android) și luați în considerare fluxul UI.
if Assigned(FCamera) then
FCamera.Active := True;
end;
procedure TQrScannerController.Stop;
begin
if not FIsRunning then
Exit;
FIsRunning := False;
// Dezactivare ordonată
if Assigned(FCamera) then
FCamera.Active := False;
// Resetați flag-ul decoder dacă Stop survine într-o fază nepotrivită
TInterlocked.Exchange(FIsDecoding, 0);
end;
function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
// Limitare: nu decodați fiecare cadru
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);
// același text în fereastra de debounce - ignorați
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;
// Un singur decodificare simultan (altfel blocaj în coadă pe dispozitive slabe)
if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
Exit;
// Copiați sample-ul camerei în FBitmap. Blocare, deoarece același buffer bitmap nu trebuie folosit în paralel.
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;
// Decodare în fundal
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
// Tăiați ROI centrat: reduce sarcina de procesare și direcționează utilizatorul.
// Atenție: pentru coduri QR foarte mici, ROI poate fi prea strâmt.
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;
// Thread UI: navigare, semnal sonor (beep), completare câmp etc.
TThread.Queue(nil,
procedure
begin
if FIsRunning and Assigned(FOnResult) then
FOnResult(AText);
end);
end;
end.
Ce rezolvă codul (și de ce este necesar)
Throttle (MinIntervalMs) reduce încărcarea CPU și disiparea de căldură. Fără o limitare, unele dispozitive încearcă să decodeze 30–60 cadre/s; în practică sunt suficiente 5–10/s, adesea mai puțin. Debounce (DebounceMs) împiedică ca un cod QR ținut stabil să fie declanșat de mai multe ori (de ex. dublă înregistrare într-un pas de proces).
Flag-ul Interlocked (FIsDecoding) asigură că rulează maximum un Decode-Task. Acesta este un truc arhitectural împotriva „blocajului de coadă”: dacă decodarea durează 200 ms, dar la fiecare 120 ms pornește un task, coada crește și rezultatele apar decalate în timp, ceea ce în exploatare se manifestă ca „scannerul reacționează greșit”.
Condiții și capcane
- TBitmap und Threading: FMX-Bitmaps pot fi bazate pe GPU. Abordarea copiază frame-ul într-un Bitmap local și decodează în background. În funcție de versiunea/platforma Delphi poate fi totuși necesară prudență: dacă observați artefacte, forțați un CPU-Bitmap (de ex. prin Pixel-Read/Write) sau lucrați cu un ByteBuffer din SampleBuffer (mai apropiat de platformă, dar mai stabil).
- Stop/Start bei Navigation: În aplicațiile mobile se oprește adesea la schimbarea Form-ului sau la evenimentul de pauză al aplicației. Important este ca
Stopsă poată fi apelat de mai multe ori fără a genera excepții (idempotent). În plus, callback-ul de rezultat ar trebui să verifice dacă scannerul mai rulează (faceDoResultOnMainThread). - ROI zu eng: Un ROI centrat accelerează procesarea, dar poate eșua dacă utilizatorii țin codul în afara zonei sau codul este foarte mic. De aceea
EnableRoieste configurabil șiRoiScaleeste limitat. - Format-Lock auf QR: Limitarea la
QR_CODEeste de regulă corectă. Dacă aveți nevoie și de Code128/EAN, extindeți formatele – așteptați-vă însă la mai multe false positives și consum CPU mai mare.
Delphi FMX Kamera-Lifecycle: Berechtigungen, Hintergrund, Rotation
Cele mai frecvente bug-uri nu apar la decodare, ci în jurul camerei:
- Android Permissions: Drepturile camerei trebuie obținute la runtime. Planificați scenariul în care un utilizator refuză sau alege „Nur diesmal”. Din punct de vedere tehnic înseamnă: păstrați separat starea UI („Scanner bereit?”) de starea camerei, altfel rămâneți în stări parțiale.
- App geht in den Hintergrund: La
OnApplicationEvent(de ex.EnteredBackground) ar trebui să apelațiStop. La revenire apelați conștientStart(și eventual o scurtă întârziere), pentru ca previzualizarea să fie stabilă. - Rotation/Mirroring: Pentru codurile QR rotația este adesea necritic, dar în unele pipeline-uri ale camerei bitmap-ul poate fi oglindit sau rotit. Dacă scanările funcționează „nur in einer Haltung”, acesta este un indiciu. În acest caz: înainte de scanare rotiți/oglindiți sau folosiți un decoder care utilizează metadatele de orientare.
Debugging im Betrieb: So finden Sie die echten Ursachen
Dacă scannerul „manchmal” nu citește, debugging-ul reproducibil este foarte valoros. Trei măsuri dovedite:
- Frame-Sampling loggen: Logați (doar în modul Debug/Support) tick-ul, dimensiunea imaginii, dimensiunea ROI, durata de decodare. Astfel vedeți imediat dacă Throttle/Debounce sau încărcarea CPU sunt problema.
- Testbilder sichern: Salvați la fiecare N secunde o imagine ROI (temporar). Cu acestea puteți analiza fără hardware de cameră dacă contrastul/neclaritatea sunt cauza problemei.
- Separarea sarcinii de lucru: Nu actualizați actualizările UI (Preview-Overlay, Status-Text) la frecvențe ridicate. Das „UI-Zittern“ kommt oft von zu vielen
Queue-Events.
Varianten: Wenn Sie mehr brauchen als „Scan und fertig“
Mehrere Ergebnisse, aber kontrolliert
Für Stapelprozesse (z. B. viele Labels nacheinander) reduzieren Sie DebounceMs und ergänzen eine Whitelist/State-Machine: Ein QR-Code darf nur dann akzeptiert werden, wenn der aktuelle Prozessschritt ihn erwartet. Das ist keine UI-Logik, sondern Domänenlogik – sie gehört in eine eigene Schicht, damit Scanner und Prozess unabhängig testbar bleiben.
Offline-Validierung und sichere Nutzdaten
In Unternehmensprozessen enthalten QR-Codes oft IDs oder Token. Verlassen Sie sich nicht darauf, dass „QR = korrekt“. Validieren Sie lokal (Format, Prüfsumme, erwartete Prefixe) und serverseitig (REST-API). Wenn Sie Token verwenden: Ablaufzeiten, Replay-Schutz, und Logging mit Vorsicht (keine Tokens im Klartext in Support-Logs).
Legacy-Situationen: FMX-Scanner als Modul in gemischten Codebasen
Wenn Sie eine gewachsene VCL-Welt haben, ist FMX als Mobile-Client oft ein separater Strang. Halten Sie den Scanner als Controller-Klasse ohne Form-Abhängigkeiten (wie oben), dann können Sie ihn in unterschiedliche Screens integrieren. Das zahlt sich auch bei Modernisierung aus: Die Business-Logik bleibt testbar, die Kamera ist nur ein Input-Kanal. Gerade in Legacy-Situationen lohnt außerdem ein klarer Schnitt für Logging, Feature-Flags und Remote-Konfiguration.
Fazit: Solider FMX-QR-Scan ist ein Lifecycle-Problem – nicht nur ein ZXing-Aufruf
Ein QR Code Scanner in Delphi FMX wird stabil, wenn Sie ihn wie eine kleine Pipeline behandeln: Kamera liefert Frames, ein Hintergrund-Decoder arbeitet kontrolliert, und Debounce/Throttle verhindern Doppel- und Spät-Events. Der Source-Schnipsel oben adressiert genau die Stellen, die in echten mobilen Business-Prozessen kippen: zu viele Decode-Tasks, unsauberer Stop, UI-Thread-Blockaden und unnötige Last.
Einsatzgrenzen: Wenn Sie extrem hohe Scanraten brauchen (z. B. Industrie-Scanning am Fließband) oder harte Anforderungen an Bildverarbeitung haben, ist die FMX-Standardkamera + Bitmap-Pipeline oft zu teuer. Dann lohnt ein plattformnaher Ansatz (Native Camera API, YUV-Buffer direkt, SIMD/NEON) oder ein spezialisierter Scanner-SDK. Für die meisten prozessnahen mobilen Anwendungen reicht der gezeigte Ansatz jedoch, sofern Lifecycle, Rechte und Threading sauber integriert sind – und die Prozesse dahinter eindeutig sind.
Wenn Sie einen QR-Scan in eine bestehende Delphi-Architektur einpassen müssen (inklusive Randfällen wie Navigation, Backgrounding, Logging und Prozessvalidierung), klären wir das gerne strukturiert:
Im fachlichen Umfeld spielen auch Zxing Delphi und Fmx Tcameracomponent eine wichtige Rolle, wenn Integrationen, Datenflüsse und Weiterentwicklung sauber zusammenspielen müssen.
Discutați un proiect sau un demers de modernizare cu Net-Base.
Următorul pas
Când o temă devine un proiect real, arhitectura, infrastructura existentă și operarea trebuie analizate împreună de la început.
Nu oferim sprijin doar pentru întrebări punctuale, ci și atunci când fragmente de cod sursă, probleme legacy sau idei de portal trebuie transformate într-un proiect robust la nivel de companie.
- Situația curentă, starea țintă și riscurile tehnice sunt evaluate împreună.
- REST, accesul la date, portalurile și Rollout nu sunt amânate ca consecințe ulterioare.
- Veți vedea din timp ce cale este viabilă din punct de vedere economic și operațional.