Net-Base Magazyn

03.06.2026

Skaner kodów QR w Delphi FMX: stabilne skanowanie z kamery, bezpieczne wątkowo i bez drgań interfejsu

Praktyczny skaner kodów QR Delphi FMX zależy od cyklu życia kamery, wielowątkowości oraz poprawnego zatrzymywania/uruchamiania. Artykuł pokazuje solidne podejście z ZXing, debounce, ograniczaniem liczby klatek (frame-throttling), przycinaniem ROI oraz szczegółami debugowania i eksploatacji dla Androida i iOS.

03.06.2026

Od tematu magazynowego do praktyki projektowej

Pasujące strony usługowe i techniczne do artykułu

Skaner kodów QR Delphi FMX w praktyce

W demie QR Code Scanner Delphi FMX można szybko złożyć: wyświetlić podgląd kamery, pobrać bitmapę, uruchomić ZXing. W rzeczywistej aplikacji biznesowej (np. przy przyjęciu towaru, przypisywaniu urządzeń, ticketingu, procesach dostępu) pojawiają się jednak warunki brzegowe: aplikacja przechodzi w tło, kamera traci fokus, użytkownik trzyma urządzenie ukośnie, zmienia się format obrazu – i nagle skanowany jest ten sam kod dwukrotnie na sekundę albo interfejs klatkuje, ponieważ dekodowanie odbywa się w wątku UI.

Typowe problemy to rzadziej „ZXing kann nicht lesen”, a raczej lifecycle i architektura: zwolnienie zasobów kamery, taktowanie klatek, bezpieczeństwo wątkowe przy dostępie do TBitmap (GPU/CPU) oraz jasny Stop/Start, który zachowuje porządek nawet gdy użytkownicy szybko nawigują lub OS tymczasowo odbiera kamerę.

Architekturüberblick: Pipeline statt „OnSampleBufferReady macht alles”

W praktyce sprawdza się niewielka pipeline z wyraźnym podziałem odpowiedzialności:

  • Adapter kamery: dostarcza klatki (lub ich kopie) w zdefiniowanym formacie.
  • Dekoder: działa w wątku tła i zwraca wyniki przez callback.
  • Gate/Debounce: zapobiega podwójnym skanom i reguluje obciążenie (throttle).
  • Warstwa UI: wyświetla podgląd, opcjonalny prostokąt fokusu (ROI, „Region of InteREST”) i reaguje na wyniki.

Dzięki temu unikasz sytuacji, w której UI, kamera i dekoder wzajemnie się blokują. „ROI” oznacza tutaj przycięty obszar wyszukiwania (np. centralne 60 %), który odciąża dekoder i zmniejsza liczbę wyników fałszywie dodatnich. Ważne: ROI to narzędzie poprawiające wydajność i użyteczność, nie mechanizm bezpieczeństwa.

Source-Schnipsel: Robuster QR Code Scanner (FMX + ZXing) mit Debounce und sauberem Stop

Poniższy kod jest przeznaczony jako zwarty, lecz nadający się do projektu komponent. Wykorzystuje ZXing (Delphi-Port) przez ZXing.ScanManager i podłącza się do TCameraComponent.OnSampleBufferReady. Kluczowe są trzy punkty:

  • Klatki są throttled (nie dekodować każdej próbki).
  • Dekodowanie nie odbywa się w wątku UI.
  • Stop/Start jest idempotentny (można wywoływać wielokrotnie bez chaosu zasobów).

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>
/// Kontroler skanera QR dla FMX (Android/iOS).
/// Zajmuje się ograniczaniem ramek kamery, dekodowaniem w tle i poprawnym zatrzymywaniem/uruchamianiem.
/// </summary>
TQrScannerController = class
private
FCamera: TCameraComponent;
FScanManager: TScanManager;
FBitmap: TBitmap;
FLock: TObject;

FOnResult: TQrScanResultEvent;

