web-dev-qa-db-fra.com

Comment nettoyer correctement les objets Excel Interop?

J'utilise l'interopérabilité Excel en C # (ApplicationClass) et j'ai placé le code suivant dans ma clause finally:

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

Bien que ce type de travail fonctionne, le processus Excel.exe est toujours en arrière-plan même après la fermeture d'Excel. Il n'est publié que lorsque mon application est fermée manuellement.

Qu'est-ce que je fais de mal ou existe-t-il une alternative pour s'assurer que les objets interop sont correctement éliminés?

696
HAdes

Excel ne se ferme pas car votre application contient toujours des références à des objets COM.

Je suppose que vous appelez au moins un membre d'un objet COM sans l'assigner à une variable.

Pour moi, c’était l’objet excelApp.Worksheets que j’ai directement utilisé sans l’affecter à une variable:

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

Je ne savais pas qu'en interne, C # avait créé un wrapper pour l'objet COM Worksheets qui n'avait pas été publié par mon code (car je n'étais pas au courant). et c’est la raison pour laquelle Excel n’a pas été déchargé.

J'ai trouvé la solution à mon problème sur cette page , qui a également une règle de Nice pour l'utilisation des objets COM en C #:

N'utilisez jamais deux points avec des objets COM.


Donc, avec cette connaissance, la bonne façon de procéder est la suivante:

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

MISE À JOUR POST MORTEM:

Je souhaite que chaque lecteur lise très attentivement cette réponse de Hans Passant, qui explique le piège dans lequel beaucoup d’autres développeurs sont tombés. Lorsque j'ai écrit cette réponse il y a des années, je ne connaissais pas l'effet du débogueur sur le ramasse-miettes et j'ai tiré des conclusions erronées. Je garde ma réponse inchangée pour des raisons d'histoire, mais veuillez lire ce lien et ne pas suivre le chemin des "deux points": Comprendre la récupération de place dans .NET et Nettoyer les objets Interop Excel avec IDisposable

668
VVS

Vous pouvez réellement libérer votre objet Application Excel proprement, mais vous devez faire attention. 

Le conseil de maintenir une référence nommée pour absolument chaque objet COM auquel vous accédez, puis de le libérer explicitement via Marshal.FinalReleaseComObject() est correct en théorie, mais, malheureusement, très difficile à gérer en pratique. Si on glisse n'importe où et utilise "deux points", ou itère des cellules via une boucle for each, ou tout autre type de commande similaire, vous aurez des objets COM non référencés et risquerez un blocage. Dans ce cas, il n'y aurait aucun moyen de trouver la cause dans le code; vous auriez à revoir tout votre code à l'œil nu et, espérons-le, à trouver la cause, tâche qui serait presque impossible pour un projet de grande envergure.

La bonne nouvelle est qu’il n’est pas nécessaire de conserver une référence de variable nommée à chaque objet COM que vous utilisez. À la place, appelez GC.Collect(), puis GC.WaitForPendingFinalizers() pour libérer tous les objets (généralement mineurs) auxquels vous ne tenez pas de référence, puis relâchez explicitement les objets pour lesquels vous tenez une référence de variable nommée. 

Vous devez également publier vos références nommées dans l'ordre inverse de leur importance: les objets de plage en premier, puis les feuilles de calcul, les classeurs, puis enfin votre objet Application Excel.

Par exemple, en supposant que vous disposiez d'une variable d'objet Range nommée xlRng, d'une variable de feuille de travail nommée xlSheet, d'une variable de classeur nommée xlBook et d'une variable d'application Excel nommée xlApp, votre code de nettoyage pourrait alors ressembler à ceci:

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

Dans la plupart des exemples de code que vous verrez pour nettoyer les objets COM à partir de .NET, les appels GC.Collect() et GC.WaitForPendingFinalizers() sont effectués DEUX FOIS comme dans:

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

Cela ne devrait toutefois pas être nécessaire, sauf si vous utilisez Visual Studio Tools pour Office (VSTO), qui utilise des finaliseurs qui permettent de promouvoir un graphique entier d'objets dans la file d'attente de finalisation. De tels objets ne seraient pas libérés avant la récupération de place next. Cependant, si vous n'utilisez pas VSTO, vous devriez pouvoir appeler GC.Collect() et GC.WaitForPendingFinalizers() une seule fois.

Je sais que le fait d'appeler explicitement GC.Collect() est un non-non (et le faire deux fois semble très pénible), mais il n'y a pas moyen de le contourner, pour être honnête. Par le biais d’opérations normales, vous allez générer des objets cachés sur lesquels vous ne possédez aucune référence que vous ne pouvez par conséquent pas libérer par un autre moyen que d’appeler GC.Collect().

C'est un sujet complexe, mais c'est tout ce qu'il y a à faire. Une fois que vous avez établi ce modèle pour votre procédure de nettoyage, vous pouvez coder normalement, sans avoir besoin de wrappers, etc. :-)

