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

Υλοποίηση της μορφής προχείρου CF_HTML σε Delphi

Αντιγράψτε ένα εύρος από ένα πλέγμα Delphi και επικολλήστε το στο Word, και η μορφοποίηση συνήθως εξαφανίζεται: απλό κείμενο, χωρίς έντονες επικεφαλίδες, χωρίς περιγράμματα, χωρίς γεμίσματα. Το HotXLS κλείνει εκείνο το κενό με το TXLSRange.CopyToClipboard, το οποίο τοποθετεί ένα φορτίο προχείρου CF_HTML — τη μορφή των Windows για μορφοποιημένο HTML με δείκτες θραύσματος ακριβείς στο byte — στο πρόχειρο δίπλα σε απλό κείμενο Unicode

Αυτό ακούγεται απλό μέχρι να δείτε τι πράγματι απαιτεί ένα φορτίο CF_HTML. Η μορφή χρειάζεται μια σύντομη κεφαλίδα κειμένου που ονομάζει ακριβώς πού ξεκινά και πού τελειώνει το θραύσμα μέσα στο μεγαλύτερο buffer προχείρου, και εκείνες οι θέσεις είναι μετατοπίσεις byte, μετρημένες μέσα από όποια κωδικοποίηση πολλαπλών byte καταλήγει να έχει το HTML. Κάντε λάθος στην αριθμητική έστω κατά ένα byte και η εφαρμογή στόχος είτε αρπάζει τη λάθος φέτα markup είτε τα παρατά και επιστρέφει σε απλό κείμενο, και καμία από τις δύο αποτυχίες δεν μοιάζει με σφάλμα στον κώδικά σας — μοιάζει με το Word να είναι Word

Γιατί η αντιγραφή-επικόλληση από ένα πλέγμα Delphi συνήθως χάνει τη μορφοποίησή της

Η προεπιλεγμένη κλήση προχείρου των Windows που καταφεύγει ο περισσότερος κώδικας Delphi, το SetClipboardData με CF_TEXT ή CF_UNICODETEXT, μεταφέρει πάντα μόνο απλούς χαρακτήρες, οπότε οποιαδήποτε μορφοποίηση εφαρμόστηκε στο πλέγμα προέλευσης δεν έχει πού να πάει. Το Word, το Outlook, και κάθε πρόγραμμα περιήγησης βασισμένο σε Chromium αναζητούν μια πλουσιότερη μορφή όταν επικολλάτε: μια αναπαράσταση HTML της επιλογής, πλήρη με ενσωματωμένα στυλ, δομή πίνακα, και συνδέσμους. Το ίδιο το Excel βασίζεται ακριβώς σε αυτό το κόλπο — αντιγράψτε ένα εύρος στο Excel και το πρόχειρο λαμβάνει σιωπηλά πολλές μορφές ταυτόχρονα, HTML μεταξύ αυτών, οπότε όποια εφαρμογή κι αν επικολλήσετε επιλέγει την πλουσιότερη που κατανοεί. Ένα εξάρτημα που γράφει πάντα μόνο CF_UNICODETEXT δίνει σε καθέναν από εκείνους τους πλουσιότερους καταναλωτές τίποτα με το οποίο να δουλέψει, και η οπτική πλούτος που μόλις αντέγραψε ο χρήστης απλά δεν είναι εκεί για επικόλληση

Τι ακριβώς είναι η μορφή προχείρου CF_HTML;

Το CF_HTML δεν είναι μια σταθερή μορφή προχείρου συστήματος όπως το CF_TEXT· είναι δυναμικά καταχωρημένη, ζητούμενη με το όνομά της μέσω RegisterClipboardFormat('HTML Format'), και το φορτίο της είναι μια σύντομη κεφαλίδα ASCII ακολουθούμενη από ένα έγγραφο ή θραύσμα HTML. Η κεφαλίδα φέρει πέντε πεδία — Version, StartHTML, EndHTML, StartFragment, EndFragment — όπου το Version είναι πάντα 0.9 και τα άλλα τέσσερα είναι δεκαδικοί αριθμοί γραμμένοι ως ψηφία ASCII. Τα StartHTML και EndHTML οριοθετούν ολόκληρο το έγγραφο όπως η λαμβάνουσα εφαρμογή θα έπρεπε να το αναλύσει για πλαίσιο, γραμματοσειρές και στυλ συμπεριλαμβανομένων, ενώ τα StartFragment και EndFragment οριοθετούν τη στενότερη φέτα που πράγματι προσγειώνεται στον δρομέα, συμβατικά σημειωμένη στο ίδιο το markup με σχόλια <!--StartFragment--> και <!--EndFragment--> ώστε τα όρια να επιβιώνουν από αφελή επανασειριοποίηση

