~楓花雪岳~
星期五, 1月 16, 2026
[OneNote] 快捷鍵 (Accelerator Keys)
使用畫面剪輯功能要把截圖直接放進 OneNote 內,常常要去點擊按鈕才能使用,研究是否有快捷鍵可以直接使用,發現 OneNote 上快捷鍵是可以在軟體本身上查詢,以畫面剪輯功能為紀錄,該筆記使用環境為
星期六, 1月 10, 2026
[C#] DataGridViewDisableButtonColumn
把該筆記 [C#] 停用 Windows Form DataGridView 控制項按鈕資料行中的按鈕 功能應用在實務環境上,發現還有些小地方還可以改善
該筆記重點
- 重新繪製 Button 時間點:之前是在 AP 端設定完成後要手動觸發 InvalidateColumn(),現在把繪製觸發點改在 Property 內,只要設定屬性就會立刻重新繪製,且使用 InvalidateCell() 只重新繪製該 cell,縮小繪製範圍
- 滑鼠外觀強化:當屬性為 false 時,滑鼠指標要變成禁止符號,增強使用者體驗
- Disable 繪製時 TextFormatFlags 設定:根據 DataGridViewContentAlignment 來產生對應 TextFormatFlags
public class DataGridViewDisableButtonCell : DataGridViewButtonCell
{
private bool _cellEnabled = true;
private bool _cellVisible = true;
public bool CellEnabled
{
get => _cellEnabled;
set
{
if (_cellEnabled != value)
{
_cellEnabled = value;
DataGridView?.InvalidateCell(ColumnIndex, RowIndex);
}
}
}
public bool CellVisible
{
get => _cellVisible;
set
{
if (_cellVisible != value)
{
_cellVisible = value;
DataGridView?.InvalidateCell(ColumnIndex, RowIndex);
}
}
}
public DataGridViewDisableButtonCell()
{
_cellEnabled = true;
_cellVisible = true;
}
public override object Clone()
{
var cell = (DataGridViewDisableButtonCell)base.Clone();
cell.CellEnabled = this.CellEnabled;
cell.CellVisible = this.CellVisible;
return cell;
}
// --- 游標指標 ---
protected override void OnMouseEnter(int rowIndex)
{
base.OnMouseEnter(rowIndex);
if ((!_cellEnabled || !_cellVisible) && DataGridView != null)
{
DataGridView.Cursor = Cursors.No;
}
}
protected override void OnMouseMove(DataGridViewCellMouseEventArgs e)
{
base.OnMouseMove(e);
if ((!_cellEnabled || !_cellVisible) && DataGridView != null && DataGridView.Cursor != Cursors.No)
{
DataGridView.Cursor = Cursors.No;
}
}
protected override void Paint(Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, int rowIndex, DataGridViewElementStates elementState, object? value, object? formattedValue, string? errorText, DataGridViewCellStyle cellStyle, DataGridViewAdvancedBorderStyle advancedBorderStyle, DataGridViewPaintParts paintParts)
{
// Visible = false 只繪製背景
if (!_cellVisible)
{
if (paintParts.HasFlag(DataGridViewPaintParts.Background))
{
using var backBrush = new SolidBrush(cellStyle.BackColor);
graphics.FillRectangle(backBrush, cellBounds);
}
return;
}
// Enabled = true 使用預設繪製方式,確保樣式一致
if (_cellEnabled)
{
base.Paint(graphics, clipBounds, cellBounds, rowIndex, elementState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, paintParts);
return;
}
// --- 自定義 Disabled 繪圖邏輯 ---
// 背景
if (paintParts.HasFlag(DataGridViewPaintParts.Background))
{
using var backBrush = new SolidBrush(cellStyle.BackColor);
graphics.FillRectangle(backBrush, cellBounds);
}
// 邊框
if (paintParts.HasFlag(DataGridViewPaintParts.Border))
{
PaintBorder(graphics, clipBounds, cellBounds, cellStyle, advancedBorderStyle);
}
// 計算按鈕區域
Rectangle buttonArea = cellBounds;
Rectangle buttonAdjustment = BorderWidths(advancedBorderStyle);
buttonArea.X += buttonAdjustment.X;
buttonArea.Y += buttonAdjustment.Y;
buttonArea.Height -= buttonAdjustment.Height;
buttonArea.Width -= buttonAdjustment.Width;
// 按鈕本身 (Disabled 樣式)
ButtonRenderer.DrawButton(graphics, buttonArea, PushButtonState.Disabled);
// 文字
if (formattedValue is string text && !string.IsNullOrEmpty(text))
{
TextFormatFlags flags = GetTextFormatFlags(cellStyle.Alignment);
TextRenderer.DrawText(graphics,
text,
cellStyle.Font,
buttonArea,
SystemColors.GrayText,
flags);
}
}
private TextFormatFlags GetTextFormatFlags(DataGridViewContentAlignment alignment)
{
// WordBreak: 如果文字太長,允許自動換行
// PreserveGraphicsClipping: 確保文字不會畫到格子外面去 (效能與安全性)
TextFormatFlags flags = TextFormatFlags.WordBreak | TextFormatFlags.PreserveGraphicsClipping;
// 水平位置
if (alignment.ToString().Contains("Center"))
flags |= TextFormatFlags.HorizontalCenter; // 加上「水平置中」標籤
else if (alignment.ToString().Contains("Right"))
flags |= TextFormatFlags.Right; // 加上「靠右」標籤
else
flags |= TextFormatFlags.Left; // 預設「靠左」
// 垂直位置
if (alignment.ToString().Contains("Top"))
flags |= TextFormatFlags.Top; // 加上「靠上」標籤
else if (alignment.ToString().Contains("Bottom"))
flags |= TextFormatFlags.Bottom; // 加上「靠下」標籤
else
flags |= TextFormatFlags.VerticalCenter; // 預設「垂直置中」
return flags;
}
}
DataGridViewDisableButtonColumn
當希望 ButtonColumn.Text 文字可以全部顯示在按鈕文字上,要開啟 UseColumnTextForButtonValue 屬性,官方文件說明
By default, the DataGridViewCell.FormattedValue of a button cell is displayed as the text on the button. The UseColumnTextForButtonValue property allows you to either set the button text for each cell, or to use the Text property value for all of the button cells.
Getting or setting this property gets or sets the UseColumnTextForButtonValue property of the object returned by the CellTemplate property. Setting this property also sets the UseColumnTextForButtonValue property of every cell in the column and refreshes the column display. To override the specified value for individual cells, set the cell values after you set the column value.
namespace DataGridViewDisableButtonColumnSample
{
public class DataGridViewDisableButtonColumn : DataGridViewButtonColumn
{
public DataGridViewDisableButtonColumn()
{
CellTemplate = new DataGridViewDisableButtonCell();
// 預設值為 false,當希望 DataGridViewColumn.Text 設定值,直接顯示在 Button.Value 上時要開啟
UseColumnTextForButtonValue = false;
}
}
}
主程式
原本是透過 CellClick 事件來顯示,現在改為使用 CellContentClick 事件,縮小事件觸發範圍
namespace DataGridViewDisableButtonColumnSample
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
dataGridView1.AutoSize = true;
dataGridView1.AllowUserToAddRows = false;
dataGridView1.ColumnHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter;
dataGridView1.RowCount = 8;
for (int i = 0; i < dataGridView1.RowCount; i++)
dataGridView1.Rows[i].Cells[ColButtonEnabled.Index].Value = $"Button{i}";
dataGridView1.CellContentClick += DataGridView1_CellContentClick;
dataGridView1.CellValueChanged += DataGridView1_CellValueChanged;
dataGridView1.CurrentCellDirtyStateChanged += DataGridView1_CurrentCellDirtyStateChanged;
}
private void DataGridView1_CellContentClick(object? sender, DataGridViewCellEventArgs e)
{
if (e.ColumnIndex != ColButtonEnabled.Index)
return;
DataGridViewDisableButtonCell buttonEnabled = (DataGridViewDisableButtonCell)dataGridView1.Rows[e.RowIndex].Cells[ColButtonEnabled.Index];
if (buttonEnabled.CellEnabled == false)
return;
MessageBox.Show($"{buttonEnabled.Value} 是 Enabled");
}
private void DataGridView1_CurrentCellDirtyStateChanged(object? sender, EventArgs e)
{
if (dataGridView1.IsCurrentCellDirty &&
dataGridView1.CurrentCell.ColumnIndex == ColChecked.Index)
// CommitEdit() 會維持在編輯模式,並立即觸發 CellValueChanged 事件
dataGridView1.CommitEdit(DataGridViewDataErrorContexts.Commit);
}
private void DataGridView1_CellValueChanged(object? sender, DataGridViewCellEventArgs e)
{
if (e.ColumnIndex != ColChecked.Index)
return;
bool value = (bool)dataGridView1.Rows[e.RowIndex].Cells[ColChecked.Index].Value;
DataGridViewDisableButtonCell buttonEnabled = (DataGridViewDisableButtonCell)dataGridView1.Rows[e.RowIndex].Cells[ColButtonEnabled.Index];
buttonEnabled.CellEnabled = !value;
DataGridViewDisableButtonCell buttonVisible = (DataGridViewDisableButtonCell)dataGridView1.Rows[e.RowIndex].Cells[ColButtonVisible.Index];
buttonVisible.CellVisible = !value;
}
}
}
星期四, 1月 08, 2026
[JS] 全域變數宣告
透過 Vibe Coding 產出的 GAS Code 要複製進線上 IDE 執行時發現錯誤,錯誤訊息如下
Code.gs 檔案
測試二:使用 let 進行宣告
Code.gs 檔案
基本就不要再用 var 宣告就是
SyntaxError: Identifier '變數名稱' has already been declared
Root Cause 是把 gas Code 拆成多個檔案,Code 複製進錯的檔案內造成,但直覺全域變數不是會同名覆蓋,理論上要可以執行才是,該 case 竟然會拋出錯誤,開兩個 gs 檔案來測試並筆記
測試一:使用 var 進行宣告Code.gs 檔案
var declareByVar = "從 Code.gs 內指定";
function GlobalVariableDemo() {
console.log(declareByVar);
}
Code2.gs 檔案var declareByVar = "從 Code2.gs 內指定";
實際執行測試二:使用 let 進行宣告
Code.gs 檔案
let declareByLet = "從 Code.gs 內指定";
function GlobalVariableDemo() {
console.log(declareByLet);
}
Code2.gs 檔案
let declareByLet = "從 Code2.gs 內指定";
實際執行基本就不要再用 var 宣告就是
星期五, 12月 26, 2025
[GAS] 上傳檔案至 Google Drive - Excel to Sheet
在 Google Apps Script 內操作檔案有兩種方式,分別為 DriveApp 和 Drive API,要把 Excel 轉為 Sheet 必須使用 Drive API 才能進行轉換
Drvie API 服務
要使用 Drive API 必須先在服務內加入,該筆記是加入 Drive API V3,該筆記記錄當下 V2 還是存在的
index.html
上傳一個 Excel 和轉換後的 Sheet 並列呈現
Drvie API 服務
要使用 Drive API 必須先在服務內加入,該筆記是加入 Drive API V3,該筆記記錄當下 V2 還是存在的
index.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<?!= include('css') ?>
</head>
<body>
<div class="app-container">
<!-- return false:避免 form onsubmit 預設 redirect 行為 -->
<form onsubmit="handleFormSubmit(this); return false;">
<label class="field-label">電子郵件:</label>
<input type="email" name="userEmail" class="input-field" placeholder="example@mail.com" required>
<label class="field-label">選擇檔案:</label>
<!-- 有限定檔案為 xls 和 xlsx -->
<input type="file" name="myFile" class="input-field"
accept=".xls,.xlsx, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
required>
<button type="submit" class="btn-submit">上傳</button>
</form>
<div class="result-message">等待操作</div>
</div>
<script>
function onSuccessHandler(response) {
const display = document.querySelector('.result-message');
display.innerText = "【成功】\n" + response;
display.className = "result-message text-success";
}
function onFailureHandler(error) {
const display = document.querySelector('.result-message');
display.innerText = "【失敗】\n" + error.message;
display.className = "result-message text-failure";
}
function handleFormSubmit(formObject) {
let display = document.querySelector('.result-message');
let fileInput = formObject.myFile;
// 透過 .files 屬性取得檔案
let file = fileInput.files[0];
let fileName = file.name.toLowerCase();
// 檢查副檔名
if (!fileName.endsWith('.xls') && !fileName.endsWith('.xlsx')) {
display.innerText = "【格式錯誤】\n檔案格式不正確。\n請只上傳 Excel 檔案 (.xls, .xlsx)";
display.className = "result-message text-failure";
return;
}
display.innerText = "正在處理檔案,請勿重整頁面";
display.className = "result-message";
// 直接傳遞 formObject
google.script.run
.withSuccessHandler(onSuccessHandler)
.withFailureHandler(onFailureHandler)
.processForm(formObject);
}
</script>
</body>
</html>
code.gsfunction doGet() {
return HtmlService.createTemplateFromFile('index')
.evaluate()
.setTitle('[GAS] 上傳檔案至 Google Drive - Excel to Sheet');
}
function include(fileName) {
return HtmlService.createHtmlOutputFromFile(fileName).getContent();
}
function processForm(formObject) {
try {
let parentFoldID = getFoldID();
let parentFolder = DriveApp.getFolderById(parentFoldID);
let timestamp = Utilities.formatDate(new Date(), "GMT+8", "yyyyMMdd_HHmmss");
let subFolder = parentFolder.createFolder(timestamp);
let subFolderId = subFolder.getId(); // 取得 ID 供後續 Drive API 使用
// 儲存 Excel 檔案
let excelFileBlob = formObject.myFile;
subFolder.createFile(excelFileBlob);
let fileMetaData = {
name: excelFileBlob.getName(),
mimeType: MimeType.GOOGLE_SHEETS, // 指定檔案類型
parents: [subFolderId] // 指定父資料夾,必須是資料夾ID (FoldID) 的陣列格式
};
// 透過 Drvie API 把 Excel 轉換為 Sheet
let sheetFileByAPI = Drive.Files.create(fileMetaData, excelFileBlob);
// 檔案 ID 是相同,單純方便操作檔案,才透過 DriveApp.getFileById() 再轉回 File Class 來使用
let sheetFileByApp = DriveApp.getFileById(sheetFileByAPI.id);
let url = sheetFileByApp.getUrl();
sheetFileByApp.setDescription("透過 DriveApp 設定:上傳者為 " + formObject.userEmail)
let message = `
上傳時間:${timestamp}
Email:${formObject.userEmail}
上傳檔案 URL:${url}`;
return message;
} catch (error) {
throw new Error("系統錯誤:" + error.toString());
}
}
CSS
<style>
.app-container {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 30px;
line-height: 1.6;
}
.field-label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.input-field {
width: 280px;
padding: 8px;
margin-bottom: 15px;
border: 1px solid #ccc;
border-radius: 4px;
display: block;
}
.btn-submit {
display: block;
padding: 8px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-submit:hover {
background-color: #0056b3;
}
.result-message {
margin-top: 20px;
padding: 15px;
border-left: 5px solid #eee;
background-color: #f9f9f9;
white-space: pre-wrap;
}
.text-success {
border-left-color: #28a745;
color: #155724;
}
.text-failure {
border-left-color: #dc3545;
color: #721c24;
}
</style>
執行結果
上傳一個 Excel 和轉換後的 Sheet 並列呈現
- 延伸閱讀
- [GAS] 上傳檔案至 Google Drive
- 參考資料
- Deive API:Method: files.create、Method: files.update、REST Resource: files
- DriveApp:getFileById(id)、Class File
星期五, 12月 19, 2025
[GAS] 上傳檔案至 Google Drive
在官方文件 在 HTML 服務:與伺服器函式通訊 - 表單 上有特別提到,當把 html 上的 form 當成參數傳遞至 gs code 內的函數時,form 會變成一個 key-value object,form field names 為 key、form field values 為 value,值會轉成 string,只有 file 會轉成 blob,該筆記紀錄該 GAS 特色功能
index.html
css.html
gs code
執行結果
index.html
<!DOCTYPE html>
<html>
<head>
<base target="_top">
<?!= include('css') ?>
</head>
<body>
<div class="app-container">
<form onsubmit="handleFormSubmit(event)">
<label class="field-label">電子郵件:</label>
<input type="email" name="userEmail" class="input-field" placeholder="example@mail.com" required>
<label class="field-label">選擇檔案:</label>
<input type="file" name="myFile" class="input-field" required>
<button type="submit" class="btn-submit">上傳</button>
</form>
<div class="result-message">等待操作</div>
</div>
<script>
function onSuccessHandler(response) {
const display = document.querySelector('.result-message');
display.innerText = "【成功】\n" + response;
display.className = "result-message text-success";
}
function onFailureHandler(error) {
const display = document.querySelector('.result-message');
display.innerText = "【失敗】\n" + error.message;
display.className = "result-message text-failure";
}
function handleFormSubmit(event) {
// 避免 form onsubmit 預設 redirect 行為
event.preventDefault();
// 取得 form
let formObject = event.target;
let display = document.querySelector('.result-message');
display.innerText = "正在上傳至 Google Drive";
display.className = "result-message";
// 呼叫 gs Code 內的 processForm() 並把 form 當成參數
google.script.run
.withSuccessHandler(onSuccessHandler)
.withFailureHandler(onFailureHandler)
.processForm(formObject);
}
</script>
</body>
</html>
css.html
<style>
.app-container {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
padding: 30px;
line-height: 1.6;
}
.field-label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.input-field {
width: 280px;
padding: 8px;
margin-bottom: 15px;
border: 1px solid #ccc;
border-radius: 4px;
display: block;
}
.btn-submit {
display: block;
padding: 8px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-submit:hover {
background-color: #0056b3;
}
.result-message {
margin-top: 20px;
padding: 15px;
border-left: 5px solid #eee;
background-color: #f9f9f9;
white-space: pre-wrap;
}
.text-success {
border-left-color: #28a745;
color: #155724;
}
.text-failure {
border-left-color: #dc3545;
color: #721c24;
}
</style>
gs code
function doGet() {
return HtmlService.createTemplateFromFile('index')
.evaluate()
.setTitle('[GAS] 上傳檔案至 Google Drive');
}
function include(fileName) {
return HtmlService.createHtmlOutputFromFile(fileName).getContent();
}
/**
* 處理表單提交
*/
function processForm(formObject) {
try {
let parentFoldID = getFoldID();
let parentFolder = DriveApp.getFolderById(parentFoldID);
// 建立時間戳記子資料夾
let timestamp = Utilities.formatDate(new Date(), "GMT+8", "yyyyMMdd_HHmmss");
let subFolder = parentFolder.createFolder(timestamp);
// 儲存檔案 (formObject.myFile 會自動被 GAS 轉為 Blob)
let fileBlob = formObject.myFile;
let file = subFolder.createFile(fileBlob);
file.setDescription("上傳者:" + formObject.userEmail)
let message = `
上傳時間:${timestamp}
Email:${formObject.userEmail}
上傳檔案 URL:${file.getUrl()}`;
return message;
} catch (error) {
throw new Error("系統錯誤:" + error.toString());
}
}
執行結果
訂閱:
意見 (Atom)











