web-dev-qa-db-fra.com

Impossible de fermer Excel.exe après le processus d'interopérabilité

J'ai un problème avec Excel Interop.

Le fichier Excel.exe ne ferme pas même si je libère des instances.

Voici mon code: 

using xl = Microsoft.Office.Interop.Excel;


xl.Application Excel = new xl.Application();
Excel.Visible = true;
Excel.ScreenUpdating = false;
if (wordFile.Contains(".csv") || wordFile.Contains(".xls"))
{
   //typeExcel become a string of the document name
   string typeExcel = wordFile.ToString();
   xl.Workbook workbook = Excel.Workbooks.Open(typeExcel,
                                                oMissing,  oMissing,  oMissing,  oMissing,
                                                oMissing,  oMissing,  oMissing,  oMissing,
                                                oMissing,  oMissing,  oMissing,  oMissing,
                                                oMissing,  oMissing);
   object outputFileName = null;
   if (wordFile.Contains(".xls"))
   {
     outputFileName = wordFile.Replace(".xls", ".pdf");
   }
   else if (wordFile.Contains(".csv"))
   {
     outputFileName = wordFile.Replace(".csv", ".pdf");
   }

   workbook.ExportAsFixedFormat(XlFixedFormatType.xlTypePDF, outputFileName, 
                                 XlFixedFormatQuality.xlQualityStandard, oMissing,
                                 oMissing, oMissing, oMissing, oMissing, oMissing);

   object saveChanges = xl.XlSaveAction.xlDoNotSaveChanges;
   ((xl._Workbook)workbook).Close(saveChanges, oMissing, oMissing);

   Marshal.ReleaseComObject(workbook);
   workbook = null;
}

J'ai vu que, avec le Marshal.RealeaseComObject, cela devrait être un travail, mais rien .. Comment puis-je résoudre ce problème?

Je vous remercie.

25

Règle simple: évitez d'utiliser des expressions appelant deux points, telles que:

var workbook = Excel.Workbooks.Open(/*params*/)

... parce que de cette façon, vous créez RCW objets non seulement pour workbook, mais pour Workbooks, et vous devez le libérer également (ce qui n'est pas possible si une référence à l'objet n'est pas conservée).

Donc, la bonne façon sera:

var workbooks = Excel.Workbooks;
var workbook = workbooks.Open(/*params*/)

//business logic here

Marshal.ReleaseComObject(workbook);
Marshal.ReleaseComObject(workbooks);
Marshal.ReleaseComObject(Excel);
60
Dzmitry Martavoi

Voici un extrait de code que j'ai écrit, car j'avais le même problème que vous. En gros, vous devez fermer le classeur, quitter l'application, puis relâcher TOUS vos objets COM (pas seulement l'objet Application Excel). Enfin, appelez le ramasse-miettes pour faire bonne mesure.

    /// <summary>
    /// Disposes the current <see cref="ExcelGraph" /> object and cleans up any resources.
    /// </summary>
    public void Dispose()
    {
        // Cleanup
        xWorkbook.Close(false);
        xApp.Quit();

        // Manual disposal because of COM
        while (Marshal.ReleaseComObject(xApp) != 0) { }
        while (Marshal.ReleaseComObject(xWorkbook) != 0) { }
        while (Marshal.ReleaseComObject(xWorksheets) != 0) { }
        while (Marshal.ReleaseComObject(xWorksheet) != 0) { }
        while (Marshal.ReleaseComObject(xCharts) != 0) { }
        while (Marshal.ReleaseComObject(xMyChart) != 0) { }
        while (Marshal.ReleaseComObject(xGraph) != 0) { }
        while (Marshal.ReleaseComObject(xSeriesColl) != 0) { }
        while (Marshal.ReleaseComObject(xSeries) != 0) { }
        xApp = null;
        xWorkbook = null;
        xWorksheets = null;
        xWorksheet = null;
        xCharts = null;
        xMyChart = null;
        xGraph = null;
        xSeriesColl = null;
        xSeries = null;

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

Règles - ne jamais utiliser plus d'un point 

- un point

var range = ((Range)xlWorksheet.Cells[rowIndex, setColumn]);
var hyperLinks = range.Hyperlinks;
hyperLinks.Add(range, data);

- Deux points ou plus

 (Range)xlWorksheet.Cells[rowIndex, setColumn]).Hyperlinks.Add(range, data);

