Teknisk artikel

Ett anpassat kalkylbladsrutnät i Delphi med HotXLS

HotXLS levererar TXLSWorkbookViewer, en nativ VCL-kontroll som renderar XLS-, XLSX-, XLSM- och ODS-arbetsböcker som ett interaktivt kalkylbladsrutnät inuti ett Delphi- eller C++Builder-formulär, utan att installera Excel eller styra det via OLE-automation. Att bygga den typen av kontroll väl innebär att lösa tre specifika problem: att mappa ett musklick som hamnar inuti en sammanslagen cell till rätt logiska cell, att hålla rullningsposition, rubrikband och cellmarkering konsekventa när en användare panorerar ett kalkylblad mycket större än det synliga fönstret, och att avgöra vad ett klick på en kommentarmarkör eller en hyperlänkcell faktiskt ska göra

De flesta Delphi-verksamheter griper efter en kalkylbladsvisare av skäl som inte har något med redigering att göra: en granskningsstation som förhandsgranskar uppladdade arbetsböcker innan de går in i en pipeline, en kiosk- eller rapportvisare där Microsoft Office inte är en del av installationsavbilden, eller ett QA-verktyg som behöver visa en arbetsboks innehåll utan oförutsägbarheten i att automatisera en riktig Excel-process över COM. Ett enkelt strängrutnät ger dig snabbt text i celler, men en kalkylbladsfil är inte ett enkelt rutnät: celler slås samman till block som bara existerar en gång i den underliggande modellen, kalkylblad bär fasta rubrikband och oberoende horisontella och vertikala rullningspositioner, och enskilda celler bär kommentarer och hyperlänkar som behöver sin egen interaktionsmodell. TXLSWorkbookViewer är HotXLS svar på den luckan, och dess interna design är en rimlig ritning för alla som bygger en liknande kontroll från grunden

Hur undviker en arbetsboksvisare att bero av Excel?

TXLSWorkbookViewer undviker Excel helt genom att läsa genom HotXLS egen tolkade objektmodell istället för att öppna ett dokument genom Excel och styra det som en marionett. Workbook-egenskapen binder en befintlig TXLSWorkbook för klassiska XLS-filer, och XlsxWorkbook binder en TXLSXWorkbook för XLSX, XLSM och mallvarianter; endera kan redan vara öppen någon annanstans i applikationen, och visaren bara läser från den. När kontrollen ska äga filen själv inspekterar LoadFromFile filändelsen, dirigerar XLSX, XLSM, XLTX, XLTM och ODS genom den moderna motorn och allt annat genom den klassiska, och frigör vilken arbetsbok den än skapade när kontrollen rensas eller förstörs

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;

Att hitta rätt cell inuti ett sammanslaget intervall

Att lösa ett klick till rätt cell i TXLSWorkbookViewer är en tvåstegsuppslagning, och uppdelningen spelar roll eftersom pixelgeometri och kalkylbladssemantik genuint är olika problem. Det första steget är ren geometri: en privat CellAtPoint-metod går igenom kolumnbredder och radhöjder från den aktuella rullningspositionen tills den hittar bandet som innehåller den klickade X- och Y-koordinaten, utan någon medvetenhet om sammanslagna celler alls. Det andra steget är semantiskt: varje väg som ändrar markeringen, ett musklick, en piltangent, Tab, eller ett direkt anrop till GoToCell, går genom en enda intern ChangeSelection-rutin, som normaliserar den råa raden och kolumnen mot eventuell sammanslagning och fäster dem vid sammanslagningens ankarcell innan markeringen faktiskt ändras

Ankaret är cellen längst upp till vänster i det sammanslagna intervallet, och det är den enda cellen i det blocket som genuint innehåller ett värde, ett format, en kommentar, eller en hyperlänk i den underliggande arbetsboksmodellen; varje annan cell sammanslagningen visuellt täcker är tom i själva datan. För klassiska XLS-arbetsböcker kommer ankaret från Cell.MergeArea, ett IXLSRange vars Row och Column pekar på den ägande cellen; för XLSX- och ODS-arbetsböcker returnerar MergedCells.FindAt ett TXLSXMergedRange som exponerar samma ankare som Row1 och Col1. Ritningen löser ett motsvarande problem oberoende, expanderar en sammanslagen cells rektangel till sitt fulla rad- och kolumnspann och hoppar över cellerna inuti det spannet, så markeringskonturen omsluter hela det sammanslagna blocket snarare än bara dess ankarhörn, och att skriva sammanslagna layouter snarare än att bara läsa tillbaka dem är ett relaterat men distinkt problem som täcks i följeartikeln om sammanslagen cell-layout för rapportmallar

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;

Vad håller rullning, rubriker och markering synkroniserade?

TXLSWorkbookViewer håller tre separata tillståndsdelar konsekventa: den logiska rullningspositionen som hålls i TopRow och LeftCol, de nativa Windows-rullningslisterna kontrollen begär genom WS_HSCROLL och WS_VSCROLL i CreateParams, och den aktuella markeringen i SelectedRow och SelectedCol. Att dra en rullningslist eller snurra på mushjulet utlöser WM_HSCROLL, WM_VSCROLL, eller WM_MOUSEWHEEL, som uppdaterar TopRow eller LeftCol och ritar om; markeringen flyttas inte, vilket matchar hur Excel självt separerar panorering från markering. Efter någon av dessa uppdateringar skjuter UpdateScrollBars in den nya positionen tillbaka i den nativa rullningslisten genom SetScrollInfo, så att tumman aldrig glider ur samklang med vad rutnätet faktiskt visar

