星期六, 8月 30, 2025

[GAS] 自訂選單

看見範例在 Sheets 內自訂選單來觸發 apps script,閱讀官方文件 - Google Workspace 中的自訂選單 發現 Docs、Sheets、Slides、Forms 都可以自訂選單,該筆記會以 Sheets 為目標,在於 Sheets 相對於其他服務,還可以使用圖片或繪圖來觸發 apps script

.gs Code

自訂選單必須寫在 onOpen() 內,把選項加入 UI 並設定觸發 apps script 函數
function onOpen() {
  let ui = SpreadsheetApp.getUi();
  
  ui.createMenu('自定義功能-第一層')
      .addItem('選項一', 'menuItem1')
      .addSeparator()
      .addSubMenu(ui.createMenu('自定義功能-第二層')
          .addItem('選項二', 'menuItem2'))
      .addToUi();
}

function menuItem1() {
  SpreadsheetApp.getUi().alert('點選選項一:使用 alert 顯示訊息');
}

function menuItem2() {

  html = HtmlService
    .createHtmlOutput('點選選項二:使用 <span style="background-color:green">HtmlService.createHtmlOutput</span> 來顯示訊息')
    .setWidth(300)
    .setHeight(100);

  SpreadsheetApp.getActive().show(html);  
}

function showMessageBox() {
    Browser.msgBox('圖片或繪圖:Browser.msgBox 顯示訊息');
  }

開啟 Sheets 會自動出現自訂選項

點選選項二

圖片或繪圖

只有 Sheets 內可以插入圖片或繪圖,並透過點擊來觸發 apps script

插入 => 圖片 => 在儲存格上方插入圖片,[將圖片插入儲存格內] 無法設定觸發 apps script
滑鼠右鍵點選「 ⋮ 」更多 (More) => 指派指令碼
填入 .gs 內觸發函式
顯示訊息

星期二, 8月 26, 2025

[GAS] LineBot 傳送訊息

在 Google Apps Script 內透過 LineBot 傳送訊息和圖片給特定使用者。

.gs Code

圖檔必須為 JPEG or PNG 格式且檔案大小限制
  • originalContentUrl:10MB
  • previewImageUrl:1MB
詳見官方網站說明 Image Message
function send()
{
  LineBot_SendText("Hello From Google Apps Script");
  LineBot_SendImage("https://live.staticflickr.com/123/456.jpg");
}

function LineBot_SendText(textMessage)
{
  let message = [{ type: "text", text: textMessage }];
  LineBot_Send(message);
}

function LineBot_SendImage(fileURL)
{
  let message = [{ 
      type: "image", 
      originalContentUrl: fileURL,
      previewImageUrl: fileURL
      }];

  LineBot_Send(message);
}

function LineBot_Send(message) {

  // 從 PropertiesService 取出 LineChannelAccessToken 和 UserID
  let scriptProperties = PropertiesService.getScriptProperties();
  let properties = scriptProperties.getProperties();
  let lineBotChannelAccessToken = properties["LineChannelAccessToken"];
  let userID = properties["UserID"];
  
  let payload = {
    to: userID,
    messages: message
  };

  let options = {
    headers: { Authorization: "Bearer " + lineBotChannelAccessToken },
    contentType: "application/json; charset=UTF-8",
    method: "post",
    payload: JSON.stringify(payload)
  };

  let lineUrl = "https://api.line.me/v2/bot/message/push";
  UrlFetchApp.fetch(lineUrl, options);  
}

實際執行
訊息費用

免費仔首選,輕用量方案
  • 免月費,每月 200 則訊息則數
  • 訊息則數僅計算主動發送,自動回覆或一對一聊天等互動不列入計算
  • 群組內假如有 5 位,主動發出 1 則訊息至群組,實際算是耗掉 5 則訊息

服務配額

Google 帳號與 Google Apps Sctipt 雖然免費,但其相關服務是有其使用限制,使用 LineBot 來互動時要特別注意,把相關服務限制列出來

功能 個人帳戶 (gmail.com) Google Workspace
觸發條件總執行階段
(Triggers total runtime)
90 分鐘 / 天 6 小時 / 天
網址擷取呼叫次數
(URL Fetch Calls)
20,000 次 / 天 100,000 次 / 天
指令碼執行階段
(Script runtime)
6 分鐘 / 次 6 分鐘 / 次

