Net-Base Magazin

03.06.2026

QR-kód-olvasó a Delphi FMX-ben: kamerás szkennelés — robusztus, szálbiztos és UI-rezgés nélküli

Gyakorlatias QR-kód-olvasó Delphi FMX sikere a kamera életciklusán, a szálkezelésen és a tiszta leállításon/indításon múlik. A cikk egy robusztus megközelítést mutat be ZXing, debounce, frame-throttling, ROI-vágás, valamint Android és iOS hibakeresési és üzemeltetési részletekkel.

03.06.2026

A magazintémától a projektgyakorlatig

A bejegyzéshez tartozó szolgáltatási és technikai oldalak

QR-kód-olvasó Delphi FMX a gyakorlatban

Egy QR-kód-olvasó Delphi FMX a demóban gyorsan összerakható: kamera-előnézet megjelenítése, Bitmap lekérése, ZXing futtatása a képen. Az igazi üzleti szoftverben (pl. áruátvétel, eszköz-hozzárendelés, jegykezelés, beléptetési folyamatok) azonban további korlátozó feltételek lépnek fel: az alkalmazás háttérbe kerül, a kamera elveszíti a fókuszt, a felhasználó ferdén tartja az eszközt, megváltozik a képformátum — és hirtelen másodpercenként kétszer olvassa be ugyanazt a kódot, vagy a felhasználói felület akadozik, mert a dekódolás a UI-szálon fut.

A tipikus problémák kevésbé a „ZXing nem tud olvasni”, sokkal inkább az életciklus és az architektúra: a kamera erőforrásainak felszabadítása, a frame-ek ütemezése, a szálbiztonság a TBitmap-hoz való hozzáférésnél (GPU/CPU), és egy egyértelmű Stop/Start, amely akkor is tiszta marad, ha a felhasználók gyorsan navigálnak, vagy az operációs rendszer rövid ideig elvonja a kamerát.

Architektúra áttekintés: pipeline a „OnSampleBufferReady macht alles” helyett

Gyakorlatban bevált egy kis pipeline, világos feladatkörökkel:

  • Kamera-Adapter: szolgáltat frame-eket (vagy azok másolatait) meghatározott formátumban.
  • Decoder: háttérszálon dolgozik és visszaadja az eredményeket egy callback-en keresztül.
  • Gate/Debounce: megakadályozza a dupla beolvasásokat és szabályozza a terhelést (throttle).
  • UI-Schicht: megjeleníti az előnézetet, opcionálisan a fókusz-keretet (ROI, „Region of InteREST”) és reagál az eredményekre.

Így elkerülhető, hogy az UI, a kamera és a dekóder egymást blokkolják. Itt a „ROI” egy levágott keresőablakot jelent (pl. középen 60%), amely tehermentesíti a dekódolót és csökkenti a tévesen pozitív találatokat. Fontos: a ROI teljesítmény- és használhatósági eszköz, nem biztonsági mechanizmus.

Source-Schnipsel: Robusztus QR Code Scanner (FMX + ZXing) Debounce-szal és tiszta leállítással

A következő kód kompakt, de projekttiszta komponensként van szánva. ZXing (Delphi-Port) használatával, a ZXing.ScanManager-en keresztül működik, és a TCameraComponent.OnSampleBufferReady-hez csatlakozik. Döntő fontosságú három pont:

  • A frame-ek throttled vannak (nem minden Sample dekódolása).
  • A dekódolás nem a UI-szálon fut.
  • A Stop/Start idempotens (többször hívható anélkül, hogy erőforrás-káoszt okozna).

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-olvasó-vezérlő FMX-hez (Android/iOS).
/// Felel a kamera keret-szabályozásáért, háttér-dekódolásért és a tiszta leállítás/indítás kezeléséért.
/// </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;

// Debounce az ismétlődő azonos kódok ellen
FLastText: string;
FLastTextTick: Int64;
FDebounceMs: Cardinal;

// ROI: a kép azon része, amelyet beolvasnak (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; // pl. 120
property DebounceMs: Cardinal read FDebounceMs write FDebounceMs; // pl. 1200
property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
property RoiScale: Single read FRoiScale write FRoiScale; // pl. 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 inicializálása és QR-re korlátozása (teljesítmény és kevesebb hamis pozitív)
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;

// Kamera aktiválása: éles alkalmazásoknál előtte ellenőrizze a jogosultságokat (Android) és vegye figyelembe az UI-folyamot.
if Assigned(FCamera) then
FCamera.Active := True;
end;

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

// Aktív komponens tiszta leállítása
if Assigned(FCamera) then
FCamera.Active := False;

// A dekóder-flag visszaállítása, ha a Stop kedvezőtlen fázisban történik
TInterlocked.Exchange(FIsDecoding, 0);
end;

function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
// Throttling: ne dekódolja minden képkockát
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);

// ugyanaz a szöveg a debounce időablakon belül — figyelmen kívül hagyva
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;

