Τεχνικό Άρθρο

Απόδοση Πίνακα Δεδομένων σε PDF στο Delphi με το HotPDF

Ένα σύνολο δεδομένων είναι σειρές και στήλες. Μια σελίδα PDF είναι ένα κενό πλέγμα συντεταγμένων χωρίς την έννοια κανενός από τα δύο. Η γεφύρωση αυτού του κενού είναι όλη η δουλειά εδώ. Δεν υπάρχει καμία κλήση DrawTable στο HotPDF που να παίρνει ένα σύνολο δεδομένων και να σας παραδίδει ένα μορφοποιημένο πλέγμα. Αυτό που λαμβάνετε αντ' αυτού είναι οι θεμελιώδεις μορφές από τις οποίες αποτελείται ένα πλέγμα: TextOut για την τοποθέτηση μιας συμβολοσειράς σε ένα σημείο, SetFont για την επιλογή της γραμματοσειράς της, Rectangle και Fill για τη σκίαση μιας ζώνης, και MoveTo / LineTo / Stroke για τη σχεδίαση κανόνων (γραμμών). Ένας λειτουργικός εξαγωγέας πίνακα είναι η πειθαρχία της μετατροπής της σκέψης σε σειρές και στήλες σε ρητές συντεταγμένες x και y, και στη συνέχεια της διατήρησης αυτών των συντεταγμένων ειλικρινών όταν τα δεδομένα περνούν το κάτω μέρος της σελίδας

Το παράδειγμα που ακολουθεί αναφέρει εγγραφές πελατών, αλλά τίποτα στον κώδικα σχεδίασης δεν γνωρίζει ή δεν ενδιαφέρεται από πού προέρχονται οι σειρές. Το αρχικό χρησιμοποιούσε έναν παλαιού τύπου TTable. Ένα ερώτημα FireDAC, ένα σύνολο δεδομένων στη μνήμη ή ένας απλός πίνακας εγγραφών τροφοδοτεί τις ίδιες ρουτίνες αμετάβλητες. Αυτό που έχει σημασία είναι ότι μπορείτε να διατρέξετε τα δεδομένα μία σειρά τη φορά και να διαβάσετε τέσσερα πεδία συμβολοσειράς από καθεμία. Διατηρήστε την απόδοση ξεχωριστή από την πηγή δεδομένων και μπορείτε να αλλάξετε οποιαδήποτε πλευρά χωρίς να ενοχλήσετε την άλλη

Η γεωμετρία της στήλης έρχεται πρώτη

Πριν σχεδιαστεί έστω και ένας χαρακτήρας, αποφασίστε πού βρίσκεται κάθε στήλη. Ένας πίνακας έχει τέσσερις στήλες εδώ, επομένως χρειάζεται τέσσερις αριστερές άκρες και ένα γνωστό δεξιό περιθώριο. Η σκληρή κωδικοποίηση (hard-coding) ενός μαγικού αριθμού σε κάθε κλήση TextOut, όπως συνηθίζουν τα γρήγορα δείγματα, είναι ακριβώς αυτό που καθιστά έναν πίνακα επώδυνο να διευρυνθεί αργότερα. Ονομάστε τις άκρες μία φορά, σε σημεία από την κάτω αριστερή προέλευση, και κάθε κλήση σχεδίασης αναφέρεται σε αυτές με το όνομά τους:

const
  ColNo   = 70;    // left edge of the "No." column
  ColName = 110;   // company name
  ColAddr = 300;   // street address
  ColCity = 480;   // city
  RowLeft = 50;    // table frame: left rule
  RowRight = 570;  // table frame: right rule
  RowStep = 20;    // vertical distance between baselines

procedure PrintRow(Page: THPDFPage; Y: Single;
  const ANo, AName, AAddr, ACity: string; Shaded: boolean);
begin
  if Shaded then
  begin
    // A shaded band behind the row. Rectangle takes X, Y, Width, Height.
    Page.SetRGBFillColor($00FFF3DD);
    Page.Rectangle(RowLeft, Y - 4, RowRight - RowLeft, RowStep);
    Page.Fill;
    Page.SetRGBFillColor(clBlack);
  end;
  Page.TextOut(ColNo,   Y, 0, ANo);
  Page.TextOut(ColName, Y, 0, AName);
  Page.TextOut(ColAddr, Y, 0, AAddr);
  Page.TextOut(ColCity, Y, 0, ACity);