完整功能限制,請參考官方文章 - Quotas for Google Services

星期日, 8月 24, 2025

[GAS] 存取 GCP Secret Manager

[GAS] Properties Service 提到存放機敏資料在 GCP 上,該筆記記錄使用 Google Apps Script 來存取存在放在 GCP Secret Manager 上的 Token

Secret Manager

在 Google Cloud 上開一個 GASLab 新 Project

搜尋 Secret Manager API 並啟用

啟用後進入密鑰管理員內建立密鑰


建立密鑰資訊

IAM 與管理 (身分與存取權限) 因為剛好就是該 GCP 專案擁有者,預設就有存取權限,假如有分享其他帳號存取,要在這邊進行授予專案存取權限
使用雲服務,當然就免不了會有費用支出,請參考 Secret Manager 定價

在 Google Apps Script 設定 oauth 授權範圍

Google Apps Script 會使用 oauth 去連接 GCP Secret Manager,要先設定 oauth 授權範圍。

Google Apps Script Project settings => 啟用 [在編輯器中顯示「appsscript.json」資訊清單檔案]

在編輯器中出現「appsscript.json」資訊清單檔案並加入 oauth 授權範圍。
appsscript.json 完整資訊,oauthScopes 為手動加入
{
  "timeZone": "Asia/Taipei",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
  "https://www.googleapis.com/auth/script.external_request",
  "https://www.googleapis.com/auth/cloud-platform"]
  }
上述授權相關內容,可以參考官方文章 - Authorization Scopes 的 Set explicit scopes 章節

Google Apps Script 存取

確定專案 ID,在 GCP 上專案資訊業上就可以找到,[專案編號] 或 [專案 ID] 都可以

存取流程大方向如下:組 API 網址  → 取得 OAuth Token  → 發出 Request 取回 payload  → 解析及解碼 payload  → 回傳 Toekn
function getLineBotToken() {

  let projectID = '請自行查詢 Project ID';
  let secretName = 'LineBot_ChannelAccessToken';
  let versionNumber = 'latest'; // 也可以填版號如 '1'、'2'
  let endpoint = `https://secretmanager.googleapis.com/v1/projects/${projectID}/secrets/${secretName}/versions/${versionNumber}:access`;
  
  // 取得 Apps Script 的 OAuth Token,身份驗證使用
  let token = ScriptApp.getOAuthToken();

  // API 呼叫
  let response = UrlFetchApp.fetch(endpoint, 
  {
    headers: {
      Authorization: 'Bearer ' + token,
      Accept: 'application/json',
    }
  });

  // payload.data 欄位是 base64 byte[]
  let data = JSON.parse(response.getContentText())['payload']['data'];

  // 將 base64 byte[] 解碼為 UTF-8 byte[]
  let decoded = Utilities.base64Decode(data);

  // 把 UTF-8 byte[] 轉回字串
  return Utilities.newBlob(decoded).getDataAsString();
}

官方文章 - SecretPayload 提到 data 是 base64-encoded 字串,所以上述流程才會有 decode 動作
實際執行

星期六, 8月 23, 2025

