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