מאמר טכני

רשומות Selection וגלילת Pane של BIFF8 ב-HotXLS

‏HotXLS מאחסן בחירות של גיליון ומיקומי גלילה לכל pane דרך API אחד מודע-pane גם ב-TXLSWorksheet וגם ב-TXLSXWorksheet: SelectAreas, GetSelectedAreas, ScrollWindow, ו-TryGetWindowScroll. עבור קבצי .xls קלאסיים, HotXLS כותב רשומות Selection של BIFF8 (0x001D) עם 1369 אזורים לכל היותר כל אחת, ממיר שמות pane לוגיים לבייטי ה-pane שהפורמט מגדיר, ומשאיר כל ציר גלילה על רשומת Window2 או Pane שם ש-Excel מצפה לו

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

למה בחירה גדולה צריכה יותר מרשומת Selection אחת?

בחירה גדולה זקוקה לכמה רשומות כי גוף של רשומת BIFF8 מוגבל ל-8224 בייטים וכל אזור נבחר עולה שישה בייטים קבועים. [MS-XLS] §2.4.248 מתאר את רשומת ה-Selection כחלק קבוע של 9 בייטים (בייט ה-pane, rwAct ו-colAct עבור התא הפעיל, irefAct עבור האזור הפעיל, ו-cref עבור מספר האזורים) ואחריו מבני RefU במספר cref, שכל אחד מחזיק שתי שורות של 16 סיביות ושתי עמודות של 8 סיביות. המספר הגדול ביותר שנכנס הוא (8224 − 9) / 6 עם עיגול כלפי מטה, כלומר 1369, וזה מניב גוף של 8223 בייטים, בייט אחד מתחת למגבלה. TXLSWorksheet.StoreSelectionGroup משתמש בקבוע הזה בתור MaxAreasPerRecord וכותב קבוצה גדולה יותר כרשומות Selection עוקבות עבור אותו pane, 1369 אזורים בכל פעם

הפרט שנושך הוא irefAct. כל חתיכה חוזרת על אותן שורה פעילה, עמודה פעילה ואינדקס אזור פעיל, ו-irefAct מאנדקס את הרצף המצטבר של כל החתיכות, לא את האזורים בתוך הרשומה שנושאת אותו. בחירה של אזור אחד מעבר למגבלה ממחישה את זה: 1370 אזורים כשהאחרון פעיל הופכים לשתי רשומות, הראשונה עם cref 1369 והשנייה עם cref 1, ושתיהן נושאות irefAct 1369. הערך הזה גדול ממספר האזורים של הרשומה השנייה עצמה. קורא שבודק את irefAct מול cref בכל רשומה דוחה קובץ תקין, וקורא שמחליף את המצב שלו בכל רשומה משליך את 1369 האזורים הראשונים. הקורא של HotXLS מצרף רשומות עוקבות של אותו pane לקבוצה אחת, מחייב כל חתיכה להסכים על התא הפעיל והאינדקס, ומריץ את בדיקת הטווח רק ברשומת ה-EOF של הגיליון, אחרי שהרצף המלא ידוע. ל-overload של SelectAreas שמתחיל מ-pane אין לכן תקרה של 1369 אזורים. הוא מאמת כל הפניית A1 ואת האינדקס הפעיל לפני שהוא לוקח את נעילת הכתיבה של הגיליון, ומחזיר False עם הבחירה הקודמת בלי שינוי אם משהו פגום

למה HotXLS כותב בחירת גיליון גדולה ככמה רשומות Selection של BIFF8: תקרת הגוף של 8,224 בייטים מכילה 9 בייטים קבועים בתוספת 1369 אזורי RefU בני שישה בייטים, כך ש-3,000 אזורים הופכים לשלוש רשומות של אותו pane, 1369, 1369 ו-262, ו-irefAct מאנדקס את הרצף המצטבר, כך ש-1370 אזורים כשהאחרון פעיל נותנים לשתי הרשומות irefAct 1369
כל חתיכה חוזרת על אותו תא פעיל ואינדקס, הקורא של HotXLS מצרף רשומות עוקבות של אותו pane לקבוצה אחת, ובדיקת הטווח רצה רק ברשומת ה-EOF אחרי שהרצף המלא ידוע
var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  Diffs: TXLSSelectedAreas;
  I: Integer;
