Страницы

Поиск по вопросам

Показаны сообщения с ярлыком office-interop. Показать все сообщения
Показаны сообщения с ярлыком office-interop. Показать все сообщения

четверг, 2 апреля 2020 г.

Создание акта в word C#

#c_sharp #winforms #ms_word #office_interop

                    
Доброго времени суток. Нужно создать акт в word на основе данных, имеющихся на winforms.
Подскажите, как лучше это сделать или с чего вообще начать? 
На листе будет отформатированный текст и таблица, число строк в которой каждый раз
будет разным, но не более 10.
Примерный внешний вид акта можно увидеть на фото ниже. Места, где замазано, как раз
надо будет заменить на свои. Ну и, как писал выше, добавить строки в таблицу. В изначальном
шаблоне есть только шапка таблицы, без строк.

    


Ответы

Ответ 1



Используй стандартное расширение Microsoft.Office.Interop.Word и подготовь шаблон с данного документа с закладками в требуемых местах. Краткий код работы с документом: Word._Document document; Word._Application application=new Word.Application(); object missingObj = Missing.Value; object templatePathObj = documentpath; //открытие файла try { document = application.Documents.Add(ref templatePathObj, ref missingObj, ref missingObj, ref missingObj); } catch { document.Close(ref falseobj, ref missingObj, ref missingObj); application.Quit(ref missingObj, ref missingObj, ref missingObj); document = null; application = null; } //заполнение закладок object bookmarkObj = "закладка"; Word.Range bookmarkRange = document.Bookmarks.get_Item(ref bookmarkObj).Range; bookmarkRange.Text = ""; //работа с таблицей Word.Table _table = _document.Tables[tableNumber]; //Выбрать уже существующую таблицу внутри документа можно по ее порядковому номеру (начиная с 1 и начала документа) _table.Rows.Add(ref _missingObj); Word.Range _currentRange = _table.Cell(rowIndex, columnIndex).Range; _currentRange.Text=""; //Вывод на печать document.PrintOut(ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj); document.Close(Word.WdSaveOptions.wdDoNotSaveChanges); application.Quit(); Подробно об этом расширении есть тут и тут.

пятница, 14 февраля 2020 г.

Почему Word открывается в режиме только для чтения

#c_sharp #com #ms_word #office_interop


Не могу понять в чем дело. Вроде бы скопировал у себя же рабочий код, но почему-то
не работает. А не работает потому что word открывается в режиме для чтения.
Хотя перед этим всё работало.

private void button5_Click(object sender, EventArgs e)
    {   
        //показываем панель для введения значений в бланк 
        panel2.Visible = true;
        //скрываем изображение 
        pictureBox1.Visible = false;

        //Создаём новый Word.Application
        Microsoft.Office.Interop.Word.Application app = new Microsoft.Office.Interop.Word.Application();

        //Загружаем документ
        Microsoft.Office.Interop.Word.Document doc = null;


        //вариант открытия существующего файла
        object fileName = "E:\\R1.rtf";
        object falseValue = false;
        object trueValue = false;
        object missing = Type.Missing;

        doc = app.Documents.Open(ref fileName, ref missing, ref trueValue,
        ref missing, ref missing, ref missing, ref missing, ref missing,
        ref missing, ref missing, ref missing, ref missing, ref missing,
        ref missing, ref missing, ref missing);

        //Теперь у нас есть документ который мы будем менять.

        //Очищаем параметры поиска
        app.Selection.Find.ClearFormatting();
        app.Selection.Find.Replacement.ClearFormatting();

        //Задаём параметры замены и выполняем замену нп поле варианта 
        object findText = "";
        object replaceWith = VarApply;
        object replace = 2;

        app.Selection.Find.Execute(ref findText, ref missing, ref missing, ref missing,
        ref missing, ref missing, ref missing, ref missing, ref missing, ref replaceWith,
        ref replace, ref missing, ref missing, ref missing, ref missing);
        app.Visible = false;

        doc.Save();
        doc.Close(Microsoft.Office.Interop.Word.WdSaveOptions.wdSaveChanges);
        app.Quit(false);
        richTextBox1.LoadFile("E:\\R1.rtf");

    }


