העתק טווח מתוך grid ב-Delphi והדבק אותו ל-Word, והעיצוב בדרך כלל נעלם: טקסט פשוט, ללא כותרות מודגשות, ללא גבולות, ללא מילויים. HotXLS סוגרת את הפער הזה עם TXLSRange.CopyToClipboard, שמניחה payload ללוח מסוג CF_HTML — פורמט Windows עבור HTML מעוצב עם סמני fragment מדויקי-בייט — על הלוח לצד טקסט יוניקוד פשוט
זה נשמע פשוט עד שאתה מסתכל על מה ש-payload מסוג CF_HTML בפועל דורש. הפורמט זקוק לכותרת טקסט קצרה ששמה בדיוק איפה ה-fragment מתחיל ומסתיים בתוך מאגר הלוח הגדול יותר, והמיקומים הללו הם היסטי בייט, נספרים דרך כל קידוד רב-בייט שה-HTML מסתיים בו. טעה בחשבון בבייט אחד ואפליקציית היעד או תופסת את פיסת ה-markup הלא-נכונה או מוותרת ונופלת בחזרה לטקסט פשוט, ואף אחד מהכשלים הללו לא נראה כמו באג בקוד שלך — הוא נראה כמו Word שהוא Word
למה העתק-הדבק מ-grid ב-Delphi בדרך כלל מאבד את העיצוב שלו
קריאת הלוח הבררת-מחדל של Windows שרוב הקוד ב-Delphi פונה אליה, SetClipboardData עם CF_TEXT או CF_UNICODETEXT, אף פעם לא נושאת אלא תווים פשוטים, כך שכל עיצוב שיושם ב-grid המקור אין לו לאן ללכת. Word, Outlook, וכל דפדפן מבוסס-Chromium מחפשים פורמט עשיר יותר כשאתה מדביק: ייצוג HTML של הבחירה, שלם עם סגנונות inline, מבנה טבלה, וקישורים. Excel עצמה נשענת בדיוק על התחבולה הזו — העתק טווח ב-Excel והלוח בשקט מקבל כמה פורמטים בבת אחת, HTML ביניהם, כך שכל אפליקציה שאתה מדביק אליה בוחרת את העשיר ביותר שהיא מבינה. רכיב שאף פעם לא כותב אלא CF_UNICODETEXT מוסר לכל אחד מהצרכנים העשירים יותר האלה שום דבר לעבוד איתו, והעושר החזותי שהמשתמש הרגע העתיק פשוט לא שם להדביק
מה בדיוק פורמט הלוח CF_HTML?
CF_HTML אינו פורמט לוח קבוע של המערכת כמו CF_TEXT; הוא רשום דינמית, מבוקש בשם דרך RegisterClipboardFormat('HTML Format'), וה-payload שלו הוא כותרת ASCII קצרה שאחריה מסמך או fragment של HTML. הכותרת נושאת חמישה שדות — Version, StartHTML, EndHTML, StartFragment, EndFragment — כאשר Version תמיד 0.9 וארבעת האחרים הם מספרים עשרוניים כתובים כספרות ASCII. StartHTML ו-EndHTML תוחמים את המסמך השלם כפי שהאפליקציה המקבלת אמורה לפענח אותו להקשר, כולל גופנים וסגנונות, בעוד StartFragment ו-EndFragment תוחמים את הפיסה הצרה יותר שבפועל נוחתת בסמן, המסומנת באופן מקובל בתוך ה-markup עצמו עם הערות <!--StartFragment--> ו-<!--EndFragment--> כך שהגבולות שורדים סידור-מחדש (serialization) תמים
היסטי בייט, לא ספירת תווים: המלכודת הקלאסית של CF_HTML
ארבעת שדות הכותרת המספריים של CF_HTML הם היסטי בייט לתוך רצף הבייטים המדויק שיושב על הלוח, נספרים מהתו הראשון ממש של הכותרת עצמה — לא ספירת תווים, לא נקודות קוד יוניקוד, ולא היסטים יחסית ל-fragment או לתג ה-<body>. ההבחנה הזו היא המקום שבו מימושי CF_HTML כתובים-ביד משתבשים בשקט: Length של UnicodeString ב-Delphi מדווח יחידות קוד UTF-16, שבמקרה שווה לספירת הבייטים עבור טקסט ASCII פשוט, כך שהבאג עובר נקי דרך כל בדיקה שנכתבה עם נתוני דוגמה באנגלית ומופיע רק ברגע שתא שהועתק מחזיק מקף-רוחב (em dash), סימן מטבע, או תו עם דיאקריטיקה — סימן יורו הוא יחידת קוד UTF-16 אחת אבל שלושה בייטים ב-UTF-8, וכל היסט שמחושב אחרי הנקודה הזו סוחף בכמה בייטים נוספים שהקידוד הוסיף. הכשל שבא אחר כך אינו קריסה; הוא האפליקציה המקבלת תופסת בדיוק את טווח הבייטים שהכותרת הצביעה עליו, מוצאת פיסת 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 שומרת על הכותרת מדויקת-בייט
HotXLS נמנעת ממחלקת הבאג הזו מבנית: TXLSRange.CopyToClipboard ויחידת lxClipboard שמתחתיה בונות את מסמך ה-CF_HTML והכותרת שלו כולם כ-AnsiString, סוג מחרוזת-הבייט של Delphi, כך ש-Length ו-Pos כבר מחזירים מיקומי-בייט בכל מקום בחישוב — אין שלב נפרד, ולכן אין שלב לשכוח, שבו ספירת תווי יוניקוד הייתה צריכה להמיר לספירת-בייט לפני שהיא נכנסת לכותרת
יש תחבולה שנייה, קטנה יותר, ששווה לדעת אם אי-פעם תבנה כותרת CF_HTML ידנית. הכותרת נכתבת פעמיים: פעם אחת עם עשר ספרות אפס שעומדות במקום כל אחד מארבעת ההיסטים, כך שהאורך-בבייט שלה עצמה יכול להימדד, ופעם נוספת עם ההיסטים האמיתיים מוטמעים. מכיוון שכל היסט אמיתי מפורמט לאותו רוחב קבוע של עשר ספרות, הכותרת השנייה יוצאת באותו אורך בייט-לבייט כמו גרסת-הפלייסהולדר, וזו בדיוק הסיבה שהמדידה המוקדמת נשארת תקפה אחרי הכתיבה-מחדש. דלג על הרוחב הקבוע, פרמט מספר עם 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;
למה ה-payload בטקסט-פשוט עדיין חייב לנסוע יחד
TXLSRange.CopyToClipboard אף פעם לא מניחה CF_HTML על הלוח לבדו; היא תמיד כותבת CF_UNICODETEXT באותה קריאה, משום ש-CF_HTML הוא פורמט רשום ולא אחד מקבועי ה-CF_* הקבועים שכל אפליקציית Windows כבר יודעת לחפש — עורך טקסט פשוט, grid ישן, או כל דבר שאף פעם לא בדק עבור 'HTML Format' לא יראה אותו בכלל, והטווח שהעתקת או מגיע כטקסט מופרד-טאב או שהוא לא מגיע. הטקסט המופרד-טאב הזה גם אינו קירוב גס: תאי נוסחה מועתקים כמחרוזת הנוסחה שלהם עם = מוביל ששוחזר אם הטקסט השמור השמיט אותו, מתאים לאיך שהטקסט של הלוח של Excel עצמה מתנהג, תאים רגילים מעתיקים את ה-FormattedText שלהם — המחרוזת כפי שמוצגת, כך שתא מטבע מועתק כ-$1,234.56, לא ה-1234.56 הבסיסי — וכל שדה שמכיל טאב, מרכאה, או שבירת שורה מוקף במרכאות עם מרכאות מוטמעות מוכפלות, אותה מוסכמה ש-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 של ה-payload הוא עיבוד מלא של הטווח, לא היטלת נתונים ערומה: גופנים, צבעי מילוי, גבולות, פורמטי מספר, ותאים ממוזגים כולם עוברים כסגנונות inline ומבנה טבלה, אותו מנגנון עיצוב המכוסה בהמדריך של HotXLS לעיצוב מותנה וטקסט עשיר, שכן ריצות הטקסט-העשיר של תא ותוצאת העיצוב המותנה שניהם מזינים את אותו עיבוד ש-CopyToClipboard קוראת ממנו. מה שלא שורד את המסע הוא התנהגות-נוסחה חיה: הצורה בטקסט-פשוט של תא נוסחה נושאת את מחרוזת הנוסחה, כך שיעד הדבקה מודע-לגיליון-אלקטרוני יכול באופן עקרוני לחשב אותה מחדש, אבל הצורה ב-HTML נושאת רק את התוצאה המחושבת האחרונה, משום של-HTML אין מושג של נוסחה עבור דפדפן או מעבד תמלילים להעריך
אימות ההדבקה, וטיפול בלוח עסוק
שתי הרגלים תופסים את רוב בעיות הלוח לפני שלקוח עושה זאת. הדבק ל-Notepad קודם כדי לאשר שהחלופה CF_UNICODETEXT היא טקסט מופרד-טאב שפוי, ואז הדבק אותו העתק לתוך Word או דפדפן כדי לאשר שהגרסה המעוצבת מופיעה — payload שנראה נכון באחד ולא-נכון בשני בדרך כלל אומר שסמני ה-fragment נחתו במקום הלא-נכון. ואז התייחס לתוצאה הבוליאנית ש-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;
הפורמט עצמו אינו אקזוטי ברגע שהכותרת מדויקת-בייט וחלופת-הטקסט-הפשוט כנה לגבי מה שהיא מכילה — הוא קיים כמעט ללא שינוי מאז ש-Internet Explorer הגדיר אותו לראשונה, וכל אפליקציית Windows מרכזית עדיין קוראת אותו באותו אופן. CopyToClipboard יושבת לצד PasteFromClipboard, צד-הקריאה של אותו חילופין, במשטח הלוח והייצוא הרחב יותר המתועד בדף המוצר של רכיב HotXLS