Net-Base マガジン

03.06.2026

Delphi FMXのQRコードスキャナー:カメラスキャンを堅牢かつスレッドセーフに、UIの揺れを生じさせずに

実運用に耐えるQR Code Scanner Delphi FMX は、カメラのライフサイクル、スレッド管理、そして確実な停止/開始に左右されます。本稿では、ZXing、デバウンス、フレームスロットリング、ROI切り出しを用いた堅牢なアプローチと、Android および iOS におけるデバッグおよび運用の詳細を示します。

03.06.2026

雑誌のテーマからプロジェクト実践へ

該当記事に関連するサービス・技術ページ

実務でのQRコードスキャナー Delphi FMX

デモではQR Code Scanner Delphi FMXは素早く組み立てられます:カメラのプレビューを表示し、ビットマップを取得してZXingで読み取るだけです。しかし実際の業務用ソフトウェア(例:入荷処理、機器割り当て、チケット管理、入退室プロセス)では周辺条件が増えます。アプリがバックグラウンドに回る、カメラのフォーカスが失われる、ユーザーが端末を斜めに持つ、画像フォーマットが切り替わる――すると同じコードを1秒間に二度読み取ってしまったり、デコード処理がUIスレッドで走っているためにUIが引っかかる、という事態が発生します。

典型的な問題は「ZXingが読めない」というよりもライフサイクルとアーキテクチャにあります:カメラのリソース解放、フレームの間引き/レート制御、TBitmap(GPU/CPU)へのアクセス時のスレッド安全性、そしてユーザーが素早く遷移したりOSがカメラを一時的に取り上げた場合でも確実に動作する明確な停止/開始処理です。

アーキテクチャ概観: パイプライン方式 — 「OnSampleBufferReadyがすべてを処理する」ではなく

実務で有効だったのは、責務を明確にした小さなパイプラインです:

  • カメラアダプタ: 定義されたフォーマットでフレーム(またはそのコピー)を供給します。
  • デコーダ: バックグラウンドスレッドで動作し、コールバックで結果を返します。
  • Gate/Debounce: 重複スキャンを防ぎ、負荷を制御します(Throttle)。
  • UI層: プレビュー、必要に応じてフォーカス枠(ROI、“Region of InteREST“)を表示し、結果に反応します。

これによりUI、カメラ、デコーダがお互いにブロックし合うことを回避できます。ここでの「ROI」は切り出した検索領域(例:中央60%)を意味し、デコーダの負担を減らし誤検出を抑えます。重要な点:ROIはパフォーマンスとユーザビリティのためのツールであり、セキュリティ機構ではありません。

ソース断片:Debounceと確実な停止を備えた堅牢なQRコードスキャナー(FMX + ZXing)

以下のコードはコンパクトながらプロジェクトで使える部品として想定しています。ZXing(Delphiポート)をZXing.ScanManager経由で利用し、TCameraComponent.OnSampleBufferReadyにフックします。重要な点は三つです:

  • フレームはスロットリングされる(すべてのサンプルをデコードしない)。
  • デコードはUIスレッドでは実行されない
  • 停止/開始は冪等である(何度呼んでもリソースの混乱を起こさない)。

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>
/// FMX(Android/iOS)向けのQRスキャナーコントローラ。
/// カメラのフレームゲーティング、バックグラウンドデコード、およびクリーンな停止/開始処理を担当する。
/// </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;

// デコーダーフラグをリセット、停止が不適切なタイミングで発生した場合に備える
TInterlocked.Exchange(FIsDecoding, 0);
end;

function TQrScannerController.ShouldDecodeNow(const ANowTick: Int64): Boolean;
begin
// スロットリング:毎フレームをデコードしない
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;

// デコードは同時に1つのみ(そうしないと低性能デバイスでキューが詰まる)
if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
Exit;

// カメラサンプルをFBitmapにコピーする。ロックは同一のビットマップバッファが並列使用されないようにするため。
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スレッド:画面遷移、ビープ、入力フィールドへの反映など。
TThread.Queue(nil,
procedure
begin
if FIsRunning and Assigned(FOnResult) then
FOnResult(AText);
end);
end;

