מאמר טכני

HotXLS תרשימים ותמונות: ציורי Excel ב-Delphi

כל דבר שמרחף מעל רשת גיליון-עבודה (תרשים, לוגו, חותמת, תיבת קריאה) הוא אובייקט ציור, ואובייקט ציור מוגדר על ידי שני דברים: מה הוא, והיכן הוא מעוגן. העוגן הוא החלק שאנשים טועים בו. תרשים לא חי בתא; הוא יושב במלבן המוצמד למקטע שורות ועמודות, והנתונים שהוא מצייר הם סט נפרד של הפניות A1 שהעוגן אינו יודע עליהם דבר. הזז את המסגרת והעלילה נשארת במקומה. הכנס שורות מתחתיו והמסגרת גולשת איתן. שמירת שתי מערכות הקואורדינטות הללו מיושרות היא רוב מה שגורם לקוד ציור להתנהג

HotXLS היא ספריית Object Pascal טבעית שקוראת וכותבת XLS ו-XLSX ללא אוטומציה של Excel, והיא נושאת שני מודלי ציור נפרדים כי שני פורמטי הקובץ מאחסנים ציורים אחרת. פורמט BIFF8 ‎.xls‎ שומר תרשימים על גיליונות ייעודיים משלהם וצורות צפות בזרם OfficeArt המצורף לגיליון. פורמט OOXML ‎.xlsx‎ יכול לשבץ תרשים בתוך הרשת, מעוגן למלבן תא, לצד אותו סוג של תמונות וצורות צפות. מודל האובייקט משקף את הפיצול הזה, והכשלים ששווה לכתוב עליהם כולם נובעים מהחלת כללי פורמט אחד על השני

איזה מיכל יכול להחזיק מה

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

תרשים משווה מיכלי ציור ב-HotXLS מ-Delphi: גליונות תרשים וצורות OfficeArt ב-XLS המורשת מול תרשימים, תמונות ותיבות טקסט מוטמעים ב-XLSX
שני פורמטי הקובץ חושפים API של ציור שונים, ולכן את המכולה צריך לבחור לפני שקוד תרשים כלשהו נכתב
  • XLS (BIFF8): תרשימים חיים על גיליונות תרשים ייעודיים שנוצרים דרך AddChartSheet על אוסף Sheets. תמונות, תיבות טקסט, מלבנים, אליפסות וקווים הם צורות OfficeArt המנוהלות דרך אוסף Shapes של הגיליון. אין API לשיבוץ תרשים בתוך רשת גיליון רגיל
  • XLSX (OOXML): תרשימים יכולים להיות משובצים ישירות בגיליון עם TXLSXWorksheet.AddChart, מעוגנים למלבן תא, או ממוקמים על גיליון תרשים ייעודי עם TXLSXWorkbook.AddChartSheet. תמונות נכנסות עם AddImage או AddImageFromFile, ותוויות צפות עם AddTextBox

אז דרישה המנוסחת כ"גיליון לוח-בקרה עם התרשים ליד המספרים" היא באמת דרישה ל-‎.xlsx‎. אפשר לקרב אותה ב-‎.xls‎ רק על ידי דחיפת התרשים לגיליון משלו, מה שמשנה כיצד המשתמש מנווט את הקובץ וכיצד הקוד שלך צריך להתנהג. הגיליון שמוחזר על ידי AddChartSheet בצד ה-XLS הוא תת-זרם תרשים, לא רשת: כתיבה אליו עם Cells.Item מייצרת זרם ציור לא-עקבי שמתחיל ללא שגיאה וש-Excel אז משליך בפתיחה. התרשים פשוט נעלם, ושום דבר בלוג הבנייה לא אומר למה. התייחס לגיליון המוחזר כתרשים-בלבד וכל מחלקת הדוחות "תרשים חסר" נעלמת

שיבוץ תרשים בגיליון XLSX

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

var
  Book: TXLSXWorkbook;
  Sheet: TXLSXWorksheet;
  Chart: TXLSXChart;