begin
  Book := TXLSWorkbook.Create;
  try
    Sheet := Book.Sheets.Add;
    Sheet.FreezePanes(1, 1);           // שורת הכותרת ועמודה A נשארות במקום

    SetLength(Diffs, 3000);
    for I := 0 to High(Diffs) do
      Diffs[I] := Format('C%d', [I + 2]);

    // הקפאה מאפסת את הבחירה השמורה, לכן בוחרים אחרי ההקפאה.
    // 3000 אזורים נשמרים כשלוש רשומות Selection: 1369 + 1369 + 262
    if not Sheet.SelectAreas(xlspBottomRight, Diffs, 0) then
      raise Exception.Create('Selection rejected');

    Book.SaveAs('reconciliation.xls');
  finally
    Book.Free;
  end;
end;

איזה בייט pane נושאת רשומת Selection?

רשומת Selection מזהה את ה-pane שלה לפי הקוד המספרי שהפורמט מגדיר: 0 עבור ימני-תחתון, 1 עבור ימני-עליון, 2 עבור שמאלי-תחתון, ו-3 עבור שמאלי-עליון. ה-enum הציבורי TXLSPanePosition מוכרז בסדר קריאה, xlspTopLeft, xlspTopRight, xlspBottomLeft, xlspBottomRight, כך ש-Ord(xlspTopLeft) הוא 0, שהוא ה-pane הימני-תחתון בקובץ. הטלה ישירה של ה-enum לבייט ה-pane הייתה כותבת כל בחירה של שמאלי-עליון על ה-pane הימני-תחתון בלי שום שגיאה. כל נקודת כניסה של HotXLS שמודעת ל-panes ממירה את ה-enum דרך משפט case מפורש, כך שאתה לעולם לא מתמודד עם הקודים המספריים בכלל. גם קיום ה-pane נבדק: ה-pane הימני-עליון קיים רק עם פיצול אנכי, השמאלי-תחתון רק עם פיצול אופקי, והימני-תחתון רק עם שניהם. עבור pane שהגאומטריה הנוכחית של פיצול או הקפאה לא מכילה, SelectAreas מחזיר False, ו-GetSelectedAreas מחזיר מערך ריק עם ActiveAreaIndex שמוגדר -1, בלי ליצור pane, אובייקט בחירה או תא בחוברת

איך HotXLS ממפה את TXLSPanePosition על בייט ה-pane של Selection ב-BIFF8: ה-enum מוכרז בסדר קריאה כך ש-Ord(xlspTopLeft) הוא 0, בזמן שהקובץ מגדיר 0 עבור ימני-תחתון, 1 עבור ימני-עליון, 2 עבור שמאלי-תחתון ו-3 עבור שמאלי-עליון, ולכן כל נקודת כניסה מודעת-pane ממירה דרך משפט case מפורש
הטלה ישירה של ה-enum לבייט ה-pane הייתה כותבת כל בחירה של שמאלי-עליון על ה-pane הימני-תחתון, ולכן HotXLS בודק גם את קיום ה-pane מול הגאומטריה הנוכחית של פיצול או הקפאה לפני הכתיבה

איפה נמצא מיקום הגלילה של כל pane?

מיקום הגלילה של כל pane מפוצל על פני שתי רשומות, כי ארבעה panes חולקים רק שני מיקומי שורה ושני מיקומי עמודה. בחוברת קלאסית, השורה הנראית הראשונה של ה-panes העליונים והעמודה הנראית הראשונה של ה-panes השמאליים הם Window2.rwTop ו-Window2.colLeft, בזמן שהשורה של ה-panes התחתונים והעמודה של ה-panes הימניים הם Pane.rwTop ו-Pane.colLeft. לכן ScrollWindow(xlspTopRight, R, C) כותב Window2.rwTop ו-Pane.colLeft, והגדרת העמודה של ה-pane הימני-עליון מזיזה גם את ה-pane הימני-תחתון, בדיוק כמו שהשניים חולקים פס גלילה אופקי אחד ב-Excel. המתודות הציבוריות משתמשות במספרי שורה ועמודה החל מ-1. pane חסר מחזיר False ומאפס את שני פלטי השאילתה, וקואורדינטה מחוץ לטווח נפסלת לפני שאחד הצירים משתנה. שום דבר כאן לא תלוי באיך viewer מצייר את הרשת. פקד רינדור מחזיק TopRow ו-LeftCol משל עצמו, כפי שהמאמר על רינדור חוברות בגריד VCL מותאם מתאר, ואלה מצב ריצה, לא מה שנשמר

