מאמר טכני

grid גיליון-אלקטרוני מותאם אישית ב-Delphi עם HotXLS

HotXLS שולחת את TXLSWorkbookViewer, פקד VCL ילידי שמעבד חוברות עבודה מסוג XLS, ‏XLSX, ‏XLSM, ו-ODS כ-grid גיליון אלקטרוני אינטראקטיבי בתוך טופס Delphi או C++Builder, בלי להתקין Excel או להניע אותו דרך אוטומציית OLE. בניית פקד מהסוג הזה היטב אומרת פתרון שלוש בעיות ספציפיות: מיפוי לחיצת עכבר שנוחתת בתוך תא ממוזג לתא הלוגי הנכון, שמירה על מיקום גלילה, פסי כותרת, ובחירת תא עקביים ככל שמשתמש פונה על פני גיליון גדול הרבה יותר מהחלון הגלוי, והחלטה מה לחיצה על סמן הערה או תא-קישור צריכה בפועל לעשות

רוב חנויות ה-Delphi פונות למציג גיליון אלקטרוני מסיבות שאין להן שום קשר לעריכה: תחנת ביקורת שמציגה תצוגה מקדימה של חוברות עבודה שהועלו לפני שהן נכנסות לצינור, קיוסק או מציג דוח שבו Microsoft Office אינו חלק מתמונת הפריסה, או כלי QA שצריך להציג את תוכן חוברת עבודה בלי חוסר-הצפיות של אוטומציית תהליך Excel אמיתי מעל COM. grid מחרוזת פשוט נותן לך טקסט בתאים במהירות, אבל קובץ גיליון אלקטרוני אינו grid פשוט: תאים ממוזגים לבלוקים שקיימים רק פעם אחת במודל הבסיסי, גיליונות נושאים פסי כותרת קבועים ומיקומי גלילה אופקיים ואנכיים עצמאיים, ותאים בודדים נושאים הערות וקישורים שזקוקים למודל אינטראקציה משלהם. ‏TXLSWorkbookViewer הוא התשובה של HotXLS לפער הזה, והעיצוב הפנימי שלו הוא שרטוט סביר עבור כל מי שבונה פקד דומה מאפס

איך מציג חוברת עבודה נמנע מתלות ב-Excel?

‏TXLSWorkbookViewer נמנע מ-Excel לגמרי על ידי קריאה דרך מודל האובייקטים המפוענח של HotXLS עצמה במקום פתיחת מסמך דרך Excel והפעלתו כבובה. מאפיין ה-Workbook מקשר TXLSWorkbook קיים עבור קובצי XLS קלאסיים, ו-XlsxWorkbook מקשר TXLSXWorkbook עבור XLSX, ‏XLSM, ווריאנטים של תבנית; כל אחד יכול כבר להיות פתוח במקום אחר באפליקציה, והמציג רק קורא ממנו. כאשר הפקד צריך להחזיק את הקובץ עצמו, LoadFromFile בודקת את הסיומת, מנתבת XLSX, ‏XLSM, ‏XLTX, ‏XLTM, ו-ODS דרך המנוע המודרני וכל דבר אחר דרך הקלאסי, ומשחררת איזו חוברת עבודה שהיא יצרה ברגע שהפקד מתנקה או נהרס

var
  Viewer: TXLSWorkbookViewer;
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  if Book.Open('quarterly-report.xlsx') <> 1 then
    raise Exception.Create('Could not open workbook');

  Viewer := TXLSWorkbookViewer.Create(Self);
  Viewer.Parent := Self;
  Viewer.Align := alClient;
  Viewer.XlsxWorkbook := Book;        // the viewer does not take ownership
  Viewer.GoToCell(1, 1);

  Caption := Viewer.WorksheetName + ': ' + Viewer.SelectedCellText;
end;

איתור התא הנכון בתוך טווח ממוזג

פתרון לחיצה לתא הנכון ב-TXLSWorkbookViewer הוא חיפוש דו-שלבי, והפיצול חשוב משום שגיאומטריית פיקסלים וסמנטיקת גיליון אלקטרוני הן בעיות שונות באמת. השלב הראשון הוא גיאומטריה טהורה: פונקציית CellAtPoint פרטית עוברת על רוחבי עמודות וגבהי שורות ממיקום הגלילה הנוכחי עד שהיא מוצאת את הפס שמכיל את קואורדינטת ה-X וה-Y שנלחצה, ללא שום מודעות לתאים ממוזגים בכלל. השלב השני הוא סמנטי: כל נתיב שמשנה את הבחירה, לחיצת עכבר, מקש חץ, Tab, או קריאה ישירה ל-GoToCell, מוזרם דרך שגרת ChangeSelection פנימית אחת, שמנרמלת את השורה והעמודה הגולמיות מול כל מיזוג ומצמידה אותן לתא-העוגן של המיזוג לפני שהבחירה בפועל משתנה