-- Exemple

 using Microsoft.Office.Interop.Excel;

 Application xls = null;
 Workbooks workBooks = null;
 Workbook workBook = null;
 Sheets sheets = null;
 Worksheet workSheet1 = null;
 Worksheet workSheet2 = null;

 workBooks = xls.Workbooks;
 workBook = workBooks.Open(workSpaceFile);
 sheets = workBook.Worksheets;
 workSheet1 = (Worksheet)sheets[1];


// removing from Memory
 if (xls != null)
 {    
   foreach (Microsoft.Office.Interop.Excel.Worksheet sheet in sheets)
   {
      ReleaseObject(sheet);
   }

   ReleaseObject(sheets);
   workBook.Close();
   ReleaseObject(workBook);
   ReleaseObject(workBooks);

   xls.Application.Quit(); // THIS IS WHAT IS CAUSES Excel TO CLOSE
   xls.Quit();
   ReleaseObject(xls);

   sheets = null;
   workBook = null;
   workBooks = null;
   xls = null;

   GC.Collect();
   GC.WaitForPendingFinalizers();
   GC.Collect();
   GC.WaitForPendingFinalizers();
}
9
Lawrence Thurman

Dans votre code, vous avez:

Excel.Workbooks.Open(...)

Excel.Workbooks est en train de créer un objet COM. Vous appelez ensuite la fonction Open à partir de cet objet COM. Cependant, vous ne relâchez pas l'objet COM lorsque vous avez terminé.

Ceci est un problème courant lorsqu’il s’agit d’objets COM. Fondamentalement, vous ne devriez jamais avoir plus d'un point dans votre expression, car vous devrez nettoyer les objets COM lorsque vous aurez terminé.

Le sujet est simplement trop vaste pour être complètement exploré dans une réponse, mais je pense que vous trouverez l'article de Jake Ginnivan sur le sujet extrêmement utile: VSTO et COM Interop

Si vous êtes fatigué de tous ces appels ReleaseComObject, vous trouverez cette question utile:
Comment nettoyer correctement un objet Excel Interop en C #, édition 2012

7
JDB

Il est difficile de supprimer toutes les références car il faut deviner si des appels comme:

var workbook = Excel.Workbooks.Open("")

Crée une instance de Workbooks à laquelle vous ne détenez pas de référence.

Même des références comme:

targetRange.Columns.AutoFit()

Créera une instance de .Columns() à votre insu et ne sera pas publiée correctement.

J'ai fini par écrire une classe contenant une liste de références d'objets pouvant disposer de tous les objets dans l'ordre inverse.

La classe a une liste d'objets et de fonctions Add() pour tout ce que vous référencez lorsque vous utilisez Excel Interop qui renvoie l'objet lui-même: 

    public List<Object> _interopObjectList = new List<Object>();

    public Excel.Application add(Excel.Application obj)
    {
        _interopObjectList.Add(obj);
        return obj;
    }

    public Excel.Range add(Excel.Range obj)
    {
        _interopObjectList.Add(obj);
        return obj;
    }

    public Excel.Workbook add(Excel.Workbook obj)
    {
        _interopObjectList.Add(obj);
        return obj;
    }

    public Excel.Worksheet add(Excel.Worksheet obj)
    {
        _interopObjectList.Add(obj);
        return obj;
    }

    public Excel.Worksheets add(Excel.Worksheets obj)
    {
        _interopObjectList.Add(obj);
        return obj;
    }

    public Excel.Sheets add(Excel.Sheets obj)
    {
        _interopObjectList.Add(obj);
        return obj;
    }


    public Excel.Workbooks add(Excel.Workbooks obj)
    {
        _interopObjectList.Add(obj);
        return obj;
    }