[C#] 自動填入的資料行在重新調整大小時,無法執行此作業

整理公司內部自訂 DataGridView Code 後,執行階段不定時發生 [自動填入的資料行在重新調整大小時,無法執行此作業] 錯誤訊息,即使在設計階段也會發生

執行階段錯誤訊息
錯誤訊息:自動填入的資料行在重新調整大小時,無法執行此作業。
 
於 System.Windows.Forms.DataGridView.PerformLayoutPrivate(Boolean useRowShortcut, Boolean computeVisibleRows, Boolean invalidInAdjustFillingColumns, Boolean repositionEditingControl)
於 System.Windows.Forms.DataGridView.SetColumnHeadersHeightInternal(Int32 columnHeadersHeight, Boolean invalidInAdjustFillingColumns)
於 System.Windows.Forms.DataGridView.AutoResizeColumnHeadersHeight(Boolean fixedRowHeadersWidth, Boolean fixedColumnsWidth)
於 System.Windows.Forms.DataGridView.OnColumnHeadersGlobalAutoSize()
於 System.Windows.Forms.DataGridView.set_TopLeftHeaderCell(DataGridViewHeaderCell value)
於 System.Windows.Forms.DataGridView.get_TopLeftHeaderCell()
於 System.Windows.Forms.DataGridView.GetCellInternal(Int32 columnIndex, Int32 rowIndex)
於 System.Windows.Forms.DataGridView.OnCellMouseEnter(DataGridViewCellEventArgs e)
於 System.Windows.Forms.DataGridView.UpdateMouseEnteredCell(HitTestInfo hti, MouseEventArgs e)
於 System.Windows.Forms.DataGridView.OnColumnWidthChanged(DataGridViewColumnEventArgs e)
於 System.Windows.Forms.DataGridView.OnBandThicknessChanged(DataGridViewBand dataGridViewBand)
於 System.Windows.Forms.DataGridViewBand.set_ThicknessInternal(Int32 value)
於 System.Windows.Forms.DataGridView.AdjustFillingColumns()
於 System.Windows.Forms.DataGridView.ComputeLayout()
於 System.Windows.Forms.DataGridView.PerformLayoutPrivate(Boolean useRowShortcut, Boolean computeVisibleRows, Boolean invalidInAdjustFillingColumns, Boolean repositionEditingControl)
於 System.Windows.Forms.DataGridView.OnHandleCreated(EventArgs e)
於 System.Windows.Forms.Control.WmCreate(Message& m)
於 System.Windows.Forms.Control.WndProc(Message& m)
於 System.Windows.Forms.DataGridView.WndProc(Message& m)
於 System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
於 System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
於 System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)

設計階段發生錯誤
從錯誤訊息推測是 ColumnHeadersHeightSizeMode = AutoSize 造成,從官方文章 DataGridView.ColumnHeadersHeightSizeMode Property 注意到,文件預設值是 EnableResizing,實際在 .NET Framework 上拉一個 DataGridView,預設值為 AutoSize,詢問 AI 是從某一個版本為了 RWD 設計,被修正為 AutoSize,語焉不詳的回答,也沒有辦法確定真偽就是。
在整理 Code 時有順道整合 DataGridView 預設字型,在設計階段時切換 ColumnHeadersHeightSizeMode 屬性 (AutoSize <-> EnableResizing),很明顯觀察到 ColumnHeader 高度一直在變化,把 ColumnHeadersHeightSizeMode 設定為 EnableResizing 並把 ColumnHeader 高度加大 (23 => 28) 可以滿足字型高度來避免該問題

星期五, 8月 22, 2025

[GAS] Utilities.formatDate

Utilities.formatDate(date, timeZone, format) 用於格式化日期物件,需提供三個參數
  • date:JavaScript Date 物件
  • timeZone:目標時區,可用縮寫 (GMT+8) 或完整時區 ID (Asia/Taipei)
  • format:格式字串,遵循 Java SimpleDateFormat 規範

簡易範例
let DateFormatTemplates = {
  ShortDate: 'yyyy/MM/dd',
  FullDateTime: 'yyyy/MM/dd HH:mm:ss'
};

function DateFormatLab() {

  let now = new Date();

  Logger.log("DateFormatTemplates.ShortDate:" + Utilities.formatDate(now, 'GMT+8', DateFormatTemplates.ShortDate));

  let userTimeZone = Session.getScriptTimeZone();
  Logger.log("Session.getScriptTimeZone:" + userTimeZone);
  Logger.log("DateFormatTemplates.FullDateTime:" + Utilities.formatDate(now, userTimeZone, DateFormatTemplates.FullDateTime));

  let date = new Date('2025/08/22');
  Logger.log(date);
}

星期二, 8月 19, 2025

[ZWCAD] 2018 啟動失敗

要在 Win11 24H2 上啟動 ZWCAD 2018 時,線上啟動失敗,然後手動啟用應該要生成的 xml 檔案也都生不出來,指定的目的資料夾內完全不會有 xml 出現,突然想到說會不會是 ZWCAD 2018 程式本身沒有檔案系統權限,雖然登入帳號已經是 adminstrator 了,果然用 [以系統管理員身分] 執行,線上啟動就成功

線上啟動失敗,但沒有提供任何錯誤訊息

[以系統管理員身分] 執行,線上啟動就成功啦
事後發現 Windows 11 KB5063878 / KB5064010 更新會造成 AutoCAD 觸發 UAC,而 UAC 需要系統管理員權限才能繼續執行,不知道是不是該原因造成

星期一, 8月 18, 2025

[GAS] Properties Service