end.

コードが解決すること(およびその必要性)

Throttle (MinIntervalMs) は CPU 負荷と発熱を抑制します。制限がないと一部のデバイスは30–60 Frames/sでデコードしようとしますが、実際には5–10/s、あるいはそれ以下で十分なことが多いです。Debounce (DebounceMs) は、安定して保持された QR-Code が複数回トリガーされるのを防ぎます(例:処理ステップでの二重登録)。

Interlocked-Flag (FIsDecoding) は最大で1つの Decode-Task しか動作しないようにします。これは「キュー詰まり」へのアーキテクチャ的な対策です:デコードに200 msかかるのに120 msごとにタスクが起動されると待ち行列が増え、結果が遅延して返ってくるため、運用上は「Scanner reagiert falsch(スキャナが誤動作している)」ように見えます。

前提条件と落とし穴

  • TBitmap und Threading: FMX の Bitmaps は GPU-backed である場合があります。本アプローチではフレームをローカル Bitmap にコピーし、バックグラウンドでデコードします。Delphi のバージョンやプラットフォームによっては依然として注意が必要です:アーティファクトが出る場合は CPU 上の Bitmap を強制する(例:Pixel の Read/Write を経由)か、SampleBuffer からの ByteBuffer を使う(プラットフォームに近いが安定)ことを検討してください。
  • Stop/Start bei Navigation: モバイルアプリではフォーム切替えやアプリの一時停止イベント時に停止することが多いです。重要なのは、Stop が複数回呼ばれても例外を発生させない(冪等である)ことです。加えて、結果コールバックはスキャナがまだ動作中かを確認するべきです(DoResultOnMainThread がこれを行います)。
  • ROI zu eng: 中央に限定した ROI は処理を高速化しますが、ユーザーがコードを画面外に持つ場合やコード自体が非常に小さい場合には失敗することがあります。したがって EnableRoi を設定可能にし、RoiScale に上限を設けています。
  • Format-Lock auf QR: QR_CODE に制限するのは多くの場合適切です。Code128 や EAN も必要な場合はフォーマットを追加してください — ただし誤検出(False Positives)や CPU 負荷の増加を見込む必要があります。

Delphi FMX カメラライフサイクル:権限、バックグラウンド、回転

最も頻出するバグはデコード自体ではなくカメラ周りで発生します:

  • Android Permissions: カメラ権限はランタイムで取得する必要があります。ユーザーが拒否する、あるいは「今回のみ」を選ぶ場合を想定してください。技術的には UI の状態(「スキャナは準備できているか?」)をカメラの状態と分離して管理しないと、中途半端な状態に陥ります。
  • App geht in den Hintergrund: OnApplicationEvent(例:EnteredBackground)で Stop を呼び出してください。復帰時は意図的に Start(必要に応じて短い遅延を入れる)を行い、プレビューが安定するようにします。
  • Rotation/Mirroring: QR コードでは回転は多くの場合問題になりませんが、特定のカメラパイプラインでは Bitmap が鏡像になったり回転したりすることがあります。スキャンが「特定の姿勢でしか」動作しない場合はその兆候です。その場合はスキャン前に回転/鏡像を補正するか、Orientation メタデータを利用するデコーダを採用してください。

運用でのデバッグ:真の原因を見つける方法

スキャナが「時々」読み取らない場合、再現可能なデバッグが非常に重要です。実践的に有効な三つの対策:

  1. Frame-Sampling loggen: (デバッグ/サポートモードのみで)Tick、画像サイズ、ROI サイズ、Decode 期間をログしてください。これにより Throttle/Debounce か CPU 負荷が原因かを即座に判断できます。
  2. Testbilder sichern: 一定間隔で ROI 画像を一時保存してください(例:N 秒ごと)。これによりカメラハードウェアなしでコントラストやボケが原因かどうかを分析できます。
  3. ワークロードを分離する: UIアップデート(Preview-Overlay、ステータステキスト)を高頻度で更新しない。「UIの揺れ」は多すぎるQueueイベントによって生じることが多い。