// Egyszerre csak egy dekódolás (különben sor torlódik gyengébb eszközökön)
if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
Exit;

// Kamera-sample másolása FBitmap-be. Zár, mert ugyanazt a bitmap-puffert nem szabad párhuzamosan használni.
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;

// Háttér-dekódolás
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 középre vágása: csökkenti a számítási terhelést és vezeti a felhasználót.
// Figyelem: nagyon kicsi QR-kódok esetén az ROI túl szűk lehet.
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-thread: navigáció, hangjelzés, mező kitöltése stb.
TThread.Queue(nil,
procedure
begin
if FIsRunning and Assigned(FOnResult) then
FOnResult(AText);
end);
end;

end.

Mit old meg a kód (és miért szükséges)

Throttle (MinIntervalMs) csökkenti a CPU-terhelést és a hőképződést. Korlátozás nélkül egyes eszközök 30–60 képkocka/s dekódolását próbálják meg; a gyakorlatban 5–10/s elegendő, gyakran kevesebb. Debounce (DebounceMs) megakadályozza, hogy egy stabilan tartott QR-kód többször is kiváltsa a műveletet (pl. kettős könyvelés egy folyamatlépésben).

Az Interlocked-Flag (FIsDecoding) biztosítja, hogy legfeljebb egy dekódolási feladat fusson egyszerre. Ez egy architekturális trükk a „sor-dugulás” ellen: ha a dekódolás 200 ms-ig tart, de 120 ms-onként indítanak egy feladatot, a várólista nő és az eredmények késleltetve érkeznek, ami üzem közben úgy tűnhet, mintha „a szkenner rosszul reagálna”.

Korlátozások és buktatók

  • TBitmap és szálkezelés: Az FMX-bitképek GPU-támogatottak lehetnek. A megközelítés azzal dolgozik, hogy a képkockát átmásolja egy helyi Bitmapra és háttérben dekódolja. A Delphi-verziótól/platformtól függően mégis óvatosság szükséges lehet: ha artefaktusokat lát, kényszerítsen CPU-alapú Bitmapot (pl. Pixel-Read/Write használatával), vagy dolgozzon közvetlenül egy ByteBufferrel a SampleBufferből (platformközeli, de stabilabb).
  • Stop/Start navigációkor: Mobilalkalmazásoknál gyakran leállítják a kamerát formváltáskor vagy az alkalmazás szünet eseményénél. Fontos, hogy a Stop többször is meghívható legyen és ne dobjon kivételeket (idempotens). Ezenkívül az eredmény-callbacknak ellenőriznie kell, hogy a szkenner még fut-e (ezt végzi a DoResultOnMainThread).
  • ROI túl szűk: A középre helyezett ROI gyorsítja a folyamatot, de meghiúsulhat, ha a felhasználó a kódot a képmezőn kívül tartja, vagy a kód nagyon kicsi. Ezért a EnableRoi konfigurálható, és a RoiScale korlátozott.
  • Formátumzár QR-re: A QR_CODE-ra történő korlátozás általában helyes. Ha szükség van Code128/EAN támogatásra is, bővítsék a formátumokat – számoljanak viszont több hamis pozitívtal és nagyobb CPU-terheléssel.

Delphi FMX kamera-életciklus: engedélyek, háttér, forgatás

A leggyakoribb hibák nem a dekódolásnál, hanem a kamera körül jelentkeznek:

  • Android jogosultságok: A kamera jogosultságait futásidőben kell lekérni. Vegyék számításba, hogy a felhasználó elutasíthatja vagy „Csak most” választ. Technikailag ez azt jelenti: tartsa külön a UI-állapotot („Szkenner készen?”) a kamera-állapottól, különben félkész állapotokba ragadhat.
  • Az alkalmazás háttérbe kerül: Az OnApplicationEvent-nél (pl. EnteredBackground) hívja meg a Stop-ot. Visszatéréskor hívja tudatosan a Start-ot (és szükség esetén rövid késleltetést), hogy az élőkép stabil legyen.
  • Forgatás/tükrözés: QR-kódoknál a forgatás gyakran nem kritikus, de egyes kamera-pipeline-oknál a bitmap tükrözött vagy elforgatott lehet. Ha a beolvasás „csak egy pózban” működik, ez figyelmeztető jel. Ilyenkor: a beolvasás előtt forgassa/tükrözze a képet, vagy válasszon olyan decodert, amely használja az orientációs metaadatokat.

Hibaelhárítás üzem közben: így találja meg a valódi okokat