Не хочет сохранять изменённый файл в строке doc.Save();
    


Ответы

Ответ 1



doc = app.Documents.Open(ref fileName, ref missing, ref trueValue, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing); Третий параметр - ReadOnly, поставьте его missing. Да и вообще можно написать кратко app.Documents.Open(ref fileName)

Обход объединенных ячеек в Word

#c_sharp #net #ms_word #office #office_interop


В общем, в продолжение своей предыдущей темы по парсингу таблиц 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; } Суть в том, что на вход я подаю эталонную ширину ячейки, а затем сравниваю ее с текущей. Если ширина болье => ячейка объеденена... Если у кого-то есть мысли лучше, то пишите.

суббота, 8 февраля 2020 г.

Не закрывается Excel в C#

#c_sharp #excel #office_interop


В программе на C# работаю с книгой Excel следующим образом.

using Excel = Microsoft.Office.Interop.Excel;

Excel.Application excel;
excel = new Excel.Application();
excel.Workbooks.Open( "1.xlsx");
Excel.Worksheet sheet = (Excel.Worksheet)excel.ActiveSheet;
//Читаю некоторые ячейки
excel.ActiveWorkbook.Save();
excel.ActiveWorkbook.Close();
excel.Quit();
GC.Collect();


После выполнения этого кода процесс EXCEL.EXE продолжает висеть в диспетчере задач,
но завершается после закрытия моей программы. Если убрать последние 3 строчки, то процесс
висит в диспетчере и не завершается после закрытия моей программы. Как же правильно
закрыть Excel?
    


Ответы

Ответ 1



Необходимо освобождать все используемые COM-объекты с помощью Marshal.ReleaseComObject. Кроме того, есть такое правило: Никогда не используйте две точки с объектами COM. Когда вы пишете: excel.Workbooks.Open( "1.xlsx"); тут на самом деле создаются два COM-объекта. Оба нужно освободить после окончания работы с ними. Поэтому нужно разбить это выражения на два, с сохранением каждого COM-объекта в переменной. Кроме того, желательно предусмотреть гарантированное освобождение даже в случае исключений. Финальный код может выглядеть примерно так: Workbooks workbooks = null; Workbook workbook = null; try { workbooks = excel.Workbooks; workbook = workbooks.Open( "1.xlsx"); } finally { Marshal.ReleaseComObject(workbook); Marshal.ReleaseComObject(workbooks); } Так нужно поступить со всеми используемыми COM-объектами. Ваш код работы с Excel должен располагаться в блоке try, код закрытия и освобождения всех используемых ресурсов - в блоке finally.

Ответ 2



if (application != null) { application.WorkbookBeforeClose -= new Excel.AppEvents_WorkbookBeforeCloseEventHandler(application_WorkbookBeforeClose); application.Quit(); Marshal.ReleaseComObject(application); GC.Collect(); GC.WaitForPendingFinalizers(); process.WaitForExit(100); }

четверг, 23 января 2020 г.

За что отвечает галочка Embed Interop Types в VS?

#c_sharp #net #office_interop


На сборках Office есть такая галочку : Embed Interop Types

За что она отвечает? С этой галочкой нет возможности копировать локально сборку.

Я заметил, что если ее отключить, то некоторые операции с объектами офиса требуют
явного приведения типа.
    


Ответы

Ответ 1



Оригинал ответа на EnSO: https://stackoverflow.com/a/20515311/5796587 Эта опция была введена для того, чтобы удалить необходимость развертывания очень больших interop-сборок (PIAs - > Primary Interop Assemblies). Включение этой опции позволяет встроить управляемый код для связи с неуправляемыми сборками в вашу сборку, но вместо того, чтобы встраивать весь код interop-сборки, встраивает только элементы, которые реально используются в вашем коде. Подробнее в блоге Скотта Хансельмана об этом и других улучшениях Visual Studio здесь. Дополнительная информация тут и тут. Перевод достаточно вольный, для практического применения используйте оригинальные материалы по ссылкам в этом или оригинальном ответе.

воскресенье, 22 декабря 2019 г.

