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.

Author: PashaPash, 2018-03-28

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.

 2
Author: nick_n_a, 2018-04-11 08:28:27

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.

 1
Author: Akina, 2018-03-28 12:46:12

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();            

        }        

    }

}
 1
Author: MSDN.WhiteKnight, 2018-04-09 05:20:59

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

enter a description of the image here

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;
            }

        }
    }
}
 1
Author: MindCleaner, 2018-04-09 13:43:41

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.

 0
Author: iluxa1810, 2018-04-01 19:00:19
Если ТипФайла = "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(Соединение); 
    КонецПопытки;
    
КонецЕсли;
 0
Author: Rustam Kuspakov, 2020-10-16 14:09:05