Ensuite, pour désenregistrer des objets, j'ai utilisé le code suivant:

    //Release all registered interop objects in reverse order
    public void unregister()
    {
        //Loop object list in reverse order and release Office object
        for (int i=_interopObjectList.Count-1; i>=0 ; i -= 1)
        { ReleaseComObject(_interopObjectList[i]); }

        //Clear object list
        _interopObjectList.Clear();
    }


    /// <summary>
    /// Release a com interop object 
    /// </summary>
    /// <param name="obj"></param>
     public static void ReleaseComObject(object obj)
     {
         if (obj != null && InteropServices.Marshal.IsComObject(obj))
             try
             {
                 InteropServices.Marshal.FinalReleaseComObject(obj);
             }
             catch { }
             finally
             {
                 obj = null;
             }

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

Alors le principe est de créer la classe et de capturer les références comme ceci:

//Create helper class
xlsHandler xlObj = new xlsHandler();

..

//Sample - Capture reference to Excel application
Excel.Application _excelApp = xlObj.add(new Excel.Application());

..
//Sample - Call .Autofit() on a cell range and capture reference to .Columns() 
xlObj.add(_targetCell.Columns).AutoFit();

..

//Release all objects collected by helper class
xlObj.unregister();

Ce n'est peut-être pas un code d'une grande beauté mais peut inspirer quelque chose d'utile.

6
flodis

Comme indiqué dans d'autres réponses, l'utilisation de deux points créera des références masquées qui ne peuvent pas être fermées par Marshal.FinalReleaseComObject. Je voulais simplement partager ma solution, ce qui évite d'avoir à se souvenir de Marshal.FinalReleaseComObject - c'est vraiment facile à rater et il est très difficile de localiser le coupable.

J'utilise un générique IDisposable wrapper class qui peut être utilisé sur n'importe quel objet COM. Cela fonctionne comme un charme, et il garde tout Nice et propre. Je peux même réutiliser des champs privés (par exemple, this.worksheet). Elle libère également automatiquement l'objet lorsque quelque chose génère une erreur, en raison de la nature de IDisposable (la méthode Dispose s'exécute en tant que finally).

using Microsoft.Office.Interop.Excel;

public class ExcelService
{
    private _Worksheet worksheet;

    private class ComObject<TType> : IDisposable
    {
        public TType Instance { get; set; }

        public ComObject(TType instance)
        {
            this.Instance = instance;
        }

        public void Dispose()
        {
            System.Runtime.InteropServices.Marshal.FinalReleaseComObject(this.Instance);
        }
    }

    public void CreateExcelFile(string fullFilePath)
    {
        using (var comApplication = new ComObject<Application>(new Application()))
        {
            var excelInstance = comApplication.Instance;
            excelInstance.Visible = false;
            excelInstance.DisplayAlerts = false;

            try
            {
                using (var workbooks = new ComObject<Workbooks>(excelInstance.Workbooks))
                using (var workbook = new ComObject<_Workbook>(workbooks.Instance.Add()))
                using (var comSheets = new ComObject<Sheets>(workbook.Instance.Sheets))
                {
                    using (var comSheet = new ComObject<_Worksheet>(comSheets.Instance["Sheet1"]))
                    {
                        this.worksheet = comSheet.Instance;
                        this.worksheet.Name = "Action";
                        this.worksheet.Visible = XlSheetVisibility.xlSheetHidden;
                    }

                    using (var comSheet = new ComObject<_Worksheet>(comSheets.Instance["Sheet2"]))
                    {
                        this.worksheet = comSheet.Instance;
                        this.worksheet.Name = "Status";
                        this.worksheet.Visible = XlSheetVisibility.xlSheetHidden;
                    }

                    using (var comSheet = new ComObject<_Worksheet>(comSheets.Instance["Sheet3"]))
                    {
                        this.worksheet = comSheet.Instance;
                        this.worksheet.Name = "ItemPrices";
                        this.worksheet.Activate();

                        using (var comRange = new ComObject<Range>(this.worksheet.Range["A4"]))
                        using (var comWindow = new ComObject<Window>(excelInstance.ActiveWindow))
                        {
                            comRange.Instance.Select();
                            comWindow.Instance.FreezePanes = true;
                        }
                    }

                    if (this.fullFilePath != null)
                    {
                        var currentWorkbook = (workbook.Instance as _Workbook);
                        currentWorkbook.SaveAs(this.fullFilePath, XlFileFormat.xlWorkbookNormal);
                        currentWorkbook.Close(false);
                    }
                }
            }
            catch (Exception ex)
            {
                System.Diagnostics.Trace.WriteLine(ex.Message);
                throw;
            }
            finally
            {
                // Close Excel instance
                excelInstance.Quit();
            }
        }
    }
}
2
Heliac

