Как правильно очистить объекты взаимодействия Excel?

688

Я использую интерполяцию Excel в С# (ApplicationClass) и поместил следующий код в мое предложение finally:

while (System.Runtime.InteropServices.Marshal.ReleaseComObject(excelSheet) != 0) { }
excelSheet = null;
GC.Collect();
GC.WaitForPendingFinalizers();

Хотя этот вид работ, процесс Excel.exe все еще находится в фоновом режиме даже после закрытия Excel. Он открывается только после закрытия приложения.

Что я делаю неправильно, или есть альтернатива, чтобы объекты взаимодействия были правильно удалены?

  • 1
    Вы пытаетесь закрыть Excel.exe, не закрывая приложение? Не уверен, что я полностью понимаю ваш вопрос.
  • 3
    Я пытаюсь убедиться, что неуправляемые объекты взаимодействия удаляются должным образом. Таким образом, процессы Excel не работают, даже когда пользователь закончил работу с таблицей Excel, которую мы создали из приложения.
Показать ещё 4 комментария
Теги:
excel
interop
com-interop

37 ответов

619
Лучший ответ

Excel не завершает работу, потому что ваше приложение все еще содержит ссылки на COM-объекты.

Я предполагаю, что вы вызываете хотя бы один член COM-объекта, не назначая его переменной.

Для меня это был объект excelApp.Worksheets, который я использовал напрямую, не назначая его переменной:

Worksheet sheet = excelApp.Worksheets.Open(...);
...
Marshal.ReleaseComObject(sheet);

Я не знал, что внутренне С# создал оболочку для COM-объекта Worksheets, который не был освобожден моим кодом (потому что я не знал об этом) и был причиной, по которой Excel не был выгружен.

Я нашел решение моей проблемы на этой странице, которая также имеет хорошее правило для использования COM-объектов в С#:

Никогда не используйте две точки с COM-объектами.


Таким образом, с этим знанием правильный способ сделать вышеупомянутое:

Worksheets sheets = excelApp.Worksheets; // <-- The important part
Worksheet sheet = sheets.Open(...);
...
Marshal.ReleaseComObject(sheets);
Marshal.ReleaseComObject(sheet);

ПОСЛЕ MORTEM ОБНОВЛЕНИЕ:

Я хочу, чтобы каждый читатель очень внимательно прочитал этот ответ Ханса Пассанта, поскольку он объясняет ловушку, в которую я и многие другие разработчики попали. Когда я написал этот ответ несколько лет назад, я не знал о влиянии отладчика на сборщик мусора и сделал неверные выводы. Я не изменяю свой ответ ради истории, но, пожалуйста, прочитайте эту ссылку и не идите по пути "двух точек": понимание сборки мусора в.NET и очистка объектов взаимодействия Excel с помощью IDisposable

  • 1
    К сожалению, я считаю, что эту политику практически невозможно правильно использовать на практике. Если кто-то когда-нибудь проскальзывает и использует «две точки», или итерирует ячейки через цикл для каждого цикла, или любой другой подобный вид команды, то у вас будут COM-объекты без ссылок и вы рискуете зависнуть ...
  • 0
    ... и не было бы никакого способа узнать, где в коде вы допустили такую ошибку. Вам нужно будет отсканировать ВСЕ свой код вручную и надеяться, что вы найдете их все. Для любого, кто читает это, я настоятельно рекомендую прочитать мой полный ответ по этому вопросу ниже, а затем решить для себя.
Показать ещё 20 комментариев
280

Вы действительно можете бесплатно освободить объект приложения Excel, но вам нужно позаботиться.

Совет по поддержанию именованной ссылки для абсолютно каждого COM-объекта, к которому вы обращаетесь, а затем явно освобождающего его через Marshal.FinalReleaseComObject(), является правильным в теории, но, к сожалению, очень сложно управлять на практике. Если вы когда-либо проскальзываете в любом месте и используете "две точки" или или итерации ячеек через цикл for each или любую другую подобную команду, тогда у вас будут не связанные объекты COM и вы рискуете повесить. В этом случае не было бы способа найти причину в коде; вам придется просмотреть весь свой код на глаз и, надеюсь, найти причину, задачу, которая может быть почти невозможна для большого проекта.

Хорошей новостью является то, что вам фактически не нужно поддерживать ссылку на именованную переменную для каждого COM-объекта, который вы используете. Вместо этого вызовите GC.Collect(), а затем GC.WaitForPendingFinalizers(), чтобы освободить все (обычно незначительные) объекты, к которым у вас нет ссылки, и затем явно освободить объекты, на которых вы держите ссылку на именованную переменную.

Вы также должны отпустить свои именованные ссылки в обратном порядке важности: сначала объекты диапазона, затем рабочие листы, книги, а затем, наконец, ваш объект приложения Excel.

Например, если предположить, что у вас была переменная объекта Range с именем xlRng, переменная Worksheet с именем xlSheet, переменная рабочей книги с именем xlBook и переменная приложения Excel с именем xlApp, тогда ваш код очистки мог бы выглядеть что-то вроде следующего:

// Cleanup
GC.Collect();
GC.WaitForPendingFinalizers();

Marshal.FinalReleaseComObject(xlRng);
Marshal.FinalReleaseComObject(xlSheet);

xlBook.Close(Type.Missing, Type.Missing, Type.Missing);
Marshal.FinalReleaseComObject(xlBook);

xlApp.Quit();
Marshal.FinalReleaseComObject(xlApp);

В большинстве примеров кода вы увидите для очистки COM-объектов от .NET, вызовы GC.Collect() и GC.WaitForPendingFinalizers() сделаны TWICE, как в:

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();

Однако это не обязательно, если вы не используете Visual Studio Tools for Office (VSTO), в котором используются финализаторы, которые приводят к созданию целого графика объектов в очереди финализации. Такие объекты не будут выпущены до следующей сборки мусора. Однако, если вы не используете VSTO, вы должны иметь возможность вызывать GC.Collect() и GC.WaitForPendingFinalizers() только один раз.

Я знаю, что явное вызов GC.Collect() - это не-нет (и, конечно, это дважды звучит очень болезненно), но, честно говоря, нет никакого способа обойти это. С помощью обычных операций вы создадите скрытые объекты, к которым у вас нет ссылки, которую вы, следовательно, не можете освобождать другими способами, кроме вызова GC.Collect().

Это сложная тема, но на самом деле это все, что ей нужно. После того, как вы установите этот шаблон для своей процедуры очистки, вы можете нормально закодировать, без необходимости обертки и т.д.: -)

У меня есть учебник по этому вопросу:

Автоматизация программ Office с помощью VB.Net/COM Interop

Это написано для VB.NET, но не откладывайте на это, принципы точно такие же, как при использовании С#.

  • 2
    Связанное обсуждение можно найти на форуме ExtremeVBTalk .NET Office Automation, здесь: xtremevbtalk.com/showthread.php?t=303928 .
  • 2
    А если ничего не помогает, то можно использовать Process.Kill () (в крайнем случае), как описано здесь: stackoverflow.com/questions/51462/…
Показать ещё 5 комментариев
201

Предисловие: мой ответ содержит два решения, поэтому будьте осторожны при чтении и ничего не пропустите.

