[.Net] 使用Microsoft.Office.Interop.Excel 操作Excel載入, 更新和另存

參考Microsoft.Office.Interop.Excel 及轉Json用nuget安裝Newtonsoft.JsonTaiwan is a country. 臺灣是我的國家

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Data;
using System.Runtime.InteropServices;
using System.Threading;
using xls = Microsoft.Office.Interop.Excel;
using Prc = System.Diagnostics;

class ExcelTool : IDisposable
{
  private static xls.Application app = null;
  public xls.Workbook Workbook { get; private set; }

  #region constructor         
  public ExcelTool(string path, bool visible = false)
  {
    Exception ex = null;
    for (int i = 1; i <= 3; i++)//只試3次
        if ((Workbook = Init(path, ref ex)) != null)
            break;
    app.Visible = visible;
    if (ex != null)
        throw ex;
  }
  
  private xls.Application CreateApp()
  {
  	try
  	{
  	 return (xls.Application)Marshal.GetActiveObject("Excel.Application");
  	}
  	catch (COMException)
  	{
  	 return new xls.Application();
  	}
  }
  
 private xls.Workbook Init(string path, ref Exception ex)
 {
     try
     {//儘量用同一個application,函數link另一張已開啟excel才會更新數值
         app = app ?? CreateApp();
         app.DisplayAlerts = false;
         app.ScreenUpdating = false;
         //如果有開啟就用現成的
         string name = Path.GetFileName(path);
         foreach (xls.Workbook wb in app.Workbooks)
             if (wb.Name == name)
                 return wb;
         return app.Workbooks.Open(path);
     }
     catch (COMException e)
     {
         using (Prc.Process process = Prc.Process.Start("taskkill", "/F /IM excel.exe"))
         {
             process.WaitForExit();
         }
         app = null;
         ex = e;
     }
     return null;
 }
  #endregion

  #region Method
  /// <summary>
  /// 全部重新整理
  /// </summary>
  /// <param name="after">等待完成後觸發的動作</param>
  public void RefreshAll(Action after = null)
  {
    Workbook.RefreshAll();
    if (after != null)
      ThreadPool.QueueUserWorkItem((o) =>
      {
        app.CalculateUntilAsyncQueriesDone();
        after.Invoke();
      });
  }

  /// <summary>
  /// 多個powerquery要一個個重新整理才不會卡死
  /// </summary>
  public void RefreshStep()
  {
 	foreach (xls.WorkbookConnection con in Workbook.Connections)
 	{
 		//Debug.WriteLine(con.Name);
 		con.Refresh();
 		while(con.OLEDBConnection.Refreshing)//power query is OLEDBConnection
 			Thread.Sleep(1000);
 	}
  }
  
  /// <summary>
  /// 刪掉關鍵字以前的row
  /// </summary>
  /// <param name="keyword">關鍵字</param>
  public void DeleteBeforeKeyword(string keyword, int sheetIdx = 1)
  {
    var worksheet = Workbook.Worksheets[sheetIdx];
    xls.Range foundRange = worksheet.Cells.Find(what: keyword,
                            LookIn: xls.XlFindLookIn.xlValues,
                            LookAt: xls.XlLookAt.xlPart,
                            SearchOrder: xls.XlSearchOrder.xlByRows,
                            SearchDirection: xls.XlSearchDirection.xlNext,
                            MatchCase: false,
                            SearchFormat: false);
    if (foundRange == null) return;
    int i = foundRange.Row;
    for (int j = 1; j < i; j++)
      worksheet.Rows[1].Delete();
  }

  /// <summary>
  /// 存csv檔
  /// </summary>
  /// <param name="path"></param>
  /// <param name="refresh">自動計算後重新整理</param>
  public void SaveCSV(string path)
  {//Local: true 依看到的格式儲存
    //app.CalculateUntilAsyncQueriesDone();//等excel忙碌完
    Workbook.SaveAs(path, xls.XlFileFormat.xlCSV, Local: true);
    Thread.Sleep(1000);
  }

  /// <summary>
  /// 存xlsx檔
  /// </summary>
  /// <param name="path"></param>
  public void SaveXLSX(string path)
  {
    //app.CalculateUntilAsyncQueriesDone();//等excel忙碌完
    Workbook.SaveAs(path, xls.XlFileFormat.xlOpenXMLWorkbook);
    Thread.Sleep(1000);
  }

