מאמר טכני

הטמעת אובייקטי תרשים בגיליונות עבודה עם HotXLS

HotXLS יכול לשים תרשים ישירות על גיליון עבודה, מעוגן לטווח תאים, במקום להציב אותו בגיליון תרשים נפרד. במונחים של BIFF8 זה אומר לכתוב צורת ציור עם רשומת OBJ מסוג 5 ולהחנות את תת-זרם התרשים בסוף זרם הרשומות של הגיליון — בדיוק הפריסה ש-Excel מייצר ובדיוק המקום שבו הקורא מצפה למצוא אותה

ההבחנה הזאת חשובה לכל מי שמייצר דוחות תפעוליים. גיליון תרשים הוא בית מצוין לתרשים ראשי בודד. דוח פירוט אזורי חודשי רוצה את התרשים לצד המספרים שהוא מסכם, באותו גיליון, בגודל של בלוק התאים שאליו הוא שייך, כך שהקורא גולל פעם אחת במקום לדלג בין טאבים ולאבד הקשר

הקריאה כבר הייתה שם, הכתיבה לא

שווה לתת לאסימטריה שם, כי היא מעצבת את העבודה. HotXLS כבר ידע לקרוא תרשימים מוטמעים: כשזרם הרשומות של גיליון העבודה מכיל BOF שמסומן כתת-זרם תרשים, ה-parser מחליף הקשר, אוסף את רשומות התרשים, וב-EOF הסוגר מחזיר אותן לצורת הציור שרשומת ה-OBJ הציגה. את הנתיב הזה הפעיל כל workbook שנוצר ב-Excel שהספרייה פתחה אי פעם

מה שהיה חסר הוא צד הכתיבה, והתוצאה השימושית היא שהכותב החדש קיבל מפרט מדויק לפגוע בו: לייצר את פריסת הבייטים שהקורא הקיים כבר מחבר מחדש. אין קריטריון קבלה טוב יותר לפיצ'ר של פורמט בינארי מאשר קורא שנכתב בנפרד שלא היה לך מותר לשנות

ממה עשוי תרשים מוטמע

שלושה חלקים צריכים להסכים זה עם זה. שכבת הציור תורמת צורת host control, שכבת האובייקטים תורמת רשומת OBJ שה-common object data שלה מכריזה על סוג אובייקט 5, וזרם הרשומות תורם את תת-זרם התרשים עצמו. דגלי האפשרויות על רשומת ה-OBJ הם אלה ש-Excel כותב עבור מסגרת תרשים: positioned, locked, automatic line ו-automatic fill, וזה בדיוק מה שגורם לתרשים המוטמע להתנהג כמו תרשים מקורי כשמשתמש לוחץ עליו

HotXLS מעגן תת-זרם תרשים של BIFF8 לגיליון Delphi באמצעות שלושה חלקים מסכימים: צורת ה-host control בשכבת הציור, רשומת ה-OBJ שה-common object data שלה מכריזה על סוג אובייקט 5, ושרשרת רשומות התרשים המוחנית בסוף זרם הרשומות של הגיליון, שם BOF של תרשים מחליף הקשר של ה-parser ו-EOF הסוגר מחבר את הרשומות מחדש
שלוש שכבות נושאות תרשים מוטמע אחד: צורת הציור מעגנת אותו, רשומת ה-OBJ מסווגת אותו כמארח תרשים, ותת-זרם התרשים בסוף זרם הגיליון מספק את הרשומות שהקורא מחבר מחדש

העיגון ראוי להערה כי הוא מקור נפוץ לבאגים מסוג off-by-one. ה-API של HotXLS מקבל מספרי שורות ועמודות החל מ-1, בהתאמה לשאר הספרייה, ואילו ה-anchor של הלקוח שנכתב לקובץ מבוסס אפס. ההמרה מתרחשת בתוך AddChartObject, כך שהקוראים לפונקציה נשארים במערכת הקואורדינטות שהם משתמשים בה בכל מקום אחר, אבל מי שמשווה hex dump מול הקריאה שלו עצמו צריך לזכור מאיזה צד של הגבול הזה הוא קורא

