Автор: Дамир, Королевство Delphi
В статьях, посвященных работе с таблицами Word, как правило, авторы избегают тем, касающихся объединенных ячеек. Оно и понятно: любое обращение к ячейке таблицы, находящейся в объединенной области, приводит к возникновению ошибки. Это внутренняя проблема редактора Word, связанная с архитектурой таблицы, и с этим ничего не поделаешь.
Однажды потребовалось перевести в базу данных нормативные данные, оформленные в редакторе Word в виде таблиц. И сразу же возникли проблемы с объединенными ячейками - как заполучить данные, находящиеся в объединенных ячейках.
Но, оказывается, именно возникновение исключений при обращении к отсутствующим ячейкам и позволяет решить эту проблему. Логика простая: раз возникло исключение при обращении к какой-либо ячейке, значит с этой ячейкой не все гладко. Значит, надо этим воспользоваться. На этом принципе основана методика распознавания таблиц Word, представленная в данной статье.
Для начала создаем запись:
TWordTableCell = record
Merged : boolean;
VertCellMerged : boolean;
PrevMerCol : integer;
NextMerCol : integer;
EndMerRow : integer;
EndMerCol : integer;
CellWidth : single;
CellHeight : single;
TableLeft : single;
Text : OleVariant;
end;
| |
и массивный тип
TWordTableCells = array of array of TWordTableCell;
| |
Данный массив будет характеризовать всю таблицу, а элементы массива - каждую ячейку. Что означают поля записи TWordTableCell, можно догадаться по названиям.
В следующем фрагменте показаны переменные, необходимые для работы с таблицей.
TMainForm = class(TForm)
…
WApp : WordApplication;
FTable : Table;
FMaxUsedRows : integer;
FMaxUsedCols : integer;
FWordTableCell : TWordTableCells;
…
procedure GetTable;
procedure CalcWordTableProp;
…
public
end;
| |
Ядром обработки таблицы является следующая процедура:
procedure TMainForm.GetTable;
var i,j, UndoCount : integer;
NumRows, NumColumns : OleVariant;
bool : boolean;
begin
FTable := WApp.ActiveDocument.Tables.Item(1);
FMaxUsedRows := FTable.Rows.Count;
FMaxUsedCols := FTable.Columns.Count;
SetLength(FWordTableCell,0, 0);
SetLength(FWordTableCell,FMaxUsedRows+1, FMaxUsedCols+1);
WApp.Visible := true;
NumColumns := 1;
for i := 1 to FMaxUsedRows do
for j := 1 to FMaxUsedCols do
begin
FWordTableCell[i,j].VertCellMerged := false;
try
FTable.Cell(i,j).HeightRule := wdRowHeightAtLeast;
if FTable.Cell(i,j).Width >= 99999 then
begin
NumRows := 1;
UndoCount := 1;
repeat
inc(NumRows);
inc(UndoCount);
if (FTable.Cell(i,j).Width >= 99999) then
begin
bool := false;
try
try
try
FTable.Cell(i,j).Split(NumRows, NumColumns);
NumRows := 1;
except
end;
except
end;
except
end;
end
else bool := true;
until bool;
FWordTableCell[i,j].CellWidth := FTable.Cell(i,j).Width;
while UndoCount <> 1 do
begin
dec(UndoCount);
WApp.ActiveDocument.Undo(EmptyParam);
end;
end else
FWordTableCell[i,j].CellWidth := FTable.Cell(i,j).Width;
FWordTableCell[i,j].CellHeight := FTable.Cell(i,j).Height;
FWordTableCell[i,j].Merged := false;
FWordTableCell[i,j].Text := FTable.Cell(i,j).Range;
except
FWordTableCell[i,j].CellWidth := 0;
FWordTableCell[i,j].CellHeight := FTable.Rows.Height;
FWordTableCell[i,j].Merged := true;
end;
FWordTableCell[i,j].PrevMerCol := -1;
FWordTableCell[i,j].NextMerCol := -1;
end;
CalcWordTableProp;
end;
| |
В этой процедуре выполняется последовательное обращение к ячейкам таблицы и формирование записи FWordTableCell для каждой ячейки, характеризующей считываемую таблицу Word (как ни странно, здесь необходима именно тройная упаковка в try except end). Нужно обратить внимание, что некоторые ячейки приходится разбивать, т.к. для них невозможно определить никаких данных. (К сожалению, при прогонке программы в ручном режиме клавишами F7, F8 или F9, всегда возникает сообщение об ошибке, даже если отключить Tools->Debugger Option->Language Exception
Stop on Delphi Exception). Как видно из кода, инициализируются не все поля записи FWordTableCell. Для определения остальных полей производится обработка данных в процедуре CalcWordTableProp:
procedure TMainForm.CalcWordTableProp;
var i, j, k, N, EndCol: integer;
FTableWidth : single;
procedure CalcTableVertMergedInfo(i,j:integer);
var Col : integer;
begin
for Col := 1 to FMaxUsedCols do
if ((abs(FWordTableCell[i-1,Col].TableLeft-
FWordTableCell[i,j].TableLeft) <= 1) and
(FWordTableCell[i-1,Col].CellWidth <> 0))then
begin
FWordTableCell[i-1,Col].VertCellMerged := true;
FWordTableCell[i-1,Col].NextMerCol := j;
FWordTableCell[i,j].PrevMerCol := col;
FWordTableCell[i,j].CellWidth := FWordTableCell[i-1,Col].CellWidth;
Exit;
end;
end;
begin
for i := 1 to FMaxUsedRows do
begin
FWordTableCell[i,0].TableLeft:=0;
for j := 1 to FMaxUsedCols do
begin
with FWordTableCell[i,j] do
begin
TableLeft := FWordTableCell[i,j - 1].TableLeft+
FWordTableCell[i,j - 1].CellWidth;
FTableWidth := 0;
for k := 1 to FMaxUsedCols do FTableWidth := FTableWidth + FWordTableCell[i-1, k].CellWidth;
VertCellMerged := false;
if Merged and not (FWordTableCell[i,j].TableLeft = FTableWidth) then
CalcTableVertMergedInfo(i,j);
end;
end;
end;
for j := 1 to FMaxUsedCols do
for i := 1 to FMaxUsedRows do
if FWordTableCell[i,j].VertCellMerged then
begin
N := 0;
EndCol := j;
while FWordTableCell[i + N, EndCol].NextMerCol <> -1 do
begin
EndCol := FWordTableCell[i + N, EndCol].NextMerCol;
inc(N);
end;
FWordTableCell[i,j].EndMerRow := i + N;
FWordTableCell[i,j].EndMerCol := EndCol;
end;
end;
| |
Итак, заполнены все поля записи FWordTableCell для всех ячеек. Есть информация о каждой ячейке, которой можно воспользоваться, например, чтобы узнать содержимое любой ячейки.
Для демонстрации работы с массивом FWordTableCell воспользуемся компонентом TMStringGrid - аналогом компонента TStringGrid, но с возможностью объединения ячеек. Можно было бы использовать компонент TStringGrid, т.к. он работает гораздо быстрее TMStringGrid, или любой другой подходящий компонент, но так будет нагляднее.
Для объединения ячеек в таблице TMStringGrid предусмотрены функции с перегрузкой:
TMStringGrid.MergeCells(ALeft, ATop, ARight, ABottom : LongInt) : integer;
| |
или
TMStringGrid. MergeCells(Selection : TMGridRect) : integer;
| |
где ALeft, ATop, ARight, ABottom - координаты левой верхней и правой нижней объединяемых ячеек таблицы TMStringGrid.
Однако, в массиве FWordTableCell эти координаты отсутствуют, и их нужно вычислить по данным массива FWordTableCell.
Для этой цели введем дополнительные типы (данные типы нужны исключительно для подготовки данных для компонента TMStringGrid):
TRowCell = record
CellWidth : single;
Index : integer;
MergRect : TMGridRect;
FirstMerRow : integer;
FirstMerCol : integer;
LastMerRow : integer;
LastMerCol : integer;
FirstMerg : boolean;
Merged : boolean;
Text : string;
end;
TGridCol = class(TCollectionItem)
private
RowCell : array of TRowCell;
end;
TGridCols = class(TCollection)
FForm : TMainForm;
function GetItem(Index : integer) : TGridCol;
protected
property Items[Index : integer] : TGridCol read GetItem;
constructor Create(AOwner : TComponent);
function Add : TGridCol;
end;
| |
а также, переменные и процедуры:
TMainForm = class(TForm)
...
private
...
FGridCols : TGridCols;
procedure WriteToGrid;
procedure CalcGridProps;
...
public
end;
| |
Здесь процедура CalcGridProps заполняет массив RowCell коллекции TGridCol для каждой ячейки (также вычисляется действительное число колонок таблицы, т.к. FMaxUsedCols не является таковым):
procedure TMainForm.CalcGridProps;
var i,j,k : integer;
RowCell : array of TRowCell;
MinCellWidth : single;
DeltaInd : array of integer;
itBreak : boolean;
begin
FGridCols.Clear;
FGridCols.Add;
SetLength(RowCell, FMaxUsedRows + 1);
SetLength(DeltaInd, FMaxUsedRows + 1);
for i := 1 to FMaxUsedRows do
begin
RowCell[i].CellWidth := FWordTableCell[i, 1].CellWidth;
RowCell[i].Text := FWordTableCell[i, 1].Text;
DeltaInd[i] := 0;
RowCell[i].LastMerRow := FWordTableCell[i, 1].EndMerRow;
if RowCell[i].LastMerRow = 0 then RowCell[i].LastMerRow := i;
RowCell[i].FirstMerg := (FWordTableCell[i, 1].PrevMerCol = -1);
RowCell[i].FirstMerCol := -1;
RowCell[i].LastMerCol := -1;
end;
k := 0;
repeat
try
MinCellWidth := RowCell[1].CellWidth;
for i := 1 to FMaxUsedRows do
begin
if MinCellWidth <= 0 then break;
if MinCellWidth > RowCell[i].CellWidth then
MinCellWidth := RowCell[i].CellWidth;
end;
if MinCellWidth <= 0 then break;
for i := 1 to FMaxUsedRows do
if Abs(MinCellWidth - RowCell[i].CellWidth) < 0.1 then
RowCell[i].CellWidth := MinCellWidth;
FGridCols.Add;
for i := 1 to FMaxUsedRows do
begin
with FGridCols.Items[k].RowCell[i] do
begin
CellWidth := MinCellWidth;
Text := RowCell[i].Text;
if RowCell[i].FirstMerg and (RowCell[i].LastMerRow <> i)and (RowCell[i].FirstMerCol = -1) then
LastMerRow := RowCell[i].LastMerRow;
end;
if MinCellWidth < RowCell[i].CellWidth then
begin
FGridCols.Items[k + 1].RowCell[i].CellWidth := RowCell[i].CellWidth - MinCellWidth;
if RowCell[i].FirstMerg and (RowCell[i].FirstMerCol = -1) then
RowCell[i].FirstMerCol := k + 1;
RowCell[i].LastMerCol := k + 2;
end;
if FGridCols.Items[k + 1].RowCell[i].CellWidth <> -1 then
begin
RowCell[i].CellWidth := FGridCols.Items[k + 1].RowCell[i].CellWidth;
inc(DeltaInd[i]);
end
else
begin
if (RowCell[i].FirstMerCol <> -1) then
begin
FGridCols.Items[RowCell[i].FirstMerCol - 1].RowCell[i].LastMerCol := RowCell[i].LastMerCol;
FGridCols.Items[RowCell[i].FirstMerCol - 1].RowCell[i].FirstMerCol := RowCell[i].FirstMerCol;
FGridCols.Items[RowCell[i].FirstMerCol - 1].RowCell[i].LastMerRow := RowCell[i].LastMerRow;
RowCell[i].LastMerCol := -1;
RowCell[i].FirstMerCol := -1;
end;
RowCell[i].CellWidth := FWordTableCell[i, k + 2 - DeltaInd[i]].CellWidth;
RowCell[i].FirstMerg := FWordTableCell[i, k + 2 - DeltaInd[i]].PrevMerCol = -1;
RowCell[i].LastMerRow := FWordTableCell[i, k + 2 - DeltaInd[i]].EndMerRow;
try
RowCell[i].Text := FWordTableCell[i, k + 2 - DeltaInd[i]].Text;
except
RowCell[i].Text := '';
end;
end;
end;
inc(k);
except
end;
until k > FMaxUsedCols + 50;
for j := 0 to FGridCols.Count - 1 do
for i := 1 to FMaxUsedRows do
with FGridCols.Items[j].RowCell[i] do
begin
MergRect := GetMGridRect(j + 1, i, j + 1, i);
if LastMerCol <> 0 then
begin
Merged := true;
MergRect := GetMGridRect(FirstMerCol, i, LastMerCol, i);
end;
if LastMerRow <> 0 then
begin
Merged := true;
MergRect := GetMGridRect(MergRect.Left, MergRect.Top, MergRect.Right, LastMerRow);
end;
end;
end;
| |
Процедура WriteToGrid формирует копию таблицы Word в компоненте TMStringGrid:
procedure TMainForm.WriteToGrid;
var i, j, col : integer;
Selection : TMGridRect;
begin
MStringGrid1.ClearMergedCells;
MStringGrid1.ColCount := FGridCols.Count - 1 ;
MStringGrid1.RowCount := FMaxUsedRows;
for j := 0 to FGridCols.Count - 1 do
begin
if j < MStringGrid1.ColCount then
MStringGrid1.ColWidths[j] :=
round(FGridCols.Items[j].RowCell[1].CellWidth);
for i := 1 to FMaxUsedRows do
begin
if j < MStringGrid1.ColCount then
MStringGrid1.Cells[j, i - 1] := FGridCols.Items[j].RowCell[i].Text;
end;
end;
for j := 0 to FGridCols.Count - 1 do
begin
for i := 1 to FMaxUsedRows do
begin
if FGridCols.Items[j].RowCell[i].Merged then
with FGridCols.Items[j].RowCell[i] do
begin
Selection.Left := MergRect.Left - 1;
Selection.Top := MergRect.Top - 1;
Selection.Right := MergRect.Right - 1;
Selection.Bottom := MergRect.Bottom - 1;
MStringGrid1.MergeCells(Selection);
MStringGrid1.Cells[Selection.Left, Selection.Top] :=
FGridCols.Items[j].RowCell[i].Text;
end;
end;
end;
end;
| |
Для демонстрации работы приведенного выше кода была создана программа DemoWordTable.exe (Рисунок 1).
Рисунок 1. Интерфейс программы
Нажав кнопку "Открыть таблицу Word", можно открыть документ Word, содержащий таблицу. Программа считывает первую таблицу и создает ее копию в компоненте TMStringGrid (Рисунок 2).
Рисунок 2. Таблица в Word (на заднем плане) и ее копия на форме MainForm
Кроме того, программа позволяет и обратное действие, т.е. создавать произвольную таблицу на форме и переносить ее в документ Word.
Для изменения числа строк и столбцов, а также фиксированных строк и столбцов предусмотрены поля Edit (После изменения значения в поле Edit нужно нажать "Enter", чтобы изменения вступили в силу). Чтобы объединить какие-либо ячейки, нужно сперва их выделить, удерживая клавишу Shift и нажимая клавиши со стрелками. Затем щелкнуть правой кнопкой мыши. Откроется Popup Menu с кнопками "Объединить ячейки" и "Разбить ячейки" (Рисунок 3.). Нажатие кнопки "Объединить ячейки" приведет к объединению ячеек выделенной области. Если выделения нет, т.е. выделена только одна ячейка, то кнопка "Объединить ячейки" открывает редактор ячеек (Рисунок 4). В редакторе можно выбрать нужные ячейки для объединения, ввести текст, который будет находиться в ячейке и выбрать положение текста в ячейке (TO_LEFT, TO_CENTER, TO_RIGHT).
Рисунок 3. Объединение ячеек
Рисунок 4. Объединение ячеек с помощью редактора
После создания таблицы можно нажать на кнопку "Записать таблицу Word", после чего будет создан новый документ Word, и таблица в нем, которая будет копией созданной таблицы (Рисунок 5).
Рисунок 5. Таблица на форме MainForm (на заднем плане) и ее копия в Word
Напоследок нужно сказать, что есть еще одна уловка при объединении ячеек таблицы Word: если заранее известны все номера ячеек, которые нужно объединить, то объединение нужно начать справа налево, снизу вверх. Тогда объединенные ячейки не спутают индексы ячеек, и все пройдет гладко:
procedure TMainForm.WriteToWordTable;
var WDoc : WordDocument;
i, j, N : integer;
Cl : Cell;
MrgedCellInfo : TMrgedCellInfo;
begin
WDoc := WApp.Documents.Add(EmptyParam, EmptyParam, EmptyParam, EmptyParam);
WApp.Visible := true;
FMaxUsedRows := MStringGrid1.RowCount;
FMaxUsedCols := MStringGrid1.ColCount;
FTable2 := WDoc.Tables.Add(WDoc.Content, FMaxUsedRows, FMaxUsedCols,
EmptyParam, EmptyParam);
for i := 1 to 6 do SetBorders(FTable2, BorderType[i]);
for i := MStringGrid1.ColCount - 1 downto 0 do
for j := MStringGrid1.RowCount - 1 downto 0 do
if MStringGrid1.GetMergedInfo(i, j, MrgedCellInfo) then
begin
if (i = MrgedCellInfo.MergedRect.Left) and (j = MrgedCellInfo.MergedRect.Top) then
begin
FTable2.Cell(j + 1, i + 1).Range.Text := MStringGrid1.Cells[i, j];
Cl := FTable2.Cell(MrgedCellInfo.MergedRect.Bottom + 1, MrgedCellInfo.MergedRect.Right + 1);
FTable2.Cell(j + 1, i + 1).Merge(Cl);
end;
end else
FTable2.Cell(j + 1, i + 1).Range.Text := MStringGrid1.Cells[i, j];
end;
| |
К сожалению, хотя данный метод и работает, но очень уж медленно из за неповоротливости редактора Word.
Ссылки по теме