Net-Base Tímarit

03.06.2026

QR-kóðaskanni í Delphi FMX: myndavélaskönnun sem er traust, þráðörugg og án viðmótshristings

Hagnýtur QR-kóðaskanni Delphi FMX ræðst af líftíma myndavélar, þráðaumsjón og hreinni stöðvun og ræsingu. Greinin sýnir trausta nálgun með ZXing, Debounce, Frame-Throttling, ROI-skurði auk debug- og rekstrarupplýsinga fyrir Android og iOS.

03.06.2026

Frá tímaritsþema til verkefnaframkvæmdar

Viðeigandi þjónustu- og tæknisíður fyrir greinina

QR Code Scanner Delphi FMX í framkvæmd

Ein QR Code Scanner Delphi FMX er fljótt settur saman í demo: birta forskoðun myndavélar, sækja Bitmap, láta ZXing keyra yfir. Í raunverulegri fyrirtækjahugbúnaði (t.d. vörumóttaka, úthlutun tækja, ticketing, aðgangsferlar) koma hins vegar viðbótar aðstæður: forrit fer í bakgrunn, myndavélin missir fókus, notandi heldur tækinu skáhallt, myndformið breytist – og skyndilega skannarðu sama kóðann tvisvar á sekúndu eða notendaviðmótið hikstar því afkóðunin fer fram í UI-þræði.

Algengustu vandamálin snúast síður um „ZXing kann nicht lesen“, heldur um lífsferil og arkitektúr: losun auðlinda myndavélarinnar, taktun ramma, þráðaröryggi við aðgang að TBitmap (GPU/CPU), og skýr Stop/Start sem helst hreinn jafnvel þegar notendur fljótt skipta um skjái eða stýrikerfið fjarlægir myndavélina tímabundið.

Arkitektúr yfirlit: Pipeline statt „OnSampleBufferReady macht alles“

Í hagnýtum verkefnum hefur lítil pipeline með skýrum verkaskiptingum reynst vel:

  • Kamera-adapter: skilar ramma (eða afritum þeirra) í skilgreindu sniði.
  • Decoder: vinnur í bakgrunnsþræði og skilar niðurstöðum í gegnum callback.
  • Gate/Debounce: kemur í veg fyrir tvískönnun og stýrir álagi (Throttle).
  • UI-lag: sýnir forskoðun, valfrjálst fókus-rétthyrning (ROI, „Region of InteREST“) og bregst við niðurstöðum.

Þetta kemur í veg fyrir að UI, myndavél og decoder hindri hvor annan. „ROI“ merkir hér afmörkuð leitarglugga (t.d. miðlægt 60%), sem léttir á decoder og minnkar fölsk-jákvæðar niðurstöður. Mikilvægt: ROI er frammistöðu- og notendaviðmótsverkfæri, ekki öryggismeðferð.

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

Eftirfarandi kóði er hugsuð sem þétt en verkefnishæf byggingareining. Hann notar ZXing (Delphi-Port) í gegnum ZXing.ScanManager og tengist TCameraComponent.OnSampleBufferReady. Ákveðnir þrír punkta skipta mestu máli:

  • Rammar eru throttled (ekki afkóða hvert sýni).
  • Afkóðun keyrir ekki í UI-þræði.
  • Stop/Start er idempotent (mörg köll möguleg án auðlindaróreiðu).
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-skannarahandfang fyrir FMX (Android/iOS).
  /// Sér um stýringu rammaflæðis, bakgrunnsafkóðun og hreint stop/start.
  /// </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;

    // Dempun gegn endurteknum sömu kóðum
    FLastText: string;
    FLastTextTick: Int64;
    FDebounceMs: Cardinal;

    // ROI: hluti myndarinnar sem er skannaður (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; // t.d. 120
    property DebounceMs: Cardinal read FDebounceMs write FDebounceMs;         // t.d. 1200
    property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
    property RoiScale: Single read FRoiScale write FRoiScale;                 // t.d. 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 initialisieren und auf QR beschränken (Performance + weniger False Positives)
  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 aktivieren: In echten Apps vorher Permissions prüfen (Android) und UI-Flow berücksichtigen.
  if Assigned(FCamera) then
    FCamera.Active := True;
end;

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

  // Aktiv sauber abschalten
  if Assigned(FCamera) then
    FCamera.Active := False;

  // Decoder-Flag zurücksetzen, falls Stop in einer ungünstigen Phase kommt
  TInterlocked.Exchange(FIsDecoding, 0);
end;

