Net-Base Περιοδικό

03.06.2026

Σαρωτής QR Code στο Delphi FMX: Ανθεκτική σάρωση με κάμερα, ασφαλής για νήματα και χωρίς τρεμόπαιγμα της διεπαφής χρήστη

Ένας πρακτικός σαρωτής QR Code Delphi FMX κρίνεται από τον κύκλο ζωής της κάμερας, τη διαχείριση νημάτων και την ορθή διαχείριση διακοπής/εκκίνησης. Το άρθρο παρουσιάζει μια στιβαρή προσέγγιση με ZXing, Debounce, Frame-Throttling, περικοπή ROI καθώς και λεπτομέρειες αποσφαλμάτωσης και λειτουργίας για Android και iOS.

03.06.2026

Από το θέμα του περιοδικού στην πρακτική εφαρμογή του έργου

Σχετικές σελίδες υπηρεσιών και τεχνολογίας για το άρθρο

Σαρωτής 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 (μπορεί να κληθεί επανειλημμένα χωρίς πρόβλημα στη διαχείριση πόρων).
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 για 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 είναι χρυσός. Τρία μέτρα που αποδεικνύονται αποτελεσματικά:

  1. Frame-Sampling loggen: Καταγράψτε (μόνο σε Debug/Support-Modus) Tick, Bildgröße, ROI-Größe, Decode-Dauer. Έτσι θα δείτε αμέσως αν το Throttle/Debounce ή το φόρτο της CPU είναι το πρόβλημα.
  2. Testbilder sichern: Αποθηκεύστε κάθε N Sekunden μια ROI-εικόνα (προσωρινά). Με αυτά μπορείτε χωρίς κάμερα hardware να αναλύσετε αν αντίθεση/θόλωση είναι το ζήτημα.
  3. Διαχωρισμός φόρτου εργασίας: Μην ενημερώνετε τις ενημερώσεις του 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, όταν οι ενσωματώσεις, οι ροές δεδομένων και η μελλοντική ανάπτυξη πρέπει να συνεργαστούν καθαρά.

Συζητήστε έργο ή εγχείρημα εκσυγχρονισμού με Net-Base.

Επόμενο βήμα

Όταν από το θέμα προκύψει ένα πραγματικό έργο, η αρχιτεκτονική, η υφιστάμενη κατάσταση και η λειτουργία πρέπει να εξεταστούν έγκαιρα από κοινού.

Υποστηρίζουμε όχι μόνο σε μεμονωμένα ζητήματα, αλλά και όταν από αποσπάσματα πηγαίου κώδικα, θέματα legacy ή ιδέες για πύλες πρέπει να προκύψει ένα αξιόπιστο εταιρικό έργο.

  • Η υφιστάμενη κατάσταση, το επιθυμητό μελλοντικό μοντέλο και οι τεχνικοί κίνδυνοι αξιολογούνται από κοινού.
  • REST, η πρόσβαση στα δεδομένα, οι πύλες και το rollout δεν αναβάλλονται ως μετέπειτα συνέπειες.
  • Αναγνωρίζετε έγκαιρα ποια προσέγγιση είναι οικονομικά και λειτουργικά βιώσιμη.

Κοινοποίηση δημοσίευσης

Μοιραστείτε αυτήν την ανάρτηση απευθείας

LinkedIn, X, XING, Facebook, WhatsApp und E‑Mail είναι άμεσα διαθέσιμα. Για το Instagram ετοιμάζουμε άμεσα τον σύνδεσμο και το σύντομο κείμενο.

Ηλεκτρονικό ταχυδρομείο

Το Instagram ανοίγει σε μια νέα καρτέλα. Ο σύνδεσμος και το σύντομο κείμενο αντιγράφονται πρώτα στο πρόχειρο.