begin
  Book := TXLSXWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Sales');
    Sheet.Cells[1, 1].Value := 'Region';
    Sheet.Cells[1, 2].Value := 'Revenue';
    Sheet.Cells[2, 1].Value := 'East';
    Sheet.Cells[2, 2].Value := 1184350;
    Sheet.Cells[3, 1].Value := 'Central';
    Sheet.Cells[3, 2].Value := 902210;
    Sheet.Cells[4, 1].Value := 'West';
    Sheet.Cells[4, 2].Value := 1010675;

    // מסגרת מעוגנת לשורות 6..22, עמודות 1..8
    Chart := Sheet.AddChart(xlsxChartColumn, 'Revenue by Region', 6, 1, 22, 8);
    Chart.AddSeries('Revenue', 'Sales!$A$2:$A$4', 'Sales!$B$2:$B$4');
    Chart.ValueAxisTitle := 'USD';

    Sheet.AddImageFromFile(1, 5, 'logo.png');
    Book.SaveAs('dashboard.xlsx');
  finally
    Book.Free;
  end;
end;

הארגומנט שנושך הוא מחרוזת הטווח המועברת ל-AddSeries. היא ליטרלית, נתפסת ברגע הקריאה, ואין לה מושג שאולי תצרף עוד עשרים שורות נתונים לאחר מכן. בנה אותה מספירת שורות שחישבת לאחר שהנתונים נכתבו, לעולם לא לפני. תרשימי פיזור ובועות עומסים את אותם שני ארגומנטים במשמעויות שונות: טווח הקטגוריות מספק כעת את ערכי ה-X וטווח הערכים מספק Y, ורדיוס הבועה מגיע מהפניה שלישית שנקבעת דרך BubbleSizeRange על TXLSXChartSeries המוחזר. קרא את הקריאה כ-"X, Y, size" במקום "categories, values" ברגע שאתה עוזב את משפחת העמודות-והמוטות

TXLSXChartType משתרע על עמודות, מוטות, קווים, עוגות, שטחים, דונאטים, פיזור, בועות, ותרשימי מכ"ם, מה שמכסה את רפרטואר הדיווח היומיומי. לתרשים בעמוד מלא ללא רשת מקיפה, Book.AddChartSheet מחזיר גיליון שמאפיין IsChartSheet שלו true. זהו מקביל ה-‎.xlsx‎ של גיליון התרשים המורשת ונושא את אותה ציפייה: אל תכתוב תוכן תא אליו

תמונות נכנסות כבתים, והן ממודדות ב-EMU

יש שני עומסים להכנסת תמונה, וערבובן הוא באג התמונה שעולה הכי הרבה בסקירת קוד. AddImage(ARow, ACol, AData, AFormat) רוצה את בתי התמונה הכבר-מקודדים ב-AData: התוכן הגולמי של PNG, JPEG, GIF, או BMP. העבר נתיב קובץ ואחסנת מחרוזת של ארבעים בתים ששום מציג לא יכול לפענח, שזה בדיוק דוח סמל-תמונה-שבורה שלא תרצה לנפות לאחר פריסה. כאשר המקור הוא קובץ על הדיסק, קרא ל-AddImageFromFile במקום ותן לספרייה לקרוא את הבתים ולסווג את הפורמט עבורך

אחר כך מגיע המידול. DrawingML לא מודד בפיקסלים; הוא מודד ביחידות English Metric, שבהן 914400 EMU מרכיבים אינץ' וב-96 DPI, 9525 EMU מרכיבים פיקסל. אובייקט TXLSXImage חושף WidthEMU ו-HeightEMU, כך שלוגו שאמור להתרנדר 180 על 60 פיקסל דרושים 1714500 על 571500 EMU. שים את ההמרה הזו בקבוע בעל שם וחשב כנגדה. מספרי קסם כמו 1714500 פזורים בקוד אינם קריאים ושקט-שגויים בפעם הראשונה שמישהו משנה את DPI היעד. שורת ועמודת העוגן, אגב, הם 1-בסיסיים, תואמים את שאר ממשק התאים ולא את חשבון ה-EMU ה-0-בסיסי

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