バリエーション:「スキャンして完了」以上を必要とする場合

複数の結果を扱うが、制御された形で

バッチ処理(例:多数のラベルを連続して処理する場合)では、DebounceMsを短くするのではなく、ホワイトリスト/ステートマシンを追加してください:QRコードは現在のプロセスステップがそれを期待している場合にのみ受け入れるべきです。これはUIロジックではなくドメインロジックであり、スキャナとプロセスを独立してテスト可能にするために専用のレイヤーに置くべきです。

オフライン検証と安全な利用データ

企業プロセスで使われるQRコードはしばしばIDやトークンを含みます。「QR = 正しい」とは断言しないでください。ローカルでの検証(フォーマット、チェックサム、期待されるプレフィックス)とサーバー側検証(REST-API)を組み合わせてください。トークンを使う場合は有効期限、リプレイ防止、そしてログ記録の扱いに注意してください(サポートログにトークンをプレーンテキストで残さない)。

レガシー状況:混在するコードベースでのFMXスキャナをモジュールとして

既存のVCL環境がある場合、モバイルクライアントとしてのFMXは別系統であることが多いです。スキャナをフォーム依存のないコントローラクラスとして保持すれば(前述のとおり)、異なる画面に統合できます。モダナイゼーションの際にも有効で、ビジネスロジックはテスト可能なまま維持され、カメラは単なる入力チャネルになります。特にレガシー環境では、ログ、機能フラグ、リモート設定のために明確な分離を設ける価値があります。

結論:堅牢なFMXのQRスキャンはライフサイクルの問題であり、単なるZXing呼び出しではない

DelphiのFMXでQRコードスキャナを安定させるには、これを小さなパイプラインとして扱います:カメラがフレームを供給し、バックグラウンドのデコーダが制御された形で動作し、Debounce/Throttleが重複や遅延イベントを防ぎます。上記のソーススニペットは、実際のモバイルのビジネスプロセスで破綻しやすい箇所に正面から対処しています:デコードタスクの過多、不適切な停止、UIスレッドのブロッキング、不必要な負荷などです。

適用限界:極めて高いスキャンレートが求められる場合(例:コンベア上の産業スキャン)や画像処理に厳しい要件がある場合、FMX標準カメラ+Bitmapパイプラインはコストが高すぎることが多いです。その場合はプラットフォームに近いアプローチ(Native Camera API、YUVバッファの直接処理、SIMD/NEON)や専用のスキャナSDKを検討してください。大多数のプロセス寄りモバイルアプリケーションでは、ライフサイクル、権限、スレッディングが適切に統合され、背後のプロセスが明確であれば、示したアプローチで十分です。

既存のDelphiアーキテクチャにQRスキャンを組み込む必要がある場合(ナビゲーション、バックグラウンド化、ログ記録、プロセス検証といった境界ケースを含む)、構造化して整理しましょう:

専門的な文脈では、統合、データフロー、今後の拡張を踏まえると、Zxing DelphiやFmx Tcameracomponentも重要な役割を果たします。

プロジェクトまたはモダナイゼーション案件をNet-Baseとご相談ください

次のステップ

テーマが実際のプロジェクトになる場合、アーキテクチャ、既存資産、運用は早い段階でまとめて検討するべきです。

私たちは単なる個別の問い合わせへの対応にとどまらず、ソースの断片やレガシー課題、ポータルの構想が堅牢な企業向けプロジェクトへと成長する段階まで支援します。

  • 既存環境、目標像、技術的リスクを一体として評価します。
  • REST、データアクセス、ポータル、ロールアウトは後工程として先送りされることはありません。
  • 早期に、どのアプローチが経済的かつ運用面で実行可能かを判断できます。

投稿を共有

この投稿を直接共有する

LinkedIn、X、XING、Facebook、WhatsApp、およびE-Mailはすぐに利用可能です。Instagram用のリンクと短文はただちに準備します。

Eメール

Instagramは新しいタブで開きます。リンクと短文は事前にクリップボードにコピーされます。