function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
  // Throttle: nicht jedes Frame dekodieren
  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);

  // gleicher Text innerhalb Debounce-Fenster - ignorieren
  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;

  // Kamera-Sample in FBitmap kopieren. Lock, weil derselbe Bitmap-Buffer nicht parallel benutzt werden soll.
  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;

  // Hintergrund-Decoding
  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 mittig ausschneiden: reduziert Rechenlast und lenkt den Nutzer.
  // Achtung: bei sehr kleinen QR-Codes kann ROI zu eng sein.
  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: Navigation, Beep, Feld füllen etc.
  TThread.Queue(nil,
    procedure
    begin
      if FIsRunning and Assigned(FOnResult) then
        FOnResult(AText);
    end);
end;

end.

Hvað kóðinn leysir (og hvers vegna það er nauðsynlegt)

Throttle (MinIntervalMs) dregur úr CPU-álagi og varmamyndun. Án takmarkana reyna sum tæki að afkóða 30–60 ramma/s; í raun duga 5–10/s, oft færri. Debounce (DebounceMs) kemur í veg fyrir að QR-kóði sem haldið er stöðugur gangi af oftar en einu sinni (t.d. tvöföld bókun í vinnuferli).

Das Interlocked-Flag (FIsDecoding) sorgt dafür, dass maximal ein Decode-Task läuft. Das ist ein Architekturkniff gegen „Queue-Stau“: Wenn Decoding 200 ms dauert, aber alle 120 ms ein Task gestartet wird, wächst die Warteschlange und Ergebnisse kommen zeitversetzt, was im Betrieb wie „Scanner reagiert falsch“ wirkt.

Randbedingungen und Stolperfallen

  • TBitmap und Threading: FMX-Bitmaps können GPU-backed sein. Der Ansatz kopiert das Frame in eine lokale Bitmap und decodiert im Hintergrund. Je nach Delphi-Version/Plattform kann trotzdem Vorsicht nötig sein: Wenn Sie Artefakte sehen, erzwingen Sie eine CPU-Bitmap (z. B. über Pixel-Read/Write) oder arbeiten Sie mit einem ByteBuffer aus dem SampleBuffer (plattformnaher, aber stabiler).
  • Stop/Start bei Navigation: In mobilen Apps wird oft beim Wechseln des Forms oder beim App-Pause-Event gestoppt. Wichtig ist, dass Stop mehrfach aufgerufen werden darf und keine Exceptions erzeugt (idempotent). Außerdem sollte das Ergebnis-Callback prüfen, ob der Scanner noch läuft (macht DoResultOnMainThread).
  • ROI zu eng: Ein mittiges ROI beschleunigt, kann aber scheitern, wenn Nutzer den Code außerhalb halten oder der Code sehr klein ist. Deshalb ist EnableRoi konfigurierbar und RoiScale begrenzt.
  • Format-Lock auf QR: Das Beschränken auf QR_CODE ist meist richtig. Wenn Sie auch Code128/EAN brauchen, erweitern Sie die Formate – rechnen Sie aber mit mehr False Positives und mehr CPU.

Delphi FMX Kamera-Lifecycle: Berechtigungen, Hintergrund, Rotation

Die häufigsten Bugs entstehen nicht beim Decoding, sondern rund um die Kamera:

  • Android Permissions: Kamera-Rechte sind zur Laufzeit einzuholen. Planen Sie den Fall ein, dass ein Nutzer ablehnt oder „Nur diesmal“ wählt. Technisch heißt das: UI-State („Scanner bereit?“) getrennt vom Kamera-State halten, sonst hängen Sie in halbfertigen States.
  • App geht in den Hintergrund: Beim OnApplicationEvent (z. B. EnteredBackground) sollten Sie Stop aufrufen. Beim Zurückkehren bewusst Start (und ggf. kurze Verzögerung), damit das Preview stabil ist.
  • Rotation/Mirroring: Für QR-Codes ist Rotation oft unkritisch, aber bei manchen Kamera-Pipelines kann das Bitmap gespiegelt oder gedreht sein. Wenn Scans „nur in einer Haltung“ funktionieren, ist das ein Hinweis. In dem Fall: vor dem Scan drehen/spiegeln oder auf einen Decoder setzen, der Orientation-Metadaten nutzt.

Debugging im Betrieb: So finden Sie die echten Ursachen