Properties service 可以用來儲存 key-value 資料且資料型態為字串,有三種方式,分別為
  • Script Properties
  • User Properties
  • Document Properties

官方文章-Properties Service 上的分析表
該分析表內關於 Script Properties 說明,應用範圍是專案全域使用,該說明舉例是提到可以拿存放外部 Database 使用者名稱、密碼,認知就算是存放機敏資料 (連線字串、Token 之類),因為找不到官方文章有提到會特別處理,所以特別去研究看看,結論是假如只有該專案只有個人使用就適合,萬一該專案分享給多人就不適合,比把機敏資料明碼放在 gs 檔案內好而已,該篇文章 How to Store Secrets in Google Apps Script 內有張流程圖可以提供思考方向,最好還是放上 GCP 保護還可以分享給多專案使用

GUI 操作

Script Properties 是三種 Property 中,唯一有 GUI 可以手動操作

語法操作

function PropertiesServiceDemo() {
  
  // 取得 ScriptProperties
  let scriptProperties = PropertiesService.getScriptProperties();

  // 新增 TOKEN Property
  scriptProperties.setProperty('TOKEN', '123456789');

  // 新增多個 Property
  scriptProperties.setProperties({
    'Key-1': 'Value1',
    'Key-2': 'Value2'
  });

  // 該 Key 已經存在的話,即為更新目前 Property 值
  scriptProperties.setProperty('TOKEN', 'ABCDEFG');

  // 顯示目前 Property 資料
  let properties = scriptProperties.getProperties();
  for (let key in properties) {
    Logger.log(`Key: ${key}, Value: ${properties[key]}`);
  }  

  // 刪除 Property
  scriptProperties.deleteProperty('Key-2');
}

執行結果


星期五, 8月 15, 2025

[GAS] Gmail 顯示圖片

該篇筆記會記錄
  • 使用 UrlFetchApp 抓 Flickr url 為 Email Inline Image
  • Google Drive 檔案為 Email 附件
function EmailImage() {
  
  // 從 Flcikr url 上抓圖片
  let flickrImage = UrlFetchApp
                          .fetch('https://live.staticflickr.com/123456/ooxx_n.jpg')
                          .getBlob()
                          .setName("自定義檔案名稱");

  // 夾帶 Google Drive 內圖片
  let file = DriveApp.getFileById('檔案 id');

  // mail 內容
  MailApp.sendEmail({
    to: "ooxx@gmail.com",
    subject: "Gmail 顯示圖片",
    htmlBody: 
    "顯示內嵌圖片,資料來源為 Flickr url<br>" +
    "<br>" +
    "<br>" +
    "<img src='cid:flickrImageSource'>",
      inlineImages:
      {
        flickrImageSource: flickrImage
      },
      attachments: [file.getAs(MimeType.JPEG)]
   });
}

執行後結果
InlineImage 重點在於 cid (Content-ID),使用時要注意 [cid 識別名稱] 跟 [InlineImages 識別名稱] 要一致,才能正確嵌進 Email 內,測試時發現識別名稱不一致,圖檔會變成附件,而 SetName("自定義檔案名稱") 函數,當圖檔是附件檔案的話,即為該附件檔案名稱

[cid 識別名稱] 和 [InlineImages 識別名稱]
下圖是故意把識別碼打錯的執行結果,可以看見 InlineImage 變成附件檔案,且附件檔案名稱為 [自訂義檔案名稱]

[RV] 指定列印份數

實務上遇上列印內容不變的標籤,不需要塞資料進去,直接指定份數來達到列印多張標籤
ReportViewer.PrinterSettings.Copies = 指定份數;

PrinterSettings.Copies 屬性 文件有特別提到,並不是每台印表機都支援該屬性的
Not all printers support printing multiple copes. You can use the MaximumCopies property to determine the maximum number of copies the printer supports. If the number of copies is set higher than the maximum copies supported by the printer, only the maximum number of copies will be printed, and no exception will occur.

星期二, 8月 12, 2025

[Forms] 移除外掛

在 [Forms] 外掛-formLimiter 測試完成後,想說要把外掛套件移除,但一時之間還真的找不到從何處進行,Google 一下才發現,原來套件安裝和移除都是從 Google Workspace Marketplace 上進行

星期一, 8月 04, 2025

[Forms] 外掛-formLimiter

