---
# System prepended metadata

title: Visual Studio Tools for Office (VSTO) 程式碼範例
tags: [C#, Excel, CSharp, VSTO, .NET Framework]

---

# Visual Studio Tools for Office (VSTO)

Visual Studio Tools for Office (VSTO) 是一個 Microsoft Visual Studio 的擴充套件，專門用於開發 Microsoft Office 應用程式的擴充功能和自訂功能。開發者可以使用 VSTO 在 Office 應用程式中建立自己的自訂功能，例如新增自訂功能表、工具列、按鈕或面板，並進行各種操作和處理。

VSTO 提供了一個基於 .NET Framework 的開發環境，支援使用 C# 或 VB.NET 等語言來開發自訂功能。透過 VSTO，開發者可以輕鬆地與 Office 應用程式進行整合和互動，進而提升應用程式的功能性和生產力。

總的來說，Visual Studio Tools for Office 提供了開發者一個方便且強大的工具，讓他們能夠進行 Office 應用程式的自訂和擴充，以滿足不同使用者的需求，並提高應用程式的效率和便利性。

## 查詢

程式執行流程：

```mermaid
graph LR;
是否有編輯中的儲存格-->初始化工作表;
初始化工作表-->設定儲存格格式;
設定儲存格格式-->資料繫結至工作表;
資料繫結至工作表-->設定工作表樣式;
設定工作表樣式-->GcExcel;
```

### 是否有編輯中的儲存格

```csharp=1
/// <summary>
/// 是否有編輯中的儲存格
/// </summary>
public static void CheckActiveCell(this Excel.Worksheet ws)
{
    try
    {
        ws.Select(Type.Missing);
    }
    catch
    {
        Excel.Range rng = ws.Application.ActiveCell;
        string activeCell = ToExcelColumn(rng.Column) + rng.Row;
        throw new Exception("儲存格(" + activeCell + ") 編輯中，請確認後再執行。");
    }
}
```

### 初始化工作表

```csharp=1
/// <summary>
/// 將工作表還原至初始默認狀態
/// </summary>
public static void InitWorksheet(this Excel.Worksheet ws)
{
    Excel.Range range;

    Cursor.Current = Cursors.WaitCursor; // 搭配 GcExcel()

    //ws.Application.ScreenUpdating = false;
    ws.Application.EnableEvents = false;

    // 解除保護
    if (ws.ProtectContents) ws.Unprotect();
    // 取消自動篩選
    if (ws.AutoFilterMode) ws.AutoFilterMode = false;

    // 清除工作表的所有内容
    ws.Cells.Clear();

    ws.Rows.Delete(); // *刪除所有插入的行
    ws.Columns.Delete(); // *刪除所有插入的列
    ws.Rows.Hidden = false; // 顯示所有隱藏的行
    ws.Columns.Hidden = false; // 顯示所有隱藏的列

    ws.Cells.ClearFormats();  // 清除儲存格格式
    ws.Cells.ClearComments(); // 清除儲存格的註解
    ws.Cells.Validation.Delete();  // 清除資料驗證(Data Validation)
    ws.UsedRange.MergeCells = false;  // 取消合併儲存格

    // 清除所有儲存格的公式和值
    ws.UsedRange.Formula = "";
    ws.UsedRange.Value = "";

    // 檢查列(直排)是否有群組
    range = ws.UsedRange.Columns;
    foreach (Excel.Range column in range)
    {
        if (column.OutlineLevel > 1)
            ws.Columns.Ungroup();
    }

    // 解除凍結窗格
    ws.Application.ActiveWindow.FreezePanes = false;

    //// 重設所有的圖表和圖形
    //foreach (Excel.ChartObject chartObject in ws.ChartObjects())
    //    chartObject.Delete();

    //ws.Application.ActiveWindow.ScrollRow = 1; // 滾動至第 1 行
    ws.Columns.AutoFit();

    //ws.Application.ScreenUpdating = true;
    ws.Application.EnableEvents = true;
}
```

### 設定儲存格格式

值"OCT1"會在儲存格尚未指定成文字格式前，因通用格式而自動轉換成日期格式。
解決方法是<span style="color:red">先設定儲存格格式，再讓資料繫結上去</span>。

```csharp=1
ws.Range[row, column].NumberFormat = "@"; // 文字格式
```

### 資料繫結至工作表

```csharp=1
/// <summary>
/// 將 DataTable 載入 Worksheet。並且設定基本樣式。
/// </summary>
public static void CreateSheet(this Excel.Worksheet ws, DataTable dtExcel, string[] showColumns)
{
    int dtRowCnt = dtExcel.Rows.Count;
    int showColumnCnt = showColumns.Length;

    // 先關閉自動更新畫面，以提高效能
    ws.Application.ScreenUpdating = false;
    // 關閉事件處理
    ws.Application.EnableEvents = false;
    //// 關閉特定的提醒及訊息
    //ws.Application.DisplayAlerts = false;

    // 使用者的習慣是公式計算選項選「手動」，所以 VSTO 程式要自己觸發公式計算 ws.Calculate();
    //ws.Application.Calculation = Excel.XlCalculation.xlCalculationManual;
    try
    {
        //【Header】
        for (int i = 0; i < showColumnCnt; i++)
        {
            var Cell = ws.Cells[1, i + 1];
            // 先設定格式再賦予值 (@ 就是原始的內容)
            Cell.NumberFormat = "@";
            //if (dtExcel.Columns.Contains(showColumns[i]))
            //    Cell.Value2 = dtExcel.Columns[showColumns[i]].ColumnName;
            //else
            Cell.Value2 = showColumns[i];
        }

        //【Row Data】
        object[,] DataList = new object[dtRowCnt, showColumnCnt];
        for (int i = 0; i < dtRowCnt; i++)
        {
            for (int j = 0; j < showColumnCnt; j++)
            {
                if (dtExcel.Columns.Contains(showColumns[j]))
                    DataList[i, j] = dtExcel.Rows[i][showColumns[j]].ToString().Replace(";", "\n");
            }
        }

        ws.Range[ws.Cells[2, 1], ws.Cells[1 + dtRowCnt, showColumnCnt]].Value2 = DataList;

        BasicStyleSettings(ws);
    }
    catch (Exception ex)
    {
        throw ex;
    }
    finally
    {
        // 啟用更新畫面
        ws.Application.ScreenUpdating = true;
        // 啟用事件處理
        ws.Application.EnableEvents = true;
        //// 啟用特定的提醒及訊息
        //ws.Application.DisplayAlerts = true;
    }
}
```

```csharp=1
/// <summary>
/// 將 List<T> 載入 Worksheet。並且設定基本樣式。
/// </summary>
public static void CreateSheet<T>(this Excel.Worksheet ws, List<T> list, string[] showColumns)
{
    int rowCnt = list.Count;
    int showColumnCnt = showColumns.Length;

    ws.Application.ScreenUpdating = false;
    ws.Application.EnableEvents = false;            
    try
    {
        //【Header】
        for (int i = 0; i < showColumnCnt; i++)
        {
            var Cell = ws.Cells[1, i + 1];
            Cell.NumberFormat = "@";
            Cell.Value2 = showColumns[i];
        }

        //【Row Data】
        object[,] DataList = new object[rowCnt, showColumnCnt];
        for (int i = 0; i < rowCnt; i++)
        {
            for (int j = 0; j < showColumnCnt; j++)
            {
                PropertyInfo prop = typeof(T).GetProperty(showColumns[j]);
                DataList[i, j] = prop.GetValue(list[i], null)?.ToString().Replace(";", "\n");
            }
        }

        ws.Range[ws.Cells[2, 1], ws.Cells[1 + rowCnt, showColumnCnt]].Value2 = DataList;

        BasicStyleSettings(ws);
    }
    catch (Exception ex)
    {
        throw ex;
    }
    finally
    {
        ws.Application.ScreenUpdating = true;
        ws.Application.EnableEvents = true;
    }
}
```

```csharp=1
/// <summary>
/// 查無資料時，只產生 Header 欄位。並且設定基本樣式。
/// </summary>
public static void GenerateHeaderColumns(this Excel.Worksheet ws, string[] showColumns)
{
    int showColumnCount = showColumns.Length;

    ws.Application.ScreenUpdating = false;
    try
    {
        for (int i = 0; i < showColumnCount; i++)
        {
            var Cell = ws.Cells[1, i + 1];
            Cell.NumberFormat = "@";
            Cell.Value2 = showColumns[i];
        }

        BasicStyleSettings(ws);
    }
    catch (Exception ex)
    {
        throw ex;
    }
    finally
    {
        ws.Application.ScreenUpdating = true;
    }
}
```

### 設定工作表樣式

```csharp=1
/// <summary>
/// 設定基本樣式
/// </summary>
public static void BasicStyleSettings(this Excel.Worksheet ws)
{
    int usedRowCnt = ws.UsedRange.Rows.Count;
    int usedColCnt = ws.UsedRange.Columns.Count;

    if (usedRowCnt <= 1) usedRowCnt++; // 如果只有 Header (查無資料)，給一行空行。

    // 格線隱藏
    //ws.Application.ActiveWindow.DisplayGridlines = false;

    //【Worksheet 樣式】
    ws.Cells.Font.Name = "Microsoft JhengHei";
    ws.Cells.Font.Name = "Gill Sans MT";
    ws.Cells.Font.Size = "11";
    ws.Cells.Font.Color = Color.FromArgb(64, 64, 64);
    ws.Cells.HorizontalAlignment = Excel.XlHAlign.xlHAlignCenter;
    ws.Cells.VerticalAlignment = Excel.XlVAlign.xlVAlignCenter;
    ws.Cells.Locked = false;

    //【Header 樣式】
    Excel.Range rngHeader = ws.Range[ws.Cells[1, 1], ws.Cells[1, usedColCnt]];
    rngHeader.Font.Bold = true;
    rngHeader.Font.Size = "12";
    rngHeader.Font.Color = Color.FromArgb(255, 255, 255); // White
    rngHeader.Interior.Color = Color.FromArgb(75, 172, 198); // Interior 儲存格背景顏色
    // 鎖定狀態在首次設定時並不會影響單元格的行為。只有當工作表被保護（Worksheet 的 Protect 方法被呼叫）後，設置為 "鎖定" 的單元格或範圍才將無法被用戶編輯。
    //rngHeader.Locked = true;

    //【資料範圍樣式】
    Excel.Range rngData = ws.Range[ws.Cells[2, 1], ws.Cells[usedRowCnt, usedColCnt]];
    rngData.Interior.Color = Color.FromArgb(255, 255, 255); // Interior 儲存格背景顏色
    rngData.Borders.LineStyle = Excel.XlLineStyle.xlContinuous;
    rngData.Borders.Weight = Excel.XlBorderWeight.xlThin;
    rngData.Borders.Color = Color.FromArgb(128, 128, 128); // Gray

    ws.Columns.AutoFit();
}
```

### 回復 Excel 基本設定、釋放資源

```csharp=1
/// <summary>
/// 回復 Excel 基本設定、釋放資源
/// </summary>
public static void GcExcel(this Excel.Worksheet ws)
{
    //if (ws != null)
    //{
    //    Marshal.ReleaseComObject(ws);
    //    ws = null;
    //}
    //GC.Collect();
    //GC.WaitForPendingFinalizers();

    ws.Application.ScreenUpdating = true;
    ws.Application.EnableEvents = true;
    ws.Application.DisplayAlerts = true;
    ws.Application.StatusBar = "";

    Cursor.Current = Cursors.Default;
}
```

## 異動資料庫(Insert、Update、Delete)

程式執行流程：

```mermaid
graph LR;
BeforeSaveWorksheet-->異動資料庫;
異動資料庫-->AfterSaveWorksheet;
```

### BeforeSaveWorksheet

```csharp=1
/// <summary>
/// 儲存資料至資料庫之前，工作表該做的設定
/// </summary>
public static void BeforeSaveWorksheet(this Excel.Worksheet ws)
{
    Cursor.Current = Cursors.WaitCursor;

    if (ws.AutoFilterMode)
        ws.AutoFilterMode = false; // 清除已應用的篩選器。
}
```

### AfterSaveWorksheet

```csharp=1
/// <summary>
/// 恢復 BeforeSaveWorksheet() 做的設定。一般用在 finally{}
/// </summary>
public static void AfterSaveWorksheet(this Excel.Worksheet ws)
{
    int usedColCnt = ws.UsedRange.Columns.Count;
    if (!ws.AutoFilterMode)
        ws.Range[ws.Cells[1, 1], ws.Cells[1, usedColCnt]].AutoFilter(1); // 加篩選器

    Cursor.Current = Cursors.Default;
}
```

## [Excel 物件模型](https://learn.microsoft.com/zh-tw/visualstudio/vsto/excel-object-model-overview?view=vs-2019&tabs=csharp)

### Application 應用程式物件

#### ActiveWindow.FreezePanes

凍結窗格

```csharp=
ws.Range["A2"].Select(); // 欄位
ws.Application.ActiveWindow.FreezePanes = true;
```

#### Calculate

計算所有開啟的活頁簿、活頁簿中的特定工作表

```csharp
ws.Calculate();
```

```csharp
ws.Application.Calculate();
```

### Range 物件

宣告 Range 變數

```csharp
Excel.Range rng = ws.Range[row, column];
```

#### AddComment

新增註解。

```csharp=1
rng.AddComment("表頭黃字為必填欄位");
rng.Comment.Shape.TextFrame.AutoSize = true;
```

#### AutoFilter

加篩選器。

```csharp=1
Excel.Range row = ws.Range[row, column];
row.AutoFilter(1);
```

#### AutoFit

將範圍內的欄寬度或範圍內的列高度變更為最合適的大小。

```csharp=1
rng.Columns.AutoFit();
```



## 其他常用方法

### 欄位編碼

|Column number|1|2|3|4|5|6|7|...|27|28|...|
|-------------|-|-|-|-|-|-|-|---|--|--|---|
|Column name  |A|B|C|D|E|F|G|...|AA|AB|...|

```csharp=1
/// <summary>
/// 將數字轉換成 Excel 的列字母
/// </summary>
public static string ToExcelColumn(this int columnNumber)
{
    int dividend = columnNumber;
    string columnName = string.Empty;
    int modulo;

    while (dividend > 0)
    {
        modulo = (dividend - 1) % 26;
        columnName = Convert.ToChar(65 + modulo).ToString() + columnName;
        dividend = (int)((dividend - modulo) / 26);
    }
    return columnName;
}
```