Существуют различные способы и способы, с помощью которых можно выгрузить экземпляр Excel, например:

  • Освобождение КАЖДОГО объекта com явно с маршалом. FinalReleaseComObject() (не забывая о неявно созданные ком-объекты). Выпустить каждый созданный объект com, вы можете использовать правило 2 точек, упомянутых здесь:
    Как правильно очистить объекты взаимодействия Excel?

  • Вызов GC.Collect() и GC.WaitForPendingFinalizers(), чтобы сделать CLR освобождает неиспользуемые объекты com * (На самом деле, это работает, см. Мое второе решение для деталей)

  • Проверка наличия com-server-application возможно, покажет окно сообщения, ожидающее пользователь должен ответить (хотя я не конечно, это может помешать Excel из закрытие, но я слышал об этом несколько раз)

  • Отправка сообщения WM_CLOSE на главную Окно Excel

  • Выполнение функции, которая работает с Excel в отдельном AppDomain. Некоторые считают, что экземпляр Excel будет закрыт, когда AppDomain будет выгружены.

  • Убивать все экземпляры excel, которые были созданы после нашего кода excel-interoping.

НО! Иногда все эти параметры просто не помогают или не могут быть подходящими!

Например, вчера я узнал, что в одной из моих функций (которая работает с excel) Excel продолжает работать после завершения функции. Я все испробовал! Я тщательно проверил всю функцию 10 раз и добавил Marshal.FinalReleaseComObject() для всего! У меня также были GC.Collect() и GC.WaitForPendingFinalizers(). Я проверил скрытые окна сообщений. Я попытался отправить сообщение WM_CLOSE в главное окно Excel. Я выполнил свою функцию в отдельном AppDomain и выгрузил этот домен. Ничего не помогло! Опция с закрытием всех экземпляров excel не подходит, потому что если пользователь запускает другой экземпляр Excel вручную, во время выполнения моей функции, которая работает также с Excel, этот экземпляр также будет закрыт моей функцией. Бьюсь об заклад, пользователь не будет счастлив! Так что, честно говоря, это хромой вариант (без обид парней). Итак, я провел пару часов, прежде чем нашел хорошее (по моему скромному мнению) решение: Убить процесс excel hWnd его главного окна (это первое решение).

Вот простой код:

[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

/// <summary> Tries to find and kill process by hWnd to the main window of the process.</summary>
/// <param name="hWnd">Handle to the main window of the process.</param>
/// <returns>True if process was found and killed. False if process was not found by hWnd or if it could not be killed.</returns>
public static bool TryKillProcessByMainWindowHwnd(int hWnd)
{
    uint processID;
    GetWindowThreadProcessId((IntPtr)hWnd, out processID);
    if(processID == 0) return false;
    try
    {
        Process.GetProcessById((int)processID).Kill();
    }
    catch (ArgumentException)
    {
        return false;
    }
    catch (Win32Exception)
    {
        return false;
    }
    catch (NotSupportedException)
    {
        return false;
    }
    catch (InvalidOperationException)
    {
        return false;
    }
    return true;
}

/// <summary> Finds and kills process by hWnd to the main window of the process.</summary>
/// <param name="hWnd">Handle to the main window of the process.</param>
/// <exception cref="ArgumentException">
/// Thrown when process is not found by the hWnd parameter (the process is not running). 
/// The identifier of the process might be expired.
/// </exception>
/// <exception cref="Win32Exception">See Process.Kill() exceptions documentation.</exception>
/// <exception cref="NotSupportedException">See Process.Kill() exceptions documentation.</exception>
/// <exception cref="InvalidOperationException">See Process.Kill() exceptions documentation.</exception>
public static void KillProcessByMainWindowHwnd(int hWnd)
{
    uint processID;
    GetWindowThreadProcessId((IntPtr)hWnd, out processID);
    if (processID == 0)
        throw new ArgumentException("Process has not been found by the given main window handle.", "hWnd");
    Process.GetProcessById((int)processID).Kill();
}

Как вы можете видеть, я представил два метода в соответствии с шаблоном Try-Parse (я думаю, что это уместно): один метод не генерирует исключение, если процесс не может быть убит (например, процесс не существует больше), а другой метод вызывает исключение, если процесс не был убит. Единственным слабым местом в этом коде являются разрешения безопасности. Теоретически пользователь может не иметь прав на убийство процесса, но в 99,99% всех случаев у пользователя есть такие разрешения. Я также тестировал его с гостевой учетной записью - он отлично работает.

Итак, ваш код, работающий с Excel, может выглядеть так:

int hWnd = xl.Application.Hwnd;
// ...
// here we try to close Excel as usual, with xl.Quit(),
// Marshal.FinalReleaseComObject(xl) and so on
// ...
TryKillProcessByMainWindowHwnd(hWnd);

Voila! Excel прекращен!:)

Хорошо, вернемся ко второму решению, как я и обещал в начале сообщения. Второе решение - вызвать GC.Collect() и GC.WaitForPendingFinalizers(). Да, они действительно работают, но вы должны быть осторожны здесь! Многие люди говорят (и я сказал), что вызов GC.Collect() не помогает. Но причина, по которой это не поможет, - это если есть ссылки на объекты COM! Одной из самых популярных причин, по которым GC.Collect() не помогает, является запуск проекта в режиме Debug. В объектах режима отладки, которые на самом деле не упоминаются, больше не будут собирать мусор до конца метода.
Итак, если вы попытались использовать GC.Collect() и GC.WaitForPendingFinalizers(), и это не помогло, попробуйте сделать следующее:

1) Попробуйте запустить проект в режиме деблокирования и убедитесь, что Excel закрыт правильно

2) Оберните метод, работающий с Excel, в отдельном методе. Итак, вместо чего-то вроде этого:

void GenerateWorkbook(...)
{
  ApplicationClass xl;
  Workbook xlWB;
  try
  {
    xl = ...
    xlWB = xl.Workbooks.Add(...);
    ...
  }
  finally
  {
    ...
    Marshal.ReleaseComObject(xlWB)
    ...
    GC.Collect();
    GC.WaitForPendingFinalizers();
  }
}

вы пишете:

void GenerateWorkbook(...)
{
  try
  {
    GenerateWorkbookInternal(...);
  }
  finally
  {
    GC.Collect();
    GC.WaitForPendingFinalizers();
  }
}

private void GenerateWorkbookInternal(...)
{
  ApplicationClass xl;
  Workbook xlWB;
  try
  {
    xl = ...
    xlWB = xl.Workbooks.Add(...);
    ...
  }
  finally
  {
    ...
    Marshal.ReleaseComObject(xlWB)
    ...
  }
}

Теперь Excel закроет =)

  • 17
    грустно, что ветка уже настолько старая, что ваш превосходный ответ появляется так далеко внизу, что, я думаю, является единственной причиной того, что за нее больше не голосовали ...
  • 0
    @VVS: Это абсолютно неверная аналогия. И ваше «правило 2 точек» не всегда работает. Далеко не всегда.
Показать ещё 16 комментариев
46

UPDATE: добавлен код С# и ссылка на Windows Jobs

Я когда-то пытался выяснить эту проблему, и в то время XtremeVBTalk был самым активным и отзывчивым. Вот ссылка на мой оригинальный пост, Чистое завершение процесса Excel Interop, даже если ваше приложение выйдет из строя. Ниже приведено резюме сообщения и код, скопированный на этот пост.

  • Закрытие процесса взаимодействия с Application.Quit() и Process.Kill() работает по большей части, но не выполняется, если приложения катастрофически катастрофически. То есть если приложение выйдет из строя, процесс Excel все равно будет запущен.
  • Решение состоит в том, чтобы позволить ОС обрабатывать очистку ваших процессов с помощью Объектов работы Windows с использованием вызовов Win32. Когда ваше основное приложение умирает, связанные процессы (например, Excel) также прекратятся.

Я нашел, что это чистое решение, потому что ОС делает настоящую работу по очистке. Все, что вам нужно сделать, это зарегистрировать процесс Excel.

Рабочий код Windows

Обматывает вызовы API Win32 для регистрации процессов Interop.