// Sterowanie przepustowością
FIsRunning: Boolean;
FIsDecoding: Integer; // 0/1 jako flaga używana przez Interlocked
FLastDecodeTick: Int64;
FMinIntervalMs: Cardinal;

// Debounce przeciwko powtarzającym się tym samym kodom
FLastText: string;
FLastTextTick: Int64;
FDebounceMs: Cardinal;

// ROI: część obrazu skanowana (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; // np. 120
property DebounceMs: Cardinal read FDebounceMs write FDebounceMs; // np. 1200
property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
property RoiScale: Single read FRoiScale write FRoiScale; // np. 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;

// Inicjalizacja ScanManager i ograniczenie do QR (wydajność + mniej fałszywych trafień)
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;

// Aktywacja kamery: w rzeczywistych aplikacjach uprzednio sprawdzić uprawnienia (Android) i uwzględnić przepływ UI.
if Assigned(FCamera) then
FCamera.Active := True;
end;

procedure TQrScannerController.Stop;
begin
if not FIsRunning then
Exit;
FIsRunning := False;

// Poprawne i bezpieczne wyłączenie
if Assigned(FCamera) then
FCamera.Active := False;

// Zresetować flagę dekodera, jeśli Stop nastąpi w niekorzystnej fazie
TInterlocked.Exchange(FIsDecoding, 0);
end;

function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
// Ograniczenie: nie dekodować każdej klatki
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);

// ten sam tekst w oknie debounce – zignorować
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;

// Tylko jedno dekodowanie jednocześnie (w przeciwnym razie zator kolejki na słabych urządzeniach)
if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
Exit;

// Skopiować próbkę kamery do FBitmap. Lock, ponieważ ten sam bufor bitmapy nie powinien być używany równolegle.
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;

// Dekodowanie w tle
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
// Wycięcie ROI na środku: zmniejsza obciążenie obliczeniowe i kieruje użytkownika.
// Uwaga: przy bardzo małych kodach QR ROI może być zbyt wąskie.
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;

// Wątek UI: nawigacja, sygnał dźwiękowy, wypełnienie pola itp.
TThread.Queue(nil,
procedure
begin
if FIsRunning and Assigned(FOnResult) then
FOnResult(AText);
end);
end;

end.

Co rozwiązuje kod (i dlaczego to konieczne)

Throttle (MinIntervalMs) zmniejsza obciążenie CPU i wydzielanie ciepła. Bez ograniczenia niektóre urządzenia próbują dekodować 30–60 klatek/s; w praktyce wystarcza 5–10/s, często mniej. Debounce (DebounceMs) zapobiega wielokrotnemu wyzwoleniu przy stabilnie utrzymanym kodzie QR (np. podwójne zaksięgowanie w kroku procesu).

Interlocked-Flag (FIsDecoding) gwarantuje, że maksymalnie jedno zadanie dekodujące jest uruchomione jednocześnie. To architektoniczny trik przeciw „zatorowi w kolejce“: jeśli dekodowanie trwa 200 ms, a zadanie jest uruchamiane co 120 ms, kolejka rośnie, a wyniki przychodzą z opóźnieniem, co w działaniu odbierane jest jako „skaner reaguje niepoprawnie”.