end;

Δύο λεπτομέρειες κερδίζουν τη θέση τους εδώ. Η σκιασμένη ζώνη σχεδιάζεται πρώτη, μετά το κείμενο από πάνω, επειδή η σειρά ζωγραφικής είναι z-order στο PDF: γεμίστε το ορθογώνιο μετά το κείμενο και θάβετε τη σειρά. Και η εναλλασσόμενη σκίαση δεν είναι διακόσμηση για χάρη της διακόσμησης. Σε μια πυκνή αναφορά είναι ο φθηνότερος τρόπος για να σταματήσετε το μάτι από το να γλιστρήσει σε λάθος γραμμή, γι' αυτό και ο βρόχος (loop) αργότερα αναστρέφει ένα boolean σε κάθε σειρά και το περνάει κατευθείαν στο Shaded

Οι θέσεις των στηλών παραπάνω είναι σταθερές, κάτι που είναι ειλικρινές για μια αναφορά της οποίας ελέγχετε το σχήμα. Όταν τα δεδομένα είναι μεταβλητά, μετρήστε αντί να μαντέψετε. Το HotPDF εκθέτει μέτρηση πλάτους κειμένου στο αντικείμενο σελίδας, επομένως η έκδοση παραγωγής του PrintRow μπορεί να λάβει τη μεγαλύτερη αναμενόμενη τιμή σε κάθε στήλη, να την μετρήσει μία φορά στο επιλεγμένο μέγεθος γραμματοσειράς και να εξαγάγει τις αριστερές άκρες από αυτά τα πλάτη συν ένα περιθώριο μεταξύ τους (gutter). Το σχήμα της ρουτίνας δεν αλλάζει. μόνο η πηγή των σταθερών αλλάζει

Η κεφαλίδα, οι κανόνες και ένα μέρος που τους κατέχει

Ένας πίνακας που κυλά έξω από μια σελίδα και συνεχίζει στην επόμενη χωρίς ετικέτες στηλών είναι δυσανάγνωστος. Η λύση είναι να αντιμετωπίζετε την κεφαλίδα ως κάτι που επανασχεδιάζετε, όχι κάτι που σχεδιάζετε μία φορά. Τοποθετήστε τους τίτλους στηλών και τους οριζόντιους κανόνες που τους πλαισιώνουν σε μία ρουτίνα, και καλέστε αυτήν τη ρουτίνα τόσο στην αρχή όσο και ξανά κάθε φορά που ανοίγετε μια νέα σελίδα. Επειδή η κεφαλίδα και το σώμα μοιράζονται τις ίδιες σταθερές στηλών, ευθυγραμμίζονται εκ κατασκευής

procedure DrawHeader(Page: THPDFPage; var Y: Single; PageNo: Integer);
begin
  // Left: source label and page number. Right: generation time.
  Page.SetFont('Arial', [fsItalic], 10);
  Page.TextOut(RowLeft, Y, 0, 'customer.db   Page ' + IntToStr(PageNo));
  Page.TextOut(ColCity, Y, 0, DateTimeToStr(Now));

  // Two horizontal rules that box the column titles.
  Page.MoveTo(RowLeft, Y + 15);
  Page.LineTo(RowRight, Y + 15);
  Page.MoveTo(RowLeft, Y + 45);
  Page.LineTo(RowRight, Y + 45);
  Page.Stroke;

  // The column titles, in a heavier face so they read as headings.
  Page.SetFont('Times New Roman', [fsBold], 12);
  Page.SetRGBFillColor(clNavy);
  PrintRow(Page, Y + 25, 'No.', 'Company', 'Address', 'City', False);
  Page.SetRGBFillColor(clBlack);

  Y := Y + RowStep + 45;  // advance past the boxed header before the first body row
end;

Παρατηρήστε ότι το DrawHeader παίρνει το Y κατά αναφορά και το προωθεί. Ο καλών δεν χρειάζεται ποτέ να θυμάται πόσο ψηλή είναι η κεφαλίδα. Η ρουτίνα που τη σχεδιάζει είναι η ρουτίνα που γνωρίζει. Αυτός ο κανόνας της ενιαίας ιδιοκτησίας είναι αυτός που εμποδίζει τη μετατόπιση της διάταξης όταν αργότερα προσθέσετε ένα λογότυπο ή μια σύνοψη φίλτρου στη ζώνη κεφαλίδας. Ο βρόχος του σώματος παραμένει ανίδεος. Απλώς συνεχίζει να σχεδιάζει σειρές από όπου δείχνει το Y αυτήν τη στιγμή