Как быстро склеить зависимые записи в одну строку

#c_sharp #sql #net #ms_access #office_interop


Допустим, есть 2 таблицы:

1)Должность

2)Персонал, которая ссылается на Должность.

На одну запись Должность могут приходится несколько записей из Персонал.

Задача обновить [Весь персонал] в Должность склеенными в одну строчку зависимыми
записями из Персонал.

Допустим приходится на 1 запись приходится 2 строчки ФИО из Персонал, мы их склеиваем
и записываем в родительскую запись Должность.

Как это сделать быстро? Используется БД Access.

Сейчас я использую Interop, открываю RecordSet из Должность, и иду сверху вниз, и
на каждую запись я открываю второй RecordSet из запроса 'Select PID,FIO From [Персонал]
where PID='rst.fields[ID].value и иду сверху вниз и склеиваю значения, а потом возвращаю
и обновляю их в первом RecordSet и мне кажется, что слишком это тормознуто и можно
как то быстрее сделать это.

Наверное, через ADO.NET было бы быстрее, но мне кажется, что если БД Access будет
здоровой, то слишком жирно все в память грузить разом...

UPD:
Если все таки загрузить все в память и работать через ADO.NET, я получу значительный
выигрыш в скорости?
    


Ответы

Ответ 1



Можно написать VB функцию, которая будет склеивать строку. Подробно можно прочитать тут: http://hiprog.com/index.php?option=com_content&task=view&id=334&Itemid=35 Тело функции: Public Function UnionStr1(ID, Fam) Static IDOld, FamUnion If IDOld <> ID Then IDOld = ID FamUnion = Null End If FamUnion = (FamUnion + ", ") & Fam UnionStr1 = FamUnion End Function Пример запроса: Select ID, Last(UnionStr1(ID,Fam)) AS FamUnion FROM Tab1 GROUP BY ID; В той же статье есть интересный способ. Только средствами ms-access sql, с промежуточной таблицей. Хотя возможно в вашем случае её роль сыграет таблица Person(должностей). Выглядит примерно так: Метод 3. Заполнение таблицы при помощи запросов (по Митину). http://c85.cemi.rssi.ru/Access/AnsPointDetail.idc?QID=14147 Оригинальный и неочевидный метод. Выполняется с помощью двух запросов. 1. Запись в Tab2 уникальных ID без фамилий. Текст запроса WriteID: INSERT INTO Tab2 (ID) Select DISTINCT ID FROM Tab1; 2. Запись списков фамилий. Текст запроса TabUnion5: UPDATE Tab2 INNER JOIN Tab1 On Tab2.ID = Tab1.ID Set Tab2.FamUnion = ([Tab2].[FamUnion]+", ") & [Tab1].[Fam]; Оценка скорости: по-видимому, работает быстрее метода 1 за счет отсутствия вызова специальных функций. Недостаток: "Однако в T-SQL это непрокатывает" (Митин). Надо побаловаться, выглядит интересно. UPD: Смысл в том, что из-за "глупости" ms-access SET будет выполнятся столько раз, сколько записей в присоединённой таблице(не той, которая апдейтится). Из-за "умности" T-SQL, в T-SQL этот способ работать не будет. К минусам этого способа я бы ещё добавил потребность в сопровождении при переходе на новую версию access. Если MS решат приблизить хоть чуть-чуть ms-access к стандарту ANSI-SQL, этот метод может перестать работать. С другой стороны, может они и для конкатенации строк что-нибудь сделают:) Хотя в этом вопросе даже MS SQL Server обделён вниманием до сих пор. MySQL, PostgreSQL, Oracle это давно умеют.

Ответ 2