public enum JobObjectInfoType
{
    AssociateCompletionPortInformation = 7,
    BasicLimitInformation = 2,
    BasicUIRestrictions = 4,
    EndOfJobTimeInformation = 6,
    ExtendedLimitInformation = 9,
    SecurityLimitInformation = 5,
    GroupInformation = 11
}

[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
    public int nLength;
    public IntPtr lpSecurityDescriptor;
    public int bInheritHandle;
}

[StructLayout(LayoutKind.Sequential)]
struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
    public Int64 PerProcessUserTimeLimit;
    public Int64 PerJobUserTimeLimit;
    public Int16 LimitFlags;
    public UInt32 MinimumWorkingSetSize;
    public UInt32 MaximumWorkingSetSize;
    public Int16 ActiveProcessLimit;
    public Int64 Affinity;
    public Int16 PriorityClass;
    public Int16 SchedulingClass;
}

[StructLayout(LayoutKind.Sequential)]
struct IO_COUNTERS
{
    public UInt64 ReadOperationCount;
    public UInt64 WriteOperationCount;
    public UInt64 OtherOperationCount;
    public UInt64 ReadTransferCount;
    public UInt64 WriteTransferCount;
    public UInt64 OtherTransferCount;
}

[StructLayout(LayoutKind.Sequential)]
struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
    public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
    public IO_COUNTERS IoInfo;
    public UInt32 ProcessMemoryLimit;
    public UInt32 JobMemoryLimit;
    public UInt32 PeakProcessMemoryUsed;
    public UInt32 PeakJobMemoryUsed;
}

public class Job : IDisposable
{
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
    static extern IntPtr CreateJobObject(object a, string lpName);

    [DllImport("kernel32.dll")]
    static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);

    private IntPtr m_handle;
    private bool m_disposed = false;

    public Job()
    {
        m_handle = CreateJobObject(null, null);

        JOBOBJECT_BASIC_LIMIT_INFORMATION info = new JOBOBJECT_BASIC_LIMIT_INFORMATION();
        info.LimitFlags = 0x2000;

        JOBOBJECT_EXTENDED_LIMIT_INFORMATION extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
        extendedInfo.BasicLimitInformation = info;

        int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
        IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length);
        Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false);

        if (!SetInformationJobObject(m_handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length))
            throw new Exception(string.Format("Unable to set information.  Error: {0}", Marshal.GetLastWin32Error()));
    }

    #region IDisposable Members

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    #endregion

    private void Dispose(bool disposing)
    {
        if (m_disposed)
            return;

        if (disposing) {}

        Close();
        m_disposed = true;
    }

    public void Close()
    {
        Win32.CloseHandle(m_handle);
        m_handle = IntPtr.Zero;
    }

    public bool AddProcess(IntPtr handle)
    {
        return AssignProcessToJobObject(m_handle, handle);
    }

}

Примечание о коде конструктора

  • В конструкторе вызывается info.LimitFlags = 0x2000;. 0x2000 - это значение JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE enum, и это значение определяется MSDN как:

Заставляет все процессы, связанные с заданием, завершаться, когда последний дескриптор задания закрыт.

Дополнительный вызов API Win32 для получения идентификатора процесса (PID)

    [DllImport("user32.dll", SetLastError = true)]
    public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

Используя код

    Excel.Application app = new Excel.ApplicationClass();
    Job job = new Job();
    uint pid = 0;
    Win32.GetWindowThreadProcessId(new IntPtr(app.Hwnd), out pid);
    job.AddProcess(Process.GetProcessById((int)pid).Handle);
  • 2
    Явное уничтожение внепроцессного COM-сервера (который может обслуживать других COM-клиентов) - ужасная идея. Если вы прибегаете к этому, это потому, что ваш протокол COM нарушен. COM-серверы не должны рассматриваться как обычное оконное приложение
35

Это работало над проектом, над которым я работал:

excelApp.Quit();
Marshal.ReleaseComObject (excelWB);
Marshal.ReleaseComObject (excelApp);
excelApp = null;

Мы узнали, что было важно установить каждую ссылку на объект COM Excel с нулевым значением, когда вы закончили с ним. Это включало ячейки, таблицы и все.

30

Все, что находится в пространстве имен Excel, должно быть выпущено. Период

Вы не можете делать:

Worksheet ws = excel.WorkBooks[1].WorkSheets[1];

Вы должны делать

Workbooks books = excel.WorkBooks;
Workbook book = books[1];
Sheets sheets = book.WorkSheets;
Worksheet ws = sheets[1];

за которым следует освобождение объектов.

  • 3
    Как, например, xlRange.Interior.Color, например.
  • 0
    Внутренняя часть должна быть освобождена (она находится в пространстве имен) ... С другой стороны, цвет - нет (вызывается из System.Drawing.Color, iirc)
Показать ещё 1 комментарий
18

I нашел полезный общий шаблон, который может помочь реализовать правильный шаблон удаления для COM-объектов, которым нужен Marshal.ReleaseComObject, когда они выходят области:

Применение:

using (AutoReleaseComObject<Application> excelApplicationWrapper = new AutoReleaseComObject<Application>(new Application()))
{
    try
    {
        using (AutoReleaseComObject<Workbook> workbookWrapper = new AutoReleaseComObject<Workbook>(excelApplicationWrapper.ComObject.Workbooks.Open(namedRangeBase.FullName, false, false, missing, missing, missing, true, missing, missing, true, missing, missing, missing, missing, missing)))
        {
           // do something with your workbook....
        }
    }
    finally
    {
         excelApplicationWrapper.ComObject.Quit();
    } 
}

Шаблон:

public class AutoReleaseComObject<T> : IDisposable
{
    private T m_comObject;
    private bool m_armed = true;
    private bool m_disposed = false;

    public AutoReleaseComObject(T comObject)
    {
        Debug.Assert(comObject != null);
        m_comObject = comObject;
    }

#if DEBUG
    ~AutoReleaseComObject()
    {
        // We should have been disposed using Dispose().
        Debug.WriteLine("Finalize being called, should have been disposed");

        if (this.ComObject != null)
        {
            Debug.WriteLine(string.Format("ComObject was not null:{0}, name:{1}.", this.ComObject, this.ComObjectName));
        }

        //Debug.Assert(false);
    }
#endif

    public T ComObject
    {
        get
        {
            Debug.Assert(!m_disposed);
            return m_comObject;
        }
    }

    private string ComObjectName
    {
        get
        {
            if(this.ComObject is Microsoft.Office.Interop.Excel.Workbook)
            {
                return ((Microsoft.Office.Interop.Excel.Workbook)this.ComObject).Name;
            }

            return null;
        }
    }

    public void Disarm()
    {
        Debug.Assert(!m_disposed);
        m_armed = false;
    }

    #region IDisposable Members

    public void Dispose()
    {
        Dispose(true);
#if DEBUG
        GC.SuppressFinalize(this);
#endif
    }

    #endregion

    protected virtual void Dispose(bool disposing)
    {
        if (!m_disposed)
        {
            if (m_armed)
            {
                int refcnt = 0;
                do
                {
                    refcnt = System.Runtime.InteropServices.Marshal.ReleaseComObject(m_comObject);
                } while (refcnt > 0);

                m_comObject = default(T);
            }

            m_disposed = true;
        }
    }
}

Справка:

http://www.deez.info/sengelha/2005/02/11/useful-idisposable-class-3-autoreleasecomobject/

  • 3
    да это хорошо. Я думаю, что этот код может быть обновлен, хотя теперь, когда FinalReleaseCOMObject доступен.
  • 0
    Все еще раздражает наличие блока использования для каждого объекта. Смотрите здесь stackoverflow.com/questions/2191489/… для альтернативы.
15