העוגן הוא התא השמאלי-עליון של הטווח הממוזג, והוא התא היחיד בבלוק הזה שבאמת מחזיק ערך, פורמט, הערה, או קישור במודל חוברת העבודה הבסיסי; כל תא אחר שהמיזוג מכסה חזותית ריק בנתונים עצמם. עבור חוברות עבודה XLS קלאסיות העוגן מגיע מ-Cell.MergeArea, ‏IXLSRange ש-Row ו-Column שלו מצביעים על התא המחזיק; עבור חוברות עבודה XLSX ו-ODS, ‏MergedCells.FindAt מחזירה TXLSXMergedRange שחושפת את אותו עוגן כ-Row1 ו-Col1. ציור פותר בעיה מקבילה באופן עצמאי, מרחיב את המלבן של תא ממוזג לכל טווח השורה והעמודה שלו ומדלג על התאים בתוך הטווח הזה, כך שמתאר הבחירה עוטף את כל הבלוק הממוזג ולא רק את פינת-העוגן שלו, וכתיבת פריסות ממוזגות ולא רק קריאתן בחזרה היא בעיה קשורה אך נבדלת המכוסה במאמר הנלווה על פריסת תאים ממוזגים עבור תבניות דוח

var
  Sheet: TXLSXWorksheet;
begin
  Sheet := Book.Sheets.Add('Summary');
  Sheet.MergeCells(2, 2, 3, 4);       // B2:D3
  Sheet.Cells[2, 2].Value := 'Region totals';

  Viewer.XlsxWorkbook := Book;
  Viewer.GoToCell(3, 4);              // targets the bottom-right corner of the merge
  // SelectedRow is now 2 and SelectedCol is now 2: normalized to the anchor cell
end;

מה שומר על גלילה, כותרות, ובחירה מסונכרנים?

‏TXLSWorkbookViewer שומרת על שלושה פיסות מצב נפרדות עקביות: מיקום הגלילה הלוגי המוחזק ב-TopRow ו-LeftCol, פסי הגלילה הילידיים של Windows שהפקד מבקש דרך WS_HSCROLL ו-WS_VSCROLL ב-CreateParams, והבחירה הנוכחית ב-SelectedRow ו-SelectedCol. גרירת פס גלילה או סיבוב גלגלת העכבר מפעיל WM_HSCROLL, ‏WM_VSCROLL, או WM_MOUSEWHEEL, שמעדכנים TopRow או LeftCol ומציירים מחדש; הבחירה לא זזה, מה שתואם איך ש-Excel עצמה מפרידה בין פניה לבחירה. אחרי כל אחד מהעדכונים האלה, UpdateScrollBars דוחפת את המיקום החדש בחזרה לתוך פס הגלילה הילידי דרך SetScrollInfo, כך שהאגודל אף פעם לא סוחף מחוסר-הסכמה עם מה שה-grid בפועל מציג

ניווט מקלדת מריץ את אותו סנכרון בכיוון ההפוך: הזזת הבחירה מעבר לקצה ה-grid הגלוי קוראת ל-EnsureSelectionVisible, שמזיזה את TopRow או LeftCol על ידי צבירת רוחבי עמודות וגבהי שורות בפועל במקום פשוט להוסיף באחד, שכן שורות ועמודות יכולות לשאת גדלים מותאמים-אישית, ואז קוראת ל-UpdateScrollBars כך שהאגודל משקף לאן שהמקלדת הרגע לקחה את התצוגה. פסי כותרת מספר-השורה ואות-העמודה, בגודל דרך RowHeaderWidth ו-ColumnHeaderHeight, הם החלק בפקד הזה שנשאר קבוע על המסך בעוד TopRow ו-LeftCol גוללים את הנתונים מתחת, וזה היקף ה"הקפאה" שהפקד הזה עושה בעצמו: זו אינה תכונת ה-Freeze Panes של Excel, ואין דרך מובנית לנעוץ שורה או עמודת נתונים שרירותית בעוד שאר הגיליון גולל על פניה. גבול אחד ששווה לבדוק לפני שילוח מציג על פני קבצים שאתה לא שולט בהם לגמרי הוא ש-TopRow ו-LeftCol לא נצמדים (clamped) מול טווח-השימוש בפועל של גיליון העבודה, כך שאגודל שנגרר לגבול המבני שלו יכול לנחות על שורה 1,048,576 או עמודה 16,384 ולהציג grid ריק במקום השורה או העמודה האחרונה שבפועל מחזיקה נתונים; חוברות עבודה גדולות מספיק כדי להפוך את זה לניכר בדרך כלל גם גדולות מספיק כדי לזקוק לתשומת-הלב בצד-הטעינה המכוסה במאמר ביצועי חוברת עבודה גדולה

חיווט הערות וקישורים לאירועי עכבר ובחירה