Я бы делал так: Допустим в таблице персонал(Person) есть три поля: Person_ID, Person_FIO, Position_ID в таблице должность(Position) есть три поля: Position_ID, Position_Name, Position_StaffList Для начала пишем запрос для DataReader'a (если я правильно понимаю это C# аналог Recordset из Basic): SELECT pos.Position_ID, pos.Position_Name, prs.Person_FIO FROM Position AS pos LEFT OUTER JOIN Person AS prs ON pos.Position_ID = prs.Position_ID ORDER BY pos.Position_ID, prs.Person_FIO; И служебный класс internal class Pos { public Pos() { StaffList = new StringBuilder(); } public int? Position_ID {get; set;} public string Position_Name {get; set;} public StringBuilder StaffList {get; set;} } Открываем ридер для нашего запроса. List positions = new List(); int previos_pos = -1; if (reader.HasRows) { int id; string position_name; string fio; Pos current; while (reader.Read()) { id = reader.GetInt32(0); position_name = reader.GetString(1); fio = reader.GetString(2); if (id != previos_pos) { current = new Pos(); positions.Add(current); current.Position_ID = id; current.Position_Name = position_name; current.StaffList.Append(fio); } else { current.StaffList.AppendFormat("{0},{1}" , current.StaffList, , fio ); } } } Закрываем наш ридер. Теперь у нас есть заполненный список positions и остается только обновить из него вашу таблицу позиции. Так как мы не ддосим базу промежуточными запросами (что может быть действительно долго если позиций много), то мы должны получить некоторое ускорение.

Ответ 3



Как это сделать быстро? Используется БД Access. Возможно ужасно знаю способности MS Access... но предполагаю, что сделать эту задачу средствами БД Access невозможно, т.к. там слабый потенциал аналитических функций и расширений SQL запросов. В более способных СУБД в которых язык SQL расширяют доп.аналитическими функциями, есть способы выполнить такую задачу, но даже они выглядят ужасно и криво - например в Oracle с помощью компоновки аналитических функций в нескольких подзапросах. Поэтому для вас самый идеальный вариант тот, что вы уже делаете - сформировать запрос должностей, переходом по должностям, выполнять второй запрос для получения всех ФИО из этой должности. Обрабатывать полученные списки. Предложенный вами вариант действий: Допустим приходится на 1 запись приходится 2 строчки ФИО из Персонал, мы их склеиваем и записываем в родительскую запись Дожность это ужасное и грубое нарушение структуры данных и логики (о нормальных формах БД надо почитать). Делается вывод - у вас скорее всего неверная постановка задачи, либо неверное принятие решения для ее выполнения. PS: поясняю.. если вы все ФИО принадлежащие для 1 должности засунете в поле этой должности, то выполняя запрос к этой должности и выводя информацию на клиенте - вы ничем не сэкономите ОЗУ, т.к. действует математическое правило "от перемены мест слагаемых сумма не меняется". Не важно получите вы эти ФИО и будете хранить в разных полях, либо в одном поле, количество байт символов не изменится, во всяком случае разница будет не заметна для ПК. Но зато когда вы будете получать список известных должностей - то объемы передаваемой информации будут расти очень быстро, что увеличит нагрузку на трафик и БД. Лучше сформулируйте задачу поставленную перед вами, чтобы вам смогли подсказать оптимальное решение, т.к. в данном вопросе вы просите помочь вам выполнить решение которое приняли самостоятельно - но оно ошибочное.

суббота, 14 декабря 2019 г.

Зависшие процессы Office interop

#c_sharp #office #office_interop


Предположим, в приложении необходимо использовать Office interop.(без вариантов)
Всем известно, что он создает приложения office без интерфейса и через них выполняет
работу.

Ну как быть, если пользователь некорректно завершил работу с программой? Например,
убил через диспетчер задач. Получается, что в процессах остаются висеть приложения офиса.

Можно ли это предотвратить? Если нет, то можно ли понять при запуске(повторном) приложения,
что в процессах висит мусор, который остался от прошлого запуска и грохнуть эти процессы,
не затронув реальные приложения офиса(например, юзер запустил Excel.)
    


Ответы

Ответ 1



