В общем, в продолжение своей предыдущей темы по парсингу таблиц Word'a с помощью
interop я столкнулся с объединенными ячейками.
Как я могу узнать на сколько ячеек произошло объединение, что бы я мог размножить
F3 на N строчек?
Капался в дебаггере в поисках нужного свойства, но что-то не нашел... Да и в интернете
поголовно пишут, что такого свойства нет
UPD 1
На одном из ресурсов подкинули вот такую идею(Код на VBA, но легко переписывается на 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
Смысл в том, что из Word все копипастится в Excel, а там уже появляются свойства,
которые показывают объединение ячеек-MergeRange. Проблема в том, что будет занят буфер
обмена(самое проблемное на мой взгляд, так как не смогу работать за ПК в момент работы
программы иначе покалечу буфер), а так же в том, что подключается второй всадник апокалипсиса-Excel.
Если Excel знает, как представить таблицу из Word и создать свойства, которые показывают,
что некая зона объединена, то значит должен иметься какой-то однозначный алгоритм для
вычисления этого средствами Word'a.
Даже при сохранении в html-формате пишется кол-во объединенных колонок и строк.
UPD 2
По объединенным строкам я нашел решение, но по колонкам нет. Накидал свое решение,
но не думаю, что оно универсально.
Может у кого-то есть еще идеи?
P.S Interop не принципиален, но в приоритете, так как много кода на нем нарисовал.
Если есть решение например для *.DOCX, то я могу сменить формат.
Ответы
Ответ 1
Получим xml с docx это не сложно вот
Давайте рассмотрим такую таблицу
1 4 < 5
2 - - ^
Где < и ^ - направление обьединения ячеек.
Тогда получим следующее (оставляю только значащие теги)
(1)
(4)
(5)
(2)
-
-
Текст хранится в тегах текст. Вычислить обьеденённый столбец этим способом
легче - по тегу gridSpan. Обьеденённые строки прийдется перелистывать vMerge тег.
Вот пример как получить доступ к тегам внутри 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");
}
}
этот пример почти решает поставленую задачу. Обьедененные столбцы - видно. Обьеденённые
строки - ещё нужно доработать.
Ответ 2
Изучение вот этого простого кода
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
на тестовых табличках с объединением ячеек даст достаточно информации для решения
проблемы.
Ответ 3
Даже при сохранении в html-формате пишется кол-во объединенных колонок и строк.
Так может и воспользоваться этой деталью? Перегнать doc в html и распарсить атрибут
colspan/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 FindMergedColumns(string html)
{
List res = new List();
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();
}
}
}
Ответ 4
Обратите внимание на строки:
WordTableAnalyser wta = new WordTableAnalyser(table,1.0);
MyWordCell c = wta.getCell(8, 1);
MessageBox.Show(c.rows.ToString() + ";" + c.columns.ToString());
Исходная таблица
Приложение на основе 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,IEqualityComparer
{
double tolerance=0;
public DoubleToleranceComparer(double tolerance)
{
this.tolerance = tolerance;
}
#region Члены IComparer
public int Compare(double x, double y)
{
double delta = x - y;
return Math.Abs(delta) <= tolerance ? 0 : Math.Sign(delta);
}
#endregion
#region Члены IEqualityComparer
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 split_xs;//встречающиеся координаты границ ячеек
таблицы по x
List curRow = new List();
List> myTable = new List>();
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(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 prevRow = curRow;
curRow = new List();
List myTableRow = new List();
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 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;
}
}
}
}
Ответ 5
Поиски привели меня на сайт, где я нашел такое решение для определения объеденных
строчек:
int GetRowSpan(Cell cell)
{
cell.Select();
return (int)_wordApp.Selection.Information[WdInformation.wdEndOfRangeRowNumber]
- (int)_wordApp.Selection.Information[WdInformation.wdStartOfRangeRowNumber] + 1;
}
К сожалению, ничего подобного я не нашел для объеденных колонок...
Написал свое решение, но не думаю, что оно универсальное:
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;
}
Суть в том, что на вход я подаю эталонную ширину ячейки, а затем сравниваю ее с текущей.
Если ширина болье => ячейка объеденена...
Если у кого-то есть мысли лучше, то пишите.