Οι ίδιοι οι κανόνες (γραμμές) είναι η διαφορά μεταξύ μιας λίστας και ενός πίνακα. Οι κάθετοι διαχωριστές στηλών είναι η ίδια ιδέα που εφαρμόζεται στον άξονα x: ένα MoveTo / LineTo / Stroke σε κάθε άκρη στήλης, τρέχοντας από τον επάνω κανόνα μέχρι το κάτω μέρος της τελευταίας σειράς στη σελίδα. Το δείγμα περιορίζεται σε οριζόντιους κανόνες για να παραμείνει ευανάγνωστο, αλλά το βήμα παραγωγής είναι μηχανικό από τη στιγμή που υπάρχουν οι σταθερές στηλών

Ο βρόχος δρομέα (cursor) κατέχει την αλλαγή σελίδας

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

var
  Pdf: THotPDF;
  Page: THPDFPage;
  Y: Single;
  PageNo: Integer;
  Shaded: boolean;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'CustomerReport.pdf';
    Pdf.BeginDoc;
    Page := Pdf.CurrentPage;

    // Report title, once, at the top of the first page.
    Page.SetFont('Arial', [fsBold], 24);
    Page.TextOut(200, 800, 0, 'Customer Report');

    PageNo := 1;
    Y := 760;
    DrawHeader(Page, Y, PageNo);
    Shaded := False;

    CustomerTable.First;
    while not CustomerTable.Eof do
    begin
      // Out of room? Open a new page and repeat the header there.
      if Y < 60 then
      begin
        Pdf.AddPage;
        Page := Pdf.CurrentPage;   // AddPage moves CurrentPage forward
        Inc(PageNo);
        Y := 760;
        DrawHeader(Page, Y, PageNo);
      end;

      Shaded := not Shaded;
      Page.SetFont('Arial', [], 10);   // SetFont must be reissued on every new page
      PrintRow(Page, Y,
        VarToStr(CustomerTable['CustNo']),
        VarToStr(CustomerTable['Company']),
        VarToStr(CustomerTable['Addr1']),
        VarToStr(CustomerTable['City']),
        Shaded);

      Y := Y - RowStep;
      CustomerTable.Next;
    end;

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Δύο γεγονότα συντεταγμένων καθοδηγούν ολόκληρο τον βρόχο. Το PDF μετράει το y προς τα πάνω από την κάτω αριστερή γωνία, επομένως οι σειρές προχωρούν κάτω στη σελίδα αφαιρώντας το RowStep από το Y κάθε φορά, και η δοκιμή γεμάτης σελίδας ενεργοποιείται όταν το Y πέσει κάτω από το κάτω περιθώριο και όχι πάνω από κάποια κορυφή. Βάλτε την κατεύθυνση ανάποδα και η πρώτη σας σειρά θα εκτυπωθεί έξω από το κάτω άκρο, ενώ ο βρόχος νομίζει ότι έχει χώρο ολόκληρης σελίδας

Το άλλο γεγονός πιάνει σχεδόν τους πάντες μία φορά. Το AddPage δημιουργεί μια νέα σελίδα και επαναπροσανατολίζει το CurrentPage σε αυτήν, αλλά δεν μεταφέρει τίποτα: ούτε τη γραμματοσειρά, ούτε το χρώμα γεμίσματος, ούτε τη θέση. Αυτός είναι ο λόγος για τον οποίο η Page ξαναδιαβάζεται από την CurrentPage μετά από κάθε AddPage, και ο λόγος για τον οποίο το SetFont επανεκδίδεται πριν από τις σειρές του σώματος. Παραλείψτε την εκ νέου ανάγνωση και θα συνεχίσετε να σχεδιάζετε στη σελίδα που μόλις αφήσατε πίσω σας. παραλείψτε τη γραμματοσειρά και η νέα σελίδα θα αποδοθεί σε όποια προεπιλογή καταφύγει το πρόγραμμα προβολής

Οι περιπτώσεις που "σπάνε" έναν εξαγωγέα πίνακα