var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  Series: array[0..1] of TXLSChartSeriesInfo;
begin
  Book := TXLSWorkbook.Create(nil);
  try
    Book.LoadFromFile('regional-sales.xls');
    Sheet := Book.Sheets[0];

    FillChar(Series, SizeOf(Series), 0);
    Series[0].Name := 'Actual';
    Series[0].Categories := 'Data!$A$2:$A$13';
    Series[0].Values := 'Data!$B$2:$B$13';
    Series[0].DataLabels.ShowValue := True;
    Series[0].HasDataLabels := True;

    Series[1].Name := 'Target';
    Series[1].Categories := 'Data!$A$2:$A$13';
    Series[1].Values := 'Data!$C$2:$C$13';
    Series[1].SecondaryAxis := True;

    // מעוגן ל-E2:M20 בגיליון הזה, מספור החל מ-1
    Sheet.AddChartObject(xlsChartTypeColumn, 'Regional sales',
      'Month', 'Amount', Series, 2, 5, 20, 13);

    Book.SaveToFile('regional-sales-charted.xls');
  finally
    Book.Free;
  end;
end;

ה-FillChar על מערך הסדרות אינו קישוט. TXLSChartSeriesInfo נושאת כמה תתי-רשומות אופציונליות — תוויות נתונים, סטייל פר-סדרה, קווי מגמה ופסי שגיאה — כל אחת נשלטת על ידי בוליאני, ורשומה שאותחלה חלקית על המחסנית תמסור ל-emitter דגלים שאף אחד לא קבע. מאפסים את המערך, ואז קובעים את השדות שהתכוונת אליהם

אילו הפניות סדרות מקבל הנתיב המוטמע?

טווחי A1 פשוטים בתוך אותו workbook עצמו, וההגבלה הזאת מכוונת ולא פליטת קולמוס. כל הפניה נפתרת מול רשימת הגיליונות של ה-workbook והופכת לאינדקס ההפניה החיצונית שרשומות התרשים צריכות. טווח בעל שם או הפניה ל-workbook חיצוני נופלים בחזרה ל-placeholder עם ביטוי מנותח באורך אפס, כך שהתרשים נכתב נקי, אבל לאותה סדרה ספציפית אין מקור נתונים עד שתכוון אותה לטווח

קבלת הפניות סדרות של HotXLS בנתיב התרשים המוטמע של BIFF8: טווחי A1 פשוטים כמו Data!$B$2:$B$13 בתוך אותו workbook נפתרים מול רשימת הגיליונות לאינדקס ההפניה החיצונית שרשומות התרשים צריכות, בזמן שטווחים בעלי שם והפניות ל-workbook חיצוני נופלים ל-placeholder עם ביטוי מנותח באורך אפס, ושניהם מכוסים על ידי AddChartSheet
רק טווחי A1 פשוטים בתוך אותו workbook מתקמפלים להפניות סדרות של תרשים; כל השאר נכתב נקי כ-placeholder עד שמכוונים אותו מחדש, והנתיב המלא נמצא ב-AddChartSheet

הסיבה היא שיקול הנדסי ישיר. נתיב הידור ההפניות המלא קיים בנתיב של גיליון התרשים, עטוף בתוך שכבת אוסף גיליונות העבודה, וחילוץ שלו בצורה נקייה היה אומר לשכפל מאה שורות של היגיון פתרון עבור מקרה שאינו שכיח בפועל. תרשים מוטמע כמעט תמיד מצייר תאים מהגיליון שלו עצמו או מגיליון נתונים שכן. הפניות בעלות שם והפניות חיצוניות מכוסות בנתיב גיליון התרשים דרך AddChartSheet, כך ששום דבר לא חסר — פשוט מגיעים אליו מנקודת כניסה אחרת

כל השאר במודל הסדרות עובד זהה בשני הנתיבים. קישור ציר משני, סטייל קו, מילוי ומארקר פר-סדרה, קווי מגמה, פסי שגיאה ותוויות נתונים — הכול חלק מ-TXLSChartSeriesInfo ומופק באותה צורה, כך שהגדרת תרשים יכולה לנוע בין אובייקט מוטמע לגיליון תרשים כשרק הקריאה משתנה. מכניקת קבוצות הצירים שמאחורי דגל הציר המשני מוסברת בקבוצות ציר משני בכתיבת BIFF