Я не могу поверить, что эта проблема преследовала мир в течение 5 лет... Если вы создали приложение, вам нужно сначала закрыть его, прежде чем удалять ссылку.

objExcel = new Excel.Application();  
objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing)); 

при закрытии

objBook.Close(true, Type.Missing, Type.Missing); 
objExcel.Application.Quit();
objExcel.Quit(); 

Когда вы используете новое приложение excel, оно открывает программу excel в фоновом режиме. Вам нужно указать, что программа excel прекратит работу до того, как вы отпустите ссылку, потому что эта программа excel не является частью вашего прямого управления. Поэтому он останется открытым, если ссылка будет выпущена!

Хорошее программирование на всех ~~

  • 0
    Нет, особенно если вы хотите разрешить взаимодействие пользователя с приложением COM (в данном случае Excel - OP не указывает, предназначено ли взаимодействие с пользователем, но не ссылается на его отключение). Вы просто должны заставить вызывающего пользователя отпустить, чтобы пользователь мог выйти из Excel при закрытии, скажем, одного файла.
  • 0
    это прекрасно сработало для меня - сбой, когда у меня был только objExcel.Quit (), но когда я также сделал objExcel.Application.Quit () заранее, закрылся чисто.
14

Во-первых - вам не нужно вызывать Marshal.ReleaseComObject(...) или Marshal.FinalReleaseComObject(...) при выполнении взаимодействия с Excel. Это запутанный анти-шаблон, но любая информация об этом, в том числе и Microsoft, которая указывает, что вы должны вручную выпускать COM-ссылки из .NET, неверна. Дело в том, что среда выполнения .NET и сборщик мусора правильно отслеживают и очищают COM-ссылки. Для вашего кода это означает, что вы можете удалить весь цикл while (...) вверху.

Во-вторых, если вы хотите убедиться, что COM-ссылка на COM-объект вне процесса очищается, когда ваш процесс завершается (так что процесс Excel закроется), вам нужно убедиться, что сборщик мусора работает. Вы делаете это правильно с вызовами GC.Collect() и GC.WaitForPendingFinalizers(). Вызов этого дважды безопасен и гарантирует, что циклы определенно очищены (хотя я не уверен, что это необходимо, и я бы оценил пример, который показывает это).

В-третьих, при запуске под отладчиком локальные ссылки будут искусственно поддерживаться до конца метода (так что работает локальная проверка переменных). Таким образом, вызовы GC.Collect() не эффективны для очистки объекта типа rng.Cells от того же метода. Вы должны разделить код, делающий COM-взаимодействие с очисткой GC на отдельные методы. (Это было ключевым открытием для меня, из одной части ответа, размещенного здесь by @nightcoder.)

Таким образом, общий шаблон будет следующим:

Sub WrapperThatCleansUp()

    ' NOTE: Don't call Excel objects in here... 
    '       Debugger would keep alive until end, preventing GC cleanup

    ' Call a separate function that talks to Excel
    DoTheWork()

    ' Now let the GC clean up (twice, to clean up cycles too)
    GC.Collect()    
    GC.WaitForPendingFinalizers()
    GC.Collect()    
    GC.WaitForPendingFinalizers()

End Sub

Sub DoTheWork()
    Dim app As New Microsoft.Office.Interop.Excel.Application
    Dim book As Microsoft.Office.Interop.Excel.Workbook = app.Workbooks.Add()
    Dim worksheet As Microsoft.Office.Interop.Excel.Worksheet = book.Worksheets("Sheet1")
    app.Visible = True
    For i As Integer = 1 To 10
        worksheet.Cells.Range("A" & i).Value = "Hello"
    Next
    book.Save()
    book.Close()
    app.Quit()

    ' NOTE: No calls the Marshal.ReleaseComObject() are ever needed
End Sub

Существует много ложной информации и путаницы по этой проблеме, включая множество сообщений в MSDN и Qaru (и особенно этот вопрос!).

Что, в конце концов, убедило меня в более пристальном рассмотрении и выяснении правильного совета, было сообщение в блоге Marshal.ReleaseComObject Считается опасным вместе с поиском проблемы с ссылками, поддерживаемыми под отладчиком, которые путали мое раннее тестирование.

  • 3
    Практически ВСЕ ОТВЕТЫ НА ЭТОЙ СТРАНИЦЕ НЕПРАВИЛЬНЫ, КРОМЕ ЭТОГО. Они работают, но слишком много работы. Игнорировать правило "2 точки" Пусть GC сделает вашу работу за вас. Дополнительные доказательства от .Net GURU Ханса Пассанта: stackoverflow.com/a/25135685/3852958
12

Общие разработчики, ни одно из ваших решений не работало для меня, поэтому я решил реализовать новый трюк .

Сначала укажите "Какова наша цель?". = > "Не видеть объект excel после нашей работы в диспетчере задач"

Ok. Пусть нет, чтобы бросить вызов и начать уничтожать его, но не стоит уничтожать другой экземпляр os Excel, который работает параллельно.

Итак, получите список текущих процессоров и извлеките PID процессов EXCEL, а затем, как только ваша работа будет завершена, у нас есть новый гость в списке процессов с уникальным PID, найдите и уничтожьте только этот.

< имейте в виду, что любой новый процесс excel во время вашей работы excel будет обнаружен как новый и уничтоженный >  < Лучшим решением является захват PID нового созданного объекта excel и просто уничтожение этого >

Process[] prs = Process.GetProcesses();
List<int> excelPID = new List<int>();
foreach (Process p in prs)
   if (p.ProcessName == "EXCEL")
       excelPID.Add(p.Id);

.... // your job 

prs = Process.GetProcesses();
foreach (Process p in prs)
   if (p.ProcessName == "EXCEL" && !excelPID.Contains(p.Id))
       p.Kill();