גיליונות תרשים וצורות בקובצי XLS מורשת

בצד ה-BIFF8 העומס העשיר יותר של AddChartSheet לוקח את סוג התרשים, את כותרות הצירים, ו-open array של רשומות TXLSChartSeriesInfo, שבה כל רשומה מחזיקה שם וטווח קטגוריות וערכים כמחרוזות. צורות צפות הן עניין נפרד: הן הולכות על גיליון הנתונים עצמו, דרך אוסף Shapes שלו, לא על גיליון התרשים

var
  Book: IXLSWorkbook;
  Data, Trend: IXLSWorksheet;
  Series: array[0..0] of TXLSChartSeriesInfo;
begin
  Book := TXLSWorkbook.Create;   // נספר-הפניות כממשק: אין לקרוא ל-Free
  Data := Book.Sheets.Add;
  Data.Name := 'Data';
  Data.Cells.Item[1, 1].Value := 'Month';
  Data.Cells.Item[1, 2].Value := 'Units';
  Data.Cells.Item[2, 1].Value := 'Apr';
  Data.Cells.Item[2, 2].Value := 1530;
  Data.Cells.Item[3, 1].Value := 'May';
  Data.Cells.Item[3, 2].Value := 1721;

  Series[0].Name := 'Units';
  Series[0].Categories := 'Data!$A$2:$A$3';
  Series[0].Values := 'Data!$B$2:$B$3';
  Trend := Book.Sheets.AddChartSheet('Trend', xlsChartTypeLine,
    'Units sold', 'Month', 'Units', Series);
  // Trend הוא תת-זרם של תרשים: לעולם אין לקרוא לו למתודות של תא

  Data.Shapes.AddTextBox('Source: ERP nightly export', 6, 1, 8, 4);
  Data.Shapes.AddPicture('approved-stamp.bmp');
  Book.SaveAs('trend.xls');
end;

שני פרטי אורך-חיים חשובים כאן, והם מושכים לכיוונים מנוגדים. TXLSWorkbook מוחזק דרך ממשק IXLSWorkbook והוא בעל ספירת-הפניות, כך שקריאה ל-Free עליו בעצמך מפעילה שחרור כפול. TXLSXWorkbook מהסעיפים הקודמים הוא אובייקט רגיל וחייב להיות משוחרר ב-try..finally. אותו סוקר קוד שמסמן Free חסר בצד ה-XLSX צריך לסמן אחד נוכח בצד ה-XLS, מה שהוא מכשול אמיתי כשעובדים בשני הפורמטים באותה יחידה. עוזרי הצורה עצמם אחידים: AddRectangle, AddOval, ו-AddLine, עם DeleteInRange לניקוי אזור מציורים, כולם מעוגנים בזוגות שורה ועמודה, כך שתבנית שמכניסה שורות מעליהן מזיזה אותן יחד עם הרשת

מאפיין נוסף משתלם על קובצי מורשת. TXLSPicture.TransparentColor ממסך צבע רקע נבחר מחוץ ל-bitmap, שזה כיצד מטילים חותמת לא-מלבנית (חותמת "Approved", סימן מים) מעל הרשת בפורמט שעיבוד ה-BIFF שלו מעולם לא למד alpha של PNG. קבע את הצבע שכנגדו החותמת נוצרה והמלבן המקיף נעלם

צבעי ערכת-נושא לא שורדים round-trip של BIFF8