Как вариант, средствами Win32 можно объединить приложение и запущенные из него процессы в задание (Job) тогда процессы Interop будут завершаться вместе с родительским приложением. Для этого нужно будет создать задание с помощью CreateJobObject, а затем добавлять созданные процессы в задание с помощью AssignProcessToJobObject Есть похожее обсуждение на английском языке: «Kill child process when parent process is killed». Из ответа @Ron можно взять код класса-обертки ChildProcessTracker: /// /// Allows processes to be automatically killed if this parent process unexpectedly quits. /// This feature requires Windows 8 or greater. On Windows 7, nothing is done. /// References: /// https://stackoverflow.com/a/4657392/386091 /// https://stackoverflow.com/a/9164742/386091 public static class ChildProcessTracker { /// /// Add the process to be tracked. If our current process is killed, the child processes /// that we are tracking will be automatically killed, too. If the child process terminates /// first, that's fine, too. /// public static void AddProcess(int processHandle) { if (s_jobHandle != IntPtr.Zero) { bool success = AssignProcessToJobObject(s_jobHandle, new IntPtr(processHandle)); if (!success) throw new Win32Exception(); } } static ChildProcessTracker() { // This feature requires Windows 8 or later. To support Windows 7 requires // registry settings to be added if you are using Visual Studio plus an // app.manifest change. // https://stackoverflow.com/a/4232259/386091 // https://stackoverflow.com/a/9507862/386091 if (Environment.OSVersion.Version < new Version(6, 2)) return; // The job name is optional (and can be null) but it helps with diagnostics. // If it's not null, it has to be unique. Use SysInternals' Handle command-line // utility: handle -a ChildProcessTracker string jobName = "ChildProcessTracker" + Process.GetCurrentProcess().Id; s_jobHandle = CreateJobObject(IntPtr.Zero, jobName); var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION { LimitFlags = JOBOBJECTLIMIT.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE }; // This is the key flag. When our process is killed, Windows will automatically // close the job handle, and when that happens, we want the child processes to // be killed, too. var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION(); extendedInfo.BasicLimitInformation = info; int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length); try { Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false); if (!SetInformationJobObject(s_jobHandle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length)) { throw new Win32Exception(); } } finally { Marshal.FreeHGlobal(extendedInfoPtr); } } [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string name); [DllImport("kernel32.dll")] static extern bool SetInformationJobObject(IntPtr job, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength); [DllImport("kernel32.dll", SetLastError = true)] static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process); // Windows will automatically close any open job handles when our process terminates. // This can be verified by using SysInternals' Handle utility. When the job handle // is closed, the child processes will be killed. private static readonly IntPtr s_jobHandle; } public enum JobObjectInfoType { AssociateCompletionPortInformation = 7, BasicLimitInformation = 2, BasicUIRestrictions = 4, EndOfJobTimeInformation = 6, ExtendedLimitInformation = 9, SecurityLimitInformation = 5, GroupInformation = 11 } [StructLayout(LayoutKind.Sequential)] public struct JOBOBJECT_BASIC_LIMIT_INFORMATION { public Int64 PerProcessUserTimeLimit; public Int64 PerJobUserTimeLimit; public JOBOBJECTLIMIT LimitFlags; public UIntPtr MinimumWorkingSetSize; public UIntPtr MaximumWorkingSetSize; public UInt32 ActiveProcessLimit; public Int64 Affinity; public UInt32 PriorityClass; public UInt32 SchedulingClass; } [Flags] public enum JOBOBJECTLIMIT : uint { JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000 } [StructLayout(LayoutKind.Sequential)] public struct IO_COUNTERS { public UInt64 ReadOperationCount; public UInt64 WriteOperationCount; public UInt64 OtherOperationCount; public UInt64 ReadTransferCount; public UInt64 WriteTransferCount; public UInt64 OtherTransferCount; } [StructLayout(LayoutKind.Sequential)] public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION { public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; public IO_COUNTERS IoInfo; public UIntPtr ProcessMemoryLimit; public UIntPtr JobMemoryLimit; public UIntPtr PeakProcessMemoryUsed; public UIntPtr PeakJobMemoryUsed; } , и регистрировать с помощью него процессы Excel как дочерние: [DllImport("user32.dll")] static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId); var app = new Microsoft.Office.Interop.Excel.Application(); int processId; GetWindowThreadProcessId(app.Hwnd, out processId); ChildProcessTracker.AddProcess(processId); , и тогда ОС будет убивать их вместе с родительским процессом. Рекоммендую ознакомиться с документацией и почитать комментарии в обсуждении по ссылке, могут возникнуть нюансы с поддержкой разных версий ОС и Visual Studio, при запуске с ограниченными правами, при запуске родительского приложения из других процессов. Если нет, то можно ли понять при запуске(повторном) приложения, что в процессах висит мусор, который остался от прошлого запуска и грохнуть эти процессы, не затронув реальные приложения офиса(например, юзер запустил Excel.) Сомневаюсь в существовании надежного метода. Процесс Excel запущенный старым процессом ничем не отличается от процесса запущенного текущим/другими приложениями/пользователем. Можно, как предложил @VadimTagil, написать код, который будет сохранять запущенные процессы в постоянное хранилище (диск/БД/реестр) и убивать их по определенной логике. Еще один вариант, вынести формирование Excel в отдельные процессы-задания, не зависящие от основного приложения. Вообще, если в нормальном ходе работы приложения мусор не остается, то проблемы будут возникать лишь в исключительных случаях (убитый извне процесс), обработку которых можно будет оставить пользователю. Убедитесь, что для всех объектов вызывается Marshal.ReleaseComObject и что все процессы завершаются и, возможно, без задания можно будет обойтись.

