From magazine topic to project implementation
Relevant service and technical pages for this post
QR Code Scanner Delphi FMX in practice
A QR Code Scanner Delphi FMX can be assembled quickly in a demo: show camera preview, grab a bitmap, run ZXing over it. In real business software (e.g. goods receiving, device assignment, ticketing, access processes), however, additional boundary conditions appear: the app goes to the background, the camera loses focus, the user holds the device at an angle, the image format changes — and suddenly you scan the same code twice per second or the UI stutters because decoding runs on the UI thread.
The typical problems are less „ZXing cannot read“ and more lifecycle and architecture: releasing camera resources, pacing frames, thread-safety when accessing TBitmap (GPU/CPU), and a clear stop/start that remains clean even when users navigate quickly or the OS briefly revokes the camera.
Architecture overview: Pipeline instead of „OnSampleBufferReady does everything“
In practice, a small pipeline with clear responsibilities has proven effective:
- Camera adapter: delivers frames (or copies thereof) in a defined format.
- Decoder: runs on a background thread and returns results via a callback.
- Gate/Debounce: prevents duplicate scans and manages load (throttle).
- UI layer: shows preview, optional focus rectangle (ROI, „Region of InteREST“) and reacts to results.
This prevents UI, camera and decoder from blocking each other. „ROI“ here means a cropped search window (e.g. centered 60%) that relieves the decoder and reduces false-positive results. Important: ROI is a performance and usability tool, not a security mechanism.
Source snippet: Robust QR Code Scanner (FMX + ZXing) with debounce and clean stop
The following code is intended as a compact but project-ready component. It uses ZXing (Delphi port) via ZXing.ScanManager and hooks into TCameraComponent.OnSampleBufferReady. Three points are critical:
- Frames are throttled (do not decode every sample).
- Decoding does not run in the UI thread.
- Stop/Start is idempotent (callable multiple times without resource chaos).
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 scanner controller for FMX (Android/iOS).
/// Handles camera frame gating, background decoding, and clean stop/start.
/// </summary>
TQrScannerController = class
private
FCamera: TCameraComponent;
FScanManager: TScanManager;
FBitmap: TBitmap;
FLock: TObject;
FOnResult: TQrScanResultEvent;
// Gating/Throttle
FIsRunning: Boolean;
FIsDecoding: Integer; // 0/1 as an Interlocked flag
FLastDecodeTick: Int64;
FMinIntervalMs: Cardinal;
// Debounce against repeated identical codes
FLastText: string;
FLastTextTick: Int64;
FDebounceMs: Cardinal;
// ROI: portion of the image that is scanned (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; // e.g. 120
property DebounceMs: Cardinal read FDebounceMs write FDebounceMs; // e.g. 1200
property EnableRoi: Boolean read FEnableRoi write FEnableRoi;
property RoiScale: Single read FRoiScale write FRoiScale; // e.g. 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;
// Initialize ScanManager and restrict to QR (performance + fewer 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;
// Activate camera: in real apps check permissions beforehand (Android) and consider UI flow.
if Assigned(FCamera) then
FCamera.Active := True;
end;
procedure TQrScannerController.Stop;
begin
if not FIsRunning then
Exit;
FIsRunning := False;
// Deactivate cleanly
if Assigned(FCamera) then
FCamera.Active := False;
// Reset decoder flag in case Stop is called at an inopportune time
TInterlocked.Exchange(FIsDecoding, 0);
end;
function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
// Throttle: do not decode every frame
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);
// same text within debounce window - ignore
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;
// Only one decode at a time (otherwise queue buildup on weak devices)
if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
Exit;
// Copy camera sample into FBitmap. Lock because the same bitmap buffer must not be used in parallel.
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;
// Background 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
// Crop ROI centered: reduces computational load and guides the user.
// Note: for very small QR codes the ROI can be too tight.
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, populate fields etc.
TThread.Queue(nil,
procedure
begin
if FIsRunning and Assigned(FOnResult) then
FOnResult(AText);
end);
end;
end.
What the code solves (and why it is necessary)
Throttle (MinIntervalMs) reduces CPU load and heat generation. Without limiting, some devices try to decode 30–60 frames/s; in practice 5–10/s is sufficient, often less. Debounce (DebounceMs) prevents a steadily held QR code from being triggered multiple times (e.g., double posting in a process step).
The Interlocked flag (FIsDecoding) ensures that at most one decode task runs. This is an architectural trick against „queue buildup“: if decoding takes 200 ms but a task is started every 120 ms, the queue grows and results arrive delayed, which in operation appears as „scanner responds incorrectly.“
Constraints and pitfalls
- TBitmap and threading: FMX bitmaps can be GPU-backed. The approach copies the frame into a local bitmap and decodes in the background. Depending on Delphi version/platform, caution may still be necessary: if you see artifacts, force a CPU bitmap (e.g., via pixel read/write) or work with a byte buffer from the sample buffer (more platform-near, but more stable).
- Stop/Start during navigation: In mobile apps it is common to stop when changing forms or on the app pause event. It is important that
Stopmay be called multiple times and does not throw exceptions (idempotent). Also the result callback should check whether the scanner is still running (that is whatDoResultOnMainThreaddoes). - ROI too tight: A centered ROI speeds up decoding but can fail if users hold the code outside it or the code is very small. Therefore
EnableRoiis configurable andRoiScaleis constrained. - Format lock to QR: Restricting to
QR_CODEis usually correct. If you also need Code128/EAN, extend the formats—but expect more false positives and higher CPU usage.
Delphi FMX camera lifecycle: permissions, background, rotation
The most common bugs do not occur during decoding, but around the camera:
- Android permissions: Camera permissions must be requested at runtime. Plan for a user denying or selecting „Only this time.“ Technically this means keeping UI state („Scanner ready?“) separate from camera state, otherwise you end up in half-complete states.
- App goes to background: On the
OnApplicationEvent(e.g.,EnteredBackground) you should callStop. On returning, deliberately callStart(and possibly add a short delay) so the preview is stable. - Rotation/mirroring: For QR codes rotation is often uncritical, but some camera pipelines may mirror or rotate the bitmap. If scans only work in one orientation, this is an indicator. In that case: rotate/mirror before scanning or use a decoder that consumes orientation metadata.
Debugging in the field: how to find the real causes
When the scanner „sometimes“ doesn’t read, reproducible debugging is invaluable. Three measures that have proven effective:
- Log frame sampling: Log (only in debug/support mode) Tick, image size, ROI size, decode duration. This immediately shows whether throttle/debounce or CPU load is the issue.
- Save test images: Save an ROI image every N seconds (temporarily). This lets you analyze, without camera hardware, whether contrast/blur are the problem.
- Separate workload: Do not update UI elements (preview overlay, status text) at a high frequency. The “UI jitter” often comes from too many
Queueevents.
Variants: When you need more than „scan and done“
Multiple results, but controlled
For batch processes (e.g. many labels in sequence) reduce DebounceMs and add a whitelist/state machine: a QR code may only be accepted when the current process step expects it. This is not UI logic but domain logic — it belongs in its own layer so scanner and process remain independently testable.
Offline validation and secure payloads
In enterprise processes QR codes often contain IDs or tokens. Do not assume „QR = correct.“ Validate locally (format, checksum, expected prefixes) and server-side (REST-API). If you use tokens: expiration times, replay protection, and careful logging (do not include tokens in plain text in support logs).
Legacy situations: FMX scanner as a module in mixed codebases
If you have an established VCL landscape, FMX as a mobile client is often a separate branch. Keep the scanner as a controller class without form dependencies (as above), then you can integrate it into different screens. That also pays off during modernization: the business logic remains testable, the camera is only an input channel. In legacy situations a clear separation for logging, feature flags and remote configuration is particularly valuable.
Conclusion: Robust FMX QR scanning is a lifecycle problem — not just a ZXing call
A QR code scanner in Delphi FMX becomes stable when you treat it like a small pipeline: the camera supplies frames, a background decoder works in a controlled manner, and debounce/throttle prevent duplicate and late events. The source snippet above addresses exactly the points that fail in real mobile business processes: too many decode tasks, improper stop, UI thread blockages and unnecessary load.
Limitations: If you require extremely high scan rates (e.g. industrial scanning on a conveyor) or strict image-processing requirements, the FMX standard camera + bitmap pipeline is often too costly. Then a platform-near approach (Native Camera API, YUV buffer directly, SIMD/NEON) or a specialized scanner SDK is worthwhile. For most process-oriented mobile applications the demonstrated approach is sufficient, provided lifecycle, permissions and threading are cleanly integrated — and the processes behind it are well defined.
If you need to fit a QR scan into an existing Delphi architecture (including edge cases such as navigation, backgrounding, logging and process validation), we are happy to clarify this in a structured way:
In professional contexts Zxing Delphi and Fmx Tcameracomponent also play an important role when integrations, data flows and further development need to work together cleanly.
Discuss a project or modernization initiative with Net-Base.
Next step
When the topic becomes a real project, architecture, the existing system landscape and operations should be considered together early on.
We support not only with individual issues, but also when source snippets, legacy topics, or portal ideas are to be turned into a robust enterprise project.
- Current state, target state and technical risks are assessed jointly.
- REST, data access, portals and rollout are not deferred as afterthoughts.
- You can determine early which path is economically and operationally viable.