Technický článek

Vlastní mřížka tabulkového procesoru v Delphi s HotXLS

HotXLS dodává TXLSWorkbookViewer, nativní ovládací prvek VCL, který vykresluje sešity XLS, XLSX, XLSM a ODS jako interaktivní mřížku tabulkového procesoru uvnitř formuláře Delphi nebo C++Builderu, bez instalace Excelu nebo jeho řízení přes automatizaci OLE. Postavit takový ovládací prvek dobře znamená vyřešit tři konkrétní problémy: namapovat klik myší, který skončí uvnitř sloučené buňky, na správnou logickou buňku, udržet pozici scrollování, pásma záhlaví a výběr buňky konzistentní, zatímco uživatel posouvá list mnohem větší, než je viditelné okno, a rozhodnout, co má klik na značku komentáře nebo buňku s hypertextovým odkazem skutečně udělat

Většina firem pracujících s Delphi sahá po prohlížeči tabulek z důvodů, které nemají nic společného s editací: auditní stanice, která zobrazuje náhled nahraných sešitů dřív, než vstoupí do pipeline, kiosek nebo prohlížeč reportů, kde Microsoft Office není součástí nasazovaného obrazu, nebo nástroj QA, který potřebuje zobrazit obsah sešitu bez nepředvídatelnosti automatizace skutečného procesu Excelu přes COM. Obyčejná řetězcová mřížka vám rychle dá text v buňkách, ale soubor tabulkového procesoru není obyčejná mřížka: buňky se slučují do bloků, které v podkladovém modelu existují jen jednou, listy nesou pevná pásma záhlaví a nezávislé vodorovné a svislé pozice scrollování, a jednotlivé buňky nesou komentáře a hypertextové odkazy, které potřebují vlastní model interakce. TXLSWorkbookViewer je odpověď HotXLS na tuto mezeru a jeho interní návrh je rozumný plán pro každého, kdo staví podobný ovládací prvek od nuly

Jak se prohlížeč sešitu vyhýbá závislosti na Excelu?

TXLSWorkbookViewer se Excelu vyhýbá úplně tím, že čte přes vlastní parsovaný objektový model HotXLS místo toho, aby otevíral dokument přes Excel a loutkově jej ovládal. Vlastnost Workbook naváže existující TXLSWorkbook pro klasické soubory XLS a XlsxWorkbook naváže TXLSXWorkbook pro varianty XLSX, XLSM a šablon; kterýkoli z nich už může být otevřený jinde v aplikaci, a prohlížeč z něj jen čte. Když má ovládací prvek soubor vlastnit sám, LoadFromFile zkontroluje příponu, nasměruje XLSX, XLSM, XLTX, XLTM a ODS přes moderní engine a všechno ostatní přes klasický, a uvolní ten sešit, který sám vytvořil, jakmile se ovládací prvek vyčistí nebo zničí

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;

Vyhledání správné buňky uvnitř sloučeného rozsahu

Vyřešit klik na správnou buňku v TXLSWorkbookViewer je dvoufázové vyhledávání, a rozdělení je důležité, protože pixelová geometrie a sémantika tabulkového procesoru jsou skutečně odlišné problémy. První fáze je čistá geometrie: soukromá metoda CellAtPoint prochází šířky sloupců a výšky řádků od aktuální pozice scrollování, dokud nenajde pásmo, které obsahuje kliknuté souřadnice X a Y, bez jakéhokoli povědomí o sloučených buňkách. Druhá fáze je sémantická: každá cesta, která mění výběr — klik myší, šipka, Tab, nebo přímé volání GoToCell — prochází přes jednu interní rutinu ChangeSelection, která normalizuje surový řádek a sloupec proti jakémukoli sloučení a před skutečnou změnou výběru je přichytí k ukotvující buňce sloučení

Ukotvující buňka je levá horní buňka sloučeného rozsahu a je to jediná buňka v tomto bloku, která v podkladovém modelu sešitu skutečně nese hodnotu, formát, komentář nebo hypertextový odkaz; každá jiná buňka, kterou sloučení vizuálně pokrývá, je v samotných datech prázdná. U klasických sešitů XLS pochází ukotvující buňka z Cell.MergeArea, IXLSRange, jehož Row a Column ukazují na vlastnící buňku; u sešitů XLSX a ODS MergedCells.FindAt vrátí TXLSXMergedRange, vystavující stejnou ukotvující buňku jako Row1 a Col1. Vykreslování řeší ekvivalentní problém nezávisle, rozšiřuje obdélník sloučené buňky na její plný rozsah řádku a sloupce a přeskakuje buňky uvnitř tohoto rozsahu, takže obrys výběru obepíná celý sloučený blok, ne jen jeho ukotvující roh, a zápis sloučených rozvržení místo pouhého zpětného čtení je příbuzný, ale odlišný problém popsaný v doprovodném článku o rozvržení sloučených buněk pro šablony reportů

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;

Co udržuje scrollování, záhlaví a výběr v souladu?

TXLSWorkbookViewer udržuje tři samostatné kusy stavu koherentní: logickou pozici scrollování drženou v TopRow a LeftCol, nativní posuvníky Windows, které ovládací prvek vyžaduje přes WS_HSCROLL a WS_VSCROLL v CreateParams, a aktuální výběr v SelectedRow a SelectedCol. Tažení posuvníku nebo otáčení kolečka myši vyvolá WM_HSCROLL, WM_VSCROLL, nebo WM_MOUSEWHEEL, což aktualizuje TopRow nebo LeftCol a překreslí; výběr se nepohne, což odpovídá tomu, jak sám Excel odděluje posouvání od výběru. Po kterékoli z těchto aktualizací UpdateScrollBars tlačí novou pozici zpět do nativního posuvníku přes SetScrollInfo, takže táhlo nikdy neodpluje z konsensu s tím, co mřížka skutečně zobrazuje