Ответ 2



Я полагаю, суть вопроса в методике отделения "мусора" от нормальных процессов Excel. Умея это, уже можно что-то смастерить: удалять мусорные процессы при запуске основной программы, или периодически - фоновым сервисом, когда основная программа закрыта. Наиболее правильное решение: добавлять Id всех создаваемых Interop-процессов в БД, при корректном их завершении - удалять. Соответственно, при некорректном завершении приложения в БД останутся Id мертвых процессов, которые можно на следующем запуске прибить (предварительно убедившись, что это все еще существующие процессы Excel, так как они могли быть прибиты чем-то другим и те же Id уже переиспользованы системой для другой программы). Но, если хочется метод попроще, можно считать "мусором" любой процесс Excel, не имеющий видимого главного окна: using System.Diagnostics; using System.Runtime.InteropServices; static class Program { [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] static extern bool IsWindowVisible(IntPtr hWnd); public static bool IsProcessDead(Process pr) { IntPtr hwnd = pr.MainWindowHandle; if (hwnd == IntPtr.Zero) return true; return !IsWindowVisible(hwnd); } void ClearProcesses() { Process[] prs=Process.GetProcessesByName("excel"); foreach (Process proc in prs) { if(IsProcessDead(proc))proc.Kill(); } } } Если на целевой машине нет других программ, использующих невидимые эксели для своих целей, это можно считать нормальным допущением.

воскресенье, 8 декабря 2019 г.

Как выполнить Сжатие и восстановление Access базы через Interop?

#c_sharp #net #com #ms_access #office_interop


Как выполнить Сжатие и восстановление Access базы через Interop?

Есть класс, который содержит метод:

public bool TryCompactAndRepair()
{
    string pathToDb = app.CurrentDb().Name;
    string pathToCompact = Path.Combine(Path.GetDirectoryName(pathToDb), 
        Path.GetFileNameWithoutExtension(pathToDb)) + 
        "_compact" + Path.GetExtension(pathToDb);

    db.Close();
    app.CloseCurrentDatabase();
    //   Marshal.ReleaseComObject(db);
    GC.Collect();
    if (app.CompactRepair(pathToDb, pathToCompact, true))
    {
        File.Delete(pathToDb);
        File.Move(pathToCompact, pathToDb);
        return true;
    }
    else
    {
        File.Delete(pathToCompact);
    }
    return false;
}


переменная app имеет тип Application.

Почему-то такой метод возвращает различные ошибки:


В текущем виде, что-то вроде "Object no longer set"
Если раскоментить Marshal.ReleaseComObject(db); ,то получаю ошибку, что доступ к
объекту не доступен.
Если вообще не делать Close, то вылетает ошибка, что нельзя сделать сжатие для текущей БД.


Подскажите, в чем косяк?
    


Ответы

Ответ 1