Μετατοπίσεις byte, όχι μετρήσεις χαρακτήρων: η κλασική παγίδα CF_HTML

Τα τέσσερα αριθμητικά πεδία κεφαλίδας του CF_HTML είναι μετατοπίσεις byte μέσα στην ακριβή ακολουθία byte που κάθεται στο πρόχειρο, μετρημένα από τον πρώτο χαρακτήρα της ίδιας της κεφαλίδας — όχι μετρήσεις χαρακτήρων, όχι σημεία κώδικα Unicode, και όχι μετατοπίσεις σχετικές με το θραύσμα ή την ετικέτα <body>. Αυτή η διάκριση είναι το σημείο όπου χειρόγραφες υλοποιήσεις CF_HTML σιωπηλά πηγαίνουν στραβά: το Length μιας UnicodeString στη Delphi αναφέρει μονάδες κώδικα UTF-16, που τυχαίνει να ισούται με τον αριθμό byte για απλό κείμενο ASCII, οπότε το σφάλμα περνά καθαρό μέσα από οποιαδήποτε δοκιμή γραμμένη με αγγλικά δείγματα δεδομένων και εμφανίζεται μόνο μόλις ένα αντιγραμμένο κελί κρατά μια παύλα em, ένα σύμβολο νομίσματος, ή έναν τονισμένο χαρακτήρα — ένα σύμβολο ευρώ είναι μία μονάδα κώδικα UTF-16 αλλά τρία byte σε UTF-8, και κάθε μετατόπιση υπολογισμένη μετά από εκείνο το σημείο παρεκκλίνει κατά όσα επιπλέον byte πρόσθεσε η κωδικοποίηση. Η αποτυχία που ακολουθεί δεν είναι κατάρρευση· είναι η λαμβάνουσα εφαρμογή να αρπάζει το ακριβές εύρος byte στο οποίο έδειχνε η κεφαλίδα, να βρίσκει μια φέτα markup που ξεκινά ή τελειώνει στη μέση μιας ετικέτας, και είτε να αποδίδει σκουπίδια είτε να τα παρατά και να επιστρέφει σε όποιο απλό κείμενο κάθεται δίπλα του στο πρόχειρο, σιωπηλά, χωρίς τίποτα στον κώδικά σας να εξηγεί γιατί — να το σχήμα κώδικα που παράγει ακριβώς αυτή την αποτυχία

// Fragile: Length() on a UnicodeString counts UTF-16 code units, not bytes
var
  Header: string;
  Fragment: string;
  StartFragmentOfs: Integer;
begin
  Header := 'Version:0.9'#13#10 + 'StartHTML:0000000000'#13#10 + '...';
  StartFragmentOfs := Length(Header) + Pos('<!--StartFragment-->', Fragment);
  // A currency symbol, an em dash, or any accented character placed
  // before this point costs one character here but two or three bytes
  // once the document is UTF-8 encoded, so StartFragmentOfs now points
  // short of where the fragment actually begins on the real clipboard
end;

Πώς κρατά το HotXLS την κεφαλίδα ακριβή σε byte

Το HotXLS αποφεύγει αυτή την κατηγορία σφάλματος δομικά: το TXLSRange.CopyToClipboard και η μονάδα lxClipboard από κάτω του χτίζουν το έγγραφο CF_HTML και την κεφαλίδα του εξ ολοκλήρου ως AnsiString, τον τύπο συμβολοσειράς byte της Delphi, οπότε τα Length και Pos ήδη επιστρέφουν θέσεις byte παντού στον υπολογισμό — δεν υπάρχει ξεχωριστό βήμα, και επομένως κανένα βήμα προς λήθη, όπου μια μέτρηση χαρακτήρων Unicode θα χρειαζόταν μετατροπή σε μέτρηση byte πριν μπει στην κεφαλίδα

