מאמר טכני

יצירת קובצי Excel ב-Delphi ללא אוטומציית Office

אם תפקידו היחיד של שרת הוא לפלוט קובצי Excel, אין לו עסק בהרצת Excel. התקנת Office על סוכן בנייה או על שירות דיווח כדי להניע אותו דרך אוטומציית COM היא התכנון השגוי, והיא הייתה התכנון השגוי כל עוד הפרקטיקה קיימת. Microsoft אומרת זאת בעצמה, בהנחיה שלא התרככה בעשרים שנה: Office אינו בנוי ואינו מורשה להפעלה אוטומטית מתהליך עורפי, בלתי-מאויש, בצד השרת. התשובה הנכונה היא לכתוב את הבתים של BIFF ושל OOXML ישירות, ללא Excel בתמונה כלל. זו כל ההנחה של HotXLS, ספריית Object Pascal ילידית שקוראת וכותבת את פורמטי הגיליונות בעצמה, כך שאין יישום שולחני שייתקע, ידלוף או יחויב לכל מושב

מדוע הנעת EXCEL.EXE משירות נכשלת

אוטומציית COM שולטת מרחוק על תוכנית שולחנית, ותוכנית שולחנית מניחה בשקט שלושה דברים ששירות Windows אינו יכול למסור לה: פרופיל משתמש טעון, תחנת חלונות אינטראקטיבית, ואדם הצופה במסך. הסירו אותם והכשלים מגיעים בצורה שאף מכונת מפתח לעולם לא משחזרת. הנחיית שחזור-קובץ, שגיאת תוסף או דו-שיח הפעלת-רישיון נפתחים על שולחן עבודה שאיש אינו יכול לראות, וקריאת האוטומציה שהפעילה אותו לעולם אינה חוזרת. הקורא בסופו של דבר חורג מהזמן הקצוב ומת; מופע ה-Excel לעתים קרובות אינו, ושורד כיתום שמחזיק נעילות קבצים ומרעיל את ההרצה הבאה. כל מי שצפה באחת-עשרה תהליכי EXCEL.EXE תועים נערמים תחת חשבון שירות מכיר את שאר הסיפור הזה

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

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

שני מנועים ילידיים, שני מודלי בעלות

HotXLS מחליפה את תהליך ה-Excel בשני מימושי פורמט ישירים. מנוע זרם-רשומות BIFF8 (TXLSWorkbook, יחידה lxHandle) מטפל ב-.xls. כותב חבילות OOXML (TXLSXWorkbook, יחידה lxHandleX) מפיק .xlsx שתואם ל-ECMA-376 / ISO/IEC 29500. אין דבר לרשום ואין דבר להתקין על השרת, ואתם יכולים להחזיק כמה חוברות עבודה פתוחות בו-זמנית ככל שהזיכרון מאפשר

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

var
  Book: IXLSWorkbook;          // הפניית ממשק: משוחררת אוטומטית
  Sheet: IXLSWorksheet;
  BookX: TXLSXWorkbook;        // אובייקט רגיל: אתם משחררים אותו
  SheetX: TXLSXWorksheet;
begin
  // פלט BIFF8 .xls - ללא Free; ספירת ההפניות של הממשק מחזיקה אותו
  Book := TXLSWorkbook.Create;
  Sheet := Book.Sheets.Add;
  Sheet.Name := 'Report';
  Sheet.Cells.Item[1, 1].Value := 'Generated without Excel';
  Book.SaveAs('report.xls');

  // פלט OOXML .xlsx - אורך חיים מפורש
  BookX := TXLSXWorkbook.Create;
  try
    SheetX := BookX.Sheets.Add('Report');
    SheetX.Cells[1, 1].Value := 'Generated without Excel';
    BookX.SaveAs('report.xlsx');
  finally
    BookX.Free;
  end;
end;

חזית ה-XLS היא ספורת-הפניות דרך הממשק IXLSWorkbook. הצהירו על המשתנה כסוג הממשק ולעולם אל תקראו ל-Free עליו; החזיקו את אותו אובייקט במשתנה אובייקט רגיל ושחררו אותו בעצמכם, וספירת ההפניות תשחרר אותו בפעם השנייה. חזית ה-XLSX היא אובייקט רגיל שרוצה try..finally רגיל. מיעון התאים מבוסס-1 בשני הצדדים, וזה המקום היחיד שבו השניים מסכימים. אוספי הגיליונות אינם: Entries בצד ה-XLS מבוסס-1, אינדקסר ה-Items של XLSX מבוסס-0, וסטייה-באחד זו מהדרת בנקיון לאיזה כיוון שלא תטעו ומראה את עצמה רק בזמן ריצה

כתיבת חוברת עבודה ישירות אל תגובת HTTP

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

Mem := TMemoryStream.Create;
Book := TXLSXWorkbook.Create;
try
  Sheet := Book.Sheets.Add('Data');
  Sheet.Cells[1, 1].Value := 'Generated ' + DateTimeToStr(Now);
  Book.SaveAs(Mem);          // כותב מהמיקום הנוכחי של הזרם
  Mem.Position := 0;         // גלגלו לאחור לפני מסירת הזרם
  Response.ContentType :=
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
  Response.ContentStream := Mem;   // המסגרת מחזיקה כעת את Mem
