VBA兩個Sub想一次執行,不必硬把程式碼接成同一個程序,保留各自完整的Sub,再新增主程序用Call依序呼叫即可。贊贊小屋以下載臺灣銀行匯率、依日期複製工作表的兩個Sub實測,記錄註解合併遇到的重複宣告與中斷模式,以及改用主程序後成功執行。
一、VBA兩個Sub,原本可以各自正常執行
贊贊小屋的活頁簿裡原本就有兩個可以單獨執行的Sub。開啟「Microsoft Visual Basic for Applications」編輯器後,左側「專案-VBAProject」的「Microsoft Excel 物件」底下有「ThisWorkbook」,以及「工作表1」「最新匯率」「261027」三張工作表;「模組」資料夾裡則有「Module1」與「Module2」。點開Module1,右側程式碼視窗的第一行是Sub 取得臺灣銀行匯率(),這是第一個Sub,負責連線到臺灣銀行的匯率頁面,把資料寫進「最新匯率」工作表。Module2放的是第二個Sub複製最新匯率並依日期命名(),負責把「最新匯率」複製成一張以日期命名的新工作表。
回到Excel打開「巨集」對話框,「巨集名稱」清單裡正好列著「取得臺灣銀行匯率」與「複製最新匯率並依日期命名」兩項,「巨集存放在」是「所有開啟的活頁簿」。選取其中一項按「執行」,就只會跑那一個Sub,所以要先下載匯率、再複製成當天的工作表,就得分別執行兩次。工作表標籤已經有「最新匯率」與「261027」,表示這兩個Sub單獨執行時都能正常完成工作,贊贊小屋想解決的是怎麼讓它們一次跑完。
Sub是VBA常用的程序(Procedure)類型,從Sub 程序名稱()開始,到End Sub結束,中間放要執行的敘述,完整的Sub寫法可以參考VBA Sub用法。以下只是說明結構的示意,不是贊贊小屋實際使用的程式:
VBA程式|Sub 的基本結構(示意)Sub 程序名稱()
'這裡放要執行的敘述
End Sub
模組(Module)則是存放程序的容器。一個模組可以放多個Sub,一個VBA專案也可以有多個模組;贊贊小屋這次剛好把兩個Sub分別放在Module1與Module2,這種擺法本身不會妨礙它們連續執行。第一個Sub用QueryTables把臺灣銀行匯率頁面的表格匯入工作表,從提出需求到取得匯率資料的操作過程,可以參考Excel 自動抓取網頁資料。
二、嘗試用註解方式,將兩個Sub合併
贊贊小屋最直覺的想法,是把兩個Sub接成一個長程序。於是新增「Module3」,把兩段程式碼放在同一個模組裡,接著在程式碼視窗選取第一個Sub結尾的End Sub,連同第二個Sub的宣告行Sub 複製最新匯率並依日期命名(),再按上方工具列的「使程式行變為註解」按鈕。
按下去之後,選取的三行前面各多了一個單引號',文字變成綠色,成為'End Sub與'Sub 複製最新匯率並依日期命名(),中間的空白行也只剩一個單引號。對VBA來說,這幾行已經不再是程式敘述,第一個Sub原本的結尾和第二個Sub的開頭都被忽略,兩段程式碼在畫面上就接成連續的一段。
VBA的註解是在文字前面加單引號,單引號之後的內容會被當成說明文字而不執行,常用來暫時停用程式或補充說明。可是註解只是讓這幾行不再生效,並不會替兩個原本獨立的程序重新安排邏輯:變數宣告、錯誤處理與程序的結束位置,全都還留在原處。這個做法後來接連引發了好幾個問題,所以它只是贊贊小屋的第一次嘗試,並不是建議的做法。
三、VBA巨集清單出現兩個同名程序,執行時跳出重複宣告錯誤
兩段程式碼放進Module3之後,再打開「巨集」對話框,「巨集名稱」清單變成三項:「Module1.取得臺灣銀行匯率」「Module3.取得臺灣銀行匯率」,以及「複製最新匯率並依日期命名」。原本只有一個Sub叫這個名字時,清單只顯示程序名稱;現在Module1與Module3各有一個同名的取得臺灣銀行匯率,Excel就在前面加上模組名稱來區分。贊贊小屋選取的是「Module3.取得臺灣銀行匯率」,準備執行合併後的版本。
執行時,VBA跳出「Microsoft Visual Basic for Applications」視窗,標題是「編譯錯誤:」,內容是「在目前的有效範圍內重複宣告」。編輯器同時把Module3裡第二段程式碼開頭的Dim ws As Worksheet反白,指出問題就在這一行;它的上方,正是剛才被註解掉的'End Sub與'Sub 複製最新匯率並依日期命名()。
這個現象要分清兩層名稱:「Module1」「Module3」是模組名稱,取得臺灣銀行匯率是模組裡的程序名稱。不同的標準模組可以各自有同名的Sub,巨集清單才會用「模組名稱.程序名稱」的格式區分。依Microsoft的說明,各模組的程序只呼叫自己模組裡的同名程序時不會衝突,但如果從第三個模組呼叫,又沒有用模組名稱限定,就可能出錯;同一個模組裡放兩個同名的程序,則會造成命名衝突。所以測試過程留下的舊版本最好刪除或改名,否則很容易從清單裡選到錯誤的那一個,之後呼叫時也可能出問題。
至於重複宣告,是因為變數的有效範圍以程序為單位。兩個獨立的Sub各自寫Dim ws As Worksheet不會衝突,因為兩個ws分別存在於各自程序的區域範圍,彼此不相干;但贊贊小屋把兩個程序之間的End Sub與Sub宣告註解掉之後,兩個Dim ws As Worksheet就落進同一個程序、同一個有效範圍,VBA便認定同一個名字被宣告了兩次,在執行前先以編譯錯誤擋下來。
四、修改宣告之後,VBA又進入中斷模式
看到重複宣告的錯誤,贊贊小屋在第二個Dim ws As Worksheet前面加上單引號,變成綠色的'Dim ws As Worksheet,下一行的Dim 新工作表名稱 As String則保留。這樣合併後的程序只剩第一次宣告的ws,重複的那一行不再生效。
再次執行時,VBA又跳出新的訊息:「處於中斷模式時,不能執行程式碼」,編輯器視窗的標題列也多了「[中斷]」。
註解掉第二個宣告,只是解決了「同一個程序重複宣告」這一個編譯錯誤,並不代表兩個Sub已經整合成功。這個錯誤有兩種處理方式:把重複的那一行註解掉,或直接刪除多餘的宣告,兩者效果相同;更根本的做法,則是一開始就不要把兩個程序接在一起。
中斷模式是VBA的暫停狀態:程式的執行被暫停在某個位置,讓開發者檢查目前的程式與變數。依Microsoft Learn的無法在中斷模式中執行程式碼說明,發生語法錯誤或執行階段錯誤時,可能在不知不覺中就進入中斷模式;程式還停在暫停狀態時,不能再另外執行程式碼,必須先繼續執行被暫停的程式,或先結束它,才能重新執行,所以才會看到「處於中斷模式時,不能執行程式碼」。這個訊息表示執行狀態被暫停,不能直接解讀成電腦記憶體不足,也不是記憶體沒有釋放。
![VBA視窗標題列顯示[中斷],並跳出「處於中斷模式時,不能執行程式碼」的訊息](assets/a00114-42.webp)
五、使用「重新設定」離開中斷模式,再次執行
要離開這個暫停狀態,贊贊小屋打開編輯器上方的「執行」選單。選單裡有「繼續(C)」(快速鍵F5)、「中斷(K)」、「重新設定(R)」與「設計模式(M)」;因為程式已經處在中斷狀態,「中斷(K)」呈現灰色無法選取,贊贊小屋選擇「重新設定(R)」,讓暫停中的程式結束,回到可以重新執行的狀態。
重新設定後再次執行,Excel跳出訊息「臺灣銀行匯率下載完成!」。但工作表標籤仍然只有「工作表1」「最新匯率」「261027」,並沒有新增當天日期的工作表。也就是說,下載那一段執行了,第二段「複製最新匯率並依日期命名」卻沒有跟著跑,這正是贊贊小屋拿去請教ChatGPT的問題。
「繼續」會從暫停的位置往下執行,「重新設定」則是結束目前暫停中的程式,並清除呼叫堆疊與模組層級變數,之後可以重新從頭執行。選單裡「重新設定」旁邊的藍色方塊圖示,與工具列上的藍色方塊按鈕是同一個功能。這一步也提醒了一件事:訊息方塊顯示「下載完成」,只代表第一段程式跑完,不代表整件工作都完成;要確認結果,還是得回到工作表標籤,檢查有沒有真的出現新的工作表。
六、使用ChatGPT找出兩個Sub合併失敗的原因
卡在這裡,贊贊小屋把問題交給ChatGPT。提問只有一句話,後面直接貼上合併後的程式碼:
傳給ChatGPT的提問我用註解方式將兩個 Sub 合併,但第二個 Sub 卻沒有複製成今天的工作表,是哪裡設錯了?
這句話交代了兩件事:做了什麼(用註解合併兩個Sub),以及實際結果(第二個Sub沒有複製出今天的工作表),再加上完整的程式碼,ChatGPT就有足夠的資料判斷問題。
ChatGPT的回覆先給結論:「不是第二個 Sub 的複製指令寫錯,而是你用註解合併兩個程序時,連第一個程序的結束位置與錯誤處理區塊也一起合併了。」接著指出目前的程式有兩個主要問題,第一個是「你把第一個 Sub 的結束指令註解掉了」,原本應該有End Sub,再接著Sub 複製最新匯率並依日期命名(),現在這兩行都被註解了。
要看懂這個診斷,得先分清楚Exit Sub與End Sub。End Sub是程序的結束界線,Exit Sub則是在程序執行到一半時立刻離開,並不定義程序在哪裡結束,Microsoft Learn的Exit陳述式說明也特別提醒不要把Exit與End混為一談。贊贊小屋的第一個Sub,在下載完成、跳出完成訊息之後,馬上就寫了Exit Sub,後面才是錯誤處理:區塊。合併後的Module3,相關片段是這樣:
VBA程式|Module3 合併版的相關片段 MsgBox "臺灣銀行匯率下載完成!", vbInformation
Exit Sub
錯誤處理:
MsgBox "下載匯率時發生錯誤。" & vbCrLf & _
"請檢查網路連線、工作表名稱或網站是否正常。", _
vbExclamation
'End Sub
'
'Sub 複製最新匯率並依日期命名()
'Dim ws As Worksheet
Dim 新工作表名稱 As String
正常下載時,程式跑到Exit Sub就離開,位在後面的複製程式碼不會執行,所以才會只看到下載完成的訊息,沒有新增工作表。反過來說,只有下載途中出錯、跳到錯誤處理:顯示錯誤訊息之後,程式才會順著往下掉進被接起來的複製程式碼,這剛好和原本想要的順序相反。
這次求助也示範了怎麼向AI提供除錯資訊:把預期的結果、實際看到的結果、做過的操作和相關的完整程式碼一起交給它,比只貼一句「程式壞了」更容易讓AI判斷問題出在哪裡。不過ChatGPT的說明只是幫忙定位問題,是否正確,還是要回到實際程式碼和Excel執行結果確認;ChatGPT後來提供的做法也一樣,要等贊贊小屋實測成功,才確定有效。
七、使用Call讓兩個程序依序執行
要讓兩個Sub依序執行,最直接的做法是保留兩個Sub各自完整,再新增一個主程序,用Call依序呼叫它們。ChatGPT接著給出的解法正是如此,標題是「二、正確做法:保留兩個程序,再新增一個總開關」。它說明不需要用註解把兩個Sub合併,最簡單、最安全的方法,是讓兩個程序各自保持完整,再增加一個主程序,依序呼叫它們,並整理成Excel 2016可以使用、可整段覆蓋貼上的完整VBA程式。畫面中的主程序先判斷取得臺灣銀行匯率()是否等於True,成立才用Call執行複製最新匯率並依日期命名。
贊贊小屋回到VBA編輯器,新增「Module4」,貼上ChatGPT整理的程式,貼入程式與執行巨集的完整步驟,可以參考ChatGPT寫ExcelVBA。程式碼視窗最上方是主程序Sub 一次執行匯率下載與複製(),它下方原本的下載程序,已經從Sub改成Function 取得臺灣銀行匯率() As Boolean,也就是會回傳「是否成功」的程序。Module4的完整程式如下:
VBA程式|Module4 的主程序與兩個程序'==================================================
' 主程序:一次執行下載匯率及複製工作表
'==================================================
Sub 一次執行匯率下載與複製()
'先下載臺灣銀行匯率
If 取得臺灣銀行匯率() = True Then
'下載成功後,才複製並命名工作表
Call 複製最新匯率並依日期命名
End If
End Sub
'==================================================
' 第一個程序:下載臺灣銀行匯率
'==================================================
Function 取得臺灣銀行匯率() As Boolean
Dim ws As Worksheet
Dim 網址 As String
Dim 最後列 As Long
Dim i As Long
'預設為下載失敗
取得臺灣銀行匯率 = False
On Error GoTo 錯誤處理
'設定臺灣銀行匯率網址
網址 = "https://rate.bot.com.tw/xrt?Lang=zh-TW"
'尋找「最新匯率」工作表
Set ws = Nothing
On Error Resume Next
Set ws = ThisWorkbook.Worksheets("最新匯率")
On Error GoTo 錯誤處理
'如果工作表不存在,就自動新增
If ws Is Nothing Then
Set ws = ThisWorkbook.Worksheets.Add
ws.Name = "最新匯率"
End If
'清除舊資料
ws.Cells.Clear
'從臺灣銀行網站匯入網頁表格
With ws.QueryTables.Add( _
Connection:="URL;" & 網址, _
Destination:=ws.Range("A1"))
.WebSelectionType = xlAllTables
.WebFormatting = xlWebFormattingNone
.Refresh BackgroundQuery:=False
.Delete
End With
'刪除沒有匯率數字的多餘資料列
最後列 = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
For i = 最後列 To 1 Step -1
If Application.WorksheetFunction.Count( _
ws.Range("B" & i & ":E" & i)) = 0 Then
ws.Rows(i).Delete
End If
Next i
'調整欄寬
ws.Columns.AutoFit
'回到最新匯率工作表
ws.Activate
'設定下載成功
取得臺灣銀行匯率 = True
MsgBox "臺灣銀行匯率下載完成!", vbInformation
Exit Function
錯誤處理:
MsgBox "下載匯率時發生錯誤。" & vbCrLf & _
"請檢查網路連線、工作表名稱或網站是否正常。", _
vbExclamation
End Function
'==================================================
' 第二個程序:複製最新匯率並依日期命名
'==================================================
Sub 複製最新匯率並依日期命名()
Dim ws As Worksheet
Dim 新工作表名稱 As String
On Error GoTo 錯誤處理
'取得今天的日期,例如 261028
新工作表名稱 = Format(Date, "yymmdd")
'確認「最新匯率」工作表是否存在
Set ws = Nothing
On Error Resume Next
Set ws = ThisWorkbook.Worksheets("最新匯率")
On Error GoTo 錯誤處理
If ws Is Nothing Then
MsgBox "找不到「最新匯率」工作表!", vbExclamation
Exit Sub
End If
'確認今天的工作表是否已經存在
Set ws = Nothing
On Error Resume Next
Set ws = ThisWorkbook.Worksheets(新工作表名稱)
On Error GoTo 錯誤處理
If Not ws Is Nothing Then
MsgBox "工作表「" & 新工作表名稱 & _
"」已經存在,請勿重複複製!", vbExclamation
Exit Sub
End If
'複製「最新匯率」工作表
ThisWorkbook.Worksheets("最新匯率").Copy _
After:=ThisWorkbook.Worksheets( _
ThisWorkbook.Worksheets.Count)
'將複製後的工作表重新命名
ActiveSheet.Name = 新工作表名稱
MsgBox "匯率工作表複製完成!" & vbCrLf & _
"新工作表名稱:" & 新工作表名稱, vbInformation
Exit Sub
錯誤處理:
MsgBox "複製或重新命名工作表時發生錯誤。" & vbCrLf & _
"錯誤原因:" & Err.Description, vbExclamation
End Sub
執行主程序一次執行匯率下載與複製之後,先執行下載,跳出「臺灣銀行匯率下載完成!」,接著執行複製,出現「匯率工作表複製完成!新工作表名稱:261028」。工作表標籤在「最新匯率」「261027」之後多了「261028」,贊贊小屋實測確認,一次執行就完成下載匯率與依日期複製工作表兩件事。
主程序用的Call是呼叫其他程序的指令:Call 程序名稱會讓VBA先去執行指定的Sub,執行完再回到主程序往下走,完整語法可以參考Microsoft Learn的Call陳述式。不過Call並不是必要的,直接寫程序名稱也能呼叫,例如把Call 複製最新匯率並依日期命名改成複製最新匯率並依日期命名,結果相同。要注意的是傳遞參數時的括號:使用Call時,參數要放在括號裡;省略Call,參數就不加括號。這次兩個程序都沒有參數,所以不會遇到這個問題。
下載程序改成Function,是因為Sub只負責執行一連串動作,不會回傳值,也不能當成運算式的一部分;Function除了執行動作,還能回傳一個值,可以直接寫在If判斷裡使用,主程序要知道「下載有沒有成功」,就需要這個值。Module4裡的Function一開始先把取得臺灣銀行匯率設成False,也就是預設失敗;只有程式一路跑到最後,才把它設成True。中途出錯會跳進錯誤處理:區塊,顯示錯誤訊息後結束,回傳值仍然是False,主程序的If條件不成立,就不會去複製工作表。這是用Boolean(布林值,只有True與False兩種結果)當成功狀態的簡單防呆。
不過回傳True的條件,只是下載程序執行到最後都沒有進入錯誤處理,並不代表網頁上的資料內容一定正確或完整,使用者仍然需要看一下「最新匯率」工作表裡的幣別與數字。日期命名也有類似的限制:Format(Date, "yymmdd")產生的名稱每天不同,但同一天再執行一次,就會得到同樣的名稱。依Module4的程式設計,複製程序會先檢查同名的工作表(檢查前先把ws清空的原因,可以參考VBA錯誤處理),如果當天已經存在,就跳出「工作表「261028」已經存在,請勿重複複製!」這類提醒並結束,不會重複建立;同一天重複執行主程序時,下載仍然會再跑一次,被擋下的只有複製這一步。
把兩個功能拆成獨立程序,再用主程序串接,就是常說的模組化設計。每個程序只負責一件事,哪個環節出問題,就只需要修改那一段;將來若有別的工作也需要「複製成日期工作表」,直接呼叫同一個程序就可以,不必再複製貼上一份。
VBA兩個Sub,保留獨立程序更容易維護
回頭看這一路,贊贊小屋原本以為合併兩個Sub,就是把程式碼接在一起,沒想到每解決一個錯誤,後面又冒出下一個:先是重複宣告,再是中斷模式,最後連複製也沒有跑。真正有效的做法反而是什麼都不硬接,讓下載和複製各自保持完整,再用一個主程序決定它們的先後順序。
這也是處理VBA兩個Sub比較穩的方式:程序保留清楚的功能與邊界,要組合時再用Call或程序名稱串起來,哪個環節需要修改,就只動那一個程序。至於ChatGPT提供的方案,贊贊小屋同樣是貼進Excel實測,看到「261028」真的出現,才確認它可用;AI給的程式碼可以省下很多摸索的時間,但最後的判斷,還是得回到自己的工作表上驗證。
