עברו על התאים של תבנית דוח שזה עתה נפתחה וכותרת ממוזגת מתנהגת כמו דליפה. אתם קוראים A1 ומקבלים "Quarterly Statement"; אתם קוראים B1 עד F1, שיושבים באופן גלוי תחת אותה כרזה, ומקבלים כלום. כתבו ערך אל C1 כדי לתקן את הכותרת והוא לעולם לא יופיע על המסך. הרשת לא איבדה את הנתונים שלכם. היא עושה בדיוק את מה שמיזוג משמעו: ב-XLS וב-XLSX כאחד, מלבן ממוזג מרנדר את התוכן של תא אחד, העוגן השמאלי-עליון, ומתייחס לשאר כשטח מכוסה שמחזיק ערכים אך לעולם אינו מציג אותם. משתמשי Excel קולטים זאת בניסוי וטעייה. מחולל דוחות חייב לקודד זאת ככלל, משום שבקוד מחולל הסימפטום הוא אזור ריק ללא חריגה שניתן לעקוב דרכה אליו. HotXLS, ספריית Object Pascal ילידית שקוראת וכותבת את שני פורמטי Excel מ-Delphi ומ-C++Builder, חושפת את טבלת המיזוג במפורש מספיק כדי שתוכלו לתכנת כנגד הכלל במקום לגלותו מחדש בכרטיס תמיכה
ערך אחד, עוגן אחד
מיזוג הוא הוראת תצוגה הנפרשת מעל רשת שאינה משנה את צורתה. כל תא מכוסה עדיין קיים בקובץ כמשבצת משלו; רשומת המיזוג רק אומרת לצרכן לצייר את תוכן העוגן על פני המלבן. ההבחנה הזו מניעה שלוש התנהגויות ששווה להפנים לפני שתכתבו קוד פריסה כלשהו. קריאת תא מכוסה מחזירה את הערך המאוחסן שלו עצמו, שעבור כרזה שבניתם הוא בדרך כלל ריק, ולכן כל קוד שבוחן כותרת ממוזגת חייב לפתור ולקרוא את העוגן. כתיבה אל תא מכוסה מצליחה ברמת הקובץ ואינה מופיעה בשום מקום, וזו מלכודת הכותרת הבלתי-נראית מהפתיחה. וביטול מיזוג של אזור חושף את מה שישב תחתיו כל הזמן, ולכן ערך תועה שנכתב אל שטח מכוסה הופך לפגם נראה ביום שבו מישהו ימוסס את המיזוג
בצד ה-XLSX הטבלה הזו היא אובייקט מהשורה הראשונה. Sheet.MergedCells נושא את Add('A1:C1'), FindAt(Row, Col), DeleteAt ו-Items, והקריאה האחת שתשלחו אליה הכי הרבה היא FindAt: מסרו לה קואורדינטה כלשהי והיא מחזירה את האזור הממוזג המכסה את אותו תא, או nil כשהתא עומד לבדו. החיפוש היחיד הזה הוא היסוד לשני חצאי הטיפול הנכון במיזוג, הקריאה הבטוחה ושומר הכתיבה, ושניהם מופיעים בהמשך
שתי חזיתות, שני ניבי מיזוג
HotXLS שומרת על מנוע ה-BIFF8 הקלאסי של .xls ועל מנוע ה-OOXML של .xlsx כמודלי אובייקטים נפרדים, והם מאייתים מיזוג באופן שונה משום שהם צאצאים למוסכמות שונות. חזית ה-XLS עוקבת אחר ניב ה-COM של Excel: אתם נוטלים טווח ממאפיין מאונדקס דו-ארגומנטי וקוראים ל-Merge עם OleVariant שערכו מכריע על הגיאומטריה שתישארו איתה
var
Book: IXLSWorkbook; // interface-counted: no manual Free
Sh: IXLSWorksheet;
begin
Book := TXLSWorkbook.Create;
Sh := Book.Sheets[1]; // אוסף הגיליונות של XLS מבוסס-1
Sh.Range['A1', 'F1'].Merge(False); // False = בלוק ממוזג אחד
Sh.Cells.Item[1, 1].Value := 'Quarterly Statement';
Sh.Range['A3', 'F4'].Merge(True); // True = מיזוג לרוחב: מיזוג אחד לכל שורה
Book.SaveAs('layout.xls');
end;
הארגומנט ל-Merge הוא החלק שאנשים טועים בו. על פני טווח של שתי שורות, Merge(True) מפיק שני מיזוגים עצמאיים של שורה אחת, וזה ה"Merge Across" של Excel ובדיוק מה שאתם רוצים עבור רצועת כותרת מוערמת שצריכה לשמור על שורותיה ניתנות להפרדה. Merge(False) מאחה את כל המלבן לבלוק יחיד. הטווח גם מדווח על MergeCells כדגל מצב, מחזיר את האזור המכיל דרך MergeArea, וממוסס את עצמו עם Unmerge. חזית ה-XLSX חושפת את אותן פעולות תחת שמות שונים: Sheet.MergeCells(Row1, Col1, Row2, Col2) נוטלת גבולות מספריים, TXLSXRange.Merge מקבל את ערך ה-Across המקביל, ואוסף ה-MergedCells מחזיק את התוצאה
תבנית שגדלה עם הנתונים שלה
תבנית דוח אמיתית אינה רשת קבועה. הכותרת והסיכומים קבועים, אך מקטע הפירוט שביניהם נמתח לכל מה שהשאילתה מחזירה. הדפוס שמחזיק מעמד שומר שורת פירוט אחת מעוצבת במלואה בתבנית, משכפל אותה פעם אחת לכל רשומה, ואז פותח פער לפני בלוק הסיכומים כך שכל המעוגן מתחת מחליק מטה מבלי לאבד את עיצובו
Sheet.Range['A1:F1'].Merge;
Sheet.Cells[1, 1].Value := 'INVOICE #2026-0611'; // הערך הולך אל העוגן, A1
Sheet.RowHeight[1] := 28;
TitleFont := Book.Fonts.Add('Calibri', 16, True, False);
Sheet.Cells[1, 1].FontIndex := TitleFont + 1; // אינדקס מאגר מבוסס-0, צד התא מבוסס-1
// שורה 5 היא שורת תבנית הפירוט המעוצבת
for I := 0 to ItemCount - 1 do
Sheet.CopyRange(5, 1, 5, 6, 6 + I, 1); // סגנונות ונוסחאות נוסעים יחד איתה
// פתחו פער מעל בלוק הסיכומים; התוכן מתחת מוזז מטה
Sheet.InsertRows(6 + ItemCount, 1);
Sheet.Range['A1:F1'].SetBorders(xlsxEdgeOutline, xlsxBorderMedium);
שתי שורות מצדיקות מבט שני. הקצאת הגופן נושאת סטייה-באחד שנושכת בשקט: Fonts.Add מחזיר מיקום מאגר מבוסס-0, בעוד תא מאחסן הפניית גופן מבוססת-1 שבה 0 משמעו גופן ברירת המחדל, ולכן השמטת ה-+ 1 אינה מעוררת דבר, היא פשוט מעצבת את הכותרת שלכם בגופן הלא נכון. השורה האחרת היא CopyRange, שמעבירה עיצוב ונוסחאות יחד עם ערכים. זו כל הסיבה לשכפל שורת תבנית בנויה-ביד במקום לשחזר את מראהּ בקוד. מעצב מחזיק במראה פעם אחת, בתבנית; המחולל רק יוצק נתונים אל עותקים שלה
החלוקה הזו מתרחבת עוד כשהפריסה הניתנת לשימוש חוזר חיה בחוברת עבודה משלה, נניח גיליון של רצועות כותרת עליונה ותחתונה המשותפות בין דוחות. CopyRangeTo מבצעת את אותו שכפול על פני גבולות גיליונות עבודה, נוטלת גיליון יעד בתוספת קואורדינטות יעד, כך שמחולל יכול לשמור גיליון תבנית בתולי אחד ולהטביע את אזוריו לתוך כמה גיליונות פלט שעבודה צריכה. החלופה, שינוי התבנית במקום וניסיון לשחזרה לאחר מכן, היא מהדברים שעובדים עד ליום שבו הרצה נקטעת באמצע
מה InsertRows מזיז, ומה לא
דפוס הצמחת-התבנית עובד רק משום ש-InsertRows של XLSX הוא עריכה מבנית ולא ערבוב תאים. כשהוא פותח פער הוא ממקם מחדש את האזורים הממוזגים, גבהי השורות, הקישורים, ההערות, החלוניות הקפואות, טווחי הסינון האוטומטי, העיצובים המותנים, אימותי הנתונים, הטבלאות, השמות המוגדרים, עוגני התמונות ועוגני התרשימים שיושבים מתחת לנקודת ההכנסה, ולא רק את ערכי התאים. זה מה שמאפשר לבלוק הסיכומים להגיע לשורתו החדשה עם מיזוגיו ותבניות המספרים שלו שלמים במקום להגיע מקולף
שני הגבולות המתועדים שלו הם אלה שיש לתכנן סביבם. התאמת הנוסחאות מוגבלת לגיליון הנערך: הפניות בתוך אותו גיליון נכתבות מחדש, ונוסחה בגיליון אחר שמצביעה אל האזור המוזז נכתבת אף היא מחדש, אך ההתאמה עוקבת רק אחר הפניות שמכוונות אל הגיליון הנערך, ולכן כל סכמת הפניה חוצת-חוברת-עבודה ראויה לביקורת משלה ולא לאמון עיוור. הגבול השני חד יותר, והוא בצד ה-XLS. טבלאות ציר שורדות מחזורי פתיחה-שמירה כרשומות שמורות גולמיות, ולא כאובייקטים ממודלים ש-HotXLS יכולה להזיז, ולכן הכנסת שורות אינה ממקמת מחדש את עקבת הציר. כל תבנית שתבנו לפורמט .xls צריכה לחנות את אזורי הציר שלה הרחק מכל רצועה שגדלה
סירוב לכתוב נתונים אל שטח פריסה
כשל התא הממוזג שבאמת מגיע לייצור אינו הקוסמטי. הוא מבני: שורת פירוט נסחפת אל רצועת פריסה ממוזגת, ערכיה נוחתים בתאים מכוסים והופכים בלתי-נראים, וסיכומי העמודה חדלים בשקט להתאים למה שכל מי שקורא את הגיליון יכול לראות. משום ש-FindAt משיב על שאלת האזור-המכסה עבור כל קואורדינטה, המחולל יכול לסרב לכתיבה הזו ברגע שהיא הייתה קורית במקום לשלוח דוח שסופר-בחסר בשקט
// סרבו לכתוב נתוני פירוט אל אזור פריסה ממוזג
if Sheet.MergedCells.FindAt(Row, 1) <> nil then
raise Exception.CreateFmt('row %d overlaps a merged layout region', [Row]);
Sheet.Cells[Row, 1].Value := Detail.Description;
אותה בדיקת גבול שייכת לכל מקום שבו משתמש ימיין או יסנן את הפלט בהמשך. טווח עם מיזוגים בתוכו אינו יכול להתמיין בנקיון, משום שהמיון מזיז שורות באופן עצמאי ולמיזוג הפרוש על פני שורות אין שורה יחידה לנסוע איתה; Excel מגיב בשגיאה או בפריסה משובשת. המשמעת ששומרת על דוחות נכונים היא גיאוגרפית. כלאו מיזוגים אל רצועות כותרת, מפרידי מקטעים ובלוקי חתימה, ושמרו על אמצע הגיליון הטבלאי שטוח. מאמר יצירת דוחות התבנית מפתח את חלוקת הפריסה-מול-נתונים הזו לזרימת עבודה מלאה מונחת-placeholder, ומאמר העיצוב המותנה והטקסט העשיר מכסה את עיצוב רצועת הנתונים השטוחה ההיא
כיצד מיזוגים מתדרדרים בדרך החוצה
מיזוג הוא מושג חוברת-עבודה, וכל פורמט ייצוא מוכוון-טקסט מכבד אותו במידה שונה. הכרת שלוש ההתנהגויות מראש חוסכת מחזור QA. ייצוא HTML משחזר מיזוגים בנאמנות, ופולט colspan ו-rowspan על טבלה יחידה, כך שדוח מיועד-דפדפן שומר על מראהו המרוצף. ייצוא RTF אינו פורש עמודות כלל: טקסט העוגן נוחת בתא משלו ושאר רוחב המיזוג יוצא כתאים ריקים, מה שמשאיר כותרת רחבה דחופה חזותית שמאלה במעבד תמלילים. ל-CSV אין מושג של מיזוג, ולכן ערך העוגן תופס שדה אחד וכל תא מכוסה נפלט כשדה ריק. המסקנה עבור חוברת עבודה שמזינה גם ייצואים מתוחמים היא לשמור כל דבר נושא-עומס מחוץ לגיאומטריה ממוזגת; מאמר ייצוא ה-CSV, ה-TSV וה-HTML עובר על כל פורמט בפירוט
הרגעה אחת לכל מי ששוקל זאת מול גודל הקובץ: מיזוגים עולים כמעט כלום בקנה מידה של דוח. טבלת המיזוג זעירה לצד נתוני התאים, וקריאת תא מכוסה עדיין עוברת דרך FindAt ולא דרך סריקה. הלחץ הביצועי על חוברות עבודה גדולות מגיע ממקום אחר, בעיקר צמיחת מאגר-הסגנונות והזיכרון שמסלול השמירה מחזיק, שאותם מאמר ביצועי חוברת העבודה הגדולה נוטל ישירות. שני ממשקי המיזוג, פעולות העריכה המבנית והדגמות התבנית מגיעים יחד עם HotXLS Component