Ha a szkenner „néha” nem olvas be, az ismételhető hibakeresés aranyat ér. Három bevált intézkedés:

  1. Frame-sampling naplózása: Naplózza (csak Debug/Support módban) a tick-et, képméretet, ROI-méretet, dekódolási időt. Így azonnal látható, hogy a Throttle/Debounce vagy a CPU-terhelés okozza-e a problémát.
  2. Tesztképek mentése: Mentse N másodpercenként az ROI-képet (ideiglenesen). Így kamera-hardware nélkül is elemezhető, hogy kontraszt/homályosság okozza-e a problémát.
  • Terhelés szétválasztása: Ne frissítse nagy gyakorisággal az UI-frissítéseket (Preview-Overlay, státuszszöveg). Az „UI-remegés” gyakran túl sok Queue-eseményből ered.
  • Variánsok: ha többre van szüksége, mint a „Scan és kész”

    Több eredmény, de kontroll alatt

    Stapel jellegű folyamatoknál (pl. sok címke egymás után) csökkentse a DebounceMs-et és egészítse ki egy fehérlista/állapotgép-gel: egy QR-kód csak akkor fogadható el, ha az aktuális folyamatlépés azt várja. Ez nem UI-logika, hanem doménlogika – külön rétegben legyen, hogy a szkennerek és a folyamat függetlenül tesztelhetők maradjanak.

    Offline-ellenőrzés és biztonságos hasznos adatok

    Vállalati folyamatokban a QR-kódok gyakran azonosítókat vagy tokeneket tartalmaznak. Ne bízzon abban, hogy „QR = helyes”. Validáljon lokálisan (formátum, ellenőrzőösszeg, elvárt prefixek) és szerveroldalon (REST-API). Tokenek használata esetén: lejárati idők, replay-védelem és naplózás óvatosan (ne írjon tokeneket tiszta szövegként a support-naplókba).

    Legacy helyzetek: FMX-szkenner modulként vegyes kódalapokban

    Ha egy felhalmozott VCL-világban dolgozik, az FMX mint mobil kliens gyakran külön szál. Tartsa a szkennert űrlapfüggőségek nélküli Controller-osztályként (mint fent), így különböző képernyőkbe integrálható. Ez a modernizáció során is megtérül: az üzleti logika tesztelhető marad, a kamera csak egy bemeneti csatorna. Különösen legacy-helyzetekben érdemes tiszta határt húzni naplózásra, feature-flagekre és távoli konfigurációra.

    Következtetés: a stabil FMX-QR-scan életciklus-probléma — nem csupán egy ZXing-hívás

    Egy QR-kód szkenner Delphi FMX-ben stabil lesz, ha úgy kezeli, mint egy kis pipeline-t: a kamera képkockákat ad, egy háttér-dekóder szabályozottan dolgozik, és Debounce/Throttle megakadályozza a duplikált és késői eseményeket. A fenti forrásrészlet pontosan azokra a pontokra céloz, amelyek valódi mobil üzleti folyamatokban megbillennek: túl sok dekódolási feladat, nem tiszta leállítás, UI-szálblokkolások és felesleges terhelés.

    Alkalmazási korlátok: Ha rendkívül magas beolvasási sebességre van szükség (pl. ipari beolvasás futószalagon) vagy szigorú képfeldolgozási követelmények vannak, az FMX-standárkamera + bitmap-pipeline gyakran költséges. Ilyenkor érdemes platformközeli megközelítést alkalmazni (Native Camera API, közvetlen YUV-buffer elérés, SIMD/NEON) vagy speciális szkenner-SDK-t. A legtöbb folyamathoz közeli mobil alkalmazás esetén azonban a bemutatott megközelítés elegendő, feltéve, hogy az életciklus, a jogosultságok és a threading tisztán integrálva vannak – és a mögöttes folyamatok egyértelműek.

    Ha QR-scan beillesztését kell megoldania egy meglévő Delphi-architektúrába (beleértve szegélyes eseteket, mint navigáció, háttérműködés, naplózás és folyamatvalidálás), strukturáltan tisztázzuk ezt:

    A szakmai környezetben a Zxing Delphi és a Fmx Tcameracomponent is fontos szerepet játszanak, ha az integrációk, az adatfolyások és a továbbfejlesztés tisztán kell, hogy együttműködjenek.

    Projekt vagy modernizációs feladat megbeszélése a Net-Base kapcsán.

    Következő lépés

    Ha egy témából valós projekt lesz, az architektúrát, a meglévő rendszert és az üzemeltetést korai fázisban együtt kell vizsgálni.

    Nemcsak egyedi kérdésekben támogatunk, hanem akkor is, amikor forráskódrészletekből, örökölt rendszerekkel kapcsolatos témákból vagy portálötletekből robusztus vállalati projektet kell kialakítani.

    • A jelenlegi állapotot, a célállapotot és a műszaki kockázatokat együttesen értékeljük.
    • REST, az adathozzáférést, a portálokat és a bevezetést nem halasztjuk későbbi fázisokra.
    • Ön korán látja, melyik út gazdaságilag és üzemeltetési szempontból tartható.

    Bejegyzés megosztása

    Ezt a bejegyzést közvetlenül megosztani

    LinkedIn, X, XING, Facebook, WhatsApp és E-Mail azonnal elérhetők. Instagramra a linket és a rövid szöveget közvetlenül előkészítjük.

    E-mail

    Az Instagram egy új lapon nyílik meg. A link és a rövid szöveg előzetesen a vágólapra másolódik.