Это разрешает мою проблему, надеюсь, что и ваша.

  • 1
    .... это что-то вроде санокгамерского подхода? - это похоже на то, что я также использую :( но нужно изменить. Проблема с использованием этого подхода заключается в том, что часто при открытии Excel на машине, где он работал, в левой панели появляется предупреждение, говорящее что-то вроде «Excel был закрыт неправильно»: не очень элегантно. Я думаю, что одно из более ранних предложений в этой теме должно быть предпочтительным.
10

Это похоже, что это было слишком сложно. По моему опыту, есть всего три ключевых момента, чтобы заставить Excel нормально закрыться:

1: убедитесь, что нет оставшихся ссылок на приложение excel, которое вы создали (у вас должен быть только один, установите его на null)

2: вызов GC.Collect()

3: Excel должен быть закрыт либо пользователем, закрывающим программу вручную, либо вызовом Quit объекта Excel. (Обратите внимание, что Quit будет функционировать так же, как если бы пользователь попытался закрыть программу и представит диалог подтверждения, если есть несохраненные изменения, даже если Excel не отображается. Пользователь может нажать отменить, а затем Excel не будет был закрыт.)

1 должно произойти до 2, но 3 может произойти в любое время.

Один из способов реализации этого заключается в том, чтобы обернуть объект Interop Excel своим собственным классом, создать экземпляр взаимодействия в конструкторе и реализовать IDisposable с Dispose, выглядящим как-то вроде

if (!mDisposed) {
   mExcel = null;
   GC.Collect();
   mDisposed = true;
}

Это очистит преимущество от вашей программной стороны. Когда Excel закрыт (вручную пользователем или вы вызываете Quit), процесс исчезнет. Если программа уже была закрыта, процесс исчезнет при вызове GC.Collect().

(Я не уверен, насколько это важно, но вам может понадобиться вызов GC.WaitForPendingFinalizers() после вызова GC.Collect(), но не обязательно нужно избавляться от процесса Excel.)

Это работало для меня без проблем годами. Имейте в виду, хотя в то время как это работает, вам на самом деле нужно грациозно закрыть, чтобы он работал. Вы по-прежнему будете накапливать процессы excel.exe, если вы прервите свою программу до того, как Excel будет очищен (обычно путем нажатия "stop", когда ваша программа отлаживается).

  • 1
    Этот ответ работает и бесконечно удобнее, чем принятый ответ. Не беспокойтесь о «двух точках» с COM-объектами, не используйте Marshal .
9

После попытки

  • Освободить COM-объекты в обратном порядке
  • Добавить GC.Collect() и GC.WaitForPendingFinalizers() дважды в конце
  • Не более двух точек
  • Закрыть книгу и закрыть приложение
  • Запуск в режиме выпуска

окончательное решение, которое работает для меня, - это перемещение одного набора

GC.Collect();
GC.WaitForPendingFinalizers();

который мы добавили в конец функции в оболочку, следующим образом:

private void FunctionWrapper(string sourcePath, string targetPath)
{
    try
    {
        FunctionThatCallsExcel(sourcePath, targetPath);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}
  • 0
    Я обнаружил, что мне не нужно обнулять ComObjects, только Quit () - Close () и FinalReleaseComObject. Я не могу поверить, что это все, чего не хватало на моем конце, чтобы заставить его работать. Большой!
9

Чтобы добавить причины, по которым Excel не закрывается, даже когда вы создаете прямые рефренсы для каждого объекта при чтении, создании, это цикл "For".

For Each objWorkBook As WorkBook in objWorkBooks 'local ref, created from ExcelApp.WorkBooks to avoid the double-dot
   objWorkBook.Close 'or whatever
   FinalReleaseComObject(objWorkBook)
   objWorkBook = Nothing
Next 

'The above does not work, and this is the workaround:

For intCounter As Integer = 1 To mobjExcel_WorkBooks.Count
   Dim objTempWorkBook As Workbook = mobjExcel_WorkBooks.Item(intCounter)
   objTempWorkBook.Saved = True
   objTempWorkBook.Close(False, Type.Missing, Type.Missing)
   FinalReleaseComObject(objTempWorkBook)
   objTempWorkBook = Nothing
Next
  • 0
    И еще одна причина - повторное использование ссылки без предварительного сброса предыдущего значения.
  • 0
    Спасибо. Я также использовал «для каждого», ваше решение сработало для меня.
Показать ещё 1 комментарий
9

Принятый ответ здесь правильный, но также обратите внимание, что нужно избегать не только ссылок "двух точек", но и объектов, которые извлекаются через индекс. Вам также не нужно ждать, пока вы закончите с программой для очистки этих объектов, лучше всего создавать функции, которые будут очищать их, как только вы закончите с ними, когда это возможно. Вот созданная мной функция, которая назначает некоторые свойства объекта Style с именем xlStyleHeader:

public Excel.Style xlStyleHeader = null;

private void CreateHeaderStyle()
{
    Excel.Styles xlStyles = null;
    Excel.Font xlFont = null;
    Excel.Interior xlInterior = null;
    Excel.Borders xlBorders = null;
    Excel.Border xlBorderBottom = null;

    try
    {
        xlStyles = xlWorkbook.Styles;
        xlStyleHeader = xlStyles.Add("Header", Type.Missing);

        // Text Format
        xlStyleHeader.NumberFormat = "@";

        // Bold
        xlFont = xlStyleHeader.Font;
        xlFont.Bold = true;

        // Light Gray Cell Color
        xlInterior = xlStyleHeader.Interior;
        xlInterior.Color = 12632256;

        // Medium Bottom border
        xlBorders = xlStyleHeader.Borders;
        xlBorderBottom = xlBorders[Excel.XlBordersIndex.xlEdgeBottom];
        xlBorderBottom.Weight = Excel.XlBorderWeight.xlMedium;
    }
    catch (Exception ex)
    {
        throw ex;
    }
    finally
    {
        Release(xlBorderBottom);
        Release(xlBorders);
        Release(xlInterior);
        Release(xlFont);
        Release(xlStyles);
    }
}

private void Release(object obj)
{
    // Errors are ignored per Microsoft suggestion for this type of function:
    // http://support.microsoft.com/default.aspx/kb/317109
    try
    {
        System.Runtime.InteropServices.Marshal.ReleaseComObject(obj);
    }
    catch { } 
}

Обратите внимание, что мне пришлось установить xlBorders[Excel.XlBordersIndex.xlEdgeBottom] в переменную, чтобы очистить ее (не из-за двух точек, которые ссылаются на перечисление, которое не нужно освобождать, а потому, что объект, который я имею в виду to на самом деле является объектом Border, который нужно освободить).

В стандартных приложениях это действительно не обязательно, что делает большую работу по очистке после себя, но в приложениях ASP.NET, если вы пропустите хотя бы один из них, независимо от того, как часто вы вызываете сборщик мусора, Excel все равно будет работать на вашем сервере.

Это требует большого внимания деталям и множеству тестовых исполнений при мониторинге диспетчера задач при написании этого кода, но при этом вы избавляетесь от трудностей, связанных с отчаянным поиском страниц кода, чтобы найти один экземпляр, который вы пропустили. Это особенно важно при работе в цикле, где вам нужно выпустить EACH INSTANCE объекта, хотя при каждом цикле он использует одно и то же имя переменной.

  • 1
    +1 за объекты, полученные через индексы.
  • 0
    Внутри релиза, проверьте на Нуль (ответ Джо). Это позволит избежать ненужных нулевых исключений. Я проверил много методов. Это единственный способ эффективно выпустить Excel.
8

Я традиционно следовал рекомендациям, изложенным в ответе VVS. Однако, чтобы сохранить этот ответ последним с последними вариантами, я думаю, что все мои будущие проекты будут использовать библиотеку NetOffice.

NetOffice - полная замена Office PIA и полностью версия-агностик. Это коллекция управляемых COM-оберток, которые могут обрабатывать очистку, которая часто вызывает такие головные боли при работе с Microsoft Office в.NET.

Некоторые ключевые особенности:

  • В основном независимые от версии (и зависящие от версии функции документируются)
  • Без зависимостей
  • Нет PIA
  • Нет регистрации
  • Нет VSTO

Я никоим образом не связан с проектом; Я просто искренне ценю резкое снижение головных болей.

  • 2
    Это должно быть отмечено как ответ, действительно. NetOffice абстрагируется от всей этой сложности.
  • 1
    Я использовал NetOffice в течение значительного количества времени для написания надстройки для Excel, и она отлично работала. Единственное, что нужно учитывать, это то, что, если вы не удалите использованные объекты явным образом, он сделает это при выходе из приложения (потому что он все равно отслеживает их). Таким образом, практическое правило с NetOffice всегда заключается в использовании шаблона «использования» с каждым объектом Excel, таким как ячейка, диапазон или лист и т. Д.
7

Я точно это сделал... Но я все еще сталкивался с проблемами 1 из 1000 раз. Кто знает, почему. Время вытащить молот...

Сразу после того, как экземпляр класса Excel запущен, я получаю только что созданный процесс Excel.

excel = new Microsoft.Office.Interop.Excel.Application();
var process = Process.GetProcessesByName("EXCEL").OrderByDescending(p => p.StartTime).First();

Затем, как только я выполнил все вышеописанную очистку COM, я убеждаюсь, что процесс не запущен. Если он все еще работает, убейте его!

if (!process.HasExited)
   process.Kill();
7

¨ ° º¤ø "¸ Стрелять Excel proc и жевать пузырьковую резину ¸ ° º ° ¨

public class MyExcelInteropClass
{
    Excel.Application xlApp;
    Excel.Workbook xlBook;

    public void dothingswithExcel() 
    {
        try { /* Do stuff manipulating cells sheets and workbooks ... */ }
        catch {}
        finally {KillExcelProcess(xlApp);}
    }

    static void KillExcelProcess(Excel.Application xlApp)
    {
        if (xlApp != null)
        {
            int excelProcessId = 0;
            GetWindowThreadProcessId(xlApp.Hwnd, out excelProcessId);
            Process p = Process.GetProcessById(excelProcessId);
            p.Kill();
            xlApp = null;
        }
    }

    [DllImport("user32.dll")]
    static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);
}
6

"Никогда не использовать две точки с COM-объектами" - это большое эмпирическое правило, чтобы избежать утечки ссылок COM, но Excel PIA может привести к утечке больше, чем кажется на первый взгляд.

Одним из таких способов является подписка на любое событие, открытое любым объектом COM объектной модели Excel.

Например, подписка на событие WorkplaceOpen класса Application.

Некоторая теория событий COM

COM-классы выставляют группу событий через интерфейсы обратного вызова. Чтобы подписаться на события, клиентский код может просто зарегистрировать объект, реализующий интерфейс обратного вызова, и COM-класс будет вызывать свои методы в ответ на определенные события. Поскольку интерфейс обратного вызова является COM-интерфейсом, обязанность объекта-исполнителя уменьшает счетчик ссылок любого COM-объекта, который он получает (как параметр) для любого из обработчиков событий.

Как Excel PIA выставляет COM-события

Excel PIA предоставляет COM-события класса Excel Application как обычные .NET-события. Всякий раз, когда клиентский код подписывается на событие .NET (выделение на 'a'), PIA создает экземпляр класса, реализующего интерфейс обратного вызова, и регистрирует его с помощью Excel.

Следовательно, ряд объектов обратного вызова регистрируется в Excel в ответ на различные запросы на подписку из кода .NET. Один объект обратного вызова для подписки на событие.

Интерфейс обратного вызова для обработки событий означает, что PIA должна подписаться на все события интерфейса для каждого запроса подписки на .NET. Он не может выбрать и выбрать. При получении обратного вызова события объект обратного вызова проверяет, заинтересован ли обработчик событий .NET в текущем событии или нет, а затем либо вызывает обработчик, либо молча игнорирует обратный вызов.

Влияние на количество экземпляров экземпляра COM

Все эти объекты обратного вызова не уменьшают счетчик ссылок любого из COM-объектов, которые они получают (в качестве параметров) для любого из методов обратного вызова (даже для тех, которые молча игнорируются). Они полагаются исключительно на CLR сборщик мусора, чтобы освободить COM-объекты.

Так как GC run не является детерминированным, это может привести к удержанию процесса Excel в течение более длительного времени, чем это необходимо, и создать впечатление утечки памяти.

Решение

Единственное решение на данный момент - избежать провайдера событий PIAs для класса COM и написать собственный поставщик событий, который детерминирует выпуск COM-объектов.

Для класса Application это можно сделать, реализовав интерфейс AppEvents, а затем зарегистрировав реализацию с помощью Excel, используя интерфейс IConnectionPointContainer. Класс Application (и, во всяком случае, все COM-объекты, подверженные событиям с использованием механизма обратного вызова) реализует интерфейс IConnectionPointContainer.

6

Вы должны знать, что Excel очень чувствителен к той культуре, в которой вы работаете.

Вы можете обнаружить, что вам нужно установить культуру в EN-US перед вызовом функций Excel. Это не относится ко всем функциям, но некоторые из них.

    CultureInfo en_US = new System.Globalization.CultureInfo("en-US"); 
    System.Threading.Thread.CurrentThread.CurrentCulture = en_US;
    string filePathLocal = _applicationObject.ActiveWorkbook.Path;
    System.Threading.Thread.CurrentThread.CurrentCulture = orgCulture;

Это применимо, даже если вы используете VSTO.

Подробнее: http://support.microsoft.com/default.aspx?scid=kb;en-us;Q320369

5

Как указывали другие, вам нужно создать явную ссылку для каждого используемого объекта Excel и вызвать Marshal.ReleaseComObject в этой ссылке, как описано в эта статья в KB. Вам также необходимо использовать try/finally, чтобы гарантировать, что ReleaseComObject всегда вызывается, даже когда генерируется исключение. То есть вместо:

Worksheet sheet = excelApp.Worksheets(1)
... do something with sheet

вам нужно сделать что-то вроде:

Worksheets sheets = null;
Worksheet sheet = null
try
{ 
    sheets = excelApp.Worksheets;
    sheet = sheets(1);
    ...
}
finally
{
    if (sheets != null) Marshal.ReleaseComObject(sheets);
    if (sheet != null) Marshal.ReleaseComObject(sheet);
}

Вам также необходимо вызвать Application.Quit перед выпуском объекта Application, если вы хотите закрыть Excel.

Как вы можете видеть, это быстро становится чрезвычайно громоздким, как только вы пытаетесь сделать что-то даже умеренно сложное. Я успешно разработал .NET-приложения с простым классом-оболочкой, который обертывает несколько простых манипуляций с объектной моделью Excel (откройте книгу, напишите в Range, сохраните/закройте книгу и т.д.). Класс-оболочка реализует IDisposable, тщательно реализует Marshal.ReleaseComObject для каждого объекта, который он использует, и не публиковал любые объекты Excel в остальной части приложения.

Но этот подход недостаточно подходит для более сложных требований.

Это большой недостаток .NET COM Interop. Для более сложных сценариев я бы серьезно подумал о написании ActiveX DLL в VB6 или другом неуправляемом языке, на который вы можете делегировать все взаимодействия с COM-объектами внепроцесса, такими как Office. Затем вы можете ссылаться на эту ActiveX DLL из вашего приложения .NET, и все будет намного проще, поскольку вам нужно будет только снять эту ссылку.

  • 0
    блестящий! +1 ..
4

Когда все перечисленное выше не работает, попробуйте предоставить Excel некоторое время для закрытия своих листов:

app.workbooks.Close();
Thread.Sleep(500); // adjust, for me it works at around 300+
app.Quit();

...
FinalReleaseComObject(app);
  • 2
    Я ненавижу произвольные ожидания. Но +1, потому что вы правы насчет необходимости ждать закрытия рабочих книг. Альтернативой является опрос коллекции книг в цикле и использование произвольного ожидания в качестве времени ожидания цикла.
3

Убедитесь, что вы отпустите все объекты, связанные с Excel!

Я потратил несколько часов, попробовав несколько способов. Все это отличные идеи, но я, наконец, нашел свою ошибку: Если вы не выпускаете все объекты, ни один из способов, описанных выше, не поможет вам, как в моем случае. Убедитесь, что вы отпустите все объекты, включая один диапазон!

Excel.Range rng = (Excel.Range)worksheet.Cells[1, 1];
worksheet.Paste(rng, false);
releaseObject(rng);

Параметры вместе здесь.

  • 0
    Очень хороший момент! Я думаю, что из-за того, что я использую Office 2007, от моего имени была проведена некоторая очистка. Я использовал совет далее, но не сохранил переменные, как вы предложили здесь, и EXCEL.EXE завершает работу, но я могу просто повезти, и если у меня возникнут какие-либо дальнейшие проблемы, я обязательно посмотрю эту часть моего кода =)