Τα περισσότερα σφάλματα (bugs) πινάκων δεν εμφανίζονται στο ιδανικό σενάριο (happy path) μερικών δεκάδων τακτοποιημένων σειρών. Ζουν στα άκρα, και τα άκρα είναι φθηνά στη δοκιμή μόλις μάθετε πού βρίσκονται

  • Κενά σύνολα δεδομένων. Ένας βρόχος πάνω από μηδέν σειρές παράγει μια σελίδα με μια κεφαλίδα και τίποτα από κάτω της, κάτι που τουλάχιστον φαίνεται σκόπιμο. Μια κενή σελίδα χωρίς κεφαλίδα μοιάζει με αποτυχία. Αποφασίστε τι θέλετε πριν από την αποστολή (shipping)
  • Η σειρά που προσγειώνεται ακριβώς στο όριο. Δημιουργήστε μια αναφορά της οποίας η τελευταία σειρά βρίσκεται ένα βήμα πάνω από το περιθώριο, μετά μία της οποίας η επόμενη σειρά είναι ένα βήμα κάτω από αυτό. Η σελιδοποίηση με σφάλμα κατά ένα (off-by-one) κρύβεται μέχρι τα δεδομένα να έχουν ακριβώς το λάθος μήκος
  • Πολύ μεγάλες τιμές. Ένα όνομα εταιρείας ευρύτερο από τη στήλη του θα "τρέξει" στην επόμενη. Μετρήστε το πεδίο και αποφασίστε για μια πολιτική: αναδίπλωση σε δεύτερη γραμμή, αποκοπή (clip), ή περικοπή με αποσιωπητικά (ellipsis). Η σιωπή δεν είναι πολιτική
  • Κενά (Null) πεδία. Η απευθείας ανάγνωση ενός null στο TextOut μπορεί να εμφανιστεί ως το κυριολεκτικό κείμενο Null ή ως κενό, ανάλογα με το πώς το μετατρέπετε. Επιλέξτε την απόδοση σκόπιμα αντί να αφήσετε τη μετατροπή της παραλλαγής να επιλέξει για εσάς

Εκτελέστε το αποτέλεσμα μέσα από περισσότερα από ένα προγράμματα προβολής προτού το θεωρήσετε ολοκληρωμένο. Η αντικατάσταση γραμματοσειράς και η αποκοπή (clipping) συμπεριφέρονται διαφορετικά στα διάφορα προγράμματα απόδοσης (renderers), και ένας πίνακας που φαίνεται τετράγωνος σε ένα πρόγραμμα ανάγνωσης PDF μπορεί να εμφανίσει μια μη ευθυγραμμισμένη στήλη ή μια αποκομμένη πόλη σε ένα άλλο. Επιβεβαιώστε ότι η επαναλαμβανόμενη κεφαλίδα, η σκίαση γραμμής και τα περιθώρια επιβιώνουν της κίνησης, και ότι οι αριθμοί σελίδων παραμένουν συνεχείς αφού τα δεδομένα περάσουν ένα όριο

Η σχεδίαση του πλέγματος από εσάς τους ίδιους αντί να στηρίζεστε σε έναν σχεδιαστή οπτικών αναφορών είναι περισσότερος κώδικας, και ο συμβιβασμός αξίζει να ονομαστεί ξεκάθαρα: κατέχετε κάθε συντεταγμένη, το οποίο είναι ακριβώς αυτό που θέλετε για μαζικές εργασίες από την πλευρά του διακομιστή (server-side batch jobs), τιμολόγια και εξαγωγές ελέγχου που πρέπει να αποδίδονται πανομοιότυπα σε κάθε μηχάνημα, και ακριβώς η επιβάρυνση που θα προτιμούσατε να αποφύγετε για μια μεμονωμένη εσωτερική λίστα. Για το πρώτο, ο έλεγχος αποσβένει την αξία του την πρώτη φορά που μια αναφορά πρέπει να φαίνεται ίδια στην παραγωγή (production) όπως φαινόταν στο γραφείο σας

Οι κανόνες (γραμμές) και οι σκιασμένες ζώνες παραπάνω στηρίζονται στα ίδια θεμελιώδη διανύσματα και χρώματα που καλύπτονται στην περιήγηση σχεδίασης καμβά (canvas drawing walkthrough), εάν θέλετε οι κλήσεις Rectangle, MoveTo και LineTo να αντιμετωπιστούν πρώτα μόνες τους. Τα θεμελιώδη στοιχεία σχεδίασης που χρησιμοποιούνται εδώ αποτελούν μέρος του Στοιχείου HotPDF για Delphi και C++Builder