finally
  Book.Free;
end;

הגלגול-לאחור הוא השורה שמצדיקה את הערתה. SaveAs(Stream) כותב מהמיקום הנוכחי של הזרם ולעולם אינו מחפש בחזרה לאפס לאחר מכן. שכחו את Mem.Position := 0 והלקוח מקבל הורדה של אפס בתים, או ש-Excel קורא לקובץ פגום. זהו הבאג הנפוץ ביותר בקוד חוברת-עבודה מול-רשת, והאכזרי ביותר, משום שהוא חולף על פני כל בדיקת יחידה שרק טוענת שאורך הזרם אינו אפס

שגרת בניית-חוברת-עבודה אחת מגיעה לכל פורמט אספקה אחר ללא ארגון מחדש. SaveAsCSV משיב לבקשת "פשוט תן לי את הנתונים הגולמיים", SaveAsHTML מטפל ב"שחרר את זה לתוך עמוד פורטל", SaveAsRTF מזין צינורות מסמכים, ו-SaveAsODS מכסה מנדט OpenDocument, כולם עם העמסות קובץ וזרם כאחד. שגרת ייצוא יחידה בתוספת פרמטר פורמט מחליפה את מה שנטה להיות ארבעה מאקרו COM נפרדים. ה-TXLSXHtmlExportOptions של מייצא ה-HTML נושא כותרת, מחלקת CSS ומתג מקטע-או-מסמך-מלא, מה שמרחיק את מקרה הפורטל מעסק עריכת-regex של תגיות מיוצאות

ערכי נוסחה ללא תהליך Excel לחשב אותם

תחת אוטומציית COM, Excel חישב מחדש הכל בחינם, ונטישת COM שוללת זאת בשקט. SaveAs מאחסן נוסחאות כטקסט מבלי להעריך אותן; המספרים מופיעים רק ברגע ש-Excel פותח את הקובץ ומחשב מחדש, התנהגות שחזית ה-XLS מאפשרת לכם לכוונן דרך RecalcOnSave ו-CalculationMode. עבור קובץ המופנה לאדם זה בדיוק נכון. זה שגוי עבור שירות שחייב לאשר סיכום לפני שהוא שולח, ושגוי עבור ייצוא CSV, שכותב את טקסט הנוסחה ולא את תוצאתה. כל מקרה חייב להעריך בשרת עם המנוע המובנה:

SheetX.Cells[1, 1].Value := 1200;
SheetX.Cells[2, 1].Value := 950;
SheetX.Cells[3, 1].Formula := 'SUM(A1:A2)';   // חזית XLSX: ללא קידומת '='
Total := BookX.Calculate('SUM(A1:A2)');       // הערכה בשרת, כעת
if Total <> 2150 then
  raise Exception.Create('reconciliation failed before delivery');

מוסכמת החזית נושכת שוב כאן. צד ה-XLSX מקצה ביטויים דרך Cell.Formula ללא סימן שוויון; צד ה-XLS כותב אותם דרך Cell.Value עם '=' מקדים. העבירו קוד מאחד לאחר ללא שינוי והמוסכמה השגויה מאחסנת מחרוזת טקסט שרק מזכירה נוסחה, בלי שגיאה לסמן זאת. כשנוסחאות חוברת עבודה צריכות להושיט יד אל לוגיקת העסק שלכם, ההתקשרות-חזרה OnUserFunction מאפשרת למנוע למסור שמות פונקציה לא-מוכרים לקוד Delphi בזמן ההערכה. זהו התחליף הילידי לתוספי ה-UDF שנוטים להתחבא בתוך אותם גיליונות שמערכת אוטומציית-COM גדלה סביבם

קצוות פריסה שצפים רק בשרת

כמה פרטים מכריעים אם ההשקה נקייה או מבלבלת, והראשון הוא גרף היחידות. מייצא מערך-הנתונים בגרירה-ושחרור TDataToXLS מושך פנימה את Forms, Controls ו-Dialogs של VCL. בלתי-מזיק בכלי שולחני; בשירות מסוף הוא גורר את כל ה-VCL אחריו. יחידות הליבה lxHandle ו-lxHandleX מושיטות יד רק אל Windows, Classes, SysUtils ו-Variants, ולכן שירות טהור מוטב לו לכתוב לולאת מערך-נתונים משלו כנגד API הליבה מאשר לייבא את הרכיב לשם נוחות

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

כיוון הפורמט הוא האחרון מהם. TXLSWorkbook.SaveAs כותב BIFF (xlExcel97) כברירת מחדל, ודחיפת תוכן XLS אל .xlsx עוברת דרך הגשר SaveXLSWorkbookAsXLSX בנאמנות מופחתת. בחרו את החזית לפי הפורמט שאתם מתכוונים לשלוח, בזמן התכנון, במקום לבנות באחת ולהמיר בקצה הצינור

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

HotXLS משלחת כקוד מקור Object Pascal עבור Delphi ו-C++Builder; מהדורות, רישוי ועיון ה-API המלא נמצאים בעמוד המוצר של HotXLS Component