Tangentbordsnavigering kör samma synkronisering i motsatt riktning: att flytta markeringen förbi kanten på det synliga rutnätet anropar EnsureSelectionVisible, som knuffar TopRow eller LeftCol genom att ackumulera faktiska kolumnbredder och radhöjder snarare än att bara öka med en, eftersom rader och kolumner kan bära anpassade storlekar, och anropar sedan UpdateScrollBars så att tumman återspeglar vart tangentbordet just tog vyn. Rad-nummer- och kolumn-bokstavs-rubrikbanden, storlekssatta genom RowHeaderWidth och ColumnHeaderHeight, är den del av den här kontrollen som förblir fast på skärmen medan TopRow och LeftCol rullar datan under, och det är omfattningen av frysning den här kontrollen gör på egen hand: det är inte Excels Freeze Panes-funktion, och det finns inget inbyggt sätt att fästa en godtycklig datarad eller kolumn medan resten av kalkylbladet rullar förbi den. En avgränsning värd att testa innan man levererar en visare över filer man inte fullt ut kontrollerar är att TopRow och LeftCol inte klämmes mot kalkylbladets faktiska använda intervall, så en tumme dragen till sin strukturella gräns kan hamna på rad 1 048 576 eller kolumn 16 384 och visa ett tomt rutnät istället för den sista raden eller kolumnen som faktiskt innehåller data; arbetsböcker stora nog att göra det märkbart är vanligtvis också stora nog att behöva den inläsningssidans uppmärksamhet som täcks i artikeln om prestanda för stora arbetsböcker

Att koppla kommentarer och hyperlänkar till mus- och markeringshändelser

TXLSWorkbookViewer behandlar kommentarer och hyperlänkar som attribut hos vilken cell som än för närvarande är markerad snarare än som hovringsmål, så SelectedCellCommentText, SelectedCellCommentAuthor, och SelectedCellHyperlink uppdateras varje gång OnSelectionChange utlöses, oavsett om markeringen flyttades genom musklick, piltangent, eller ett anrop till GoToCell. En kommenterad cell får en liten röd triangel målad i sitt övre högra hörn som en visuell ledtråd, liknande Excels egen kommentarflagga, men den markören är rent visuell; det finns ingen hovringsutlöst verktygstips inbyggd i kontrollen, så en applikation som vill ha en popup vid mus-över snarare än vid markering måste bygga det lagret själv. Hyperlänksaktivering fungerar på samma markering-först-sätt: att dubbelklicka en cell anropar ActivateSelectedCell, som läser SelectedCellHyperlink och, om den inte är tom, utlöser OnHyperlinkClick med måladressen och en var Handled: Boolean-parameter för hanteraren att sätta

Vad OnHyperlinkClick inte gör är precis lika viktigt: TXLSWorkbookViewer anropar aldrig ShellExecute eller öppnar en webbläsare på egen hand, oavsett om hanteraren sätter Handled till true eller lämnar den false. Navigering, och alla beslut om vad som räknas som ett säkert mål, är helt värdapplikationens ansvar, vilket är rätt standard för en komponent som inte har någon aning om huruvida den är inbäddad i ett betrott internt verktyg eller en visare för filer en kund just laddat upp

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;

Markeringsomfång och gränser för tangentbordsnavigering

Markering i TXLSWorkbookViewer är alltid en enda logisk cell, spårad som SelectedRow och SelectedCol; det finns ingen rektangulär flercellsintervallmarkering i baskontrollen, så alla funktioner som behöver agera på ett block av celler måste byggas ovanpå den snarare än läsas av från ett markeringsobjekt. Tangentbordstäckningen är avsiktligt grundläggande: piltangenter flyttar en cell åt gången, Home återgår till radens början eller, med Ctrl, till cell A1, Page Up och Page Down hoppar tio rader, och Tab och Shift+Tab stegar över kolumner; det finns inget Ctrl+Pil-hopp till kanten av en dataregion och ingen Shift-utökad intervallmarkering, så användare som kommer direkt från Excel kommer att märka luckan på ett tätt kalkylblad

Kolumngränser verkställs vid samma ChangeSelection-flaskhals som hanterar sammanslagningsnormalisering, och de skiljer sig medvetet mellan motorer: en visare bunden till en klassisk TXLSWorkbook klämmer vid kolumn 256, det strukturella taket för BIFF8-formatet, medan en bunden till TXLSXWorkbook respekterar den moderna 16 384-kolumnsgränsen som XLSX ärvde från Excel 2007 och framåt. Rader begränsas till 1 048 576 oavsett, så den praktiska skillnaden mellan att öppna en äldre XLS-fil och en XLSX-fil i samma visare handlar helt om hur långt åt höger rutnätet är villigt att låta dig gå

Inget av detta är exotiskt när det väl bryts ner i pixeluppslagning, ankarnormalisering, och en handfull meddelandehanterare, men att få de tre att samstämma under riktiga filer, med riktiga sammanslagningar, kommentarer, och hyperlänkar, är merparten av arbetet i en komponent som denna. TXLSWorkbookViewer levereras som en del av standard-HotXLS Excel-komponenten för Delphi och C++Builder, tillsammans med de klassiska och XLSX-objektmodeller den renderar från