Υπάρχει ένα δεύτερο, μικρότερο κόλπο που αξίζει να γνωρίζετε αν ποτέ χτίσετε μια κεφαλίδα CF_HTML με το χέρι. Η κεφαλίδα γράφεται δύο φορές: μία με δέκα μηδενικά ψηφία να αντικαθιστούν καθεμία από τις τέσσερις μετατοπίσεις, ώστε το δικό της μήκος byte να μπορεί να μετρηθεί, και μία ακόμη με τις πραγματικές μετατοπίσεις μπαλωμένες μέσα. Επειδή κάθε πραγματική μετατόπιση μορφοποιείται σε εκείνο το ίδιο σταθερό πλάτος δέκα ψηφίων, η δεύτερη κεφαλίδα βγαίνει byte-προς-byte με το ίδιο μήκος με την έκδοση placeholder, κάτι που εξηγεί ακριβώς γιατί η προηγούμενη μέτρηση παραμένει έγκυρη μετά την επανεγγραφή. Παραλείψτε το σταθερό πλάτος, μορφοποιήστε έναν αριθμό με απλό IntToStr αντ' αυτού, και η κεφαλίδα μπορεί να συρρικνωθεί ή να μεγαλώσει κατά ένα ψηφίο μεταξύ των δύο περασμάτων, ακυρώνοντας σιωπηλά κάθε μετατόπιση που την ακολουθεί

const
  Placeholder = '0000000000';   // 10 ASCII digits: fixed width in, fixed width out
var
  Header: AnsiString;           // AnsiString.Length is a byte count, not a char count
  StartHtmlOfs: Integer;
begin
  Header := 'Version:0.9'#13#10 +
    'StartHTML:' + Placeholder + #13#10 +
    'EndHTML:' + Placeholder + #13#10 +
    'StartFragment:' + Placeholder + #13#10 +
    'EndFragment:' + Placeholder + #13#10;
  StartHtmlOfs := Length(Header);   // safe to measure once, up front
  // ...compute the real offsets against the AnsiString document...
  // then rebuild Header with the real numbers formatted to the same
  // 10-digit width, so its byte length -- and therefore StartHtmlOfs --
  // never moves between the placeholder pass and the final one
end;

Γιατί το φορτίο απλού κειμένου εξακολουθεί να πρέπει να συνοδεύει

Το TXLSRange.CopyToClipboard ποτέ δεν τοποθετεί το CF_HTML στο πρόχειρο μόνο του· πάντα γράφει CF_UNICODETEXT στην ίδια κλήση, επειδή το CF_HTML είναι μια καταχωρημένη μορφή παρά μία από τις σταθερές σταθερές CF_* που κάθε εφαρμογή Windows ήδη ξέρει να αναζητά — ένας απλός επεξεργαστής κειμένου, ένα παλαιό πλέγμα, ή οτιδήποτε ποτέ δεν έλεγξε για 'HTML Format' δεν θα το δει καθόλου, και το εύρος που αντιγράψατε είτε φτάνει ως κείμενο διαχωρισμένο με tab είτε δεν φτάνει. Εκείνο το κείμενο διαχωρισμένο με tab δεν είναι ούτε καν μια πρόχειρη προσέγγιση: τα κελιά τύπου αντιγράφονται ως η συμβολοσειρά τύπου τους με ένα αρχικό = αποκατεστημένο αν το αποθηκευμένο κείμενο το είχε παραλείψει, ταιριάζοντας με το πώς συμπεριφέρεται το κείμενο προχείρου του ίδιου του Excel, τα συνηθισμένα κελιά αντιγράφουν το FormattedText τους — τη συμβολοσειρά όπως εμφανίζεται, οπότε ένα κελί νομίσματος αντιγράφεται ως $1,234.56, όχι το υποκείμενο 1234.56 — και οποιοδήποτε πεδίο περιέχει tab, εισαγωγικό, ή αλλαγή γραμμής μπαίνει σε εισαγωγικά με τα ενσωματωμένα εισαγωγικά διπλασιασμένα, την ίδια σύμβαση που χρησιμοποιεί το CSV

