Од тема во магазинот до проектна пракса
Соодветни страници за услуги и технички информации поврзани со објавата
QR код скенер Delphi FMX во пракса
Еден QR код скенер Delphi FMX во демо-верзија се составува брзо: прикажете преглед од камерата, извлечете Bitmap, и пуштете ZXing за да го декодира. Во вистинска бизнис-софтверска средина (на пр. прием на стоки, асоцијација на уреди, тикетинг, пристапни процеси) се јавуваат дополнителни услови: апликацијата оди во позадина, камерата го губи фокусот, корисникот го држи уредот накриво, форматот на сликата се менува – и одеднаш скенирате ист код двапати во секунда или корисничкиот интерфејс нерамномерно се ажурира, бидејќи декодирањето се извршува во UI-нитка.
Типичните проблеми се помалку „ZXing не може да чита“, а повеќе се поврзани со жизнениот циклус и архитектурата: ослободување на ресурсите на камерата, темпирање на фрејмовите, нитска/тред-безбедност при пристап до TBitmap (GPU/CPU), и јасен механизам за зауставување/старт што останува чист и кога корисниците брзо навигираат или кога ОС-от привремено ја одзема камерата.
Архитектурен преглед: Pipeline наместо „OnSampleBufferReady macht alles“
Практично се покажа како корисна мала Pipeline со јасни одговорности:
- Kamera-Adapter: доставува фрејмови (или нивни копии) во дефиниран формат.
- Decoder: работи во позадинска нит и ги враќа резултатите преку callback.
- Gate/Debounce: спречува двојни скенирања и ја регулира оптовареноста (Throttle).
- UI-Schicht: прикажува преглед, по избор рамка за фокус (ROI, „Region of InteREST“) и реагира на резултатите.
Со тоа избегнувате дека UI, камерата и декодерот ќе се блокираат меѓусебно. „ROI“ тука означува исечено поле за пребарување (на пр. централни 60 %), кое го растоварува декодерот и ги намалува лажно-позитивните резултати. Важно: ROI е алатка за перформанси и употребливост, не е механизам за безбедност.
Source-Schnipsel: Робустен QR код скенер (FMX + ZXing) со Debounce и правилно зауставување
Следниот код е замислен како компактна, но применлива компонента за проект. Го користи ZXing (Delphi-порт) преку ZXing.ScanManager и се поврзува на TCameraComponent.OnSampleBufferReady. Клучни се три точки:
- Фрејмовите се throttled (не секој Sample да се декодира).
- Декодирањето не се извршува во UI-нитка.
- Зауставување/Старт е идемпотентен (повеќекратно повикување без хаос со ресурсите).
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-скенер за FMX (Android/iOS).
/// Се грижи за контролирање на обработката на рамки од камерата, декодирање во позадина и чисто старт/стоп.
/// </summary>
TQrScannerController = class
private
FCamera: TCameraComponent;
FScanManager: TScanManager;
FBitmap: TBitmap;
FLock: TObject;
FOnResult: TQrScanResultEvent;
// Гатење/ограничување на фрејмови
FIsRunning: Boolean;
FIsDecoding: Integer; // 0/1 als Interlocked-Flag
FLastDecodeTick: Int64;
FMinIntervalMs: Cardinal;
// Debounce против повторувања на исти кодови
FLastText: string;
FLastTextTick: Int64;
FDebounceMs: Cardinal;
// ROI: дел од сликата што се скенира (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; // на пр. 120
property DebounceMs: Cardinal read FDebounceMs write FDebounceMs; // на пр. 1200
property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
property RoiScale: Single read FRoiScale write FRoiScale; // на пр. 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 и ограничување на QR (перформанси и помалку лажни позитиви)
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;
// Активирање на камерата: во вистински апликации прво проверете дозволи (Android) и земете предвид редоследот на UI.
if Assigned(FCamera) then
FCamera.Active := True;
end;
procedure TQrScannerController.Stop;
begin
if not FIsRunning then
Exit;
FIsRunning := False;
// Чисто исклучување на активноста
if Assigned(FCamera) then
FCamera.Active := False;
// Ресетирај флагот на декодерот ако Stop дојде во неповолен момент
TInterlocked.Exchange(FIsDecoding, 0);
end;
function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
// Ограничување: не декодирај секоја рамка
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);
// истиот текст во Debounce-прозорецот – игнорирај
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;
// Само едно декодирање истовремено (инаку застој во редот кај слаби уреди)
if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
Exit;
// Копирај камера-семпл во FBitmap. Блокирај, затоа што истиот bitmap-buffer не смее да се користи паралелно.
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;
// Декодирање во позадина
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 централно: ја намалува пресметковната сложеност и го насочува корисникот.
// Внимание: кај многу мали QR-кодови ROI може да биде преголемо ограничување.
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: навигација, звук, пополнување на поле итн.
TThread.Queue(nil,
procedure
begin
if FIsRunning and Assigned(FOnResult) then
FOnResult(AText);
end);
end;
end.
Што решава кодот (и зошто е потребно)
Throttle (MinIntervalMs) ја намалува оптовареноста на CPU и создавањето топлина. Без ограничување некои уреди ќе се обидуваат да декодираат 30–60 фрејмови/с; во пракса доволни се 5–10/с, често помалку. Debounce (DebounceMs) спречува стабилно задржан QR-код да се активира повеќекратно (н.п. двојно биење во еден процесен чекор).
Тоa што Interlocked-Flag (FIsDecoding) го прави е да овозможи да тече најмногу еден Decode-Task. Ова е архитектонски трик против „заглавување на редот“: ако декодирањето трае 200 ms, но секои 120 ms се стартува нов Task, редот расте и резултатите доаѓаат со временско задоцнување, што во оперативна работа изгледа како „скенерот реагира непопрaвно“.
Услови и потенцијални проблеми
- TBitmap и Threading: FMX-Bitmap-ите можат да бидат GPU-backed. Пристапот ја копира рамката во локална Bitmap и декодира во позадина. Во зависност од верзијата/платформата на Delphi сепак може да е потребна претпазливост: ако видите артефакти, принудете CPU-битмапа (н.п. преку читање/запишување пиксели) или работете со еден ByteBuffer од SampleBuffer (пристап поблиску до платформата, но постабилен).
- Stop/Start при навигација: Во мобилни апликации често се прави стоп при менување на формата или при App-Pause-настан. Важно е
Stopда може да се повикува повеќекратно и да не фрла исклучоци (идемпотентно). Исто така резултатниот callback треба да провери дали скенерот сè уште е во работа (тоа го правиDoResultOnMainThread). - ROI премногу тесен: Централно ROI го забрзува процесот, но може да пропадне ако корисникот го држи кодот надвор од областа или ако кодот е многу мал. Затоа
EnableRoiе конфигурируем иRoiScaleе ограничен. - Format-Lock на QR: Ограничувањето на
QR_CODEво повеќето случаи е правилно. Ако ви трeбаат и Code128/EAN, проширете ги форматите — сепак предвидете повеќе false positives и поголема оптовареност на CPU.
Delphi FMX камера-лајфсајкл: дозволи, позадина, ротација
Најчестите багови не настануваат при декодирањето, туку околу камерата:
- Android Permissions: Правата за камера треба да се побараат во текот на извршувањето. Предвидете сценарио во кое корисникот одбива или избира „Само овој пат“. Технички тоа значи: одржувајте го UI-статусот („Скенер подготвен?“) одделно од статусот на камерата, во спротивно ќе останете во полуготови состојби.
- Апликацијата отива во позадина: При
OnApplicationEvent(н.п.EnteredBackground) треба да повикатеStop. При враќање намерно повикајтеStart(и евентуално кратко одложување), за да preview-от биде стабилен. - Rotation/Mirroring: За QR-кодовите ротацијата често не е критична, но кај одредени камера-пајплајни bitmap-от може да биде огледално или свртен. Ако скенирањата функционираат „само во една положба“, тоа е индикатор. Во таков случај: пред скенирање свртете/огледајте го сегментот или употребете декодер кој ги користи метаподатоците за ориентација.
Дебагирање во работа: Како да ги откриете вистинските причини
Ако скенерот „понекогаш“ не чита, репродуцираното дебагирање е исклучително вредно. Три мерки кои се покажале корисни:
- Логирање на Frame-Sampling: Логирајте (само во Debug/Support-Mod) Tick, големина на слика, големина на ROI, времетраење на декодирањето. Така веднаш ќе видите дали Throttle/Debounce или оптовареноста на CPU е причината.
- Зачувување на тест-слики: Сочувајте секои N секунди едно ROI-слика (привремено). Со тоа можете без хардвер на камера да анализирате дали контрастот/заматеноста е проблемот.
- Workload trennen: UI-Updates (Preview-Overlay, Status-Text) nicht in hoher Frequenz aktualisieren. 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.
Projekt oder Modernisierungsvorhaben mit Net-Base besprechen.
Следен чекор
Кога темата ќе прерасне во реален проект, архитектурата, постоечката средина и експлоатацијата треба рано да се разгледаат заедно.
Не поддржуваме само при поединечни прашања, туку и кога од исечоци од изворен код, legacy-теми или идеи за портали треба да прерасне во робустен корпоративен проект.
- Постоечката состојба, целната слика и техничките ризици се проценуваат заедно.
- REST, пристапот до податоци, порталите и Rollout не се одложуваат како подоцнежни последици.
- Уште рано идентификувате кој пат е економски и оперативно одржлив.