J'ai un tutoriel à ce sujet ici:

Automatisation des programmes Office avec VB.Net/COM Interop

Il est écrit pour VB.NET, mais ne vous découragez pas, les principes sont exactement les mêmes que lorsque vous utilisez C #.

268
Mike Rosenblum

Préface: ma réponse contient deux solutions, soyez donc prudent lorsque vous lisez et ne manquez rien.

Il existe différents moyens et conseils pour décharger une instance Excel, tels que: 

  • Libérer explicitement chaque objet com .__ avec Marshal.FinalReleaseComObject () (Sans oublier implicitement les com-objets créés ). Pour libérer Chaque objet com créé, vous pouvez utiliser La règle des 2 points mentionnée ici:
    Comment nettoyer correctement les objets Excel?

  • Appel de GC.Collect () et de GC.WaitForPendingFinalizers () pour que CLR libère des objets com * non utilisés (en fait, cela fonctionne, voir ma deuxième solution pour plus de détails)

  • Vérifier si com-server-application Affiche peut-être une boîte de message attendant que L'utilisateur réponde (bien que je ne sois pas Sûr que cela peut empêcher Excel de fermer Mais j'en ai entendu parler quelques fois)

  • Envoi du message WM_CLOSE à la fenêtre principale Excel

  • L'exécution de la fonction qui fonctionne Avec Excel dans un AppDomain séparé . Certaines personnes croient que l'instance d'Excel Sera fermée, lorsque AppDomain sera Déchargé.

  • Suppression de toutes les instances Excel instanciées après le démarrage de notre code Excel.

_ {MAIS! Parfois, toutes ces options ne sont d'aucun secours ou ne peuvent pas convenir!

Par exemple, hier, j'ai découvert que dans l'une de mes fonctions (qui fonctionne avec Excel), Excel continue à s'exécuter après la fin de la fonction. J'ai tout essayé! J'ai soigneusement vérifié l'ensemble de la fonction 10 fois et ajouté Marshal.FinalReleaseComObject () pour tout! J'ai également eu GC.Collect () et GC.WaitForPendingFinalizers (). J'ai vérifié les boîtes de message cachées. J'ai essayé d'envoyer un message WM_CLOSE à la fenêtre principale d'Excel. J'ai exécuté ma fonction dans un AppDomain séparé et déchargé ce domaine. Rien n'a aidé! L'option permettant de fermer toutes les instances Excel est inappropriée, car si l'utilisateur démarre une autre instance Excel manuellement, lors de l'exécution de ma fonction qui fonctionne également avec Excel, cette instance sera également fermée par ma fonction. Je parie que l'utilisateur ne sera pas heureux! Donc, honnêtement, c’est une option boiteuse (pas d’offensive types). J'ai donc passé quelques heures avant de trouver un bon (à mon humble avis) solution: Tuez le processus Excel par sa fenêtre principale (c'est la première solution).

Voici le code simple:

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

Comme vous pouvez le voir, j'ai fourni deux méthodes, selon le modèle Try-Parse (je pense que c'est approprié ici): une méthode ne lève pas l'exception si le processus ne peut pas être tué (par exemple, le processus n'existe plus) et une autre méthode lève l'exception si le processus n'a pas été tué. Le seul endroit faible de ce code concerne les autorisations de sécurité. Théoriquement, l'utilisateur peut ne pas avoir l'autorisation de tuer le processus, mais dans 99,99% des cas, il dispose de telles autorisations. Je l'ai également testé avec un compte invité - cela fonctionne parfaitement.

Ainsi, votre code, fonctionnant avec Excel, peut ressembler à ceci:

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 est terminé! :)

Ok, revenons à la deuxième solution, comme je l'avais promis au début du message .La deuxième solution consiste à appeler GC.Collect () et GC.WaitForPendingFinalizers (). Oui, ils effectivement travailler, mais vous devez faire attention ici!
Beaucoup de gens disent (et j'ai dit) que l'appel de GC.Collect () n'aide pas. Mais la raison pour laquelle cela n’aiderait pas, c’est qu’il y ait toujours des références aux objets COM! L'une des raisons les plus courantes pour lesquelles GC.Collect () n'est pas utile est l'exécution du projet en mode débogage. En mode débogage, les objets qui ne sont plus vraiment référencés ne seront pas récupérés jusqu'à la fin de la méthode.
Donc, si vous avez essayé GC.Collect () et GC.WaitForPendingFinalizers () et que cela n’a pas aidé, essayez de procéder comme suit: 

1) Essayez d’exécuter votre projet en mode Release et vérifiez si Excel est fermé correctement 

2) Enveloppez la méthode de travail avec Excel dans une méthode séparée . Donc, au lieu de quelque chose comme ceci:

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

vous écrivez:

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)
    ...
  }
}

Maintenant, Excel va fermer =)

202
nightcoder

