想像你正在編輯一份投影片,點一下自訂選單,「插入封面」「刷新樣板」唰唰兩下就完成——背後的關鍵就是 getActivePresentation()。
它像把門鑰匙,直接打開「眼前那份」簡報,省去管理 ID 的麻煩。不過,這把鑰匙只在「前台」好使:有 UI、有焦點、有人按鈕的時候。丟到背景排程或遠端觸發,十之八九撲空。
這篇文章會把情境分清楚:互動用 Active、批次用 ID;還有權限、安全、選單觸發的最佳實務、以及踩坑紀錄。目標很單純——讓你的 Slides 自動化少寫一行錯一半,該快的地方快,該穩的地方穩。希望本篇文章能幫助到需要的您。
目錄
{tocify} $title={目錄}
先說結論
SlidesApp.getActivePresentation() 會回傳目前在瀏覽器中處於作用中的簡報(active presentation)的 Presentation 物件;如果當前沒有可用的「作用中簡報」,就會回傳 null 或在某些觸發情境下直接拋錯。
它只適合:從 Google 簡報檔案裡的「附加元件按鈕、選單、巨集、Apps Script 編輯器(綁定同一份檔)」觸發的互動流程;不適合定時觸發器(time-driven)、獨立腳本在背景跑、Webhook、或沒有開著簡報的情況。
若你的流程不保證有作用中簡報,請改用 SlidesApp.openById(id) 或 SlidesApp.openByUrl(url) 以檔案 ID/URL 開啟;這是最保險的寫法。
權限方面,操作簡報通常需要 https://www.googleapis.com/auth/presentations 或較窄的 presentations.currentonly scope;Apps Script 會自動偵測,但發佈附加元件時建議明確縮限。
getActivePresentation() 是什麼?為什麼大家會用它?
在 Apps Script 的 Slides 服務裡,SlidesApp.getActivePresentation() 是拿到「目前被使用者在前端開啟、且具有焦點」的那份 Google 簡報的捷徑。拿到這個 Presentation 物件後,你就能:
讀取標題、擁有者、ID(getId())、版面配置、投影片清單。
編輯內容:插入文字方塊、圖片、形狀、註解、連結、或複製其他簡報的投影片。
它的好處是「少傳一個參數」:不用自己管理簡報 ID、也不用硬寫死路徑。只要確定腳本的執行環境真有「作用中簡報」,就能秒取物件、直接改。
什麼情境該用?什麼情境別用?
適合:
1. 簡報內的自訂選單/巨集
在 Slides 檔開著時,點自訂選單的功能,腳本便能抓到作用中簡報並立即處理。
2. 編輯器附加元件(Slides Add-on) 的前端互動
使用者在簡報中操作附加元件 UI,呼叫後端 Apps Script 時,多半會期望對「眼前這一份」檔案動手。
不適合(高風險):
1. 定時觸發器(time-driven)或 onFormSubmit 這類非 UI 觸發
沒有任何「使用者前端」與「作用中簡報」概念,getActivePresentation() 可能是 null 或直接拋錯。請改用 openById()。
2. 獨立腳本(standalone)在 Apps Script 主控台執行
若當下不是從某份簡報的「綁定腳本」開啟,通常沒有 active 檔案上下文。
3. App 外掛、Webhook、Apps Script API 遠端喚起
這些多半在 server-to-server 或背景流程裡執行,沒有作用中的文件。
4. 跨帳號或多帳號登入混用情境
使用者同時在多個 Google 帳號登入時,Active 檔可能被錯誤解析,甚至拋錯;務必以檔案 ID 明確開啟。
前置作業:建立安全、可維護的專案
1. 準備一份測試簡報,並在其「擴充功能 → Apps Script」建立綁定腳本(container-bound)。
2. 在 appsscript.json 縮限權限(若你要發佈附加元件)
一般讀寫請用 presentations,若只需讀取目前文件可考慮 presentations.currentonly。
3. 避免在 Simple Trigger 做重手術
Slides 僅支援 onOpen(e) 的簡單觸發,且很多操作在 simple trigger 會受限;若要做大量修改,改由使用者點擊選單來觸發。
4. 必要時改用 openById()/openByUrl()
把目標簡報 ID 存設定檔或試算表,避免「找不到 active presentation」的脆弱依賴。
一步步操作示範
範例 A:抓基本資訊(標題、ID、投影片數)
function logActivePresentationInfo() {
const p = SlidesApp.getActivePresentation(); // 需有作用中簡報
if (!p) {
throw new Error('找不到作用中簡報:請從簡報檔內的綁定腳本執行,或改用 openById()。');
}
const title = p.getName();
const id = p.getId(); // 取得簡報 ID,亦可用於 openById() 打開同一份檔
const slideCount = p.getSlides().length;
Logger.log({ title, id, slideCount });
}
重點:getId() 能拿到當前簡報 ID,方便後續記錄或日後背景批次用 openById() 操作。
範例 B:在目前頁面插入方塊並套超連結
function insertCalloutOnCurrentPage() {
const p = SlidesApp.getActivePresentation();
if (!p) throw new Error('沒有作用中簡報。');
// 取得「目前選取的頁面」
const selection = p.getSelection();
const page = selection ? selection.getCurrentPage() : null; // 可能為 null
const slide = page && page.asSlide ? page.asSlide() : p.getSlides()[0];
const shape = slide.insertShape(SlidesApp.ShapeType.RECTANGLE, 60, 60, 420, 80);
const textRange = shape.getText();
textRange.setText('文件自動化成功 立即開啟說明');
textRange.getTextStyle().setBold(true);
// 附上外部教學或專案文件的連結
const link = textRange.getLink();
if (!link) textRange.setLinkUrl('https://your-docs.example.com/');
}
小知識:你也能在 Slides 內操作連結物件與連結到其他投影片的「非 URL 連結類型」。
範例 C:把另一份範本簡報的第一張投影片「連結式」插入到目前簡報
function appendLinkedSlideFromTemplate(templateId) {
const current = SlidesApp.getActivePresentation();
if (!current) throw new Error('沒有作用中簡報。');
const source = SlidesApp.openById(templateId); // 明確以 ID 開啟
const srcSlide = source.getSlides()[0];
// 以「連結」模式插入,後續可刷新來源更新
const linked = current.appendSlide(
srcSlide,
SlidesApp.SlideLinkingMode.LINKED
);
// 你也可稍後用 linked.refresh() 更新,或 linked.unlink() 解除連結
return linked;
}
連結式投影片可保持與來源同步,但刷新時會覆蓋本地變更;務必告知使用者。
避免踩雷:十大常見錯誤與解法
1. TypeError: Cannot read properties of null(或 p 為 null)
成因:沒有作用中簡報、在背景觸發跑。
解法:改用 openById();或要求使用者從簡報內的選單觸發。
2. 在 onOpen(e) 做大量修改導致不穩
成因:Slides 的 simple trigger 能力有限;某些操作(包含 getActivePresentation())在觸發初期易失敗。
解法:在 onOpen 只建立選單;實際操作放到使用者點選的函式裡。
3. 多帳號登入導致 Active 解析錯亂或授權錯
成因:同時登入多個 Google 帳號,前端與後端會話不一致。
解法:建議改 openById(),並請使用者以單一帳號測試。
4. 授權被放大或不足
成因:Apps Script 自動偵測 scopes,但註解的程式碼也可能被掃描;附加元件上架需要最小權限。
解法:在 appsscript.json 明確設定需要的 Scopes(例如 presentations.currentonly),並審視每一次變更。
5. 以為 getActivePresentation() 到處都能用
成因:混淆文件情境(Docs/Sheets/Slides 各有 Active 概念,但觸發環境不同)。
解法:只在有 UI 的互動流程用;其他一律 openById()。
6. 把「樣板投影片」硬貼上導致後續維護困難
成因:單純複製而非用「連結式」插入,來源更新不會跟上。
解法:若要長期維護,改用 SlideLinkingMode.LINKED,必要時再 unlink()。
7. 在觸發器中讀取「目前選到的頁面」卻拿到 null
成因:沒有使用者前端選取狀態。
解法:流程設計上改為由使用者在前端點擊後再執行,或自動定位預設頁面。
8. 把 getActiveSpreadsheet() 的行為套用到 Slides
成因:跨產品遷移經驗,忽略不同容器差異。
解法:檢查對應產品可用的觸發、授權、與 Active 規則;若在附加元件或所有者轉移後出現 null,先以 ID 開啟驗證。
9. 在 onOpen 裡嘗試 openById() 期望切換到別份簡報
說明:onOpen 只是目前檔案的生命週期事件,並不是用來「跳檔」。
建議:保持 onOpen 輕量,將跨檔操作放在使用者指令流程。
10. 未建立「ID 管理機制」
成因:過度依賴 Active,導致批次/背景流程不可移植。
解法:把簡報 ID 集中存放於試算表或屬性服務(PropertiesService),並建立讀寫 API;所有批次流程一律以 ID 開啟。
實務設計建議(可直接套入專案)
1. 雙路徑策略(互動 vs. 批次)
互動路徑:Active 模式 → getActivePresentation(),體驗快、少參數。
批次路徑:明確檔案模式 → openById(),穩定可重跑。
2. 選單只做導航,不做重工
onOpen(e) 只建立 UI;真正修改內容的函式由使用者點下去再跑。
3. 權限最小化
發佈附加元件前,檢查 manifest scopes,盡量使用 presentations.currentonly;只有在需要跨檔存取時才升級到完整 presentations。
4. 以「連結式」維護範本
用 SlideLinkingMode.LINKED 管理模板同步;教育使用者「刷新會覆蓋本地變更」。
除錯指南(快速路徑)
先判斷執行環境:是從簡報裡的選單按鈕觸發嗎?若不是,直接改用 openById()。
檢查多帳號:請測試者暫時只登入一個 Google 帳號。
看授權:到專案設定開啟 manifest 檢視,核對 scopes 是否過大或過小;重新授權一次。
簡化 onOpen:把重邏輯移到使用者點選的命令函式。
記錄 ID:用 getId() 把當前檔案 ID 打到 Logger,複製下來改跑 openById() 比對行為。
問題集
Q1:為什麼我在 Apps Script 編輯器直接跑 getActivePresentation() 會是 null?
A:你多半不是從簡報的「綁定專案」執行,或當下沒有作用中的 Slides UI。建議改從簡報裡的「擴充功能 → Apps Script」執行,或直接使用 openById()。
Q2:可不可以在定時觸發器裡跑 getActivePresentation()?
A:不建議。定時觸發器沒有前端環境,拿不到 Active。請以 openById() 開檔。
Q3:onOpen(e) 能不能直接幫我把模板插入?
A:技術上可行但不穩定,且會影響開檔速度。建議 onOpen 只做 UI;插入動作由使用者點選後再執行。
Q4:我要發佈附加元件,要用哪些 Scopes?
A:視你的功能而定。若只操作當前檔案,優先考慮 presentations.currentonly;若需跨檔讀寫再用 presentations。在 manifest 明確列出,並確保說明文件解釋用途。
Q5:如何取得目前選到的投影片?
A:SlidesApp.getActivePresentation().getSelection().getCurrentPage() 可取到當前頁,再用 asSlide() 轉為 Slide 操作;在沒有前端選取的情境下可能為 null。
實作模板
1. 建立選單 + 安全操作骨架
function onOpen(e) {
SlidesApp.getUi()
.createMenu('自動化工具')
.addItem('插入標準版頭', 'action_insertHeader')
.addItem('同步模板首頁', 'action_syncCover')
.addToUi();
}
function getCurrentPresentationOrFail() {
const p = SlidesApp.getActivePresentation();
if (!p) throw new Error('找不到作用中簡報。請從簡報內使用選單執行,或改用 openById()。');
return p;
}
function action_insertHeader() {
const p = getCurrentPresentationOrFail();
const slide = p.getSlides()[0];
const shape = slide.insertShape(SlidesApp.ShapeType.TEXT_BOX, 40, 40, 520, 48);
shape.getText().setText('公司名|報告主題|YYYY-MM-DD').getTextStyle().setBold(true);
}
function action_syncCover() {
const TEMPLATE_ID = '你的範本簡報ID';
const p = getCurrentPresentationOrFail();
const src = SlidesApp.openById(TEMPLATE_ID);
const srcSlide = src.getSlides()[0];
const linked = p.appendSlide(srcSlide, SlidesApp.SlideLinkingMode.LINKED);
// 可另外提供 UI 告知:連結頁面刷新時會覆蓋本地改動
}
這個骨架把「抓 Active」包在小函式裡,集中處理錯誤訊息;也示範了互動式(點選單)才執行重操作的做法。
2. 批次流程版本(沒有 Active 的情境)
function batchUpdateCoverById(presentationId) {
const p = SlidesApp.openById(presentationId); // 關鍵:不要依賴 Active
const slide = p.getSlides()[0];
slide.replaceAllText('{{DATE}}', Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy-MM-dd'));
// 其他批次作業...
}
批次流程用 openById() 是王道,也利於部署到多份檔。