Вызов app.CurrentDb().Name создает управляемую обертку COM-объекта dao.Database, ссылка на которую нигде не сохраняется, так что ее никак нельзя впоследствии освободить, кроме как финализатором. Из-за этого программа держит базу даже после вызовов app.CloseCurrentDatabase() и Marshal.ReleaseComObject на втором экземпляре объекта. Вызов GC.Collect() не помогает, так как финализатор выполняется не сразу, а только через некоторое время. Чтобы нормально освободить базу, нужно избавиться от создания второго экземпляра объекта (так как он не нужен): dao.Database db = app.CurrentDb(); string pathToDb = db.Name; string pathToCompact = Path.Combine(Path.GetDirectoryName(pathToDb), Path.GetFileNameWithoutExtension(pathToDb)) + "_compact" + Path.GetExtension(pathToDb); app.CloseCurrentDatabase(); Marshal.ReleaseComObject(db); app.CompactRepair(pathToDb, pathToCompact, true);

воскресенье, 9 июня 2019 г.

Создание акта в word C#

Доброго времени суток. Нужно создать акт в word на основе данных, имеющихся на winforms. Подскажите, как лучше это сделать или с чего вообще начать? На листе будет отформатированный текст и таблица, число строк в которой каждый раз будет разным, но не более 10. Примерный внешний вид акта можно увидеть на фото ниже. Места, где замазано, как раз надо будет заменить на свои. Ну и, как писал выше, добавить строки в таблицу. В изначальном шаблоне есть только шапка таблицы, без строк.


Ответ

Используй стандартное расширение Microsoft.Office.Interop.Word и подготовь шаблон с данного документа с закладками в требуемых местах. Краткий код работы с документом:
Word._Document document; Word._Application application=new Word.Application(); object missingObj = Missing.Value; object templatePathObj = documentpath; //открытие файла try { document = application.Documents.Add(ref templatePathObj, ref missingObj, ref missingObj, ref missingObj); } catch { document.Close(ref falseobj, ref missingObj, ref missingObj); application.Quit(ref missingObj, ref missingObj, ref missingObj); document = null; application = null; }
//заполнение закладок object bookmarkObj = "закладка"; Word.Range bookmarkRange = document.Bookmarks.get_Item(ref bookmarkObj).Range; bookmarkRange.Text = "";
//работа с таблицей Word.Table _table = _document.Tables[tableNumber]; //Выбрать уже существующую таблицу внутри документа можно по ее порядковому номеру (начиная с 1 и начала документа) _table.Rows.Add(ref _missingObj); Word.Range _currentRange = _table.Cell(rowIndex, columnIndex).Range; _currentRange.Text="";
//Вывод на печать document.PrintOut(ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj, ref missingObj);
document.Close(Word.WdSaveOptions.wdDoNotSaveChanges); application.Quit();
Подробно об этом расширении есть тут и тут.

среда, 24 апреля 2019 г.

Почему Word открывается в режиме только для чтения

Не могу понять в чем дело. Вроде бы скопировал у себя же рабочий код, но почему-то не работает. А не работает потому что word открывается в режиме для чтения. Хотя перед этим всё работало.
private void button5_Click(object sender, EventArgs e) { //показываем панель для введения значений в бланк panel2.Visible = true; //скрываем изображение pictureBox1.Visible = false;
//Создаём новый Word.Application Microsoft.Office.Interop.Word.Application app = new Microsoft.Office.Interop.Word.Application();
//Загружаем документ Microsoft.Office.Interop.Word.Document doc = null;
//вариант открытия существующего файла object fileName = "E:\\R1.rtf"; object falseValue = false; object trueValue = false; object missing = Type.Missing;
doc = app.Documents.Open(ref fileName, ref missing, ref trueValue, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing);
//Теперь у нас есть документ который мы будем менять.
//Очищаем параметры поиска app.Selection.Find.ClearFormatting(); app.Selection.Find.Replacement.ClearFormatting();
//Задаём параметры замены и выполняем замену нп поле варианта object findText = ""; object replaceWith = VarApply; object replace = 2;
app.Selection.Find.Execute(ref findText, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref replaceWith, ref replace, ref missing, ref missing, ref missing, ref missing); app.Visible = false;
doc.Save(); doc.Close(Microsoft.Office.Interop.Word.WdSaveOptions.wdSaveChanges); app.Quit(false); richTextBox1.LoadFile("E:\\R1.rtf");
}
Не хочет сохранять изменённый файл в строке doc.Save()