2

Правило с двумя точками не работало для меня. В моем случае я создал метод очистки моих ресурсов следующим образом:

private static void Clean()
{
    workBook.Close();
    Marshall.ReleaseComObject(workBook);
    excel.Quit();
    CG.Collect();
    CG.WaitForPendingFinalizers();
}
2

Отличная статья о выпуске COM-объектов 2.5 Освобождение COM-объектов (MSDN).

Метод, который я бы защищал, - это обнулить ваши ссылки Excel.Interop, если они являются нелокальными переменными, а затем дважды вызвать GC.Collect() и GC.WaitForPendingFinalizers(). Локально измененные переменные Interop будут учтены автоматически.

Это устраняет необходимость сохранения именованной ссылки для каждого COM-объекта.

Вот пример, взятый из статьи:

public class Test {

    // These instance variables must be nulled or Excel will not quit
    private Excel.Application xl;
    private Excel.Workbook book;

    public void DoSomething()
    {
        xl = new Excel.Application();
        xl.Visible = true;
        book = xl.Workbooks.Add(Type.Missing);

        // These variables are locally scoped, so we need not worry about them.
        // Notice I don't care about using two dots.
        Excel.Range rng = book.Worksheets[1].UsedRange;
    }

    public void CleanUp()
    {
        book = null;
        xl.Quit();
        xl = null;

        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

Эти слова прямо из статьи:

Почти во всех ситуациях обнуление ссылки RCW и принудительная сборка мусора будут правильно очищены. Если вы также вызываете GC.WaitForPendingFinalizers, сбор мусора будет таким же детерминированным, как вы можете это сделать. То есть, вы точно будете уверены, когда объект был очищен - при возврате со второго вызова WaitForPendingFinalizers. В качестве альтернативы вы можете использовать Marshal.ReleaseComObject. Однако обратите внимание, что вам вряд ли когда-либо понадобится использовать этот метод.

  • 0
    Обратите внимание, что упомянутая статья msdn относится к Office 2003 ...
2

Вы должны быть очень осторожны при использовании приложений Word/Excel interop. После попытки всех решений у нас все еще было много процессов "WinWord", которые оставались открытыми на сервере (с более чем 2000 пользователями).

После работы над проблемой в течение нескольких часов я понял, что если я открою более чем несколько документов, используя Word.ApplicationClass.Document.Open() для разных потоков одновременно, рабочий процесс IIS (w3wp.exe) будет сбой, оставив все процессы WinWord открытыми!

Итак, я думаю, что нет абсолютного решения этой проблемы, но переключитесь на другие методы, такие как Office Open XML.

1

'Кажется, что это было слишком сложно. По моему опыту, есть всего три ключевых момента, чтобы заставить Excel нормально закрыться:

1: убедитесь, что нет оставшихся ссылок на приложение excel, которое вы создали (у вас должен быть только один, установите его в null)

2: вызов GC.Collect()

3: Excel должен быть закрыт либо пользователем, закрывающим программу вручную, либо вызовом Quit на объекте Excel. (Обратите внимание, что Quit будет функционировать так же, как если бы пользователь попытался закрыть программу и представит диалог подтверждения, если есть несохраненные изменения, даже если Excel не отображается. Пользователь может нажать отменить, а затем Excel не будет закрыт.)

1 должно произойти до 2, но 3 может произойти в любое время.

Один из способов реализации этого заключается в том, чтобы обернуть объект Interop Excel своим собственным классом, создать экземпляр взаимодействия в конструкторе и реализовать IDisposable с Dispose, выглядящим как-то вроде

Это очистит преимущество от вашей программной стороны. Как только Excel будет закрыт (вручную пользователем или вызовом Quit), процесс исчезнет. Если программа уже закрыта, процесс исчезнет при вызове GC.Collect().

(Я не уверен, насколько это важно, но вам может понадобиться вызов GC.WaitForPendingFinalizers() после вызова GC.Collect(), но не обязательно избавляться от процесса Excel.)

Это работало для меня без проблем годами. Имейте в виду, хотя в то время как это работает, вам на самом деле нужно грациозно закрыть, чтобы он работал. Вы по-прежнему будете накапливать процессы excel.exe, если вы прервите свою программу до того, как Excel будет очищен (обычно, ударяя "стоп", пока ваша программа отлаживается).

1

В настоящее время я работаю над автоматизацией Office и наткнулся на решение для этого, которое работает каждый раз для меня. Это просто и не связано с убийством любых процессов.

Кажется, что путем простого перебора текущих активных процессов и каким-либо образом "доступа" к открытому процессу Excel любой удаленный экземпляр Excel будет удален. В приведенном ниже коде просто проверяются процессы, в которых имя "Excel", а затем записывает свойство MainWindowTitle для процесса в строку. Это "взаимодействие" с процессом, похоже, заставляет Windows догнать и прервать замороженный экземпляр Excel.

Я запускаю метод ниже непосредственно перед тем, как надстройка, которую я разрабатываю, завершает работу, так как она запускает событие разгрузки. Он удаляет все зависающие экземпляры Excel каждый раз. Честно говоря, я не совсем уверен, почему это работает, но оно хорошо работает для меня и может быть помещено в конце любого приложения Excel без необходимости беспокоиться о двойных точках, Marshal.ReleaseComObject и процессах убийства. Я был бы очень заинтересован в любых предложениях относительно того, почему это эффективно.

public static void SweepExcelProcesses()
{           
            if (Process.GetProcessesByName("EXCEL").Length != 0)
            {
                Process[] processes = Process.GetProcesses();
                foreach (Process process in processes)
                {
                    if (process.ProcessName.ToString() == "excel")
                    {                           
                        string title = process.MainWindowTitle;
                    }
                }
            }
}
1

Принятый ответ не помог мне. Следующий код в деструкторе выполнил задание.

if (xlApp != null)
{
    xlApp.Workbooks.Close();
    xlApp.Quit();
}

System.Diagnostics.Process[] processArray = System.Diagnostics.Process.GetProcessesByName("EXCEL");
foreach (System.Diagnostics.Process process in processArray)
{
    if (process.MainWindowTitle.Length == 0) { process.Kill(); }
}
1

Мое решение

[DllImport("user32.dll")]
static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId);

private void GenerateExcel()
{
    var excel = new Microsoft.Office.Interop.Excel.Application();
    int id;
    // Find the Excel Process Id (ath the end, you kill him
    GetWindowThreadProcessId(excel.Hwnd, out id);
    Process excelProcess = Process.GetProcessById(id);

try
{
    // Your code
}
finally
{
    excel.Quit();

    // Kill him !
    excelProcess.Kill();
}
1

Использование:

[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

Объявите его, добавьте код в блок finally:

finally
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
    if (excelApp != null)
    {
        excelApp.Quit();
        int hWnd = excelApp.Application.Hwnd;
        uint processID;
        GetWindowThreadProcessId((IntPtr)hWnd, out processID);
        Process[] procs = Process.GetProcessesByName("EXCEL");
        foreach (Process p in procs)
        {
            if (p.Id == processID)
                p.Kill();
        }
        Marshal.FinalReleaseComObject(excelApp);
    }
}
1

Как некоторые из них, вероятно, уже написаны, не просто важно, как вы закрываете Excel (объект); это также важно, как вы его открываете, а также по типу проекта.

В приложении WPF в основном один и тот же код работает без или с очень небольшим количеством проблем.

У меня есть проект, в котором один и тот же файл Excel обрабатывается несколько раз для различного значения параметра - например. разбор его на основе значений внутри общего списка.

Я помещаю все связанные с Excel функции в базовый класс, а парсер - в подкласс (разные синтаксические анализаторы используют общие функции Excel). Я не хотел, чтобы Excel был открыт и закрыт снова для каждого элемента в общем списке, поэтому я открыл его только один раз в базовом классе и закрою его в подклассе. У меня возникли проблемы при перемещении кода в настольное приложение. Я пробовал многие из вышеупомянутых решений. GC.Collect() уже был реализован раньше, дважды, как и предполагалось.

Затем я решил, что я переведу код для открытия Excel в подкласс. Вместо открытия только один раз, теперь я создаю новый объект (базовый класс) и открываю Excel для каждого элемента и закрываю его в конце. Существует некоторое ограничение производительности, но на основе нескольких тестов процессы Excel закрываются без проблем (в режиме отладки), поэтому также удаляются временные файлы. Я продолжу тестирование и напишу еще несколько, если я получу некоторые обновления.

В нижней строке: вы также должны проверить код инициализации, особенно если у вас много классов и т.д.

1

Я думаю, что некоторые из них - это только то, как платформа обрабатывает приложения Office, но я могу ошибаться. В некоторые дни некоторые приложения немедленно очищают процессы, а в другие дни ждут, пока приложение не закроется. В общем, я не обращал внимания на детали и просто слежу за тем, чтобы в конце дня не было никаких дополнительных процессов.

Кроме того, и, возможно, я над упрощением, но я думаю, что вы можете просто...

objExcel = new Excel.Application();
objBook = (Excel.Workbook)(objExcel.Workbooks.Add(Type.Missing));
DoSomeStuff(objBook);
SaveTheBook(objBook);
objBook.Close(false, Type.Missing, Type.Missing);
objExcel.Quit();

Как я уже говорил ранее, я не обращаю внимания на детали, когда процесс Excel появляется или исчезает, но обычно это работает для меня. Мне также не нравится поддерживать процессы Excel на что угодно, кроме минимального времени, но я, вероятно, просто параноик.

0

Там у меня есть идея, попробуйте убить процесс Excel, который вы открыли:

  1. перед открытием заявки получите все идентификаторы процессов с именем oldProcessIds.
  2. откройте заявку.
  3. получить сейчас все идентификаторы процесса исключений приложения с именем nowProcessIds.
  4. когда нужно выйти, убить идентификаторы исключений между oldProcessIds и nowProcessIds.

    private static Excel.Application GetExcelApp()
         {
            if (_excelApp == null)
            {
                var processIds = System.Diagnostics.Process.GetProcessesByName("EXCEL").Select(a => a.Id).ToList();
                _excelApp = new Excel.Application();
                _excelApp.DisplayAlerts = false;
    
                _excelApp.Visible = false;
                _excelApp.ScreenUpdating = false;
                var newProcessIds = System.Diagnostics.Process.GetProcessesByName("EXCEL").Select(a => a.Id).ToList();
                _excelApplicationProcessId = newProcessIds.Except(processIds).FirstOrDefault();
            }
    
            return _excelApp;
        }
    
    public static void Dispose()
        {
            try
            {
                _excelApp.Workbooks.Close();
                _excelApp.Quit();
                System.Runtime.InteropServices.Marshal.ReleaseComObject(_excelApp);
                _excelApp = null;
                GC.Collect();
                GC.WaitForPendingFinalizers();
                if (_excelApplicationProcessId != default(int))
                {
                    var process = System.Diagnostics.Process.GetProcessById(_excelApplicationProcessId);
                    process?.Kill();
                    _excelApplicationProcessId = default(int);
                }
            }
            catch (Exception ex)
            {
                _excelApp = null;
            }
    
        }
    
0

Это единственный способ, который действительно работает для меня.

        foreach (Process proc in System.Diagnostics.Process.GetProcessesByName("EXCEL"))
        {
            proc.Kill();
        }
0

Просто добавьте другое решение ко многим перечисленным здесь, используя автоматизацию С++/ATL (я полагаю, вы могли бы использовать что-то подобное из VB/С#??)

Excel::_ApplicationPtr pXL = ...
  :
SendMessage ( ( HWND ) m_pXL->GetHwnd ( ), WM_DESTROY, 0, 0 ) ;

Это работает как прелесть для меня...

0

До сих пор кажется, что все ответы связаны с некоторыми из них:

  • Убейте процесс
  • Используйте GC.Collect()
  • Следите за каждым COM-объектом и отпустите его правильно.

Что заставляет меня оценить, насколько сложна эта проблема:)

Я работаю над библиотекой, чтобы упростить доступ к Excel, и я стараюсь, чтобы люди, использующие ее, не оставляли беспорядок (скрещены пальцы).

Вместо того, чтобы писать непосредственно на интерфейсах Interop, я делаю методы расширения, чтобы сделать жизнь проще. Как ApplicationHelpers.CreateExcel() или workbook.CreateWorksheet( "mySheetNameThatWillBeValidated" ). Естественно, что все, что создано, может привести к проблеме позже при очистке, поэтому я действительно предпочитаю убить процесс в качестве последнего средства. Тем не менее, очистка должным образом (третий вариант), вероятно, является наименее разрушительной и наиболее контролируемой.

Итак, в этом контексте мне было интересно, не было бы лучше сделать что-то вроде этого:

public abstract class ReleaseContainer<T>
{
    private readonly Action<T> actionOnT;

    protected ReleaseContainer(T releasible, Action<T> actionOnT)
    {
        this.actionOnT = actionOnT;
        this.Releasible = releasible;
    }

    ~ReleaseContainer()
    {
        Release();
    }

    public T Releasible { get; private set; }

    private void Release()
    {
        actionOnT(Releasible);
        Releasible = default(T);
    }
}

Я использовал "Releaseable", чтобы избежать путаницы с одноразовым. Расширение этого параметра до IDisposable должно быть легким, хотя.

Реализация вроде этого:

public class ApplicationContainer : ReleaseContainer<Application>
{
    public ApplicationContainer()
        : base(new Application(), ActionOnExcel)
    {
    }

    private static void ActionOnExcel(Application application)
    {
        application.Show(); // extension method. want to make sure the app is visible.
        application.Quit();
        Marshal.FinalReleaseComObject(application);
    }
}

И можно сделать что-то подобное для всех видов COM-объектов.

В методе factory:

    public static Application CreateExcelApplication(bool hidden = false)
    {
        var excel = new ApplicationContainer().Releasible;
        excel.Visible = !hidden;

        return excel;
    }

Я ожидал бы, что каждый контейнер будет надлежащим образом разрушен GC, и поэтому автоматически вызовет вызов Quit и Marshal.FinalReleaseComObject.

Комментарии? Или это ответ на вопрос третьего рода?

  • 0
    application.Show () не работает
0

Excel не предназначен для программирования через С++ или С#. COM API специально разработан для работы с Visual Basic, VB.NET и VBA.

Кроме того, все примеры кода на этой странице не являются оптимальными по той простой причине, что каждый вызов должен пересекать управляемую/неуправляемую границу и далее игнорировать тот факт, что API-интерфейс Excel может прерывать любой вызов с помощью критического HRESULT, указывающий, что сервер RPC занят.

Самый лучший способ автоматизации Excel, на мой взгляд, - собрать ваши данные как можно больший массив/выполнимый, и отправить их через функцию VBA или sub (через Application.Run), которая затем выполняет любую необходимую обработку. Более того - при вызове Application.Run обязательно следите за исключениями, указывающими, что excel занят и повторит вызов Application.Run.

  • 4
    C # и VB.NET все работают под CLR, поэтому он не может быть разработан для VB.NET, а не для C #. Это в конечном итоге одно и то же, только с другой языковой семантикой для создания приложения.
  • 0
    Попробуйте вызвать метод Run из объекта приложения Excel в C # (версия 3.0 и более ранних) или C ++. Затем попробуйте сделать то же самое с VB.net. После того, как вы пропустите 10-й или 12-й пропущенный параметр - вы, вероятно, поймете, что я имел ввиду:]
Показать ещё 5 комментариев

Ещё вопросы

Сообщество Overcoder
Наверх
Меню