HotXLS, ספריית ה-Excel הטבעית עבור דלפי ו-C++Builder, בנויה עבור סבב מלא (round-trip) של XLSX ללא אובדן נתונים: פתחו חוברת עבודה, שנו תא אחד, שמרו, וערכת הנושא המותאמת אישית של הלקוח, בלוקי הרחבה חיצוניים של extLst ושרשרת החישוב (calculation chain) כולם ישרדו. שלושה מנגנונים גורמים לכך לעבוד — שמירה במטמון כלשונה של xl/theme/theme1.xml, ייצוג מחדש מבוסס אירועים של בלוקי <ext> לא מוכרים, וקובץ xl/calcChain.xml חדש ותקין לפי המפרט בכל שמירה של חוברת עבודה המכילה נוסחאות
התרחיש שמניע את שלושתם נפוץ באופן מדכא. שירות חיוב טעון תבנית שהלקוח עיצב ב-Excel — ערכת צבעים ארגונית, קווי מגמה (sparklines) בעמודת KPI, וכלל עיצוב מותנה שנוסף על ידי גרסת Excel חדשה יותר — כותב סך חשבונית יחיד לתא B3 ושומר. הלקוח פותח את התוצאה וצבעי המותג חזרו לכחול ברירת המחדל של Office, קווי המגמה נעלמו ו-Excel מציע "לתקן" את הקובץ. שום דבר בקוד לא נגע באף אחת מהתכונות הללו. הספרייה עשתה זאת, פשוט על ידי שמירה
מדוע קובצי Excel מאבדים עיצוב לאחר עריכה באמצעות ספרייה?
קובצי Excel מאבדים עיצוב לאחר עריכה באמצעות ספרייה מכיוון שרוב הספריות אינן עורכות את הקובץ — הן בונות אותו מחדש. חבילת .xlsx היא קובץ ZIP המכיל חלקי XML: הקובץ xl/workbook.xml, קובץ xl/worksheets/sheetN.xml אחד לכל גיליון, xl/styles.xml, xl/theme/theme1.xml, xl/calcChain.xml ועוד. ספרייה טיפוסית מפענחת את החלקים הללו למותאם אישית למודל אובייקטים בעת הפתיחה ומייצרת מחדש כל חלק מתוך המודל הזה בעת השמירה. כל תכונה שהמודל אינו מייצג — ערכת נושא שהספרייה לא פענחה מעולם, או בלוק הרחבה מגרסת Excel חדשה יותר — אינה מוצאת מקום בזיכרון, ולכן החלק שנבנה מחדש פשוט משמיט אותה
כיצד HotXLS שומר על ערכת נושא מותאמת אישית ברמת הבתים?
HotXLS משמר את ערכת הנושא של חוברת העבודה על ידי שמירה במטמון של הבתים המקוריים של xl/theme/theme1.xml בעת הפתיחה וכתיבתם בחזרה מילה במילה בעת השמירה. חלק ערכת הנושא (תקן ECMA-376 Part 1, סעיף §14.2.7) הוא DrawingML ולא SpreadsheetML — סכמות צבעים, סכמות גופנים, סכמות עיצוב — ולמנוע גיליונות אלקטרוניים אין סיבה למדל אותו לעומק. גרסאות קודמות של HotXLS ייצרו מחדש ערכת נושא קבועה של Office בכל שמירה, וזהו בדיוק כשל "צבעי המותג שחזרו" שהוזכר לעיל; מאז גרסה v2.89.46 ערכת הנושא של החבילה שנפתחה נשמרת בצורתה הגולמית ונפלטת מחדש ללא שינוי, וערכת הנושא המובנית של Office מיוצרת רק עבור חוברות עבודה שנוצרו מאפס. בתים גולמיים הם ערובת הנאמנות החזקה ביותר האפשרית: ללא פענוח, ללא ייצוג מחדש, ללא סיכוי לשינויים
ההעתקה מילה במילה גוברת במכוון על גישה תכנותית לערכת הנושא. המחלקה TXLSXWorkbook חושפת את ThemeMajorFont ו-ThemeMinorFont כדי שתוכלו לבחור גופני כותרות וגוף עבור חוברות עבודה חדשות, אך כאשר ערכת נושא הועתקה כלשונה בעת הפתיחה, למאפיינים אלו אין השפעה על הקובץ שנשמר — הסבב המלא מקבל עדיפות. אם אתם באמת צריכים לשנות ערכת נושא של חוברת עבודה קיימת, זהו סימן לערוך את התבנית ב-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 חדשות יותר שורדות את הסבב המלא ללא פגע. מאז גרסה 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;
פרט המימוש שראוי לדעת הוא שהלכידה היא ייצוג מחדש ברמת האירועים, ולא העתקת בתים גולמיים. קורא ה-XML המוזרם של HotXLS אינו חושף היסטי מקור, ולכן תת-העץ הלא מוכר נבנה מחדש מתוך אירועי Element, Text ו-EndElement בזמן שהם מוזרמים. גישה זו מסתירה מלכודת קלאסית אחת: אלמנט בעל סגירה עצמית כמו <a/> מפעיל רק אירוע Element המסומן כריק ולעולם לא אירוע EndElement, כך שכל מונה עומק שיורד רק ב-EndElement לעולם לא יראה את תת-העץ נסגר. טפלו בכך, והקטע שנבנה מחדש שווה ערך סמנטית למקור — מירכאות של מאפיינים וצורות של סגירה עצמית מנורמלים, כך שהוא אינו זהה ברמת הבתים, אך Excel קורא משמעות, לא בתים. שתי תכונות של הפלט של Excel עצמו הופכות את השחזור לבטוח: Excel מצהיר על מאפייני ה-xmlns הנדרשים על גבי אלמנט ה-<ext> או בתוכו, כך שכל קטע שנלכד מכיל את מרחב השמות שלו בעצמו, ואותה הגדרה עצמית היא הסיבה לכך ששכפול גיליון עבודה בתוך או בין חוברות עבודה יכול להעביר את הבלוקים החיצוניים יחד עם הקצאת רשימת מחרוזות פשוטה
כתיבת calcChain.xml כדי ש-Excel יבטח בנוסחאות שלכם
HotXLS כותב את xl/calcChain.xml (חלק Calculation Chain, תקן ECMA-376 Part 1, סעיף §12.3.1) בכל פעם שחוברת העבודה שנשמרת מכילה נוסחאות, והוא בוחר בין שני סדרי מיון. אם גרף התלות של הנוסחאות כבר נבנה והוא מעודכן — קראתם ל-Recalculate לאחר העריכה האחרונה — השרשרת נפלטת בסדר טופולוגי מלא, תלויות לפני תאים תלויים, כאשר חברי הפניות מעגליות מתווספים בסוף. אחרת, התאים מפורטים לפי סדר המסמך. שני המצבים נכונים: הערות המימוש של מיקרוסופט עבור הפורמט, [MS-XLSX], מתייחסות לשרשרת החישוב כרמז ש-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 מתייחס אליו כהמלצה בלבד? מכיוון שהיעדרו הוא סימן. צרכנים מסוימים — יוריסטיקות תיקון, מציגים של צד שלישי, כלי השוואה — מצפים מחוברת עבודה המכילה נוסחאות לשאת שרשרת חישוב, וספרייה השומטת בשקט את החלק הזה בזמן השמירה מייצרת קבצים ששונים במקצת מכל מה ש-Excel כותב. פליטת שרשרת תקפה שומרת על הפלט בתוך המעטפת שיתר המערכת נבדקה מולה, וזהו הגלעין השקט והלא-מפואר של הנדסת סבב מלא (round-trip engineering)
היכן מסתיימת נאמנות הסבב המלא
כנות חשובה כאן יותר מסימון תיבת שיווק, ולכן הגבולות ראויים להתייחסות זהה. HotXLS אינו מעתיק את כל החבילה בית אחר בית: קובצי XML של גיליונות, סגנונות, מחרוזות משותפות וחלקי חוברת עבודה מיוצרים מחדש מתוך המודל המפוענח, כך שהפלט נאמן סמנטית אך אינו זהה בינארית — כותרות ה-ZIP המקומיות לבדן נושאות חותמות זמן DOS טריות. קטעי <ext> שנלכדו חוזרים מנורמלים, כפי שמתואר לעיל. עקיפות תכנותיות של גופני ערכת נושא זוכות להתעלמות כאשר קיימת ערכת נושא כלשונה. ולרשת שימור יש מפתח מוגדר: תכונות ש-HotXLS ממדל באופן טבעי (קווי מגמה, למשל, מפוענחים ונכתבים מחדש במקום להיות מועתקים בעיוורון) בתוספת תוכן extLst חיצוני בתוספת החלקים שנשמרו במטמון מילה במילה. חלק שאינו ממודל ואינו נמצא בתוך נקודת הרחבה — חלק מותאם אישית של תוסף אקזוטי, למשל — נופל מחוץ לשלושת המנגנונים שמאמר זה מכסה, לכן בדקו את התבניות בפועל שלכם במקום להניח מראש
עבודת שימור סמוכה משלימה את התמונה. פרויקטי VBA והפניות לחוברות עבודה חיצוניות עוברים את השמירה על בסיס אותה פילוסופיה של "לשמור את מה שאיננו ממדלים", המכוסה במאמר המלווה על שימור VBA וקישורים חיצוניים, ומאפייני מסמך בתוך docProps הם בעלי ממשק API משלהם לקריאה-כתיבה במקום להישמט בשקט. כאשר אתם מעריכים ספריית גיליונות אלקטרוניים כלשהי, הרצו את בדיקת התא הבודד: פתחו חוברת עבודה עשירה בתכונות הנמצאת בייצור, שנו ערך בודד, שמרו, והשוו (diff) את החלקים שנפרסו מול המקור. מה שהשתנה מעבר לגיליון שבו נגעתם מספר לכם על הספרייה יותר מכל מטריצת תכונות
מנגנוני הסבב המלא המתוארים כאן — שמירת ערכת נושא מילה במילה מאז גרסה v2.89.46, לכידת extLst חיצונית ופליטת calcChain.xml מאז גרסה v2.131.0 — מסופקים בגרסה הנוכחית של רכיב HotXLS Delphi Excel, שדף המוצר שלו מתעד את ערכת התכונות המלאה של קריאה וכתיבה של XLSX עבור דלפי ו-C++Builder