Crawling merged cells in Word
In general, in continuation of my previous topic on parsing Word tables using interop, I came across merged cells.
How can I find out how many cells were merged so that I can multiply F3 by N rows?
I dripped in the debagger in search of the desired property, but I did not find something... And on the Internet they write that there is no such property
UPD 1
On one of the resources we threw this idea(Code in VBA, but easily rewritten in C#):
Sub ParseInExcel()
Dim doc As Document, tbl As Table, eObj, eWbk, eWst, eRng, eCell
Dim i As Long, j As Long
Set doc = ThisDocument
Set tbl = doc.Tables(1)
tbl.Range.Copy
Set eObj = CreateObject("Excel.Application")
Set eWbk = eObj.Workbooks.Add
Set eWst = eWbk.Sheets(1)
eWst.Paste
Set eRng = eWst.Cells(1).CurrentRegion
For i = 1 To eRng.Rows.Count
For j = 1 To eRng.Columns.Count
Set eCell = eRng.Cells(i, j)
If eCell.MergeCells Then
Debug.Print eCell.MergeArea.Cells(1).Value & " ";
Else
Debug.Print eCell.Value & " ";
End If
Next
Debug.Print
Next
eWbk.Saved = True
eWbk.Close
Set eWbk = Nothing
eObj.Quit
Set eObj = Nothing
End Sub
The point is that everything from Word is copied to Excel, and there already appear properties that show the union of cells - MergeRange
. The problem is that the clipboard will be busy(the most problematic in my opinion, since I will not be able to work at the PC at the time of the program, otherwise I will cripple the buffer), and also that the second horseman of the apocalypse-Excel is connected.
If Excel knows how to present a table from Word and create properties that show that a certain zone is combined, which means that there must be some unambiguous algorithm for calculating this by means of Word.
Even when saving in html format, the number of combined columns and rows is written.
UPD 2
I found a solution for the combined rows, but not for the columns. I threw my solution, but I don't think it's universal.
Maybe someone has more ideas?
P. S Interop is not it is fundamental, but it is a priority, as I have drawn a lot of code on it.
If there is a solution for example for *. DOCX, then I can change the format.
6 answers
Getting xml with docx is not difficult here
Let's look at such a table
1 4 < 5 2 - - ^
Where
<w:document>
<w:body>
<w:tbl>
<w:tr>
<w:tc>(1)</w:tc>
<w:tc><w:rcPr><w:gridSpan w:val="2"/></w:rcPr>(4)</w:tc>
<!-- отсутствует колонка -->
<w:tc><w:rcPr><w:vMerge w:val="restart"/></w:rcPr>(5)</w:tc>
</w:tr>
<w:tr>
<w:tc>(2)</w:tc>
<w:tc>-</w:tc>
<w:tc>-</w:tc>
<w:tc><w:rcPr><w:vMerge/></w:rcPr></w:tc>
</w:tr>
The text is stored in the <w:t>текст</w:t>
tags. It is easier to calculate the combined column in this way-by the gridSpan tag. The combined lines will have to be flipped through the vMerge
tag.
Here is an example of how to access tags inside docx
using System;
using System.IO;
using System.Xml;
public class Demo {
public static void docx2process(Stream file) {
int isTable = 0; /*Для парсинга xml*/
int col = 0;
int row = 0;
int colSpan = 1;
/*Парсинг архива*/
int ready = 0; while (ready++ < 10){ /*защита от повисания, обход архива*/
byte[] head = new byte[30]; file.Read(head, 0, 30); if (head[0] != 'P') break; //zip-header
int i = (head[27] + head[29]) * 256 + head[28]; // extra len
long paked = BitConverter.ToInt32(head,18);
byte[] nam = new byte[255];
file.Read(nam, 0, head[26]);
if (i != 0) file.Seek(i, SeekOrigin.Current);
String aname = System.Text.Encoding.ASCII.GetString(nam, 0, head[26]);
if (aname == "word/document.xml"){
long lastpos = file.Position;
using (System.IO.Compression.DeflateStream deflate = new System.IO.Compression.DeflateStream(file, System.IO.Compression.CompressionMode.Decompress)){
System.Xml.XmlReader rd = System.Xml.XmlReader.Create(deflate);
// Тут парсинг документа
while (rd.Read()){
if (rd.NodeType == XmlNodeType.Element)
switch (rd.Name){
case "w:gridSpan": colSpan = int.Parse(rd.GetAttribute("w:val"));break;
case "w:vMerge":; /*Тут нужно допилить обьединение строк*/ break;
case "w:tbl": row=0; isTable = rd.Depth ; break;
case "w:tr": col=0; row++; break;
case "w:tc": col+= colSpan; colSpan=1; break;
case "w:t":/*Теги с текстом*/
if (rd.Read() && rd.NodeType == XmlNodeType.Text)
/*Тут нужно написать свой вывод куда-либо*/
if (isTable>0)
Console.WriteLine(string.Format("c={0}.{3} r={1} t={2}",col,row,rd.Value, colSpan));
else Console.WriteLine(rd.Value);
break;
case "w:p": /*Параграф*/ if (isTable == rd.Depth) isTable = 0; break;
}
}
return;
}
file.Position = lastpos + paked;
}else file.Seek(paked, SeekOrigin.Current);
};
}
public static void docx2process(string filename) {
using (Stream f = File.OpenRead(filename)) docx2process(f);
}
public static void Main(string[] args) {
docx2process("1.docx");
}
}
This example almost solves the problem. The combined columns are visible. Combined lines-still need to be finalized.
Learning this simple code
Sub test()
Dim oneTable As Table
Dim oneCell As Cell
Dim i As Integer
Set oneTable = ThisDocument.Tables(1)
For Each oneCell In oneTable.Range.Cells
i = i + 1
Debug.Print i, oneCell.RowIndex, oneCell.ColumnIndex
Next
End Sub
On the test plates, combining the cells will give enough information to solve the problem.
Even when saving in html format, the number of combined columns and rows is written.
So can you use this part? Convert doc to html and parse the colspan attribute/rowspan
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Text;
using Microsoft.Office.Interop.Word;
using System.Runtime.InteropServices;
//Add Reference: Microsoft HTML Object Library
namespace ConsoleApplication1
{
class Program
{
public static List<string> FindMergedColumns(string html)
{
List<string> res = new List<string>();
mshtml.HTMLDocument doc = null;
mshtml.IHTMLDocument2 d2 = null;
mshtml.IHTMLDocument3 d = null;
try
{
doc = new mshtml.HTMLDocument();
d2 = (mshtml.IHTMLDocument2)doc;
d2.write(html);
d2.close();
d = (mshtml.IHTMLDocument3)doc;
var coll = d.getElementsByTagName("table");
object val;
int numtable = 1;
int row = 1, column = 1;
int span;
foreach (mshtml.IHTMLElement2 table in coll)
{
var rows = table.getElementsByTagName("tr");
foreach (mshtml.IHTMLElement2 tr in rows)
{
var cells = tr.getElementsByTagName("td");
foreach (mshtml.IHTMLElement td in cells)
{
val = td.getAttribute("colspan");
if(val == null)val = 0;
span = Convert.ToInt32(val);
if (span > 1)
{
res.Add(String.Format("Table {0}, Row {1}, Column {2}: {3} columns merged",numtable,row,column,span));
}
column++;
}
row++;
column = 1;
}
numtable++;
row = 1; column = 1;
}
doc.close();
}
finally
{
//освобождение ресурсов
if (doc != null) Marshal.ReleaseComObject(doc);
if (d2 != null) Marshal.ReleaseComObject(d2);
if (d != null) Marshal.ReleaseComObject(d);
}
return res;
}
public static void Main(string[] argv)
{
var word = new Microsoft.Office.Interop.Word.Application();
object miss = System.Reflection.Missing.Value;
object path = "c:\\test\\test.doc";
object readOnly = true;
var docs = word.Documents.Open(ref path, ref miss, ref readOnly,
ref miss, ref miss, ref miss, ref miss,
ref miss, ref miss, ref miss, ref miss,
ref miss, ref miss, ref miss, ref miss,
ref miss);
string tmp = Path.GetTempPath() + "file.htm";
//конвертируем doc в html
docs.SaveAs(FileName: tmp, FileFormat: WdSaveFormat.wdFormatHTML);
((_Document)docs).Close();
((_Application)word).Quit();
//парсим HTML
string html = File.ReadAllText(tmp);
var res = FindMergedColumns(html);
File.Delete(tmp);
foreach (var line in res) Console.WriteLine(line);
Console.ReadKey();
}
}
}
Pay attention to the lines:
WordTableAnalyser wta = new WordTableAnalyser(table,1.0);
MyWordCell c = wta.getCell(8, 1);
MessageBox.Show(c.rows.ToString() + ";" + c.columns.ToString());
Source table
Application based on Windows Forms
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Microsoft.Office.Interop.Word;
using System.Reflection;
namespace WorkWithWord
{
using Word = Microsoft.Office.Interop.Word;
public class MyWordCell
{
public double x = 0;
public double x2 = 0;
public double w = 0;
public int columns = 1;
public int rows = 1;
public int ini_row=-1;
public int ini_column=-1;
public Word.Cell wc;
public MyWordCell(Word.Cell cell)
{
wc = cell;
}
}
public class DoubleToleranceComparer : IComparer<double>,IEqualityComparer<double>
{
double tolerance=0;
public DoubleToleranceComparer(double tolerance)
{
this.tolerance = tolerance;
}
#region Члены IComparer<double>
public int Compare(double x, double y)
{
double delta = x - y;
return Math.Abs(delta) <= tolerance ? 0 : Math.Sign(delta);
}
#endregion
#region Члены IEqualityComparer<double>
public bool Equals(double x, double y)
{
throw new NotImplementedException();
}
public int GetHashCode(double obj)
{
throw new NotImplementedException();
}
#endregion
}
public class WordTableAnalyser
{
public static Object missingObj = System.Reflection.Missing.Value;
public static Object trueObj = true;
public static Object falseObj = false;
Word.Table wtable = null;
double cc = 0;//количество необъединенных столбцов
double rc = 0;//количество необъединенных строк
double WT = 0;//ширина таблицы
double HT = 0;//высота таблицы
DoubleToleranceComparer dt_comparer;//сравниватель double c допуском
SortedList<double,double> split_xs;//встречающиеся координаты границ ячеек таблицы по x
List<MyWordCell> curRow = new List<MyWordCell>();
List<List<MyWordCell>> myTable = new List<List<MyWordCell>>();
public MyWordCell getCell(int ini_row,int ini_column)
{
MyWordCell c = null;
for (int ci = 0; ci < myTable[ini_row-1].Count; ++ci)
{
if (myTable[ini_row - 1][ci].ini_column == ini_column)
{
c = myTable[ini_row - 1][ci];
break;
}
}
return c;
}
public WordTableAnalyser(Word.Table table,double tolerance)
{
wtable = table;
cc = table.Columns.Count;
rc = table.Rows.Count;
dt_comparer = new DoubleToleranceComparer(tolerance);
split_xs = new SortedList<double,double>(dt_comparer);
split_xs.Add(0.0, 0.0);
for (int i = 1; i <= cc; ++i)
{
try
{
Word.Cell cell=table.Cell(1,i);
MyWordCell myCell = new MyWordCell(cell);
myCell.x = WT;
myCell.w = cell.PreferredWidth;
myCell.x2 = myCell.x + myCell.w;
myCell.ini_row = 1;
myCell.ini_column = i;
curRow.Add(myCell);
WT += cell.PreferredWidth;//Width не всегда определена
//cell.
split_xs.Add(WT, WT);
}
catch(Exception ex)
{
}
}
myTable.Add(curRow);
for(int j=2;j<=rc;++j)
{
List<MyWordCell> prevRow = curRow;
curRow = new List<MyWordCell>();
List<MyWordCell> myTableRow = new List<MyWordCell>();
int prevColInd = 0;
double curX=0;
for (int i = 1; i <= cc; ++i)
{
try
{
Word.Cell cell=table.Cell(j,i);
MyWordCell myCell = new MyWordCell(cell);
myCell.x = curX;
myCell.w = cell.PreferredWidth;
myCell.x2 = myCell.x + myCell.w;
myCell.ini_row = j;
myCell.ini_column = i;
curRow.Add(myCell);
//while(myTable.Count<myCell.in)
myTableRow.Add(myCell);
//cell.
try
{
split_xs.Add(myCell.x2, myCell.x2);
}
catch (Exception ex) { }
curX = myCell.x2;
if (dt_comparer.Compare(myCell.x, prevRow[prevColInd].x) == 0)
{
while ((prevColInd<prevRow.Count) && (dt_comparer.Compare(prevRow[prevColInd].x, myCell.x2) < 0))
prevColInd++;
}
}
catch(Exception ex)//для текущего ряда по данному индексу пропуск
{
if (prevColInd < prevRow.Count)
{
prevRow[prevColInd].rows++;
curRow.Add(prevRow[prevColInd]);
curX = prevRow[prevColInd].x2;
prevColInd++;
}
else
{
break;
}
}
}
myTable.Add(myTableRow);
}
//добавляем недостающие колонки
for (int j = 0; j < myTable.Count; ++j)
{
for (int i = 0; i < myTable[j].Count; ++i)
{
MyWordCell c = myTable[j][i];
double x = c.x;
double x2 = c.x2;
int ind_x=split_xs.IndexOfKey(x);
int ind_x2 = split_xs.IndexOfKey(x2);
int delta_ind=ind_x2 - ind_x;
if (delta_ind > 1) c.columns += delta_ind - 1;
}
}
}
}
public partial class Form1 : Form
{
public static Object missingObj = System.Reflection.Missing.Value;
public static Object trueObj = true;
public static Object falseObj = false;
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
}
private void buttonTest_Click(object sender, EventArgs e)
{
Word._Application application;
Word._Document document=null;
//создаем обьект приложения word
application = new Word.Application();
// создаем путь к файлу
Object templatePathObj = @"D:\Work\Life\StackOverflow\Разбиение объединённых ячеек в таблице.docm"; ;
// если вылетим не этом этапе, приложение останется открытым
try
{
document = application.Documents.Add(ref templatePathObj, ref missingObj, ref missingObj, ref missingObj);
Word.Table table = document.Tables[1];//в файле примера одна единственная таблица
int rcount = table.Rows.Count;
int ccount = table.Columns.Count;
WordTableAnalyser wta = new WordTableAnalyser(table,1.0);
MyWordCell c = wta.getCell(8, 1);
MessageBox.Show(c.rows.ToString() + ";" + c.columns.ToString());
application.Visible = true;
}
catch (Exception error)
{
if(document!=null)document.Close(ref falseObj, ref missingObj, ref missingObj);
application.Quit(ref missingObj, ref missingObj, ref missingObj);
document = null;
application = null;
MessageBox.Show(error.Message, "", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
}
}
}
My search led me to the site, where I found such a solution for determining the combined lines:
int GetRowSpan(Cell cell)
{
cell.Select();
return (int)_wordApp.Selection.Information[WdInformation.wdEndOfRangeRowNumber] - (int)_wordApp.Selection.Information[WdInformation.wdStartOfRangeRowNumber] + 1;
}
Unfortunately, I didn't find anything like this for the devoured speakers...
I wrote my own solution, but I don't think it's universal:
int GetColumnSpan(WordTable table, Cell cell)
{
var columnIndex = cell.ColumnIndex;
var etalonSize = table.Cell(1, columnIndex).Width;
if (cell.Width - etalonSize < 0.5)
{
return 1;
}
var curWidth = cell.Width;
var mergCount = 1;
while (curWidth - etalonSize > 0.5)
{
curWidth -= etalonSize;
mergCount++;
}
return mergCount;
}
The bottom line is that I input the reference width of the cell, and then compare it with the current one. If the beaulieu width => the cell is combined...
If someone has better thoughts, then write.
Если ТипФайла = "Word" Тогда
Попытка
Соединение = ПоключитьсяКWord(ПутьКФайлу);
Если Соединение = Неопределено Тогда
Возврат;
КонецЕсли;
//Открываем и активируем документ Word
//Док = ПолучитьCOMОбъект(ПутьКФайлу);
Word = Соединение.Application.Documents(1);
Таблица = Word.Content.Tables(1);
ТекущаяСтрока = 1;
ТекущийСтолбец = 1;
Пока ТекущаяСтрока <= Таблица.Rows.Count Цикл
Пока ТекущийСтолбец <= Таблица.Columns.Count Цикл
Если ТекущийСтолбец = 1 И ТекущийСтолбец <= Таблица.Columns.Count Тогда
Попытка
П1 = Таблица.Cell(ТекущаяСтрока, 1).Range.Text;
Исключение
П1 = "";
КонецПопытки;
КонецЕсли;
Если ТекущийСтолбец = 2 И ТекущийСтолбец <= Таблица.Columns.Count Тогда
Попытка
П2 = Таблица.Cell(ТекущаяСтрока, 2).Range.Text;
Исключение
П2 = "";
КонецПопытки;
КонецЕсли;
Если ТекущийСтолбец = 3 И ТекущийСтолбец <= Таблица.Columns.Count Тогда
Попытка
П3 = Таблица.Cell(ТекущаяСтрока, 3).Range.Text;
Исключение
П3 = "";
КонецПопытки;
КонецЕсли;
Если ТекущийСтолбец = 4 И ТекущийСтолбец <= Таблица.Columns.Count Тогда
Попытка
П4 = Таблица.Cell(ТекущаяСтрока, 4).Range.Text;
Исключение
П4 = "";
КонецПопытки;
КонецЕсли;
Если ТекущийСтолбец = 5 И ТекущийСтолбец <= Таблица.Columns.Count Тогда
Попытка
П5 = Таблица.Cell(ТекущаяСтрока, 5).Range.Text;
Исключение
П5 = "";
КонецПопытки;
КонецЕсли;
Если ТекущийСтолбец = 6 И ТекущийСтолбец <= Таблица.Columns.Count Тогда
Попытка
П6 = Таблица.Cell(ТекущаяСтрока, 6).Range.Text;
Исключение
П6 = "";
КонецПопытки;
КонецЕсли;
Если ТекущийСтолбец = 7 И ТекущийСтолбец <= Таблица.Columns.Count Тогда
Попытка
П7 = Таблица.Cell(ТекущаяСтрока, 7).Range.Text;
Исключение
П7 = "";
КонецПопытки;
КонецЕсли;
Если ТекущийСтолбец = 8 И ТекущийСтолбец <= Таблица.Columns.Count Тогда
Попытка
П8 = Таблица.Cell(ТекущаяСтрока, 8).Range.Text;
Исключение
П8 = "";
КонецПопытки;
КонецЕсли;
Если ТекущийСтолбец = 9 И ТекущийСтолбец <= Таблица.Columns.Count Тогда
Попытка
П9 = Таблица.Cell(ТекущаяСтрока, 9).Range.Text;
Исключение
П9 = "";
КонецПопытки;
КонецЕсли;
Если ТекущийСтолбец = 10 И ТекущийСтолбец <= Таблица.Columns.Count Тогда
Попытка
П10 = Таблица.Cell(ТекущаяСтрока, 10).Range.Text;
Исключение
П10 = "";
КонецПопытки;
КонецЕсли;
Если ТекущийСтолбец = 11 И ТекущийСтолбец <= Таблица.Columns.Count Тогда
Попытка
П11 = Таблица.Cell(ТекущаяСтрока, 11).Range.Text;
Исключение
П11 = "";
КонецПопытки;
КонецЕсли;
Если ТекущийСтолбец = 12 И ТекущийСтолбец <= Таблица.Columns.Count Тогда
Попытка
П12 = Таблица.Cell(ТекущаяСтрока, 12).Range.Text;
Исключение
П12 = "";
КонецПопытки;
КонецЕсли;
ТекущийСтолбец = ТекущийСтолбец + 1;
КонецЦикла;
ТекущаяСтрока = ТекущаяСтрока + 1;
ТекущийСтолбец = 1;
НоваяСтрока = Описание.Добавить();
НоваяСтрока.П1 = СокрЛП(П1);
НоваяСтрока.П2 = СокрЛП(П2);
НоваяСтрока.П3 = СокрЛП(П3);
НоваяСтрока.П4 = СокрЛП(П4);
НоваяСтрока.П5 = СокрЛП(П5);
НоваяСтрока.П6 = СокрЛП(П6);
НоваяСтрока.П7 = СокрЛП(П7);
НоваяСтрока.П8 = СокрЛП(П8);
НоваяСтрока.П9 = СокрЛП(П9);
НоваяСтрока.П10 = СокрЛП(П10);
НоваяСтрока.П11 = СокрЛП(П11);
НоваяСтрока.П12 = СокрЛП(П12);
П1 = "";
П2 = "";
П3 = "";
П4 = "";
П5 = "";
П6 = "";
П7 = "";
П8 = "";
П9 = "";
П10 = "";
П11 = "";
П11 = "";
КонецЦикла;
// Закрытие Word и освобождение памяти
ОтключатьсяОтWord(Соединение);
Исключение
//Отключится в случае ошибки
ОтключатьсяОтWord(Соединение);
КонецПопытки;
КонецЕсли;