Wenn der Scanner „manchmal“ nicht liest, ist reproduzierbares Debugging Gold wert. Drei Maßnahmen, die sich bewähren:

  1. Frame-Sampling loggen: Loggen Sie (nur im Debug/Support-Modus) Tick, Bildgröße, ROI-Größe, Decode-Dauer. So sehen Sie sofort, ob Throttle/Debounce oder CPU-Last das Problem ist.
  2. Testbilder sichern: Speichern Sie alle N Sekunden ein ROI-Bild (temporär). Damit können Sie ohne Kamera-Hardware analysieren, ob Kontrast/Unschärfe das Problem ist.
  • Aðskilja vinnuálag: UI-Updates (Preview-Overlay, Status-Text) ekki uppfæra með mikilli tíðni. „UI-skjálfti“ stafar oft af of mörgum Queue-atburðum.
  • Útfærslur: Þegar þú þarft meira en „Skanna og klárt“

    Fleiri niðurstöður, en stýrt

    Fyrir hrinuvinnslu (t.d. marga merkimiða í röð) lækkið DebounceMs og bætið við hvílista/ástandsvél: QR-kóði má aðeins samþykkja ef núverandi ferliskafli gerir ráð fyrir honum. Þetta er ekki UI-rökfræði heldur Domänenlogik – hún á heima í sérstöku lagi svo skannarinn og ferlið verði prófanleg hvort í sínu lagi.

    Offline-staðfesting og örugg notendagögn

    Í fyrirtækjaferlum innihalda QR-kóðar oft auðkenni eða token. Treystið ekki á að „QR = korrekt“. Staðfestið staðbundið (snið, athugunarsumma, væntanleg forskeyti) og á netþjóns-hlið (REST-API). Ef þið notið token: skilgreinið gildistíma, endurspilunarvörn og farið varlega með skráningu (ekki vista token í hreinum texta í stuðningsskráum).

    Arfalegar aðstæður: FMX-Scanner als Modul in gemischten Codebasen

    Ef þið eigið vaxið VCL-umhverfi er FMX sem farsíma-klienti oft aðskilin grein. Haldið skannaranum sem Controller-Klasse ohne Form-Abhängigkeiten (eins og að ofan), þá er hægt að innlima hann í mismunandi skjái. Þetta borgar sig einnig við endurnýjun: viðskipta-rökfræði helst prófanleg, myndavélin er einungis inntaksrás. Sérstaklega í arfalegum aðstæðum er gagnlegt að hafa skýran aðskilnað fyrir logging, Feature-Flags og remote-konfigurering.

    Niðurstaða: Traustur FMX-QR-Scan er Lifecycle-Problem – ekki aðeins ein ZXing-Aufruf

    QR Code Scanner í Delphi FMX verður stöðugur ef hann er meðhöndlaður sem lítil pípunet: myndavélin skilar ramma, bakgrunns-afkóðari vinnur með stjórn, og Debounce/Throttle hindra tvöfalt og seint-atburði. Kóðabúturinn að ofan bendir á nákvæmlega þá punkta sem detta út í raunverulegum farsímaviðskiptaferlum: of margar Decode-Tasks, ósamfellt stopp, UI-þráðar hindranir og óþarfur álag.

    Takmarkanir notkunar: Ef þið þurfið mjög háa skönnunarhraða (t.d. iðnaðar-skan á færibandi) eða ströng kröfur um myndvinnslu, er FMX-staðalmyndavél + bitmap-pípa oft of kostnaðarsöm. Þá borgar sig plattformsnær nálgun (Native Camera API, YUV-Buffer direkt, SIMD/NEON) eða sérhæft Scanner-SDK. Fyrir flest ferlanæm farsímaforrit dugar hins vegar sýndi nálgunin, ef Lifecycle, réttindi og þráða-handleiðsla eru rétt samþætt – og ferlarnir á bak við eru skýrir.

    Ef þið þurfið að aðlaga QR-skanningu inn í núverandi Delphi-arkitektúr (þ.á.m. jaðartilvik eins og navigation, backgrounding, logging og ferlisstaðfestingu), ræðum við það gjarnan uppbyggilega:

    Í faglegu samhengi gegna einnig Zxing Delphi og Fmx Tcameracomponent mikilvægu hlutverki þegar samþættingar, gagnastreymi og áframhaldandi þróun þurfa að vinna hnökralaust saman.

    Ræddu verkefni eða uppfærsluverkefni með Net-Base.

    Næsta skref

    Þegar úr málinu verður raunverulegt verkefni ber að skoða arkitektúr, núverandi kerfi og rekstur snemma saman.

    Við styðjum ekki aðeins við einstakar spurningar, heldur einnig þegar úr kóðabútum, eldri kerfum eða gáttahugmyndum þarf að verða traust fyrirtækjaverkefni.

    • Núverandi staða, markmynd og tæknileg áhætta eru metin saman.
    • REST, gagnaaðgangur, gáttir og innleiðing eru ekki skildir eftir til síðar.
    • Það sést snemma hvaða leið er fjárhagslega og rekstrarlega sjálfbær.

    Deila færslu

    Deila þessari færslu beint

    LinkedIn, X, XING, Facebook, WhatsApp og tölvupóstur eru strax tiltækir. Fyrir Instagram undirbúum við hlekk og stuttan texta beint.

    Tölvupóstur

    Instagram opnast í nýjum flipa. Tengill og stuttur texti eru afritaðir í klippiborðið á undan.