Ответ

doc = app.Documents.Open(ref fileName, ref missing, ref trueValue, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing, ref missing);
Третий параметр - ReadOnly, поставьте его missing.
Да и вообще можно написать кратко
app.Documents.Open(ref fileName)

четверг, 15 ноября 2018 г.

Как быстро склеить зависимые записи в одну строку

Допустим, есть 2 таблицы:
1)Должность
2)Персонал, которая ссылается на Должность.
На одну запись Должность могут приходится несколько записей из Персонал.
Задача обновить [Весь персонал] в Должность склеенными в одну строчку зависимыми записями из Персонал.
Допустим приходится на 1 запись приходится 2 строчки ФИО из Персонал, мы их склеиваем и записываем в родительскую запись Должность.
Как это сделать быстро? Используется БД Access.
Сейчас я использую Interop, открываю RecordSet из Должность, и иду сверху вниз, и на каждую запись я открываю второй RecordSet из запроса 'Select PID,FIO From [Персонал] where PID='rst.fields[ID].value и иду сверху вниз и склеиваю значения, а потом возвращаю и обновляю их в первом RecordSet и мне кажется, что слишком это тормознуто и можно как то быстрее сделать это.
Наверное, через ADO.NET было бы быстрее, но мне кажется, что если БД Access будет здоровой, то слишком жирно все в память грузить разом...
UPD: Если все таки загрузить все в память и работать через ADO.NET, я получу значительный выигрыш в скорости?


Ответ

Можно написать VB функцию, которая будет склеивать строку. Подробно можно прочитать тут:
http://hiprog.com/index.php?option=com_content&task=view&id=334&Itemid=35
Тело функции:
Public Function UnionStr1(ID, Fam) Static IDOld, FamUnion If IDOld <> ID Then IDOld = ID FamUnion = Null End If
FamUnion = (FamUnion + ", ") & Fam UnionStr1 = FamUnion End Function
Пример запроса:
Select ID, Last(UnionStr1(ID,Fam)) AS FamUnion FROM Tab1 GROUP BY ID;
В той же статье есть интересный способ. Только средствами ms-access sql, с промежуточной таблицей. Хотя возможно в вашем случае её роль сыграет таблица Person(должностей). Выглядит примерно так:
Метод 3. Заполнение таблицы при помощи запросов (по Митину). http://c85.cemi.rssi.ru/Access/AnsPointDetail.idc?QID=14147
Оригинальный и неочевидный метод. Выполняется с помощью двух запросов. 1. Запись в Tab2 уникальных ID без фамилий. Текст запроса WriteID: INSERT INTO Tab2 (ID) Select DISTINCT ID FROM Tab1;
2. Запись списков фамилий. Текст запроса TabUnion5: UPDATE Tab2 INNER JOIN Tab1 On Tab2.ID = Tab1.ID Set Tab2.FamUnion = ([Tab2].[FamUnion]+", ") & [Tab1].[Fam];
Оценка скорости: по-видимому, работает быстрее метода 1 за счет отсутствия вызова специальных функций. Недостаток: "Однако в T-SQL это непрокатывает" (Митин).
Надо побаловаться, выглядит интересно.
UPD: Смысл в том, что из-за "глупости" ms-access SET будет выполнятся столько раз, сколько записей в присоединённой таблице(не той, которая апдейтится). Из-за "умности" T-SQL, в T-SQL этот способ работать не будет.
К минусам этого способа я бы ещё добавил потребность в сопровождении при переходе на новую версию access. Если MS решат приблизить хоть чуть-чуть ms-access к стандарту ANSI-SQL, этот метод может перестать работать.
С другой стороны, может они и для конкатенации строк что-нибудь сделают:) Хотя в этом вопросе даже MS SQL Server обделён вниманием до сих пор. MySQL, PostgreSQL, Oracle это давно умеют.