UPDATE: Ajout du code C # et lien vers les travaux Windows

J'ai parfois essayé de résoudre ce problème et à l'époque, XtremeVBTalk était le plus actif et le plus réactif. Voici un lien vers mon message original, Fermer un processus Excel Interop proprement, même si votre application se bloque . Vous trouverez ci-dessous un résumé du message et le code copié dans ce message. 

  • La fermeture du processus d'interopérabilité avec Application.Quit() et Process.Kill() fonctionne pour la plupart, mais échoue si les applications se bloquent de manière catastrophique. C'est à dire. si l'application se bloque, le processus Excel sera toujours en cours d'exécution.
  • La solution consiste à laisser le système d'exploitation gérer le nettoyage de vos processus via Windows Job Objects à l'aide d'appels Win32. À la mort de votre application principale, les processus associés (Excel) seront également arrêtés. 

J'ai trouvé cette solution propre, car le système d'exploitation effectue un vrai travail de nettoyage. Tout ce que vous avez à faire est de enregistrer le processus Excel.

Code de travail Windows

Termine les appels de l'API Win32 pour enregistrer les processus 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);
    }

}

Note sur le code constructeur

  • Dans le constructeur, le info.LimitFlags = 0x2000; est appelé. 0x2000 est la valeur JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE enum, et cette valeur est définie par MSDN as:

Entraîne la fin de tous les processus associés au travail lorsque le fichier La dernière poignée du travail est fermée.

Extra Win32 API Call pour obtenir l'ID de processus (PID)

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

Utiliser le code

    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);
46
joshgo

Cela a fonctionné pour un projet sur lequel je travaillais:

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

Nous avons appris qu'il était important de définir la valeur every reference sur un objet COM Excel sur null lorsque vous avez terminé. Cela comprenait les cellules, les feuilles et tout.

35
Philip Fourie

Tout ce qui se trouve dans l'espace de noms Excel doit être publié. Période

Vous ne pouvez pas faire:

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

Tu dois faire

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

suivi de la libération des objets.

29
MagicKat

I trouvé un modèle générique utile pouvant aider à implémenter le modèle d'élimination correct pour les objets COM, pour lesquels Marshal.ReleaseComObject doit être appelé lorsqu'il sort du champ d'application:

Utilisation:

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

Modèle:

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

Référence:

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

18
Edward Wilde

Premièrement, vous jamais devez appeler Marshal.ReleaseComObject(...) ou Marshal.FinalReleaseComObject(...) lorsque vous utilisez Excel interop. C'est un anti-modèle déroutant, mais toute information à ce sujet, y compris celle de Microsoft, qui indique que vous devez libérer manuellement les références COM à partir de .NET est incorrecte. Le fait est que le runtime .NET et le garbage collector assurent le suivi et le nettoyage des références COM. Pour votre code, cela signifie que vous pouvez supprimer toute la boucle `while (...) en haut.

Deuxièmement, si vous voulez vous assurer que les références COM à un objet COM en dehors du processus sont nettoyées à la fin de votre processus (afin que le processus Excel se ferme), vous devez vous assurer que le ramasse-miettes s'exécute. Vous faites cela correctement avec les appels à GC.Collect() et GC.WaitForPendingFinalizers(). Le fait d'appeler deux fois est sûr et garantit que les cycles sont également nettoyés (bien que je ne sois pas sûr que cela soit nécessaire et apprécierais un exemple qui le montre).