Randbedingungen und Stolperfallen

  • TBitmap und Threading: Bitmapy FMX mogą być obsługiwane przez GPU. Podejście polega na skopiowaniu klatki do lokalnej bitmapy i dekodowaniu jej w tle. W zależności od wersji/Platformy Delphi nadal może być wymagana ostrożność: jeśli pojawiają się artefakty, wymuś bitmapę CPU (np. przez odczyt/zapis pikseli) lub pracuj z ByteBuffer z SampleBuffer (bardziej niskopoziomowe, ale stabilniejsze).
  • Stop/Start bei Navigation: W aplikacjach mobilnych często zatrzymuje się skaner przy zmianie formularza lub przy zdarzeniu pauzy aplikacji. Ważne jest, by Stop można było wywołać wielokrotnie bez wyrzucania wyjątków (idempotentnie). Dodatkowo callback z wynikiem powinien sprawdzać, czy skaner wciąż działa (to robi DoResultOnMainThread).
  • ROI zu eng: Ścisłe ROI skoncentrowane na środku przyspiesza działanie, ale może zawieść, gdy użytkownik trzyma kod poza środkiem lub kod jest bardzo mały. Dlatego EnableRoi jest konfigurowalne, a RoiScale ograniczone.
  • Format-Lock auf QR: Ograniczenie do QR_CODE jest najczęściej właściwe. Jeśli potrzebujesz także Code128/EAN, rozszerz formaty – licząc się jednak z większą liczbą fałszywych pozytywów i większym obciążeniem CPU.

Delphi FMX Kamera-Lifecycle: Berechtigungen, Hintergrund, Rotation

Najczęstsze błędy nie wynikają z dekodowania, lecz z otoczenia kamery:

  • Android Permissions: Uprawnienia do kamery trzeba uzyskać w czasie działania aplikacji. Zaplanuj przypadek, gdy użytkownik odmówi lub wybierze „Tylko tym razem”. Technicznie oznacza to: trzymaj stan UI („Skaner gotowy?”) oddzielnie od stanu kamery, inaczej możesz utknąć w półgotowych stanach.
  • App geht in den Hintergrund: Przy OnApplicationEvent (np. EnteredBackground) powinieneś wywołać Stop. Po powrocie świadomie wywołaj Start (i ewentualnie krótkie opóźnienie), aby podgląd (preview) był stabilny.
  • Rotation/Mirroring: Dla QR często rotacja nie jest krytyczna, ale w niektórych pipeline’ach kamery bitmapa może być odwrócona lub obrócona. Jeśli skany działają „tylko w jednej orientacji”, to wskazówka. W takim przypadku: przed skanem obróć/odwróć obraz lub użyj dekodera, który wykorzystuje metadane orientacji.

Debugging im Betrieb: So finden Sie die echten Ursachen