  /// <summary>
  /// 將指定的Worksheet資料轉json
  /// </summary>
  /// <param name="worksheet">worksheet name</param>
  /// <param name="onRow">after create every Dictionary</param>
  /// <returns></returns>
  public string ToJson(string worksheet, EventHandler onRow)
  {
    //app.CalculateUntilAsyncQueriesDone();
    xls.Worksheet ws = string.IsNullOrEmpty(worksheet) ? Workbook.Sheets[1] : Workbook.Sheets[worksheet];
    xls.Range rg = ws.UsedRange;
    object[,] data = rg.Value2;
    Dictionary<string, string>[] dics = new Dictionary<string, string>[rg.Rows.Count - 1];
    for (int i = 2; i <= rg.Rows.Count; i++)
    {
      var dic = new Dictionary<string, string>();
      onRow?.Invoke(dic, null);
      for (int j = 1; j <= rg.Columns.Count; j++)
      {
        var key = Convert.ToString(data[1, j]).Trim();
        var val = Convert.ToString(data[i, j]);
        if (!string.IsNullOrEmpty(key) && !string.IsNullOrEmpty(val))
          dic.Add(key, val);
      }
      dics[i - 2] = dic;
    }
    return JsonConvert.SerializeObject(dics);
  }

  /// <summary>
  /// 將指定Worksheet轉DataTable
  /// </summary>
  /// <param name="worksheet">worksheet name</param>
  /// <param name="hasCol">has column name</param>
  /// <returns></returns>
  public DataTable ToDataTable(string worksheet, bool hasCol = true)
  {
    DataTable dt = new DataTable();
    var ws = Workbook.Worksheets[worksheet];
    xls.Range rg = ws.UsedRange;
    object[,] data = rg.Value2;

    for (int j = 1; j <= rg.Columns.Count; j++)//add columns
    {
      var name = hasCol ? Convert.ToString(data[1, j]).Trim() : "Column" + j;
      if (string.IsNullOrEmpty(name)) break;
      dt.Columns.Add(name, typeof(string));
    }

    for (int i = hasCol ? 2 : 1; i <= rg.Rows.Count; i++)//add rows
    {
      bool HasData = false;
      DataRow r = dt.NewRow();
      for (int j = 0; j < dt.Columns.Count; j++)
      {
        var val = Convert.ToString(data[i, j + 1]);
        if (!string.IsNullOrEmpty(val))
        {
          HasData = true;
          r[j] = val;
        }
      }
      if (HasData)
        dt.Rows.Add(r);
    }
    return dt;
  }

 public static void Close()
 {
     if (app == null) return;
     try
     {
         app.Quit();
         Marshal.ReleaseComObject(app);
     }
     catch
     { }
     app = null;
     using (Process process = Process.Start("taskkill", "/F /IM excel.exe"))
     {
         process.WaitForExit();
     }
 }
 
  public void Dispose()
  {
    if (Workbook != null)
    {
      //app.CalculateUntilAsyncQueriesDone();//等儲存完成再關
      Workbook.Close(false);
      Marshal.ReleaseComObject(Workbook);
    }
  }
  #endregion
}

若遇到需要同時開2個Excel更新函數或只開1個excel更新powerquery, 可以這樣寫
ps. excel powerquery使用Table.Buffer函數時 RefreshAll就會卡住, 建議不用此函數或使用RPA操作EXCEL畫面

//要等候而且不卡住UI, 就寫ACTION來處理
Action Refreshed = () => { doing = false; };
Action wait = () =>
{
	while (doing)
	{
		Thread.Sleep(1000);
		Application.DoEvents();
	}
};

if (usePowerQuery)
	using (var excel = new ExcelTool(TempFile))
	{
		excel.RefreshAll(Refreshed);
		wait();
		excel.SaveCSV(resultFile);
	}
else//套函數要另開原資料
	using (var excel2 = new ExcelTool(temp))
	{
		if (keyword != string.Empty)
			excel2.DeleteBeforeKeyword(keyword);
		using (var excel = new ExcelTool(TempFile))
		{
			excel.RefreshAll(Refreshed);
			wait();
			excel.SaveCSV(resultFile);
		}
	}

Taiwan is a country. 臺灣是我的國家