使用 Google Forms 常遇上的問題,在於表單常常有填寫結束限制,例如要在某個時間點前填寫完畢,在表單內可以手動透過設定 [不接受回應] 來關閉表單
formLimiter 安裝

該筆記使用 formLimiter 套件達到自動關閉表單,安裝步驟為,點擊「 ⋮ 」更多 (More)  => 取得外掛程式

Google Workspace Marketplace 內搜尋 FormLimiter 就可以找到,安裝時會有授權機制必須允許才能進行安裝

formLimiter 操作和設定

點選上方 [拼圖圖示] => 點選 [formLimiter--PROD] 選項

進入 formLimiter 後 => 點選 Set Limit 
設定頁面

formLimiter 自動關閉表單觸發條件

有三種條件可以進行設定,分別為
  • date and time:特定時間,例如預購活動收單截止日
  • number of form responses:填表人數,例如活動報名人數上限
  • spreadSheet cell value:spreadSheet 指定 cell 值為特定值,該功能必須 Forms 有連結 spreadSheet 時才可以使用,要不然在設定畫面會反灰,例如購買咖啡豆,是以販售總數量為截止條件,在 SpreadSheet 內透過公式累積數量至特定 cell 內,當數量達到販售總數量即關閉表單
以 number of form respnses 條件來記錄該套件使用
when responses are greater then:該設定時有特別需要理解之處,例如該活動允許 100 人報名,不能輸入 100,必須輸入 99,當第 100 位輸入送出表單後即關閉表單,當表單關閉後手動再開啟接受回應,第 101 位填寫送出後,表單會再度被關閉

Message when submissions are closed:可以輸入表單關閉時要顯示自訂資訊,實務上測試並沒有顯示,還是顯示官方訊息,但在該篇文章-Google Form 限制填單筆數上限 是有的
Email form owner when submissions are closed:預設為勾選,測試發現表單自動關閉後,沒有發出 Email 通知

目前測試是表單自動關閉功能正常,雖然關閉文字訊息和關閉 Email 通知沒有如預期運作,整體來說不影響使用

特殊情況

當點選拼圖顯示外掛套件時,正常來說是顯示 [formLimit--PROD],萬一只顯示 [formLimit] 的話,點選進去 [Set Limit] 選項會消失,該情況我重新開表單就會恢復正常,但套件留言內有人提到得重新安裝該套件才會恢復正常

星期日, 8月 03, 2025

[GAS] ScriptApp.getService().getUrl()

[GAS] HTMLService - createTemplateFromFile 練習時,最大困惱莫過於每次重新佈署後,都必須至 index.html 內手動替換掉 form action url,該 url 變成是一種 HardCode 狀態,查詢資料發現,可以在 .gs 檔案內透過 ScriptApp.getService().getUrl() 來取得部屬後的 url

.gs 檔案

透過 ScriptApp.getService().getUrl() 取得 url 後,可以使用方法一,把 url 當成參數塞進 index.html 去,如同上篇筆記傳遞參數做法,該筆記是改採方法二,設定一個 GetUrl()
function doGet(e) {
    let htmloutput = HtmlService.createTemplateFromFile('index')
    htmloutput.date = Date();
    
    // 方法一:當成變數塞進去
    // htmloutput.url = ScriptApp.getService().getUrl();
    // Logger.log(htmloutput.url);

    return htmloutput.evaluate();
}

function doPost(e) {
  return Activity(e);
}

// 方法二:建立 GetUrl()
function GetUrl() {
  let url = ScriptApp.getService().getUrl();
  Logger.log(url);
  return url;
}

function Activity(e) {
  let spreadSheet = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = spreadSheet.getSheetByName("工作表1");
  let data = [e.parameter.email , e.parameter.date , e.parameter.meal , e.parameter.note];
  sheet.appendRow(data);
}
index.html

form action 內直接使用 .gs GetUrl() 來取的部屬後 url
<!DOCTYPE html>
<html>

<head>
  <base target="_top">
</head>

<p>目前時間:<br>
  <?= date ?>!
</p>