Jeśli skaner „czasami” nie odczytuje, odtwarzalne debugowanie jest na wagę złota. Trzy sprawdzone działania:

  1. Frame-Sampling loggen: Loguj (tylko w trybie Debug/Support) tick, rozmiar obrazu, rozmiar ROI, czas dekodowania. W ten sposób natychmiast zobaczysz, czy problem leży po stronie Throttle/Debounce czy obciążenia CPU.
  2. Testbilder sichern: Zapisuj co N sekund obraz ROI (tymczasowo). Pozwala to bez sprzętu kamery analizować, czy problemem są kontrast/rozmycie.
  • Oddziel obciążenie: Nie aktualizuj UI (Preview-Overlay, tekst statusu) z dużą częstotliwością. „Drżenie UI” często wynika z zbyt wielu zdarzeń Queue.
  • Warianty: jeśli potrzebują Państwo więcej niż „Skan i gotowe”

    Wiele wyników, lecz kontrolowane

    Dla procesów wsadowych (np. wielu etykiet po kolei) zmniejszyć DebounceMs i dodać Whitelist/State-Machine: kod QR może być zaakceptowany tylko wtedy, gdy bieżący krok procesu go oczekuje. To nie jest logika UI, lecz logika domenowa — powinna znaleźć się w odrębnej warstwie, aby skaner i proces były testowalne niezależnie.

    Weryfikacja offline i bezpieczny ładunek danych

    W procesach korporacyjnych kody QR często zawierają identyfikatory lub tokeny. Nie należy zakładać, że „QR = poprawne”. Weryfikować lokalnie (format, suma kontrolna, oczekiwane prefiksy) i po stronie serwera (REST-API). Jeśli stosuje się tokeny: uwzględnić czasy wygaśnięcia, ochronę przed powtórnym odtworzeniem (replay) oraz ostrożne logowanie (nie zapisywać tokenów jawnie w logach wsparcia).

    Sytuacje legacy: FMX-Scanner jako moduł w mieszanych bazach kodu

    Jeżeli w istniejącym środowisku dominuje VCL, FMX jako klient mobilny często jest odrębnym wątkiem. Należy utrzymać skaner jako klasę kontrolera bez zależności od form (jak wyżej), co umożliwia jego integrację z różnymi ekranami. To się opłaca przy modernizacji: logika biznesowa pozostaje testowalna, a kamera pełni jedynie rolę kanału wejściowego. W sytuacjach legacy warto też wprowadzić wyraźne rozdzielenie dla logowania, feature flagów i zdalnej konfiguracji.

    Wniosek: Solidny FMX-QR-skan to problem lifecycle — nie tylko wywołanie ZXing

    Skaner QR w Delphi FMX będzie stabilny, jeśli potraktować go jak niewielki potok przetwarzania: kamera dostarcza klatki, dekoder działający w tle pracuje kontrolowanie, a Debounce/Throttle zapobiegają zdublowanym i opóźnionym zdarzeniom. Powyższy fragment kodu adresuje dokładnie miejsca, które w rzeczywistych mobilnych procesach biznesowych zawodzą: zbyt wiele zadań dekodujących, nieczyste zatrzymanie, blokady w wątku UI i niepotrzebne obciążenie.

    Granice zastosowania: Jeśli potrzebne są ekstremalnie wysokie szybkości skanowania (np. skanowanie przemysłowe na taśmie) lub istnieją rygorystyczne wymagania dotyczące przetwarzania obrazu, standardowa kamera FMX + pipeline bitmap często okazuje się zbyt kosztowna. W takim przypadku warto rozważyć podejście niskopoziomowe (Native Camera API, bezpośredni bufor YUV, SIMD/NEON) lub wyspecjalizowane SDK skanera. Dla większości aplikacji mobilnych związanych z procesem przedstawione podejście będzie jednak wystarczające, pod warunkiem że cykl życia, uprawnienia i zarządzanie wątkami są prawidłowo zintegrowane — oraz że procesy stojące za tym są jednoznaczne.

    Jeśli trzeba dopasować skan QR do istniejącej architektury Delphi (włączając przypadki brzegowe takie jak nawigacja, przejścia do tła, logowanie i walidacja procesu), można to uporządkować strukturalnie:

    W kontekście merytorycznym ważną rolę odgrywają także Zxing Delphi i Fmx Tcameracomponent, gdy integracje, przepływy danych i rozwój muszą współdziałać w uporządkowany sposób.

    Omów projekt lub przedsięwzięcie modernizacyjne z Net-Base.

    Następny krok

    Gdy temat stanie się rzeczywistym projektem, architekturę, stan istniejący i eksploatację należy wcześnie rozpatrywać wspólnie.

    Wspieramy nie tylko w pojedynczych zagadnieniach, lecz także wtedy, gdy z fragmentów kodu źródłowego, kwestii związanych z systemami legacy lub koncepcji portalu ma powstać solidny projekt dla przedsiębiorstwa.

    • Stan istniejący, obraz docelowy i ryzyka techniczne są oceniane łącznie.
    • REST, dostęp do danych, portale i Rollout nie są odkładane na później.
    • Wcześnie widzą Państwo, która droga jest ekonomicznie opłacalna i operacyjnie wykonalna.

    Udostępnij wpis

    Udostępnij ten wpis bezpośrednio

    LinkedIn, X, XING, Facebook, WhatsApp i e‑mail są natychmiast dostępne. Dla Instagrama przygotowujemy od razu link i krótki tekst.

    E-mail

    Instagram otwiera się w nowej karcie. Link i krótki tekst są wcześniej kopiowane do schowka.