איפה נמצא כל ציר גלילה של pane ב-HotXLS: ארבעה panes חולקים שני מיקומי שורה ושני מיקומי עמודה, כך שהשורה העליונה והעמודה השמאלית הם Window2.rwTop ו-Window2.colLeft בזמן שהשורה התחתונה והעמודה הימנית הם Pane.rwTop ו-Pane.colLeft, ו-ScrollWindow(xlspTopRight, 1, 6) כותת שדה Window2 אחד בתוספת שדה Pane אחד כך שהימני-תחתון מתלווה
XLSX מפזר את אותם נתונים על פני התכונות topLeftCell של sheetView ושל pane, וצמצום שתי השכבות לאחת הוא בדיוק הדרך שבה מיקום גלילה עליון או שמאלי נעלם בשקט בטעינה

‏XLSX מפזר את אותם נתונים על פני שני אלמנטים: sheetView/@topLeftCell (ECMA-376 Part 1, §18.3.1.87) עבור החלון כולו והבן pane/@topLeftCell (§18.3.1.66) עבור הצד הימני-תחתון של פיצול. שתי התכונות יכולות להופיע יחד. HotXLS קורא קודם את התכונה החיצונית לשדות ברמת החלון, נותן לבן pane לדרוס רק את השדות ברמת ה-pane, וכותב את שתיהן בחזרה בנפרד. צמצום שתי השכבות לאחת הוא בדיוק הדרך שבה מיקום גלילה עליון או שמאלי נעלם בשקט בטעינה. עותקי גיליונות נושאים את שתי השכבות בשני המנועים. נקודות הכניסה הוותיקות שומרות על ההתנהגות המקורית שלהן: המאפיינים הקלאסיים ScrollRow ו-ScrollColumn, וה-XLSX SetPaneScroll ו-GetPaneScroll המבוססים על אפס. גאומטריית ההקפאה והפיצול עצמה מוגדרת עם הגדרות רמת הגיליון שמכוסות במאמר על הגנת גיליון, הגדרת עמוד והדפסה

var
  Row, Col: Integer;
begin
  Sheet.FreezePanes(1, 1);

  // ימני-תחתון: ציר השורה התחתונה (Pane.rwTop) וציר העמודה הימנית (Pane.colLeft)
  Sheet.ScrollWindow(xlspBottomRight, 500, 3);

  // ימני-עליון חולק את ציר העמודה הימנית, ולכן זה גם מזיז את הימני-תחתון לעמודה 6
  Sheet.ScrollWindow(xlspTopRight, 1, 6);

  if Sheet.TryGetWindowScroll(xlspBottomRight, Row, Col) then
    Memo1.Lines.Add(Format('Bottom-right starts at row %d, column %d', [Row, Col]));
    // הימני-תחתון מתחיל בשורה 500, עמודה 6
end;

מה קורה כשרשומת Selection פגומה?

כשרשומת Selection פגומה, HotXLS משאיר אותה כבייטים אטומים, מדווח על קוד אבחון 1304 (xlsDiagnosticSelectionRecordInvalid), וכותב את הגוף המקורי בחזרה בייט אחר בייט בשמירה. לפני שרשומה מצטרפת לקבוצה של ה-pane שלה, הקורא בודק אותה בסדר. בייט ה-pane חייב להיות 3 או פחות. רשומות עבור pane אחד חייבות להיות רצופות בזרם. 9 הבייטים הקבועים חייבים להיות קיימים. cref חייב להיות בין 1 ל-1369, והגוף חייב להיות בדיוק 9 + cref × 6 בייטים. כל חתיכה בקבוצה חייבת להסכים על התא הפעיל ו-irefAct, ל-irefAct אסור שתהיה סיבית סימן דלוקה, העמודה הפעילה חייבת להיות על הרשת, ואף אזור לא רשאי להיות עם גבולות הפוכים. בעיות ברשומה פיזית בודדת מדווחות פעם אחת לרשומה. סתירות שמופיעות רק אחרי הצבירה, כמו irefAct שמצביע מעבר למספר האזורים הכולל או תא פעיל מחוץ לאזור המאונדקס, מדווחות פעם אחת לקבוצה ב-EOF. קבוצה לא תקפה נשארת בלתי נראית ל-API הטיפוסי: GetSelectedAreas מחזיר מערך ריק עם אינדקס -1 עבור אותו pane, בזמן שכל pane אחר ממשיך לעבוד

var
  I: Integer;
  D: TXLSDiagnostic;
begin
  if Book.Open('supplier-upload.xls') <> 1 then
    Exit;
  for I := 0 to Book.Diagnostics.Count - 1 do
  begin
    D := Book.Diagnostics[I];
    if D.Code = xlsDiagnosticSelectionRecordInvalid then
      Log.Add(Format('%s: record $%.4x kept opaque (%s)',
        [D.SheetName, D.RecordId, D.Message]));
  end;
end;

איך בחירות שורדות הוספות שורות ועמודות?

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

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