Navigace klávesnicí spouští stejnou synchronizaci opačným směrem: posun výběru za okraj viditelné mřížky zavolá EnsureSelectionVisible, což pošťouchne TopRow nebo LeftCol akumulací skutečných šířek sloupců a výšek řádků místo prostého zvyšování po jedné, protože řádky a sloupce mohou nést vlastní velikosti, a pak zavolá UpdateScrollBars, aby táhlo odráželo, kam právě klávesnice pohled odnesla. Pásma záhlaví čísla řádku a písmene sloupce, velikostí přes RowHeaderWidth a ColumnHeaderHeight, jsou tou částí tohoto ovládacího prvku, která zůstává na obrazovce pevná, zatímco TopRow a LeftCol scrollují data pod ní, a to je rozsah zamrazení, který tento ovládací prvek sám dělá: není to funkce Excelu Ukotvit příčky a neexistuje žádný vestavěný způsob, jak připnout libovolný datový řádek nebo sloupec, zatímco zbytek listu jím proscrolluje. Jedna hranice, kterou se vyplatí otestovat dřív, než nasadíte prohlížeč nad soubory, které plně nekontrolujete, je, že TopRow a LeftCol nejsou ořezávány proti skutečně použitému rozsahu listu, takže táhlo přetažené na svůj strukturální limit může skončit na řádku 1 048 576 nebo sloupci 16 384 a ukázat prázdnou mřížku místo posledního řádku nebo sloupce, který skutečně drží data; sešity dost velké na to, aby to bylo znatelné, jsou obvykle také dost velké na to, aby potřebovaly pozornost na straně načítání popsanou v článku o výkonu velkých sešitů

Zapojení komentářů a hypertextových odkazů na myš a události výběru

TXLSWorkbookViewer bere komentáře a hypertextové odkazy jako atributy aktuálně vybrané buňky, ne jako cíle najetí myší, takže SelectedCellCommentText, SelectedCellCommentAuthor a SelectedCellHyperlink se aktualizují pokaždé, když se vyvolá OnSelectionChange, ať se výběr posunul kliknutím myší, šipkou, nebo voláním GoToCell. Buňka s komentářem dostane do pravého horního rohu vykreslený malý červený trojúhelník jako vizuální nápovědu, podobně jako vlastní příznak komentáře v Excelu, ale tato značka je čistě vizuální; do ovládacího prvku není vestavěná žádná bublinová nápověda vyvolaná najetím myší, takže aplikace, která chce vyskakovací okno při najetí myší místo při výběru, si tuto vrstvu musí postavit sama. Aktivace hypertextového odkazu funguje stejným způsobem řízeným výběrem: dvojklik na buňku zavolá ActivateSelectedCell, což přečte SelectedCellHyperlink a, pokud není prázdný, vyvolá OnHyperlinkClick s cílovou adresou a parametrem var Handled: Boolean pro obsluhu, aby jej nastavila

Co OnHyperlinkClick nedělá, je stejně důležité: TXLSWorkbookViewer nikdy sám nevolá ShellExecute ani neotevírá prohlížeč, bez ohledu na to, zda obsluha nastaví Handled na true, nebo jej nechá false. Navigace, a jakékoli rozhodnutí o tom, co se počítá za bezpečný cíl, je zcela odpovědností hostitelské aplikace, což je správné výchozí chování pro komponentu, která nemá ponětí, zda je vložena do důvěryhodného interního nástroje, nebo prohlížeče pro soubory, které zákazník právě nahrál

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;

Rozsah výběru a limity navigace klávesnicí

Výběr v TXLSWorkbookViewer je vždy jedna logická buňka, sledovaná jako SelectedRow a SelectedCol; v základním ovládacím prvku neexistuje žádný obdélníkový výběr více buněk, takže jakákoli funkce, která potřebuje pracovat s blokem buněk, se musí postavit nad ním, ne přečíst z objektu výběru. Pokrytí klávesnicí je záměrně základní: šipky posunou o jednu buňku, Home se vrátí na začátek řádku, nebo s Ctrl na buňku A1, Page Up a Page Down skočí o deset řádků, a Tab a Shift+Tab kráčí přes sloupce; neexistuje žádný skok Ctrl+šipka na okraj datové oblasti a žádný výběr rozsahu rozšiřovaný pomocí Shift, takže uživatelé přicházející přímo z Excelu si na hustém listu tuto mezeru všimnou

Limity sloupců se vynucují na stejném hrdle ChangeSelection, které zpracovává normalizaci sloučení, a záměrně se liší podle enginu: prohlížeč navázaný na klasický TXLSWorkbook se ořízne na sloupec 256, strukturální strop formátu BIFF8, zatímco ten navázaný na TXLSXWorkbook respektuje moderní limit 16 384 sloupců, který XLSX zdědilo od Excelu 2007 dál. Řádky jsou v obou případech omezeny na 1 048 576, takže praktický rozdíl mezi otevřením staršího souboru XLS a souboru XLSX ve stejném prohlížeči je zcela v tom, jak daleko doprava vás mřížka nechá jít

Nic z tohoto není exotické, jakmile se to rozloží na vyhledání pixelu, normalizaci ukotvení a hrstku obsluhovačů zpráv, ale dostat všechny tři do souladu na skutečných souborech, se skutečnými sloučeními, komentáři a hypertextovými odkazy, je většina práce v komponentě tohoto druhu. TXLSWorkbookViewer se dodává jako součást standardní komponenty HotXLS Excel pro Delphi a C++Builder, spolu s klasickým i XLSX objektovým modelem, ze kterého vykresluje