<body>
  <!-- action 內直接設定使用 .gs GetUrl() -->
  <form
    action="<?= GetUrl() ?>"
    method="POST">

    <label for="email">Email:</label><br>
    <input type="email" id="email" name="email" size="30"><br><br>

    <label for="date">報名活動日期:</label><br>
    <input type="date" id="date" name="date" size="30" style="width:228px"><br><br>

    <label for="meal">餐點:</label><br>
    <select id="meal" name="meal" style="width:228px">
    <option value="meat">葷</option>
    <option value="vegetarian">素</option>
  </select><br><br>

    <label for="note">備註:</label><br>
    <textarea id="note" name="note" rows="4" cols="30"></textarea><br><br>

    <button type="submit" style="width:228px">送出</button>
  </form>
</body>

</html>
執行結果

因為 GetUrl() 內有使用 Logger.log 輸出,可以在執行項目找到,對應部屬 url 是一致的

學習 ScriptApp.getService().getUrl() 時,發現不少人反應有 bug,目前測試是沒有遇上,先紀錄相關文章就是

星期六, 8月 02, 2025

[GAS] HTMLService - createTemplateFromFile

在 .gs 內使用 HtmlService.createTemplateFromFile(file) 呼叫 html 並把參數傳遞至 html 內

.gs Code

部屬後在瀏覽器上輸入 url 觸發 doGet(),透過 HtmlService.createTemplateFromFile 呼叫 Index.html,並把現在時間塞進去
function doGet(e) {
    let htmloutput = HtmlService.createTemplateFromFile('index')
    // 把現在時間塞進去 index.html 內
    htmloutput.date = Date();
    return htmloutput.evaluate();
}

function doPost(e) {
  return Activity(e);
}

function Activity(e) {
  let spreadSheet = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = spreadSheet.getSheetByName("工作表1");
  let data = [e.parameter.email , e.parameter.date , e.parameter.meal , e.parameter.note];
  sheet.appendRow(data);
}


Index.html

現在時間 <?= date ?> 寫法,就是準備接收 .gs 傳遞過來的參數,html form 標籤內的
  • action:該 url 是 .gs 檔案部屬後取得的 url 網址
  • method:使用 post, .gs 檔案內會呼叫 doPost() 並把使用者輸入資料,儲存進 SpreadSheet 內
<!DOCTYPE html>
<html>

<head>
  <base target="_top">
</head>

<!-- 從 .gs 檔案內把現在時間塞進來 -->
<p>目前時間:<br>
  <?= date ?>!
</p>

<body>
  <form
    action="https://script.google.com/macros/s/AKfycbytjXZ2nGDMG5_0x_Q-8qhLBaJwQEJQqmYyNoiWtsO9utVLedltBUxSJt07luHf5FtPlg/exec"
    method="POST">

    <!-- Email 輸入框 -->
    <label for="email">Email:</label><br>
    <input type="email" id="email" name="email" size="30"><br><br>

    <!-- 報名活動日期 Date Picker -->
    <label for="date">報名活動日期:</label><br>
    <input type="date" id="date" name="date" size="30" style="width:228px"><br><br>

    <!-- 餐點選擇 ComboBox (葷素) -->
    <label for="meal">餐點:</label><br>
    <select id="meal" name="meal" style="width:228px">
    <option value="meat">葷</option>
    <option value="vegetarian">素</option>
  </select><br><br>

    <!-- 備註 TextArea -->
    <label for="note">備註:</label><br>
    <textarea id="note" name="note" rows="4" cols="30"></textarea><br><br>

    <!-- 送出按鈕 -->
    <button type="submit" style="width:228px">送出</button>
  </form>
</body>

</html>

執行畫面
圖像化

把 .gs 和 html 重點弄成一張圖來記憶

星期五, 8月 01, 2025

[RV] PrintDocument.OriginAtMargins 屬性

[RV] 列印本機報表而不進行預覽 內是使用 PrintDocument 來做到直接列印不預覽,文章內沒有特別提到說 OriginAtMargins 屬性,預設值為 false,把官方文件 PrintDocument.OriginAtMargins 屬性 內容整理為表格來呈現

false true
座標原點 印表機可列印區域 (Printable Area) 左上角 使用者設定邊界 (PageSettings.Marign) 的內側
座標計算 忽略 PageSettings.Marign 設定 將 PageSettings.Marign 納入考量

簡易繪製一張 A4 報表往 Windows 內建印表機 Microsoft Print to PDF 輸出來驗證

A4 紙張預設邊界,下圖是從 Word 內擷取出來的
OriginAtMargins = false,以可列印範圍左上角為主
OriginAtMargins = true,以 PageSettings.Margin 為主,也就是 A4 紙張預設 Margin