Από το θέμα του περιοδικού στην πρακτική εφαρμογή του έργου
Σχετικές σελίδες υπηρεσιών και τεχνολογίας για το άρθρο
Σαρωτής QR Code Delphi FMX στην πράξη
Σε μια επίδειξη, ένας QR Code Scanner Delphi FMX συναρμολογείται γρήγορα: εμφάνιση προεπισκόπησης κάμερας, εξαγωγή Bitmap, εκτέλεση ZXing πάνω του. Σε πραγματικό επιχειρησιακό λογισμικό (π.χ. παραλαβή εμπορευμάτων, αντιστοίχιση συσκευών, ticketing, διαδικασίες πρόσβασης) όμως προστίθενται περιφερειακοί περιορισμοί: η εφαρμογή πηγαίνει στο παρασκήνιο, η κάμερα χάνει το focus, ο χρήστης κρατά τη συσκευή στραβά, αλλάζει το φορμά της εικόνας — και ξαφνικά σαρώνετε δύο φορές το δευτερόλεπτο τον ίδιο κωδικό ή το UI κολλάει επειδή η αποκωδικοποίηση τρέχει στο UI-Thread.
Τα τυπικά προβλήματα δεν είναι τόσο «το ZXing δεν μπορεί να διαβάσει», όσο ο κύκλος ζωής και η αρχιτεκτονική: απελευθέρωση πόρων της κάμερας, ρυθμολόγηση των καρέ, ασφάλεια νημάτων στην πρόσβαση στο TBitmap (GPU/CPU), και ένας σαφής μηχανισμός διακοπής/εκκίνησης που παραμένει καθαρός ακόμη κι όταν οι χρήστες μετακινούνται γρήγορα ή το OS αφαιρεί προσωρινά την κάμερα.
Επισκόπηση αρχιτεκτονικής: Pipeline αντί για «OnSampleBufferReady κάνει τα πάντα»
Στην πράξη έχει αποδειχθεί αποτελεσματική μια μικρή pipeline με σαφείς αρμοδιότητες:
- Kamera-Adapter: παρέχει καρέ (ή αντίγραφά τους) σε καθορισμένο φορμά.
- Decoder: λειτουργεί σε background-thread και επιστρέφει αποτελέσματα μέσω callback.
- Gate/Debounce: αποτρέπει διπλά σκαναρίσματα και ρυθμίζει το φόρτο (throttle).
- UI-Schicht: εμφανίζει προεπισκόπηση, προαιρετικό πλαίσιο εστίασης (ROI, „Region of InteREST“) και αντιδρά στα αποτελέσματα.
Με αυτόν τον τρόπο αποφεύγετε το αλληλομπλοκάρισμα μεταξύ UI, κάμερας και decoder. Με τον όρο „ROI“ εννοείται εδώ ένα περικομμένο παράθυρο αναζήτησης (π.χ. στο κέντρο 60 %), που ελαφρύνει τον decoder και μειώνει τα ψευδώς θετικά αποτελέσματα. Σημαντικό: το ROI είναι εργαλείο επιδόσεων και χρηστικότητας, όχι μηχανισμός ασφάλειας.
Source-Schnipsel: Robuster QR Code Scanner (FMX + ZXing) mit Debounce und sauberem Stop
Ο παρακάτω κώδικας προορίζεται ως συμπαγές, αλλά κατάλληλο για έργο στοιχείο. Χρησιμοποιεί ZXing (Delphi-Port) μέσω ZXing.ScanManager και συνδέεται στο TCameraComponent.OnSampleBufferReady. Καίρια σημεία είναι τρία:
- Τα καρέ υπόκεινται σε throttling (δεν αποκωδικοποιείται κάθε sample).
- Η αποκωδικοποίηση δεν εκτελείται στο UI-Thread.
- Η διακοπή/εκκίνηση είναι idempotent (μπορεί να κληθεί επανειλημμένα χωρίς πρόβλημα στη διαχείριση πόρων).
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 για FMX (Android/iOS).
/// Διαχειρίζεται το gating των καρέ της κάμερας, αποκωδικοποίηση στο παρασκήνιο και ασφαλές stop/start.
/// </summary>
TQrScannerController = class
private
FCamera: TCameraComponent;
FScanManager: TScanManager;
FBitmap: TBitmap;
FLock: TObject;
FOnResult: TQrScanResultEvent;
// Έλεγχος ροής/περιορισμός
FIsRunning: Boolean;
FIsDecoding: Integer; // 0/1 ως σημαία Interlocked
FLastDecodeTick: Int64;
FMinIntervalMs: Cardinal;
// Απόσβεση (debounce) για επαναλαμβανόμενους ίδιους κωδικούς
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 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;
// Ενεργοποίηση κάμερας: Σε πραγματικές εφαρμογές ελέγξτε πρώτα τα δικαιώματα (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;
// Επαναφορά σημαίας αποκωδικοποιητή σε περίπτωση που το Stop εμφανιστεί σε ακατάλληλη φάση
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);
// ίδιο κείμενο εντός του παραθύρου debounce - αγνόηση
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;
// Μόνο μία αποκωδικοποίηση ταυτόχρονα (διαφορετικά μπούκωμα στην ουρά σε αδύναμες συσκευές)
if TInterlocked.CompareExchange(FIsDecoding, 1, 0) <> 0 then
Exit;
// Αντιγραφή δείγματος κάμερας σε FBitmap. Κλείδωμα επειδή ο ίδιος buffer του Bitmap δεν πρέπει να χρησιμοποιείται παράλληλα.
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-Thread: πλοήγηση, beep, συμπλήρωση πεδίου κ.λπ.
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) εξασφαλίζει ότι τρέχει το πολύ ένα Decode-Task. Πρόκειται για ένα αρχιτεκτονικό τέχνασμα κατά του «φρακαρίσματος της ουράς»: αν το decoding χρειάζεται 200 ms αλλά κάθε 120 ms ξεκινάει ένα Task, η ουρά μεγαλώνει και τα αποτελέσματα φτάνουν με καθυστέρηση, κάτι που στη λειτουργία μοιάζει με «ο σαρωτής αντιδρά λάθος».
Προϋποθέσεις und Stolperfallen
- TBitmap und Threading: Οι FMX-Bitmaps μπορεί να είναι GPU-backed. Η προσέγγιση αντιγράφει το Frame σε μια τοπική Bitmap και αποκωδικοποιεί στο παρασκήνιο. Ανάλογα με την έκδοση/πλατφόρμα της Delphi μπορεί παρ1 να χρειάζεται προσοχή: αν βλέπετε παραμορφώσεις, εξαναγκάστε σε CPU-Bitmap (π.χ. μέσω ανάγνωσης/εγγραφής Pixel) ή δουλέψτε με ByteBuffer από το SampleBuffer (πιο πλατφορμοειδές, αλλά πιο σταθερό).
- Stop/Start bei Navigation: Σε mobile εφαρμογές συχνά σταματάμε κατά την αλλαγή της φόρμας ή στο app-pause-event. Σημαντικό είναι το
Stopνα μπορεί να καλείται πολλαπλές φορές χωρίς να πετάει Exceptions (idempotent). Επίσης το callback αποτελέσματος πρέπει να ελέγχει αν ο σαρωτής τρέχει ακόμη (το υλοποιεί τοDoResultOnMainThread). - ROI zu eng: Ένα κεντρικό ROI επιταχύνει, αλλά μπορεί να αποτύχει αν οι χρήστες κρατούν τον κώδικα εκτός ή ο κώδικας είναι πολύ μικρός. Γι’αυτό το
EnableRoiείναι ρυθμιζόμενο και τοRoiScaleπεριορισμένο. - Format-Lock auf QR: Ο περιορισμός στο
QR_CODEείναι συνήθως σωστός. Αν χρειάζεστε επίσης Code128/EAN, επεκτείνετε τα formats — υπολογίστε όμως περισσότερα false positives και μεγαλύτερο φόρτο CPU.
Delphi FMX Kamera-Lifecycle: Berechtigungen, Hintergrund, Rotation
Τα πιο συχνά bugs δεν προκύπτουν στο Decoding, αλλά γύρω από την κάμερα:
- Android Permissions: Τα δικαιώματα κάμερας πρέπει να ζητηθούν κατά την εκτέλεση. Προβλέψτε την περίπτωση που ο χρήστης αρνηθεί ή επιλέξει «Nur diesmal». Τεχνικά αυτό σημαίνει: κρατήστε το UI-State («Scanner bereit?») ξεχωριστό από το Kamera-State, αλλιώς θα μείνετε σε ημιτελείς καταστάσεις.
- App geht in den Hintergrund: Στο
OnApplicationEvent(π.χ.EnteredBackground) πρέπει να καλείτεStop. Κατά την επιστροφή καλέστε ρητάStart(και ενδεχομένως μικρή καθυστέρηση), ώστε το preview να είναι σταθερό. - Rotation/Mirroring: Για QR-Codes η περιστροφή συχνά δεν είναι κρίσιμη, αλλά σε κάποιες camera-pipelines το Bitmap μπορεί να είναι καθρεφτισμένο ή περιστραμμένο. Αν οι σαρώσεις δουλεύουν «μόνο σε μία στάση», αυτό είναι ένδειξη. Σε αυτή την περίπτωση: περιστρέψτε/καθρεφτίστε πριν το scan ή χρησιμοποιήστε έναν decoder που αξιοποιεί Orientation-Metadaten.
Debugging im Betrieb: So finden Sie die echten Ursachen
Αν ο σαρωτής «manchmal» δεν διαβάζει, το αναπαραγώγιμο debugging είναι χρυσός. Τρία μέτρα που αποδεικνύονται αποτελεσματικά:
- Frame-Sampling loggen: Καταγράψτε (μόνο σε Debug/Support-Modus) Tick, Bildgröße, ROI-Größe, Decode-Dauer. Έτσι θα δείτε αμέσως αν το Throttle/Debounce ή το φόρτο της CPU είναι το πρόβλημα.
- Testbilder sichern: Αποθηκεύστε κάθε N Sekunden μια ROI-εικόνα (προσωρινά). Με αυτά μπορείτε χωρίς κάμερα hardware να αναλύσετε αν αντίθεση/θόλωση είναι το ζήτημα.
- Διαχωρισμός φόρτου εργασίας: Μην ενημερώνετε τις ενημερώσεις του UI (Preview-Overlay, Status-Text) με υψηλή συχνότητα. Το «τρεμούλιασμα» του UI προέρχεται συχνά από υπερβολικά πολλά
Queue-events.
Παραλλαγές: Όταν χρειάζεστε περισσότερα από «σάρωση και έτοιμο»
Πολλαπλά αποτελέσματα, αλλά ελεγχόμενα
Για διαδικασίες παρτίδας (π.χ. πολλές ετικέτες η μία μετά την άλλη) μειώστε το DebounceMs και προσθέστε μια λευκή λίστα / μηχανή καταστάσεων: Ένας QR‑κωδικός πρέπει να αποδεχθεί μόνο όταν το τρέχον βήμα της διαδικασίας τον αναμένει. Αυτή δεν είναι λογική διεπαφής χρήστη αλλά λογική τομέα — ανήκει σε ξεχωριστό στρώμα, ώστε ο σαρωτής και η διαδικασία να παραμένουν ανεξάρτητα ελέγξιμα.
Έλεγχος εκτός σύνδεσης και ασφαλή δεδομένα χρήστη
Σε επιχειρησιακές ροές, οι QR‑κωδικοί συχνά περιέχουν IDs ή tokens. Μην στηρίζεστε στην υπόθεση «QR = έγκυρο». Επαληθεύστε τοπικά (μορφή, αθροιστικός έλεγχος, αναμενόμενα prefix) και στην πλευρά του server (REST‑API). Εάν χρησιμοποιείτε tokens: φροντίστε για χρόνους λήξης, προστασία κατά replay και προσεκτικό logging (μην καταγράφετε tokens σε απλό κείμενο σε logs υποστήριξης).
Legacy‑σενάρια: FMX‑σαρωτής ως module σε μικτές βάσεις κώδικα
Εάν έχετε μια αναπτυγμένη VCL‑πλατφόρμα, το FMX ως mobile client είναι συχνά ξεχωριστό σκέλος. Διατηρήστε τον σαρωτή ως κλάση controller χωρίς εξαρτήσεις από φόρμες (όπως παραπάνω), ώστε να μπορείτε να τον ενσωματώσετε σε διαφορετικές οθόνες. Αυτό πληρώνει και στη φάση εκσυγχρονισμού: η επιχειρησιακή λογική παραμένει ελέγξιμη, ενώ η κάμερα είναι μόνο ένας κανάλι εισόδου. Σε legacy καταστάσεις αξίζει επίσης ένας σαφής διαχωρισμός για καταγραφή (logging), feature‑flags και απομακρυσμένη διαμόρφωση.
Συμπέρασμα: Σταθερή FMX‑QR‑σάρωση είναι ζήτημα lifecycle — όχι μόνο μια κλήση σε ZXing
Ένας QR Code Scanner σε Delphi FMX γίνεται σταθερός όταν τον αντιμετωπίζετε ως μικρή pipeline: η κάμερα παρέχει frames, ένας background‑decoder δουλεύει ελεγχόμενα, και Debounce/Throttle εμποδίζουν διπλά και καθυστερημένα events. Το απόσπασμα κώδικα πιο πάνω στοχεύει ακριβώς τα σημεία που σε πραγματικές mobile επιχειρησιακές ροές προκαλούν αποτυχίες: υπερβολικές εργασίες decode, μη καθαρό stop, αποκλεισμοί του UI‑thread και περιττός φόρτος.
Όρια εφαρμογής: Εάν απαιτείτε εξαιρετικά υψηλές ρυθμοαπαιτήσεις σάρωσης (π.χ. βιομηχανική σάρωση σε γραμμή παραγωγής) ή έχετε σκληρές απαιτήσεις στην επεξεργασία εικόνας, η FMX‑πρότυπη κάμερα + bitmap‑pipeline συχνά είναι υπερβολικά αργή/βαριά. Τότε αξίζει μια πλατφορμο‑εγγύτερη προσέγγιση (Native Camera API, άμεσος YUV‑buffer, SIMD/NEON) ή ένα εξειδικευμένο scanner‑SDK. Για τις περισσότερες κινητές εφαρμογές κοντά στη διαδικασία ο παρουσιαζόμενος τρόπος είναι επαρκής, εφόσον lifecycle, δικαιώματα και threading ενσωματωθούν σωστά — και οι διαδικασίες από πίσω είναι σαφείς.
Εάν πρέπει να ενσωματώσετε έναν QR‑σάρωση σε υπάρχουσα αρχιτεκτονική Delphi (συμπεριλαμβανομένων οριακών περιπτώσεων όπως πλοήγηση, backgrounding, logging και επικύρωση διαδικασίας), το διευκρινίζουμε δομημένα με ευχαρίστηση:
Σε επαγγελματικό πλαίσιο παίζουν επίσης σημαντικό ρόλο το Zxing Delphi και το Fmx Tcameracomponent, όταν οι ενσωματώσεις, οι ροές δεδομένων και η μελλοντική ανάπτυξη πρέπει να συνεργαστούν καθαρά.
Επόμενο βήμα
Όταν από το θέμα προκύψει ένα πραγματικό έργο, η αρχιτεκτονική, η υφιστάμενη κατάσταση και η λειτουργία πρέπει να εξεταστούν έγκαιρα από κοινού.
Υποστηρίζουμε όχι μόνο σε μεμονωμένα ζητήματα, αλλά και όταν από αποσπάσματα πηγαίου κώδικα, θέματα legacy ή ιδέες για πύλες πρέπει να προκύψει ένα αξιόπιστο εταιρικό έργο.
- Η υφιστάμενη κατάσταση, το επιθυμητό μελλοντικό μοντέλο και οι τεχνικοί κίνδυνοι αξιολογούνται από κοινού.
- REST, η πρόσβαση στα δεδομένα, οι πύλες και το rollout δεν αναβάλλονται ως μετέπειτα συνέπειες.
- Αναγνωρίζετε έγκαιρα ποια προσέγγιση είναι οικονομικά και λειτουργικά βιώσιμη.