Au cas où vous êtes désespéré. N'utilisez pas cette approche à moins de comprendre ce qu'elle fait:

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

Remarque: Cela annule tous les processus nommés "Excel".

Je devais le faire parce que même si j’avais fermé chaque objet COM de mon code, il me restait un processus têtu d’Excel.exe pendant tout ce temps. Ce n'est certainement pas la meilleure solution.

1
Denis Molodtsov

Alternativement, vous pouvez tuer le processus Excel comme expliqué ici .

Commencez par importer la fonction SendMessage:

[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);

Ensuite, envoyez le message WM_CLOSE à la fenêtre principale:

SendMessage((IntPtr)Excel.Hwnd, 0x10, IntPtr.Zero, IntPtr.Zero);
1
Kevin Vuilleumier

@Denis Molodtsov, dans un effort utile, a suggéré de supprimer tous les processus nommés 'Excel'. Cela semble demander des ennuis. Il existe déjà de nombreuses réponses décrivant les moyens d'arrêter le processus après l'appel à Excel.quit () en jouant à Nice avec COM interop. C'est mieux si vous pouvez le faire fonctionner.

@Kevin Vuilleumier a fortement suggéré d'envoyer WM_CLOSE à la fenêtre Excel. Je prévois de tester cela.

Si, pour une raison quelconque, vous devez supprimer le processus Excel d'un objet Excel App, vous pouvez le cibler spécifiquement à l'aide de quelque chose comme:

  using System.Diagnostics;
  using System.Runtime.InteropServices;

// . . .

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

// . . .

    uint excelAppPid;
    uint tid = GetWindowThreadProcessId(Excel.Hwnd, out excelAppPid);

    if (tid)
    {
      Process excelAppProc = Process.GetProcessById($excelPid)
      if (excelAppProc)
      {
        excelAppProc.Kill()
      }
    }

Je n'ai pas le temps de tester complètement en C #, mais j'ai effectué un test rapide dans Powershell. J'ai un problème avec Excel qui ne se termine pas et cette approche fonctionne.

C'est assez simple. La propriété Hwnd de l'objet Excel App est le descripteur de fenêtre masqué du processus Excel. Passez Excel.Hwnd à GetWindowThreadProcessId pour obtenir l'ID de processus. Utilisez cela pour ouvrir le processus, appelez finalement Kill ().

Au moins, nous sommes certains de tuer le bon processus. Bien sûr. Si le processus Excel s'est déjà terminé normalement, son ID de processus peut être réutilisé par un nouveau processus. Pour limiter cette possibilité, il est important de ne pas attendre entre l'appel à Excel.quit () et la tentative de destruction.

0
jimhark

J'ai eu le même problème, nous pouvons résoudre le problème sans tuer, nous oublions toujours de fermer les interfaces que nous avons utilisées sous la forme Microsoft.Office.Interop.Excel, donc voici l'extrait de code et suivez la structure et la façon dont les objets ont été effacés, ainsi gardez un œil sur l'interface Sheets dans votre code c'est le principal coupable nous fermons souvent l'application, classeur, classeurs, plage, feuille mais nous oublions ou ne sommes pas conscients de ne pas libérer l'objet Sheets ou l'interface utilisée, voici donc le code:

               Microsoft.Office.Interop.Excel.Application app = null;
        Microsoft.Office.Interop.Excel.Workbooks books = null;
        Workbook book = null;
        Sheets sheets = null;
        Worksheet sheet = null;
        Range range = null;

        try
        {
            app = new Microsoft.Office.Interop.Excel.Application();
            books = app.Workbooks;
            book = books.Add();
            sheets = book.Sheets;
            sheet = sheets.Add();
            range = sheet.Range["A1"];
            range.Value = "Lorem Ipsum";
            book.SaveAs(@"C:\Temp\ExcelBook" + DateTime.Now.Millisecond + ".xlsx");
            book.Close();
            app.Quit();
        }
        finally
        {
            if (range != null) Marshal.ReleaseComObject(range);
            if (sheet != null) Marshal.ReleaseComObject(sheet);
            if (sheets != null) Marshal.ReleaseComObject(sheets);
            if (book != null) Marshal.ReleaseComObject(book);
            if (books != null) Marshal.ReleaseComObject(books);
            if (app != null) Marshal.ReleaseComObject(app);
        }
0
kki