למה כותרת התרשים נקראה כשני תווים?

כי הועבר מספר תווים במקום שציפו למספר בייטים, ומחרוזות BIFF Unicode הופכות את הטעות הזאת לקלה לכתיבה וקשה לראות. מחרוזת BIFF Unicode קצרה מתחילה במספר תווים ובייט דגלים, ובייט הדגלים נושא את הביט של high-byte שאומר אם ה-payload הוא בייט אחד לכל תו או שניים. קוראים payload של 16 ביט עם מספר התווים כאילו היה אורך בבייטים ומקבלים בדיוק חצי מהמחרוזת: סדרה בשם Sales חוזרת בתור Sa, וכותרת תרשים נחתכת באותה צורה כי כותרות ותוויות סדרות חולקות נתיב פענוח

מה שהופך את הפגם הזה לבולט הוא שהוא חזר שלוש פעמים באותה משפחת רשומות — פעם בשמות של קווי מגמה, פעם בשמות של pivot charts ופעם בכותרות תרשים. כל מופע נראה כמו באג טרי בפיצ'ר חדש. שלושתם היו אותו כפל חסר. הכלל שסגר את זה סופית הוא מכני וצריך ליישם אותו בלי שיקול דעת: בכל פעם שקוראים אחת מהמחרוזות האלה, בודקים קודם את דגל ה-high-byte ומכפילים את מספר התווים ברוחב ה-payload לפני שנוגעים בחוצץ. הפרטים ברמת הרשומה נמצאים בפענוח מספרי תווים של XLUnicodeString ודגל ה-high-byte

// התרשים המוטמע חולק את שכבת הציור עם תמונות וצורות,
// ולכן ציור קיים בגיליון נשמר. AddChartObject
// מחזיר את האינדקס של האובייקט שנוצר
var
  ObjIndex: Integer;
begin
  ObjIndex := Sheet.AddChartObject(xlsChartTypeLine, 'Trend',
    'Week', 'Units', Series, 2, 8, 18, 16);
  if ObjIndex < 0 then
    raise Exception.Create('chart object was not created');
end;

איפה תרשימים מוטמעים משתלבים מול החלופות

קיימים שלושה נתיבים והם עונים על שאלות שונות. אובייקט תרשים מוטמע שייך לצד הנתונים שלו על גיליון עבודה, וזה מה שרוב הדוחות רוצים. גיליון תרשים מתאים לתצוגת מצגת אחת ונותן לך את נתיב הידור ההפניות המלא. שימור תרשים קיים מקובץ שנטען, בלי לגעת, הוא התשובה הנכונה כשה-workbook הגיע מ-Excel עם עיצוב שאף אחד לא רוצה שספרייה תפרש מחדש; התנהגות ה-pass-through הזאת מתוארת בChartML משומר ותרשימים משולבים

כי התרשים המוטמע רוכב על שכבת הציור, הוא מתקיים במקביל לתמונות ולצורות באותו גיליון במקום להחליף אותן, והמודל הכללי של השכבה הזאת מוסבר בתרשימים, תמונות וציורים ב-HotXLS. שלושת הנתיבים מגיעים כולם ברכיב הגיליונות של HotXLS ל-Delphi, כך שהבחירה היא בשאלה איך הדוח צריך להיראות ולא בשאלה מה הספרייה יודעת לבטא

הנקודה המתודולוגית היא זו ששווה לשמור. כשלפיצ'ר של פורמט בינארי יש קורא קיים, בונים את הכותב מול הקורא ולא מול הפרשנות שלך למפרט. הקורא מקודד שנים של מגע עם קבצים שאפליקציות אמיתיות ייצרו בפועל, כולל החלקים שהמפרט מנסח בצורה רופפת, וכותב שמספק אותו סביר הרבה יותר שיספק גם את Excel