‏TXLSWorkbookViewer מתייחסת להערות וקישורים כתכונות של איזה תא כרגע נבחר ולא כיעדי-ריחוף, כך ש-SelectedCellCommentText, ‏SelectedCellCommentAuthor, ו-SelectedCellHyperlink מתעדכנים בכל פעם ש-OnSelectionChange מופעל, בין אם הבחירה זזה בלחיצת עכבר, מקש חץ, או קריאה ל-GoToCell. תא עם הערה מקבל משולש אדום קטן שמצויר בפינה הימנית-עליונה שלו כרמז חזותי, דומה לדגל ההערה של Excel עצמה, אבל הסמן הזה חזותי בלבד; אין tooltip מופעל-על-ידי-ריחוף מובנה בפקד, כך שאפליקציה שרוצה popup בריחוף-עכבר ולא בבחירה חייבת לבנות את השכבה הזו בעצמה. הפעלת קישור עובדת באותו אופן מבוסס-בחירה-קודם: לחיצה כפולה על תא קוראת ל-ActivateSelectedCell, שקוראת את SelectedCellHyperlink ו, אם היא לא ריקה, מעלה OnHyperlinkClick עם כתובת היעד ופרמטר var Handled: Boolean ל-handler להגדיר

מה ש-OnHyperlinkClick לא עושה חשוב באותה מידה: TXLSWorkbookViewer אף פעם לא קוראת ל-ShellExecute או פותחת דפדפן בעצמה, ללא קשר אם ה-handler מגדיר את Handled ל-true או משאיר אותו false. ניווט, וכל החלטה על מה נחשב יעד בטוח, הוא לגמרי באחריות אפליקציית המארח, שזו ברירת המחדל הנכונה עבור רכיב שאין לו מושג אם הוא משובץ בכלי פנימי מהימן או במציג עבור קבצים שלקוח הרגע העלה

procedure TMainForm.ViewerSelectionChange(Sender: TObject; Row, Col: Integer);
begin
  if Viewer.SelectedCellCommentText <> '' then
    StatusBar.SimpleText := Viewer.SelectedCellCommentAuthor + ': ' +
      Viewer.SelectedCellCommentText
  else
    StatusBar.SimpleText := Viewer.SelectedCellHyperlink;
end;

procedure TMainForm.ViewerHyperlinkClick(Sender: TObject;
  const Target: WideString; var Handled: Boolean);
begin
  ShellExecute(0, 'open', PWideChar(Target), nil, nil, SW_SHOWNORMAL);
  Handled := True;
end;

היקף בחירה ומגבלות ניווט מקלדת

בחירה ב-TXLSWorkbookViewer היא תמיד תא לוגי בודד, מוקלטת כ-SelectedRow ו-SelectedCol; אין בחירת טווח-רב-תאים מלבני בפקד הבסיס, כך שכל תכונה שצריכה לפעול על בלוק תאים חייבת להיבנות מעליו ולא להיקרא מאובייקט בחירה. כיסוי המקלדת בסיסי במכוון: מקשי חץ זזים תא אחד בכל פעם, Home חוזר לתחילת השורה או, עם Ctrl, לתא A1, ‏Page Up ו-Page Down קופצים עשר שורות, ו-Tab ו-Shift+Tab צועדים על פני עמודות; אין קפיצת Ctrl+חץ לקצה אזור נתונים ואין בחירת טווח מורחבת-Shift, כך שמשתמשים שמגיעים ישר מ-Excel יבחינו בפער בגיליון צפוף

מגבלות עמודה נאכפות באותה נקודת-צוואר ChangeSelection שמטפלת בנרמול מיזוג, והן שונות לפי מנוע במכוון: מציג שקשור ל-TXLSWorkbook קלאסי נצמד בעמודה 256, התקרה המבנית של פורמט BIFF8, בעוד אחד שקשור ל-TXLSXWorkbook מכבד את מגבלת 16,384 העמודות המודרנית ש-XLSX ירשה מ-Excel 2007 ואילך. שורות מוגבלות ב-1,048,576 בכל מקרה, כך שההבדל המעשי בין פתיחת קובץ XLS מדור-קודם וקובץ XLSX באותו מציג הוא לגמרי עד כמה ימינה ה-grid מוכן לתת לך ללכת

שום דבר מזה אינו אקזוטי ברגע שהוא מפורק לחיפוש-פיקסל, נרמול-עוגן, ומספר handler-ים של הודעות, אבל להביא את השלושה להסכים תחת קבצים אמיתיים, עם מיזוגים, הערות, וקישורים אמיתיים, הוא רוב העבודה ברכיב כזה. ‏TXLSWorkbookViewer נשלח כחלק מרכיב ה-Excel HotXLS הסטנדרטי עבור Delphi ו-C++Builder, לצד מודלי האובייקטים הקלאסי וה-XLSX שהוא מעבד מהם