Net-Base списание

03.06.2026

Скенирање QR код во Delphi FMX: Робустно скенирање со камера, безбедно за нишки и без тресење на корисничкиот интерфејс

Практичен QR Code скенер Delphi FMX зависи од животниот циклус на камерата, управувањето со нишки и прецизно стартување/запирање. Написот прикажува робустен пристап со ZXing, Debounce, Frame‑Throttling, исек на ROI, како и детали за дебагирање и оперативна работа за Android и iOS.

03.06.2026

Од тема во магазинот до проектна пракса

Соодветни страници за услуги и технички информации поврзани со објавата

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-от може да биде огледално или свртен. Ако скенирањата функционираат „само во една положба“, тоа е индикатор. Во таков случај: пред скенирање свртете/огледајте го сегментот или употребете декодер кој ги користи метаподатоците за ориентација.

Дебагирање во работа: Како да ги откриете вистинските причини

Ако скенерот „понекогаш“ не чита, репродуцираното дебагирање е исклучително вредно. Три мерки кои се покажале корисни:

  1. Логирање на Frame-Sampling: Логирајте (само во Debug/Support-Mod) Tick, големина на слика, големина на ROI, времетраење на декодирањето. Така веднаш ќе видите дали Throttle/Debounce или оптовареноста на CPU е причината.
  2. Зачувување на тест-слики: Сочувајте секои N секунди едно ROI-слика (привремено). Со тоа можете без хардвер на камера да анализирате дали контрастот/заматеноста е проблемот.
  3. 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 не се одложуваат како подоцнежни последици.
  • Уште рано идентификувате кој пат е економски и оперативно одржлив.

Сподели објава

Споделете го овој пост директно.

LinkedIn, X, XING, Facebook, WhatsApp и е-пошта се веднаш достапни. За Instagram директно подготвуваме линк и краток текст.

Е-пошта

Instagram се отвора во нов таб. Линкот и краткиот текст претходно се копираат во меѓуспремникот.