Το SaveAsHTML δεν είναι μια ξεχωριστή διαδρομή απόδοσης βιδωμένη μόνο για την περίπτωση προχείρου. Το CopyToClipboard καλεί ακριβώς τον ίδιο γραφέα HTML που περιγράφεται στην εξαγωγή CSV, TSV, και HTML του HotXLS, έπειτα τυλίγει ό,τι παράγει εκείνος ο γραφέας στον φάκελο CF_HTML αντί να το αποθηκεύει ως αυτοτελές αρχείο, οπότε οτιδήποτε ισχύει για εκείνο το HTML περνά κατευθείαν σε ό,τι προσγειώνεται στο πρόχειρο. Το να τραβήξετε μαζί ένα εύρος φύλλου εργασίας ως αμφότερες τις μορφές σε μία κλήση μοιάζει έτσι

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly-report.xlsx');
    // Classic TXLSWorkbook ranges expose the identical method as
    // Workbook.Sheets[1].Range['A1', 'F40'].CopyToClipboard
    if Book.Sheets[1].Range['A1:F40'].CopyToClipboard then
      ShowMessage('Range copied - press Ctrl+V in Word or a browser')
    else
      ShowMessage('Clipboard was busy; see the retry pattern below');
  finally
    Book.Free;
  end;
end;

Κρατά το επικολλημένο εύρος τις γραμματοσειρές, τα χρώματα, και τα συγχωνευμένα κελιά του;

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

Επαλήθευση της επικόλλησης, και χειρισμός ενός απασχολημένου προχείρου

Δύο συνήθειες πιάνουν τα περισσότερα προβλήματα προχείρου πριν το κάνει ένας πελάτης. Επικολλήστε πρώτα στο Notepad για να επιβεβαιώσετε ότι η εφεδρεία CF_UNICODETEXT είναι λογικό κείμενο διαχωρισμένο με tab, έπειτα επικολλήστε την ίδια αντιγραφή στο Word ή σε πρόγραμμα περιήγησης για να επιβεβαιώσετε ότι εμφανίζεται η μορφοποιημένη έκδοση — ένα φορτίο που φαίνεται σωστό στο ένα και λάθος στο άλλο συνήθως σημαίνει ότι οι δείκτες θραύσματος προσγειώθηκαν στη λάθος θέση. Έπειτα αντιμετωπίστε το boolean αποτέλεσμα που επιστρέφει το CopyToClipboard ως ουσιαστικό, όχι διακοσμητικό: το OpenClipboard μπορεί να αποτύχει όταν μια άλλη διεργασία κρατά το πρόχειρο ανοιχτό, αρκετά συνηθισμένο σε μια απασχολημένη επιφάνεια εργασίας ώστε μία μη ελεγμένη κλήση τελικά να μην επικολλά τίποτα χωρίς σφάλμα να εξηγεί γιατί, κάτι από το οποίο προστατεύει η παρακάτω επανάληψη

function TryCopyRangeToClipboard(Workbook: TXLSXWorkbook): Boolean;
var
  Attempt: Integer;
begin
  Result := False;
  for Attempt := 1 to 5 do
  begin
    Result := Workbook.Sheets[1].Range['A1:F40'].CopyToClipboard;
    if Result then
      Break;
    Sleep(50);   // give whichever app is holding the clipboard a moment
  end;
  if not Result then
    raise Exception.Create('Could not take ownership of the clipboard');
end;

Η ίδια η μορφή δεν είναι εξωτική μόλις η κεφαλίδα είναι ακριβής σε byte και η εφεδρεία απλού κειμένου είναι ειλικρινής για το τι περιέχει — υπάρχει σε μεγάλο βαθμό αμετάβλητη από τότε που ο Internet Explorer την όρισε πρώτη φορά, και κάθε μεγάλη εφαρμογή Windows εξακολουθεί να τη διαβάζει με τον ίδιο τρόπο. Το CopyToClipboard κάθεται δίπλα στο PasteFromClipboard, την πλευρά ανάγνωσης της ίδιας ανταλλαγής, στην ευρύτερη επιφάνεια προχείρου και εξαγωγής που τεκμηριώνεται στη σελίδα προϊόντος εξαρτήματος HotXLS