Troisièmement, lors de l'exécution sous le débogueur, les références locales seront artificiellement maintenues en vie jusqu'à la fin de la méthode (pour que l'inspection des variables locales fonctionne). Donc, les appels GC.Collect() ne sont pas efficaces pour nettoyer un objet tel que rng.Cells de la même méthode. Vous devez scinder le code effectuant l'interopérabilité COM du nettoyage du CPG en méthodes distinctes. (Ce fut une découverte clé pour moi, à partir d'une partie de la réponse publiée ici par @nightcoder.)

Le schéma général serait donc le suivant:

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

Il y a beaucoup de fausses informations et de confusion à propos de ce problème, y compris de nombreux articles sur MSDN et sur Stack Overflow (et en particulier sur cette question!).

Ce qui m'a finalement convaincu de regarder de plus près et de trouver le bon conseil, c'est l'article de blog Marshal.ReleaseComObject considéré comme dangereux et la recherche du problème avec des références conservées sous le débogueur qui confondait mes tests antérieurs.

16
Govert

Je ne peux pas croire que ce problème hante le monde depuis 5 ans ... Si vous avez créé une application, vous devez la fermer avant de supprimer le lien.

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

à la fermeture

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

Lorsque vous créez une application Excel, un programme Excel est ouvert en arrière-plan. Vous devez commander ce programme Excel à quitter avant de libérer le lien, car ce programme Excel ne fait pas partie de votre contrôle direct. Par conséquent, il restera ouvert si le lien est libéré!

Bonne programmation à tous ~~

15
Colin

Développeurs communs, aucune de vos solutions ne fonctionnait pour moi, alors je décide de mettre en œuvre un nouveau trick .

D'abord, précisons "Quel est notre objectif?" => "Ne pas voir l'objet Excel après notre travail dans le gestionnaire de tâches" 

D'accord. Ne laissez pas le contester et commencer à le détruire, mais songez à ne pas détruire les autres instances d'Excel qui fonctionnent en parallèle.

Donc, obtenez la liste des processeurs actuels et récupérez le PID des processus Excel, puis une fois votre travail terminé, nous avons un nouvel invité dans la liste des processus avec un PID unique. Trouvez et détruisez-le.

<Gardez à l'esprit que tout nouveau processus Excel au cours de votre travail Excel sera détecté comme nouveau et détruit> <Une meilleure solution consiste à capturer le PID du nouvel objet Excel créé et à le détruire uniquement>

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

Cela résout mon problème, espérons le vôtre aussi.

12
Mohsen Afshin

Cela semble être trop compliqué. D'après mon expérience, il n'y a que trois choses clés pour bien fermer Excel:

1: assurez-vous qu'il ne reste aucune référence à l'application Excel que vous avez créée (vous ne devriez en avoir qu'une; de ​​toute façon, définissez-la sur null)

2: appelez GC.Collect()

3: Excel doit être fermé, soit en fermant manuellement le programme, soit en appelant Quit sur l'objet Excel. (Notez que Quit fonctionnera exactement comme si l'utilisateur tentait de fermer le programme et présenterait une boîte de dialogue de confirmation s'il y avait des modifications non enregistrées, même si Excel n'était pas visible. L'utilisateur pouvait appuyer sur annuler, puis Excel ne serait pas fermé .)

1 doit se produire avant 2, mais 3 peut arriver à tout moment.

Une façon d'implémenter cela consiste à envelopper l'objet Excel d'interopérabilité avec votre propre classe, à créer l'instance d'interopérabilité dans le constructeur et à implémenter IDisposable avec Dispose comme

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

Cela va nettoyer Excel du côté de votre programme. Une fois Excel fermé (manuellement par l'utilisateur ou par vous appelez Quit), le processus disparaîtra. Si le programme a déjà été fermé, le processus disparaît lors de l'appel GC.Collect().

(Je ne suis pas sûr de son importance, mais vous voudrez peut-être un appel GC.WaitForPendingFinalizers() après l'appel GC.Collect() mais il n'est pas strictement nécessaire de vous débarrasser du processus Excel.)

Cela a fonctionné pour moi sans problème pendant des années. Gardez à l'esprit que, même si cela fonctionne, vous devez en fait fermer en douceur pour que cela fonctionne. Vous continuerez à accumuler des processus Excel.exe si vous interrompez votre programme avant qu'Excel ne soit nettoyé (généralement en appuyant sur "stop" pendant que votre programme est en cours de débogage).

10
Dave Cousineau

Pour ajouter aux raisons pour lesquelles Excel ne ferme pas, même lorsque vous créez des références directes à chaque objet lors de la lecture, la création correspond à la boucle 'Pour'.

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
9
Grimfort

Après avoir essayé

  1. Libérer les objets COM dans l'ordre inverse
  2. Ajouter GC.Collect() et GC.WaitForPendingFinalizers() deux fois à la fin
  3. Pas plus de deux points
  4. Fermer le classeur et quitter l'application
  5. Exécuter en mode release

la solution finale qui fonctionne pour moi est de déplacer un ensemble de

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

que nous avons ajoutés à la fin de la fonction dans un wrapper, comme suit:

private void FunctionWrapper(string sourcePath, string targetPath)
{
    try
    {
        FunctionThatCallsExcel(sourcePath, targetPath);
    }
    finally
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}
9
D.G.

La réponse acceptée ici est correcte, mais notez également que non seulement les références "à deux points" doivent être évitées, mais également les objets récupérés via l'index. Vous n'avez pas non plus besoin d'attendre la fin du programme pour nettoyer ces objets, il est préférable de créer des fonctions qui les nettoieront dès que vous en aurez fini, lorsque cela est possible. Voici une fonction que j'ai créée qui attribue certaines propriétés d'un objet Style appelé 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's suggestion for this type of function:
    // http://support.Microsoft.com/default.aspx/kb/317109
    try
    {
        System.Runtime.InteropServices.Marshal.ReleaseComObject(obj);
    }
    catch { } 
}

Notez que je devais définir xlBorders[Excel.XlBordersIndex.xlEdgeBottom] sur une variable afin de la nettoyer (pas à cause des deux points, qui font référence à une énumération qui n'a pas besoin d'être libérée, mais parce que l'objet auquel je fais référence est en réalité une bordure objet qui doit être libéré).

Ce type de tâche n’est pas vraiment nécessaire dans les applications standard, qui font un excellent travail de nettoyage, mais dans les applications ASP.NET, si vous en oubliez un, quelle que soit la fréquence à laquelle vous appelez le ramasse-miettes, Excel toujours en cours d'exécution sur votre serveur. 

Il faut beaucoup d’attention aux détails et de nombreuses exécutions de test lors de la surveillance du Gestionnaire des tâches lors de l’écriture de ce code, mais vous évitez ainsi de chercher désespérément des pages de code pour rechercher l’instance manquante. Ceci est particulièrement important lorsque vous travaillez dans des boucles, où vous devez libérer CHAQUE INSTANCE d'un objet, même s'il utilise le même nom de variable à chaque boucle.

9
Chris McGrath

J'ai toujours suivi les conseils donnés dans La réponse de VVS . Cependant, afin de maintenir cette réponse à jour avec les dernières options, je pense que tous mes projets futurs utiliseront la bibliothèque "NetOffice".

NetOffice est un remplacement complet des PIA Office et est totalement indépendant de la version. C'est une collection de wrappers COM gérés pouvant gérer le nettoyage qui provoque souvent de tels maux de tête lors de l'utilisation de Microsoft Office dans .NET.

Certaines caractéristiques clés sont:

  • Généralement indépendant de la version (les fonctionnalités dépendantes de la version sont documentées)
  • Pas de dépendances
  • Pas de PIA
  • Pas d'inscription
  • Pas de VSTO

Je ne suis aucunement affilié au projet. J'apprécie sincèrement la réduction drastique des maux de tête.

8
BTownTKD

J'ai suivi cela exactement ... Mais j'ai quand même rencontré des problèmes 1 fois sur 1000. Qui sait pourquoi Il est temps de sortir le marteau ...

Juste après l'instanciation de la classe d'application Excel, je récupère le processus Excel qui vient d'être créé.

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

Ensuite, une fois que j'ai effectué tous les nettoyages COM ci-dessus, je m'assure que ce processus ne fonctionne pas. S'il est toujours en cours d'exécution, tuez-le!

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

¨ ° º¤ø „¸ Shoot Excel proc et mâcher du chewing-gum„ ø¤º ° ¨

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);
}
7
Antoine Meltzheim

"Ne jamais utiliser deux points avec des objets COM" est une bonne règle empirique pour éviter les fuites de références COM, mais Excel PIA peut provoquer des fuites de manière plus qu'apparente à première vue.

L'une de ces méthodes consiste à s'abonner à tout événement exposé par l'un des objets COM du modèle objet Excel.

Par exemple, vous abonner à l'événement WorkbookOpen de la classe Application.

Un peu de théorie sur les événements COM

Les classes COM exposent un groupe d'événements via des interfaces de rappel. Pour s'abonner à des événements, le code client peut simplement enregistrer un objet implémentant l'interface de rappel et la classe COM appelle ses méthodes en réponse à des événements spécifiques. Etant donné que l'interface de rappel est une interface COM, il est du devoir de l'objet d'implémentation de décrémenter le compte de références de tout objet COM qu'il reçoit (en tant que paramètre) pour n'importe lequel des gestionnaires d'événements.

Comment Excel PIA expose les événements COM

PIA Excel expose les événements COM de la classe d’application Excel en tant qu’événements .NET classiques. Chaque fois que le code client s'abonne à un événement .NET (accent mis sur 'a'), PIA crée l'instance an d'une classe implémentant l'interface de rappel et l'enregistre avec Excel.

Par conséquent, un certain nombre d'objets de rappel sont enregistrés avec Excel en réponse à différentes demandes d'abonnement du code .NET. Un objet de rappel par abonnement à un événement.

Une interface de rappel pour la gestion des événements signifie que PIA doit s'abonner à tous les événements d'interface pour chaque demande d'abonnement aux événements .NET. Il ne peut pas choisir et choisir. Lors de la réception d'un rappel d'événement, l'objet de rappel vérifie si le gestionnaire d'événements .NET associé est intéressé par l'événement en cours ou non, puis appelle le gestionnaire ou ignore silencieusement le rappel.

Effet sur le nombre de références d'instances COM

Tous ces objets de rappel ne décrémentent pas le compte de référence des objets COM qu'ils reçoivent (en tant que paramètres) pour les méthodes de rappel (même pour ceux qui sont ignorés en mode silencieux). Ils s'appuient uniquement sur le CLR garbage collector pour libérer les objets COM.

Étant donné que l'exécution du CPG n'est pas déterministe, cela peut entraîner le blocage du processus Excel pendant une durée plus longue que celle souhaitée et créer une impression de "fuite de mémoire".

Solution

Pour l’instant, la seule solution consiste à éviter le fournisseur d’événements de la PIA pour la classe COM et à écrire votre propre fournisseur d’événements qui libère de manière déterministe les objets COM.

Pour la classe Application, cela peut être fait en implémentant l'interface AppEvents puis en enregistrant l'implémentation avec Excel à l'aide de IConnectionPointContainer interface . La classe Application (ainsi que tous les objets COM exposant des événements à l'aide du mécanisme de rappel) implémente l'interface IConnectionPointContainer.

6
Amit Mittal

Vous devez également savoir qu'Excel est très sensible à la culture dans laquelle vous évoluez.

Vous constaterez peut-être que vous devez définir la culture sur EN-US avant d'appeler des fonctions Excel ..__ Cela ne s'applique pas à toutes les fonctions, mais à certaines d'entre elles.

    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;

Ceci s’applique même si vous utilisez VSTO.

Pour plus de détails: http://support.Microsoft.com/default.aspx?scid=kb;en-us;Q320369

6

Comme d'autres l'ont souligné, vous devez créer une référence explicite pour chaque objet Excel utilisé et appeler Marshal.ReleaseComObject sur cette référence, comme décrit dans cet article de la base de connaissances . Vous devez également utiliser try/finally pour vous assurer que ReleaseComObject est toujours appelé, même lorsqu'une exception est levée. C'est à dire. au lieu de:

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

vous devez faire quelque chose comme:

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

Vous devez également appeler Application.Quit avant de libérer l'objet Application si vous souhaitez que Excel soit fermé.

Comme vous pouvez le constater, cela devient rapidement extrêmement difficile à manier dès que vous essayez de faire quelque chose, même moyennement complexe. J'ai développé avec succès des applications .NET avec une simple classe wrapper qui encapsule quelques manipulations simples du modèle objet Excel (ouvrir un classeur, écrire dans une plage, enregistrer/fermer le classeur, etc.). La classe wrapper implémente IDisposable, implémente avec précaution Marshal.ReleaseComObject sur chaque objet qu’elle utilise et n’expose publiquement aucun objet Excel au reste de l’application.

Mais cette approche ne s'adapte pas bien aux exigences plus complexes. 

C'est un gros défaut de .NET COM Interop. Pour des scénarios plus complexes, j’envisagerais sérieusement d’écrire un ActiveX DLL en VB6 ou dans un autre langage non géré auquel vous pouvez déléguer toutes les interactions avec des objets COM hors processus, tels que Office. Vous pouvez ensuite référencer cet ActiveX DLL à partir de votre application .NET, et les choses seront beaucoup plus simples car il vous suffira de publier cette référence.

5
Joe

Lorsque tout ce qui précède ne fonctionne pas, essayez de donner à Excel un peu de temps pour fermer ses feuilles:

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

...
FinalReleaseComObject(app);
4
spiderman

Assurez-vous que vous libérez tous les objets liés à Excel!

J'ai passé quelques heures à essayer de plusieurs façons. Toutes les idées sont bonnes mais j'ai finalement trouvé mon erreur: Si vous ne libérez pas tous les objets, aucun des moyens ci-dessus ne peut vous aider comme dans mon cas. Assurez-vous de libérer tous les objets, y compris le premier.

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

Les options sont ensemble ici .

3
Ned

Faites très attention lorsque vous utilisez des applications d'interopérabilité Word/Excel. Après avoir essayé toutes les solutions, il restait encore beaucoup de processus "WinWord" sur le serveur (avec plus de 2000 utilisateurs).

Après avoir travaillé sur le problème pendant des heures, j'ai compris que si j'ouvrais plusieurs documents simultanément à l'aide de Word.ApplicationClass.Document.Open() sur différents threads, le processus de travail IIS (w3wp.exe) se bloquait, laissant tous les processus WinWord ouverts!

Je suppose donc qu’il n’existe pas de solution absolue à ce problème, mais que nous passions à d’autres méthodes telles que Office Open XML development.

2
Arvand

Un bon article sur la libération des objets COM est 2.5 Libération des objets COM (MSDN).

La méthode que je préconise consiste à annuler vos références Excel.Interop s’il s’agit de variables non locales, puis à appeler deux fois les fonctions GC.Collect() et GC.WaitForPendingFinalizers(). Les variables Interop à portée locale seront traitées automatiquement.

Cela évite de conserver une référence nommée pour chaque objet COM.

Voici un exemple tiré de l'article:

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

Ces mots sont directement tirés de l'article:

Dans presque toutes les situations, l'annulation de la référence RCW et le forçage d'une récupération de place nettoient correctement. Si vous appelez également GC.WaitForPendingFinalizers, le garbage collection sera aussi déterministe que vous pourrez le faire. En d'autres termes, vous serez à peu près sûr du moment exact où l'objet aura été nettoyé, au retour du deuxième appel à WaitForPendingFinalizers. Vous pouvez également utiliser Marshal.ReleaseComObject. Cependant, notez qu'il est très peu probable que vous utilisiez cette méthode.

2
Porkbutts

La règle des deux points ne fonctionnait pas pour moi. Dans mon cas, j'ai créé une méthode pour nettoyer mes ressources comme suit:

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

Je travaille actuellement sur la bureautique et je suis tombé sur une solution qui fonctionne à chaque fois pour moi. C'est simple et n'implique pas de tuer aucun processus.

Il semble qu'en ne parcourant que les processus actifs actuels et en "accédant" de quelque manière que ce soit à un processus Excel ouvert, toute instance suspendue et suspendue d'Excel sera supprimée. Le code ci-dessous recherche simplement les processus dont le nom est 'Excel', puis écrit la propriété MainWindowTitle du processus dans une chaîne. Cette "interaction" avec le processus semble amener Windows à rattraper son retard et à abandonner l'instance gelée d'Excel. 

J'exécute la méthode ci-dessous juste avant le complément que je développe est arrêté, car il déclenche l'événement de déchargement. Il supprime toutes les instances suspendues d'Excel à chaque fois. En toute honnêteté, je ne suis pas tout à fait sûr pourquoi cela fonctionne, mais cela fonctionne bien pour moi et pourrait être placé à la fin de n'importe quelle application Excel sans avoir à vous soucier des points doubles, Marshal.ReleaseComObject, ni des processus de mise à mort. Je serais très intéressé par toute suggestion quant à la raison pour laquelle cela est efficace.

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
Tom Brearley

Je pense que c'est en partie la manière dont le framework gère les applications Office, mais je peux me tromper. Certains jours, certaines applications nettoient les processus immédiatement et d'autres jours, il semble attendre que l'application se ferme. En général, j'ai arrêté de prêter attention aux détails et je me suis assuré qu'il ne reste aucun processus supplémentaire à la fin de la journée.

De plus, je simplifie peut-être les choses, mais je pense que vous pouvez simplement ...

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

Comme je l'ai dit plus tôt, je n'ai pas tendance à faire attention aux détails du moment où le processus Excel apparaît ou disparaît, mais cela fonctionne généralement pour moi. De plus, je n'aime pas garder les processus Excel plus longtemps, mais je suis probablement paranoïaque à ce sujet.

1
bill_the_loser

«Cela semble être comme si c'était trop compliqué. D'après mon expérience, il n'y a que trois choses clés pour bien fermer Excel:

1: assurez-vous qu'il ne reste aucune référence à l'application Excel que vous avez créée (vous ne devriez en avoir qu'une; de ​​toute façon, définissez-la sur null)

2: appelez GC.Collect ()

3: Excel doit être fermé, soit en fermant manuellement le programme, soit en appelant Quit sur l'objet Excel. (Notez que Quit fonctionne comme si l'utilisateur essayait de fermer le programme et présentait une boîte de dialogue de confirmation s'il y avait des modifications non enregistrées, même si Excel n'était pas visible. L'utilisateur pouvait appuyer sur Annuler, puis Excel ne serait pas fermé. .)

1 doit se produire avant 2, mais 3 peut arriver à tout moment.

Une façon d'implémenter cela consiste à envelopper l'objet Excel d'interopérabilité avec votre propre classe, à créer l'instance d'interopérabilité dans le constructeur et à implémenter IDisposable avec Dispose comme

Cela va nettoyer Excel du côté de votre programme. Une fois Excel fermé (manuellement par l'utilisateur ou par votre appel à Quit), le processus disparaîtra. Si le programme a déjà été fermé, le processus disparaîtra lors de l'appel de GC.Collect ().

(Je ne sais pas à quel point c'est important, mais vous voudrez peut-être un appel à GC.WaitForPendingFinalizers () après l'appel de GC.Collect () mais il n'est pas strictement nécessaire de supprimer le processus Excel.)

Cela a fonctionné pour moi sans problème pendant des années. Gardez à l'esprit que, même si cela fonctionne, vous devez en fait fermer en douceur pour que cela fonctionne. Vous continuerez à accumuler des processus Excel.exe si vous interrompez votre programme avant qu'Excel ne soit nettoyé (généralement en appuyant sur "stop" pendant que le programme est en cours de débogage). '

La réponse acceptée n'a pas fonctionné pour moi. Le code suivant dans le destructeur a fait le travail.

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
Martin

Utilisation:

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

Déclarez-le, ajoutez du code dans le bloc 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
Shivam Srivastava

Ma solution

[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
Loart

Comme certains l’ont probablement déjà écrit, la manière dont vous fermez Excel (objet) n’est pas seulement importante; il est également important de savoir comment vous ouvrez et le type de projet.

Dans une application WPF, le même code fonctionne sans problème ou avec très peu de problèmes.

J'ai un projet dans lequel le même fichier Excel est traité plusieurs fois pour une valeur de paramètre différente - par exemple. l'analyse syntaxique en fonction des valeurs d'une liste générique. 

J'ai mis toutes les fonctions liées à Excel dans la classe de base et l'analyseur dans une sous-classe (différents analyseurs utilisent des fonctions Excel communes). Je ne voulais pas qu'Excel soit ouvert et refermé pour chaque élément d'une liste générique. Je ne l'ai donc ouvert qu'une fois dans la classe de base et je l'ai fermé dans la sous-classe. J'ai eu des problèmes pour transférer le code dans une application de bureau. J'ai essayé plusieurs des solutions mentionnées ci-dessus. GC.Collect() a déjà été implémenté auparavant, deux fois comme suggéré.

Ensuite, j'ai décidé de déplacer le code d'ouverture d'Excel dans une sous-classe. Au lieu de n'ouvrir qu'une seule fois, je crée maintenant un nouvel objet (classe de base), ouvre Excel pour chaque élément et le ferme à la fin. Les performances ont été pénalisées, mais sur la base de plusieurs tests, les processus Excel se ferment sans problème (en mode débogage). Les fichiers temporaires sont donc également supprimés. Je vais continuer à tester et à en écrire d'autres si j'obtiens des mises à jour.

La ligne du bas est: Vous devez également vérifier le code d'initialisation, surtout si vous avez plusieurs classes, etc.

1
Blaz Brencic

Là j'ai une idée, essayez de tuer le processus Excel que vous avez ouvert:

  1. avant d'ouvrir une application excel, obtenez tous les identifiants de processus nommés oldProcessIds.
  2. ouvrez l'application excel.
  3. obtenez maintenant tous les identifiants de processus excelapplication nommés nowProcessIds.
  4. lorsque vous avez besoin de quitter, supprimez les ID sauf entre oldProcessIds et 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
Aloha

Juste pour ajouter une autre solution parmi les nombreuses énumérées ici, en utilisant l’automatisation C++/ATL (j'imagine que vous pourriez utiliser quelque chose de similaire à partir de VB/C # ??)

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

Cela fonctionne comme un charme pour moi ...

0
dicksters

J'ai eu le même problème lors de la fermeture de PowerPoint après la création de l'objet Application dans mon complément VSTO. J'ai essayé toutes les réponses ici avec un succès limité.

C’est la solution que j’ai trouvée pour mon cas - NE PAS utiliser 'nouvelle application', la classe de base AddInBase de ThisAddIn a déjà un identifiant pour 'Application'. Si vous utilisez cette poignée là où vous en avez besoin (rendez-la statique le cas échéant), vous n'avez pas à vous soucier de la nettoyer et PowerPoint ne se fermera pas.

0
swax

Voici un moyen très simple de le faire:

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

int objExcelProcessId = 0;

Excel.Application objExcel = new Excel.Application();

GetWindowThreadProcessId(new IntPtr(objExcel.Hwnd), out objExcelProcessId);

Process.GetProcessById(objExcelProcessId).Kill();
0
tjarrett

Testé avec Microsoft Excel 2016

Une solution vraiment testée.

Pour référence C # s'il vous plaît voir: https://stackoverflow.com/a/1307180/10442623

Pour les références VB.net, veuillez consulter: https://stackoverflow.com/a/54044646/10442623

1 inclure le travail de classe

2 implémente la classe pour gérer le traitement approprié des processus Excel

0
SamSar

Jusqu'à présent, il semble que toutes les réponses impliquent certaines de ces réponses:

  1. Tuez le processus
  2. Utilisez GC.Collect ()
  3. Gardez une trace de chaque objet COM et libérez-le correctement.

Ce qui me fait comprendre à quel point cette question est difficile :)

J'ai travaillé sur une bibliothèque pour simplifier l'accès à Excel et j'essaie de faire en sorte que les utilisateurs ne l'utilisent pas (les doigts croisés).

Au lieu d'écrire directement sur les interfaces fournies par Interop, je simplifie l'utilisation des méthodes d'extension. Comme pour ApplicationHelpers.CreateExcel () ou classeur.CreateWorksheet ("mySheetNameThatWillBeValidated"). Naturellement, tout ce qui est créé peut poser problème plus tard, alors je suis en faveur de l'abandon du processus en dernier recours. Pourtant, nettoyer correctement (troisième option) est probablement le moins destructeur et le plus contrôlé.

Donc, dans ce contexte, je me demandais s'il ne serait pas préférable de faire quelque chose comme ceci:

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

J'ai utilisé 'Releasible' pour éviter toute confusion avec Disposable. Étendre cela à IDisposable devrait être facile cependant.

Une implémentation comme celle-ci:

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

Et on pourrait faire quelque chose de similaire pour toutes sortes d'objets COM.

Dans la méthode d'usine:

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

        return Excel;
    }

Je m'attendrais à ce que chaque conteneur soit détruit correctement par le GC, et appelle donc automatiquement Quit et Marshal.FinalReleaseComObject.

Commentaires? Ou est-ce une réponse à la question du troisième type?

0
akirakonenu