Net-Base Списание

03.06.2026

QR Code скенер в Delphi FMX: надеждно сканиране с камера, потокобезопасно и без трептене на потребителския интерфейс

Практически приложимият QR Code Scanner Delphi FMX зависи критично от жизнения цикъл на камерата, от управлението на нишките и от коректното спиране/стартиране. В материала е представен устойчив подход с ZXing, Debounce, Frame-Throttling, изрязване по ROI, както и детайли за отстраняване на грешки и експлоатация за Android и iOS.

03.06.2026

От темата в списанието към проектната практика

Подходящи страници за услуги и технологии към публикацията

Скенер за 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 (може да се извиква многократно без хаос с ресурсите).
Delphi
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 метаданни.

Отстраняване на грешки в експлоатация: Как да намерите истинските причини

Ако скенерът „понякога“ не чете, възпроизведимото дебъгване е злато. Три мерки, които дават резултат:

  1. Логване на frame-sampling: Логвайте (само в Debug/Support режим) Tick, размер на изображението, размер на ROI, продължителност на декодирането. Така веднага ще видите дали Throttle/Debounce или натоварване на CPU са проблемът.
  2. Съхраняване на тестови изображения: Запазвайте на всеки N секунди ROI-изображение (временно). Това ви позволява да анализирате без хардуер на камерата дали контраст/разфокус са причината.
  3. Разделяне на натоварването: Не актуализирайте 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 също играят важна роля, когато интеграции, потоци от данни и по-нататъшно развитие трябва да работят чисто заедно.

Обсъдете проект или модернизационно задание с Net-Base.

Следваща стъпка

Когато темата прерасне в реален проект, архитектурата, съществуващото състояние и експлоатацията трябва да бъдат разгледани съвместно още в ранна фаза.

Подпомагаме не само при отделни въпроси, но и когато от фрагменти от изходен код, проблеми с наследени системи или идеи за портал трябва да бъде реализиран надежден корпоративен проект.

  • Сегашното състояние, целевото състояние и техническите рискове се оценяват съвместно.
  • REST, достъпът до данни, порталите и разгръщането не се отлагат като по-късни последици.
  • Виждате рано кой път е икономически и експлоатационно жизнеспособен.

Сподели публикацията

Споделете тази публикация директно

LinkedIn, X, XING, Facebook, WhatsApp и имейл са незабавно достъпни. За Instagram ще подготвим връзка и кратък текст.

Електронна поща

Instagram се отваря в нов раздел. Връзката и краткият текст се копират предварително в клипборда.