Το HotXLS, η εγγενής βιβλιοθήκη Excel για Delphi και C++Builder, είναι κατασκευασμένο για απώλεια-ελεύθερους κύκλους XLSX (lossless round-trips): ανοίξτε ένα βιβλίο εργασίας, αλλάξτε ένα κελί, αποθηκεύστε, και το προσαρμοσμένο θέμα (theme) του πελάτη, τα άγνωστα μπλοκ επεκτάσεων extLst και η αλυσίδα υπολογισμών (calculation chain) θα επιβιώσουν όλα. Τρεις μηχανισμοί το κάνουν αυτό να λειτουργεί — η αυτολεξεί προσωρινή αποθήκευση (caching) του xl/theme/theme1.xml, η βασισμένη σε συμβάντα επανασειριοποίηση των άγνωστων μπλοκ <ext>, και ένα νέο, έγκυρο σύμφωνα με τις προδιαγραφές xl/calcChain.xml σε κάθε αποθήκευση ενός βιβλίου εργασίας με τύπους
Το σενάριο που παρακινεί και τους τρεις μηχανισμούς είναι απογοητευτικά κοινό. Μια υπηρεσία τιμολόγησης φορτώνει ένα πρότυπο που σχεδίασε ο πελάτης στο Excel — εταιρικό χρωματικό θέμα, sparklines σε μια στήλη KPI, έναν κανόνα υπό όρους μορφοποίησης που προστέθηκε από μια νεότερη έκδοση του Excel — γράφει ένα σύνολο τιμολογίου στο κελί B3 και αποθηκεύει. Ο πελάτης ανοίγει το αποτέλεσμα και τα χρώματα της μάρκας έχουν επανέλθει στο τυπικό μπλε του Office, τα sparklines έχουν εξαφανιστεί και το Excel προσφέρεται να "διορθώσει" το αρχείο. Τίποτα στον κώδικα δεν άγγιξε κανένα από αυτά τα χαρακτηριστικά. Η βιβλιοθήκη το έκανε, απλώς αποθηκεύοντας
Γιατί τα αρχεία Excel χάνουν τη μορφοποίηση μετά από επεξεργασίες με βιβλιοθήκες;
Τα αρχεία Excel χάνουν τη μορφοποίησή τους μετά από επεξεργασίες με βιβλιοθήκες επειδή οι περισσότερες βιβλιοθήκες δεν επεξεργάζονται το αρχείο — το ανακατασκευάζουν. Ένα πακέτο .xlsx είναι ένα αρχείο ZIP από τμήματα XML: xl/workbook.xml, ένα xl/worksheets/sheetN.xml ανά φύλλο, xl/styles.xml, xl/theme/theme1.xml, xl/calcChain.xml και άλλα. Μια τυπική βιβλιοθήκη αναλύει αυτά τα τμήματα σε ένα μοντέλο αντικειμένων κατά το άνοιγμα και αναγεννά κάθε τμήμα από αυτό το μοντέλο κατά την αποθήκευση. Οποιοδήποτε χαρακτηριστικό δεν αντιπροσωπεύεται από το μοντέλο — ένα θέμα που δεν αναλύθηκε ποτέ, ένα μπλοκ επεκτάσεων από ένα νεότερο Excel — δεν έχει πού να ζήσει στη μνήμη, οπότε το αναγεννημένο τμήμα το παραλείπει σιωπηρά
Το πρότυπο ECMA-376 είχε προβλέψει το μισό αυτού του προβλήματος. Η γλώσσα SpreadsheetML ορίζει το στοιχείο extLst (ECMA-376 Μέρος 1, η "Περιοχή Αποθήκευσης Δεδομένων Μελλοντικών Χαρακτηριστικών", §18.2.10 για το στοιχείο επιπέδου βιβλίου εργασίας) ως ένα καθορισμένο σημείο επέκτασης: οι νεότεροι παραγωγοί τοποθετούν εκεί χαρακτηριστικά, καθένα τυλιγμένο σε ένα στοιχείο <ext> που φέρει ένα χαρακτηριστικό uri που ταυτοποιεί το χαρακτηριστικό, και οι παλαιότεροι καταναλωτές αναμένεται να διατηρήσουν ό,τι δεν κατανοούν. Τα sparklines, οι slicers και οι νεότεροι τύποι μορφοποίησης υπό όρους ταξιδεύουν όλοι με αυτόν τον τρόπο. Μια βιβλιοθήκη που απορρίπτει άγνωστα μπλοκ <ext> δεν είναι απλώς απωλεστική — παραβιάζει το συμβόλαιο συμβατότητας προς τα εμπρός γύρω από το οποίο σχεδιάστηκε η μορφή αρχείου. Η ερώτηση που πρέπει να θέσετε σε οποιαδήποτε βιβλιοθήκη υπολογιστικών φύλλων αξιολογείτε είναι ωμή: αν αλλάξω ένα κελί, τι άλλο αλλάζει
Πώς το HotXLS διατηρεί ένα προσαρμοσμένο θέμα byte-προς-byte;
Το HotXLS διατηρεί το θέμα ενός βιβλίου εργασίας αποθηκεύοντας προσωρινά τα αρχικά bytes του xl/theme/theme1.xml κατά το άνοιγμα και γράφοντάς τα πίσω αυτολεξεί κατά την αποθήκευση. Το τμήμα θέματος (ECMA-376 Μέρος 1, §14.2.7) είναι DrawingML, not SpreadsheetML — συνδυασμοί χρωμάτων, γραμματοσειρών, μορφοποίησης — και μια μηχανή υπολογιστικών φύλλων δεν έχει λόγο να το μοντελοποιήσει σε βάθος. Οι παλαιότερες εκδόσεις του HotXLS αναγεννούσαν ένα σταθερό θέμα Office σε κάθε αποθήκευση, κάτι που αποτελεί ακριβώς την αστοχία επαναφοράς των εταιρικών χρωμάτων που αναφέρθηκε παραπάνω· από την έκδοση v2.89.46 το θέμα του ανοιγμένου πακέτου αποθηκεύεται σε ακατέργαστη μορφή και εκπέμπεται ξανά ανέπαφο, ενώ το ενσωματωμένο θέμα Office δημιουργείται μόνο για βιβλία εργασίας που δημιουργούνται από το μηδέν. Τα ακατέργαστα bytes είναι η ισχυρότερη δυνατή εγγύηση πιστότητας: καμία ανάλυση, καμία επανασειριοποίηση, καμία πιθανότητα απόκλισης
Το αυτολεξεί αντίγραφο κερδίζει σκόπιμα έναντι της προγραμματιστικής πρόσβασης στο θέμα. Η TXLSXWorkbook εκθέτει τις ThemeMajorFont και ThemeMinorFont ώστε να μπορείτε να επιλέγετε γραμματοσειρές επικεφαλίδων και σώματος για νέα βιβλία εργασίας, αλλά όταν ένα αυτολεξεί θέμα καταγράφεται κατά το άνοιγμα, αυτοί οι setters δεν έχουν καμία επίδραση στο αποθηκευμένο αρχείο — ο κύκλος round-trip έχει προτεραιότητα. Εάν χρειάζεται πραγματικά να τροποποιήσετε το θέμα ενός υπάρχοντος βιβλίου εργασίας, αυτό είναι ένα σήμα για να επεξεργαστείτε το πρότυπο στο ίδιο το Excel και όχι μέσω ενός API προσανατολισμένου στα δεδομένα. Η καθημερινή περίπτωση δεν χρειάζεται καθόλου API:
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('branded-invoice.xlsx');
Book.Sheets[0].Cells[3, 2].Value := 42750.00; // the one edit
Book.SaveAs('branded-invoice-out.xlsx');
// theme1.xml in the output is byte-identical to the input
finally
Book.Free;
end;
end;
Τι συμβαίνει με τα άγνωστα μπλοκ extLst κατά την αποθήκευση;
Το HotXLS καταγράφει κάθε μπλοκ <ext> επιπέδου φύλλου εργασίας που δεν μοντελοποιεί εγγενώς και το αναπαράγει στο extLst του αποθηκευμένου φύλλου εργασίας, έτσι ώστε τα χαρακτηριστικά που γράφονται από νεότερες εκδόσεις του Excel να επιβιώνουν ανέπαφα από το round-trip. Από την έκδοση v2.131.0, τα καταγεγραμμένα τμήματα είναι ορατά μέσω της ιδιότητας μόνο για ανάγνωση RawWorksheetExts, μιας TStringList σε κάθε φύλλο εργασίας XLSX, γεγονός που καθιστά την εγγύηση ελέγξιμη από τον κώδικα δοκιμών αντί για πράξη πίστης:
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
i: Integer;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('from-newer-excel.xlsx');
Sheet := Book.Sheets[0];
WriteLn(Format('%d foreign ext block(s) captured',
[Sheet.RawWorksheetExts.Count]));
for i := 0 to Sheet.RawWorksheetExts.Count - 1 do
WriteLn(Copy(Sheet.RawWorksheetExts[i], 1, 100)); // peek at each uri
finally
Book.Free;
end;
end;
Η λεπτομέρεια υλοποίησης που αξίζει να γνωρίζετε είναι ότι η καταγραφή είναι μια επανασειριοποίηση σε επίπεδο συμβάντος, όχι μια ακατέργαστη αντιγραφή byte. Ο αναγνώστης ροής XML του HotXLS δεν εκθέτει μετατοπίσεις προέλευσης (source offsets), επομένως το άγνωστο υποδέντρο ανακατασκευάζεται από συμβάντα Element, Text και EndElement καθώς ρέουν. Αυτή η προσέγγιση κρύβει μια κλασική παγίδα: ένα αυτο-κλειόμενο στοιχείο όπως το <a/> πυροδοτεί μόνο ένα συμβάν Element με σημαία empty και ποτέ ένα EndElement, επομένως οποιοσδήποτε μετρητής βάθους που μειώνεται αποκλειστικά στο EndElement δεν θα δει ποτέ το υποδέντρο να κλείνει. Αντιμετωπίστε το αυτό, και το ανακατασκευασμένο τμήμα είναι σημασιολογικά ισοδύναμο με το αρχικό — τα εισαγωγικά των ιδιοτήτων και οι αυτο-κλειόμενες μορφές κανονικοποιούνται, οπότε δεν είναι byte-προς-byte πανομοιότυπο, αλλά το Excel διαβάζει τη σημασία, όχι τα bytes. Δύο ιδιότητες της ίδιας της εξόδου του Excel καθιστούν την αναπαραγωγή ασφαλή: το Excel δηλώνει τα απαραίτητα χαρακτηριστικά xmlns στο στοιχείο <ext> ή μέσα σε αυτό, οπότε κάθε καταγεγραμμένο τμήμα είναι αυτόνομο ως προς το namespace, και αυτή η ίδια αυτονομία είναι ο λόγος για τον οποίο η διπλότυπη δημιουργία ενός φύλλου εργασίας εντός ή μεταξύ βιβλίων εργασίας μπορεί να μεταφέρει τα ξένα μπλοκ μαζί με μια απλή ανάθεση λίστας συμβολοσειρών
Συγγραφή του calcChain.xml ώστε το Excel να εμπιστεύεται τους τύπους σας
Το HotXLS γράφει το xl/calcChain.xml (το τμήμα Calculation Chain, ECMA-376 Μέρος 1, §12.3.1) κάθε φορά που το αποθηκευμένο βιβλίο εργασίας περιέχει τύπους, και επιλέγει μεταξύ δύο διατάξεων. Εάν ο γράφος εξαρτήσεων τύπων έχει ήδη κατασκευαστεί και είναι ενημερωμένος — καλέσατε τη Recalculate μετά την τελευταία σας επεξεργασία — η αλυσίδα εκπέμπεται σε πλήρη τοπολογική σειρά, οι εξαρτήσεις πριν από τα εξαρτώμενα κελιά, με τυχόν μέλη κυκλικής αναφοράς να προσαρτώνται στο τέλος. Διαφορετικά, τα κελιά παρατίθενται με τη σειρά του εγγράφου. Και τα δύο είναι σωστά: οι σημειώσεις υλοποίησης της Microsoft για τη μορφή αρχείου, [MS-XLSX], αντιμετωπίζουν την αλυσίδα υπολογισμών ως υπόδειξη (hint) την οποία το Excel επαληθεύει και αναδιατάσσει κατά τη φόρτωση, οπότε οποιαδήποτε πλήρης καταγραφή είναι νόμιμη, και το HotXLS αρνείται σκόπιμα να επιβάλει την κατασκευή γράφου μέσα στη SaveAs — η κατασκευή ακμών είναι τετραγωνική ως προς τον αριθμό των κελιών, ένα μη αποδεκτό κρυφό κόστος σε μια αποθήκευση εκατομμυρίων κελιών
Book.Open('model.xlsx');
Book.Sheets[0].Cells[10, 4].Formula := '=SUM(D2:D9)';
// Saved now, calcChain.xml lists formula cells in document order.
// After Recalculate the dependency graph exists, so the same save
// emits a full topological order instead:
Book.Recalculate;
Book.SaveAs('model-out.xlsx');
Γιατί να ενδιαφέρεστε για ένα τμήμα που το Excel αντιμετωπίζει ως συμβουλευτικό; Επειδή η απουσία του είναι ένα σήμα. Ορισμένοι καταναλωτές — ευρετικές μέθοδοι επιδιόρθωσης, θεατές τρίτων, εργαλεία diff — αναμένουν από ένα βιβλίο εργασίας με τύπους να φέρει μια αλυσίδα υπολογισμών, και μια βιβλιοθήκη που απορρίπτει σιωπηρά αυτό το τμήμα κατά την αποθήκευση παράγει αρχεία που διαφέρουν ανεπαίσθητα από οτιδήποτε γράφει το Excel. Η εκπομπή μιας έγκυρης αλυσίδας διατηρεί την έξοδο εντός των ορίων αυτού για το οποίο έχει δοκιμαστεί το υπόλοιπο οικοσύστημα, που είναι ο ήσυχος, μη λαμπερός πυρήνας της μηχανικής round-trip
Πού τελειώνει ο απώλεια-ελεύθερος κύκλος round-trip
Η ειλικρίνεια έχει μεγαλύτερη σημασία από ένα πλαίσιο ελέγχου μάρκετινγκ εδώ, οπότε τα όρια αξίζουν εξίσου αναφορά. Το HotXLS δεν αντιγράφει ολόκληρο το πακέτο byte-προς-byte: το XML του φύλλου εργασίας, τα στυλ, οι κοινόχρηστες συμβολοσειρές και τα τμήματα του βιβλίου εργασίας αναγεννώνται από το αναλυμένο μοντέλο, οπότε η έξοδος είναι σημασιολογικά πιστή αλλά όχι δυαδικά πανομοιότυπη — οι τοπικές κεφαλίδες του ZIP και μόνο φέρουν νέες χρονικές σημάνσεις DOS. Τα καταγεγραμμένα τμήματα <ext> επιστρέφουν κανονικοποιημένα, όπως περιγράφεται παραπάνω. Οι προγραμματιστικές υπερισχύσεις γραμματοσειρών θέματος αγνοούνται όταν υπάρχει ένα αυτολεξεί θέμα. Και το δίχτυ διατήρησης έχει συγκεκριμένο μέγεθος: χαρακτηριστικά που το HotXLS μοντελοποιεί εγγενώς (τα sparklines, για παράδειγμα, αναλύονται και ξαναγράφονται αντί να αντιγράφονται τυφλά) συν το ξένο περιεχόμενο extLst συν τα τμήματα που έχουν αποθηκευτεί προσωρινά αυτολεξεί. Ένα τμήμα που δεν είναι ούτε μοντελοποιημένο ούτε βρίσκεται μέσα σε ένα σημείο επέκτασης — για παράδειγμα, το προσαρμοσμένο τμήμα ενός εξωτικού πρόσθετου — εμπίπτει εκτός των τριών μηχανισμών που καλύπτει αυτό το άρθρο, οπότε δοκιμάστε τα πραγματικά σας πρότυπα αντί να κάνετε υποθέσεις
Οι γειτονικές εργασίες διατήρησης συμπληρώνουν την εικόνα. Τα έργα VBA και οι εξωτερικές αναφορές βιβλίων εργασίας διατηρούνται κατά την αποθήκευση με την ίδια φιλοσοφία "κρατήστε ό,τι δεν μοντελοποιείτε", που καλύπτεται στο συνοδευτικό άρθρο σχετικά με τη διατήρηση έργων VBA και εξωτερικών συνδέσμων, και οι ιδιότητες εγγράφου στο docProps έχουν το δικό τους API ανάγνωσης-συγγραφής αντί να απορρίπτονται σιωπηρά. Όταν αξιολογείτε οποιαδήποτε βιβλιοθήκη υπολογιστικών φύλλων, εκτελέστε τη δοκιμή του ενός κελιού: ανοίξτε ένα πλούσιο σε χαρακτηριστικά βιβλίο εργασίας παραγωγής, αλλάξτε μια μεμονωμένη τιμή, αποθηκεύστε, και συγκρίνετε (diff) τα αποσυμπιεσμένα τμήματα με το πρωτότυπο. Το τι άλλαξε πέρα από το φύλλο που αγγίξατε σας λέει περισσότερα για τη βιβλιοθήκη από οποιονδήποτε πίνακα χαρακτηριστικών
Οι μηχανισμοί round-trip που περιγράφονται εδώ — η αυτολεξεί διατήρηση θέματος από την έκδοση v2.89.46, η καταγραφή ξένου περιεχομένου extLst και η εκπομπή του calcChain.xml από την έκδοση v2.131.0 — αποστέλλονται στο τρέχον HotXLS Delphi Excel Component, του οποίου η σελίδα προϊόντος τεκμηριώνει το πλήρες σύνολο χαρακτηριστικών ανάγνωσης-συγγραφής XLSX για Delphi και C++Builder