HotXLS поставляется с TXLSWorkbookViewer — нативным элементом управления VCL, который отрисовывает книги XLS, XLSX, XLSM и ODS как интерактивную сетку электронной таблицы внутри формы на Delphi или C++Builder, без установки Excel или управления им через автоматизацию OLE. Хорошая реализация такого элемента управления означает решение трёх конкретных задач: сопоставление клика мышью, попавшего внутрь объединённой ячейки, с правильной логической ячейкой; сохранение согласованности позиции прокрутки, полос заголовков и выделения ячеек, пока пользователь панорамирует лист, значительно превышающий видимое окно; и решение, что должен на самом деле делать клик по маркеру комментария или ячейке с гиперссылкой
Большинство команд на Delphi обращаются к просмотрщику электронных таблиц по причинам, не имеющим ничего общего с редактированием: станция аудита, предпросматривающая загруженные книги перед их попаданием в конвейер, киоск или просмотрщик отчётов, где Microsoft Office не входит в образ развёртывания, или инструмент контроля качества, которому нужно показать содержимое книги без непредсказуемости автоматизации реального процесса Excel через COM. Обычная строковая сетка быстро даёт вам текст в ячейках, но файл электронной таблицы — не обычная сетка: ячейки объединяются в блоки, которые в лежащей в основе модели существуют лишь однажды, листы несут фиксированные полосы заголовков и независимые горизонтальные и вертикальные позиции прокрутки, а отдельные ячейки несут комментарии и гиперссылки, которым нужна собственная модель взаимодействия. 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, так что ползунок никогда не расходится с тем, что сетка реально показывает
Навигация с клавиатуры выполняет ту же синхронизацию в обратном направлении: перемещение выделения за край видимой сетки вызывает EnsureSelectionVisible, который сдвигает TopRow или LeftCol, накапливая реальные ширины столбцов и высоты строк, а не просто увеличивая на единицу, поскольку строки и столбцы могут нести пользовательские размеры, а затем вызывает UpdateScrollBars, чтобы ползунок отражал то место, куда клавиатура только что переместила вид. Полосы заголовков с номерами строк и буквами столбцов, размер которых задаётся через RowHeaderWidth и ColumnHeaderHeight, — та часть этого элемента управления, что остаётся зафиксированной на экране, пока TopRow и LeftCol прокручивают данные под ней, и это предел того, что этот элемент управления делает с закреплением сам по себе: это не функция «Закрепить области» из Excel, и здесь нет встроенного способа закрепить произвольную строку или столбец данных, пока остальной лист прокручивается мимо неё. Одна граница, которую стоит протестировать перед выпуском просмотрщика для файлов, которые вы не полностью контролируете, — это то, что TopRow и LeftCol не ограничены реально используемым диапазоном листа, так что ползунок, перетащенный до своего структурного предела, может оказаться на строке 1 048 576 или столбце 16 384 и показать пустую сетку вместо последней строки или столбца, реально содержащих данные; книги, достаточно крупные, чтобы это стало заметным, обычно также достаточно велики, чтобы требовать внимания на стороне загрузки, описанного в статье о производительности крупных книг
Подключение комментариев и гиперссылок к событиям мыши и выделения
TXLSWorkbookViewer трактует комментарии и гиперссылки как атрибуты той ячейки, что сейчас выделена, а не как цели наведения, так что SelectedCellCommentText, SelectedCellCommentAuthor и SelectedCellHyperlink обновляются каждый раз, когда срабатывает OnSelectionChange, независимо от того, переместилось ли выделение кликом мыши, клавишей со стрелкой или вызовом GoToCell. Ячейка с комментарием получает небольшой красный треугольник, нарисованный в её верхнем правом углу как визуальную подсказку, похожую на собственный флаг комментария Excel, но этот маркер чисто визуален; во встроенный элемент управления не встроена всплывающая подсказка при наведении, так что приложению, желающему всплывающее окно при наведении мыши, а не при выделении, придётся строить этот слой самостоятельно. Активация гиперссылки работает тем же способом, ориентированным на выделение: двойной клик по ячейке вызывает ActivateSelectedCell, который читает SelectedCellHyperlink и, если оно не пусто, порождает OnHyperlinkClick с целевым адресом и параметром var Handled: Boolean, который обработчик может установить
Что OnHyperlinkClick не делает — так же важно: TXLSWorkbookViewer никогда сам не вызывает ShellExecute и не открывает браузер, независимо от того, устанавливает ли обработчик 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 в одном и том же просмотрщике целиком в том, насколько далеко вправо сетка готова вас пустить
Ничто из этого не экзотично, если разбить на пиксельный поиск, нормализацию якоря и горстку обработчиков сообщений, но заставить эти три вещи согласоваться на реальных файлах, с реальными объединениями, комментариями и гиперссылками, — вот в чём основная работа в компоненте вроде этого. TXLSWorkbookViewer поставляется как часть стандартного компонента Excel HotXLS для Delphi и C++Builder, наряду с классической и XLSX объектными моделями, из которых он отрисовывает