מילויי ציור של OOXML יכולים להצביע על חריץ צבע ערכת-נושא, מה שהופך צביעה-מחדש של ‎.xlsx‎ שלם לזול על ידי החלפת ערכת הנושא שלו. לרשומות ציור BIFF8 אין חריץ כזה. כאשר HotXLS מחיל צבע ערכת-נושא על ציור XLS הוא פותר את הצבע לערך RGB ליטרלי ומאחסן אותו; אינדקס ערכת-הנושא שממנו הגיע נעלם ברגע שהקובץ נכתב, ופתיחה מחדש לא יכולה לשחזרו. זה תופס במיוחד כלי דיווח white-label, מהסוג שממתג-מחדש את אותו מסמך מחולל עבור לקוחות רבים. שמור את מיפוי ערכת-הנושא-ל-RGB בתצורה שלך והחל מחדש בכל פעם שאתה מחולל, במקום לצפות לקרוא אותו בחזרה מתוך ‎.xls‎ שמור

תרשים של הוספת תמונות ב-HotXLS מ-Delphi: AddImage דורש בייטים מקודדים בעוד AddImageFromFile קורא את הקובץ, ופיקסלים ב-96 DPI מומרים אל ערכי WidthEMU ו-HeightEMU
בתי תמונה ונתיבי קובץ שייכים ל-overload שונים, וגדלי פיקסלים על המסך מומרים אל EMU לפני שהם מגיעים אל אובייקט התמונה

החלטה קשורה עולה בצד הביצועים. אפשר לומר ל-facade של XLS לדלג על פענוח שכבת הציור לחלוטין כאשר כל מה שרוצים מקובץ מורשת גדול הוא נתוני התא שלו, על ידי הגדרת _DisableGraphics ל-true, וזה מוריד זמן אמיתי מקריאות כמותיות. התפיסה קבועה: לחוברת עבודה שנפתחת כך אין זרם OfficeArt בזיכרון, כך ששמירתה כותבת את הציורים מחוץ לקיום. שמור את הדגל לעבודות אנליטיקה לקריאה-בלבד. תמונת הביצועים הרחבה יותר נמצאת ב-הערותינו על ביצועי חוברת-עבודה גדולה ב-HotXLS

שמירת עוגנים יציבים בזמן שהרשת משתנה

דוחות רק לעיתים רחוקות נשארים בגודל שבו נוצרו, וכאן מודל העוגן מהפתיחה משתלם. הפעולות המבניות של facade ה-XLSX (InsertRows, DeleteRows, ומקבילות העמודות) מזיזות את השכבות התלויות יחד עם התאים. אזורים ממוזגים, היפר-קישורים, הערות, חלוניות מוקפאות, טווחי סינון, עיצובים מותנים, אימותים, טבלאות, שמות מוגדרים, ו, לנושא זה, עוגני תמונה ותרשים כולם נוסעים יחד. לוגו המעוגן בשורה 1 נשאר בראש כשעשר שורות נכנסות מתחתיו. מסגרת תרשים המעוגנת מתחת לבלוק הנתונים גולשת למטה ככל שהבלוק גדל. הדבר היחיד שלא נכתב מחדש הוא כל מחרוזת טווח שתפסת כליטרל לפני שההכנסה קרתה, שכן היא רק טקסט שאין לספרייה סיבה לחזור אליו. זה מקבע את הסדר הבטוח למילוי תבנית: כתוב ועצב מחדש את הנתונים תחילה, וצור תרשימים והצב תמונות כמעבר הסופי, עם כל מחרוזת טווח נגזרת מספירות השורות שיש לך לאחר ההכנסות, לא לפני

שני כלים קטנים יותר משלימים את ערכת המיקום. TXLSTextBox.SetArea בצד ה-XLS מעגן מחדש תיבת טקסט קיימת או צורה אוטומטית על מלבן תא חדש, מה שעדיף על מחיקה ויצירה מחדש כשבלוק כותרת-תחתונה זז. ועומס ה-bitmap של AddPicture לוקח TBitmap חי עם דגל שקיפות אופציונלי, כך שכל דבר שקוד ה-VCL שלך יכול לצייר (מד, רצועת sparkline, סוג תרשים שהרשימה הטבעית לא מציעה) יכול להיות מוטבע ישירות לגיליון בלי לכתוב קובץ זמני תחילה

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