От темата в списанието към проектната практика
Подходящи страници за услуги и технологии към публикацията
Скенер за QR код Delphi FMX в практиката
Един QR Code Scanner Delphi FMX се сглобява бързо в демото: показване на preview от камерата, взимане на Bitmap, пускане на ZXing за разчитане. В истински бизнес софтуер (напр. Wareneingang, Gerätezuordnung, Ticketing, Zutrittsprozesse) обаче възникват допълнителни условия: приложението отива на заден план, камерата губи фокуса, потребителят държи устройството наклонено, форматът на изображението се променя – и изведнъж сканирате един и същ код два пъти в секунда или UI-то заеква, тъй като декодирането се изпълнява в UI-нишката.
Типичните проблеми не са толкова „ZXing kann nicht lesen“, колкото lifecycle и архитектура: освобождаване на ресурсите на камерата, управление на честотата на кадрите, thread-safety при достъп до TBitmap (GPU/CPU) и ясен Stop/Start, който остава чист дори когато потребителите бързо навигират или ОС временно отнеме камерата.
Преглед на архитектурата: Пайплайн вместо „OnSampleBufferReady macht alles“
В практиката се е утвърдил малък пайплайн с ясни отговорности:
- Адаптер за камера: доставя кадри (или техни копия) в дефиниран формат.
- Декодер: работи в бекграунд нишка и връща резултатите чрез callback.
- Gate/Debounce: предотвратява двойни сканирания и регулира натоварването (Throttle).
- UI слой: показва preview, опционално правоъгълник за фокус (ROI, „Region of InteREST“) и реагира на резултатите.
Така избягвате блокирането между UI, камерата и декодера. „ROI“ тук означава изрязано търсещо поле (напр. централно 60 %), което облекчава декодера и намалява фалшиво-положителните резултати. Важно: ROI е инструмент за производителност и използваемост, не е механизъм за сигурност.
Source-Schnipsel: Robuster QR Code Scanner (FMX + ZXing) mit Debounce und sauberem Stop
Следният код е предназначен като компактен, но годен за проект компонент. Той използва ZXing (Delphi-порт) чрез ZXing.ScanManager и се закача за TCameraComponent.OnSampleBufferReady. Решаващи са три точки:
- Кадрите се ограничават по честота (throttled) (не се декодира всеки Sample).
- Декодирането не се изпълнява в UI-нишката.
- Stop/Start е idempotent (може да се извиква многократно без хаос с ресурсите).
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).
/// Отговаря за гейтуване на камерните кадри (frame-gating), фоново декодиране и коректно спиране/стартиране.
/// </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;
// Дебаунс срещу повторно разпознаване на един и същи код
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
// Throttle: да не се декодира всеки кадър
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);
// Един и същи текст в рамките на дебаунс-прозореца — игнориране
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;
// Копиране на кадъра от камерата в FBitmap. Заключване, защото един и същи bitmap буфер не трябва да се използва паралелно.
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 нишка: навигация, звук (beep), попълване на поле и т.н.
TThread.Queue(nil,
procedure
begin
if FIsRunning and Assigned(FOnResult) then
FOnResult(AText);
end);
end;
end.
Какво решава кодът (и защо е необходимо)
Throttle (MinIntervalMs) намалява натоварването на CPU и отделянето на топлина. Без ограничение някои устройства се опитват да декодират 30–60 кадъра/с; на практика 5–10/s са достатъчни, често и по-малко. Debounce (DebounceMs) предотвратява многократното тригериране при стабилно задържан QR код (напр. двойно записване в един стъпка на процеса).
Interlocked-флаг (FIsDecoding) гарантира, че максимално един Decode-Task работи едновременно. Това е архитектурен трик срещу „запушване на опашката“: ако декодирането отнема 200 ms, но всеки 120 ms се стартира нов Task, опашката расте и резултатите пристигат със закъснение, което в експлоатация изглежда като „скенерът реагира неправилно“.
Условия и капани
- TBitmap и многонитово изпълнение: FMX-битмапите могат да са GPU-backed. Подходът копира кадъра в локален битмап и декодира на заден план. В зависимост от версията/платформата на Delphi все пак може да е нужна предпазливост: ако виждате артефакти, принудете CPU-битмап (напр. чрез Pixel-Read/Write) или работете с ByteBuffer от SampleBuffer (по-платформено, но по-стабилно).
- Stop/Start при навигация: В мобилните приложения често се спира при смяна на формата или при събитие за пауза на приложението. Важно е
Stopда може да се вика многократно без да хвърля изключения (идемпотентно). Освен това callback-ът за резултатите трябва да проверява дали скенерът все още работи (както правиDoResultOnMainThread). - ROI твърде тясен: Центрирано ROI ускорява, но може да се провали, ако потребителят държи кода извън центъра или кодът е много малък. Поради това
EnableRoiе конфигурируем иRoiScaleе ограничен. - Ограничаване на формата до QR: Ограничаването до
QR_CODEв повечето случаи е правилно. Ако ви трябват и Code128/EAN, разширете форматирането – но предвидете повече false positives и по-голямо натоварване на CPU.
Delphi FMX камера жизнен цикъл: разрешения, фон, ротация
Най-честите бъгове не са при декодирането, а около камерата:
- Android разрешения: Правата за камера трябва да се искат по време на изпълнение. Планирайте случая, в който потребителят откаже или избере „Само този път“. Технически това означава: държете UI-състоянието („Scanner готов?“) отделно от състоянието на камерата, в противен случай ще останете в полуготови състояния.
- Приложението влиза във фон: При
OnApplicationEvent(напр.EnteredBackground) трябва да извикатеStop. При връщане извиквайте нарочноStart(и евентуално кратко забавяне), за да е стабилен preview-ът. - Ротация/Огледално отражение: За QR кодовете ротацията често не е критична, но при някои камера-пайплайни битмапът може да е обърнат или отразен. Ако сканирането работи „само в една позиция“, това е индикатор. В такъв случай: обърнете/отразете изображението преди сканиране или използвайте декодер, който използва orientation метаданни.
Отстраняване на грешки в експлоатация: Как да намерите истинските причини
Ако скенерът „понякога“ не чете, възпроизведимото дебъгване е злато. Три мерки, които дават резултат:
- Логване на frame-sampling: Логвайте (само в Debug/Support режим) Tick, размер на изображението, размер на ROI, продължителност на декодирането. Така веднага ще видите дали Throttle/Debounce или натоварване на CPU са проблемът.
- Съхраняване на тестови изображения: Запазвайте на всеки N секунди ROI-изображение (временно). Това ви позволява да анализирате без хардуер на камерата дали контраст/разфокус са причината.
- Разделяне на натоварването: Не актуализирайте UI-елементите (Preview-Overlay, Status-Text) с висока честота. „Трептенето на UI“ често идва от твърде много
Queue-събития.
Варианти: Ако ви трябва повече от „сканирай и готово“
Множество резултати, но контролирани
За пакетни процеси (напр. много етикети един след друг) намалете DebounceMs и добавете Whitelist/State-Machine: QR кодът трябва да бъде приет само когато текущата стъпка от процеса го очаква. Това не е UI-логика, а домейн-логика — тя принадлежи в отделен слой, за да останат скенерът и процесът независимо тестируеми.
Офлайн валидация и защитени полезни данни
В корпоративни процеси QR кодовете често съдържат ID-та или токени. Не разчитайте на „QR = коректно“. Валидирайте локално (формат, контролна сума, очаквани префикси) и на сървъра (REST-API). Ако използвате токени: задайте срокове на годност, защита срещу повторно изпълнение и внимавайте при логиране (без токени в ясен текст в логовете за поддръжка).
Legacy ситуации: FMX-скенер като модул в смесени кодови бази
Ако имате развита VCL-среда, FMX като мобилен клиент често е отделна нишка. Поддържайте скенера като Controller-класа без зависимости от Form (както по-горе), така ще можете да го интегрирате в различни екрани. Това се отплаща и при модернизация: бизнес-логиката остава тестируема, камерата е само входен канал. Особено в legacy ситуации е полезен и ясен интерфейс за логиране, feature-флагове и отдалечена конфигурация.
Извод: Надеждният FMX-QR-скан е проблем на жизнения цикъл — не само извикване на ZXing
QR Code скенер в Delphi FMX става стабилен, когато го третирате като малка pipeline: камерата доставя кадри, фонов декодер работи контролирано, а Debounce/Throttle предотвратяват дублирани и закъснели събития. Примерният изходен фрагмент по-горе адресира точно точките, където в реални мобилни бизнес-процеси възникват проблеми: твърде много Decode-таскове, неясно спиране, блокиране на UI нишката и ненужно натоварване.
Граници на приложимост: Ако ви трябват екстремно високи нива на сканиране (напр. индустриално сканиране на конвейрна линия) или строги изисквания за обработка на изображения, FMX стандартната камера + bitmap-пайплайн често е твърде тежка. Тогава има смисъл от по-платформено-нивов подход (Native Camera API, YUV-Buffer direkt, SIMD/NEON) или специализиран Scanner-SDK. За повечето процесно-близки мобилни приложения обаче показаният подход е достатъчен, при условие че жизненият цикъл, правата и threading-ът са чисто интегрирани — и процесите зад тях са ясно дефинирани.
Ако трябва да приспособите QR-сканиране към съществуваща Delphi архитектура (включително гранични случаи като навигация, backgrounding, логиране и валидация на процеси), можем да го изясним структурирано:
В професионален контекст Zxing Delphi и Fmx Tcameracomponent също играят важна роля, когато интеграции, потоци от данни и по-нататъшно развитие трябва да работят чисто заедно.
Следваща стъпка
Когато темата прерасне в реален проект, архитектурата, съществуващото състояние и експлоатацията трябва да бъдат разгледани съвместно още в ранна фаза.
Подпомагаме не само при отделни въпроси, но и когато от фрагменти от изходен код, проблеми с наследени системи или идеи за портал трябва да бъде реализиран надежден корпоративен проект.
- Сегашното състояние, целевото състояние и техническите рискове се оценяват съвместно.
- REST, достъпът до данни, порталите и разгръщането не се отлагат като по-късни последици.
- Виждате рано кой път е икономически и експлоатационно жизнеспособен.