Initial commit
This commit is contained in:
298
skills/xlsx/scenes/vba.md
Executable file
298
skills/xlsx/scenes/vba.md
Executable file
@@ -0,0 +1,298 @@
|
||||
# VBA — Macro Generation & Management Guide
|
||||
|
||||
Load this reference when the task involves: creating Excel macros, writing VBA code, automating Excel workflows, adding buttons/forms, modifying existing macros, or any `.xlsm` deliverable that needs programmatic automation.
|
||||
|
||||
Also load `engines/vba-templates.md` for ready-to-use code templates.
|
||||
|
||||
---
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. Safety First
|
||||
- **Never** generate VBA that deletes files, accesses filesystem outside the workbook, or sends data to external URLs without explicit user request
|
||||
- **Always** include error handling (`On Error GoTo`)
|
||||
- **Always** add `Application.ScreenUpdating` toggle for performance
|
||||
- Generated macros must be **read-audit-friendly**: clear naming, comments, structured layout
|
||||
|
||||
### 2. openpyxl VBA Workflow
|
||||
openpyxl can read/preserve/inject VBA but **cannot execute** it. The workflow:
|
||||
|
||||
```python
|
||||
# READ existing VBA
|
||||
from openpyxl import load_workbook
|
||||
wb = load_workbook('file.xlsm', keep_vba=True)
|
||||
# wb.vba_archive contains all VBA modules
|
||||
|
||||
# CREATE new .xlsm with VBA
|
||||
from openpyxl import Workbook
|
||||
wb = Workbook()
|
||||
# ... build sheets ...
|
||||
# Inject VBA via vbaProject.bin (see Injection section)
|
||||
wb.save('output.xlsm')
|
||||
```
|
||||
|
||||
### 3. File Format Rules
|
||||
| Need | Format | Extension |
|
||||
|------|--------|-----------|
|
||||
| Data only, no macros | OpenXML | `.xlsx` |
|
||||
| Contains VBA macros | Macro-Enabled | `.xlsm` |
|
||||
| Binary with macros | Binary | `.xlsb` |
|
||||
|
||||
**Critical**: If user gives `.xlsx` but wants macros → output must be `.xlsm`. Always warn about format change.
|
||||
|
||||
---
|
||||
|
||||
## VBA Code Structure Standard
|
||||
|
||||
Every generated VBA module must follow this structure:
|
||||
|
||||
```vba
|
||||
Option Explicit
|
||||
|
||||
' ============================================================
|
||||
' Module: [ModuleName]
|
||||
' Purpose: [One-line description]
|
||||
' Author: Z.ai
|
||||
' Date: [YYYY-MM-DD]
|
||||
' ============================================================
|
||||
|
||||
' --- Constants ---
|
||||
Private Const MODULE_NAME As String = "[ModuleName]"
|
||||
|
||||
' --- Main Entry Point ---
|
||||
Public Sub Main()
|
||||
On Error GoTo ErrHandler
|
||||
Application.ScreenUpdating = False
|
||||
Application.Calculation = xlCalculationManual
|
||||
|
||||
' [Main logic here]
|
||||
|
||||
CleanUp:
|
||||
Application.ScreenUpdating = True
|
||||
Application.Calculation = xlCalculationAutomatic
|
||||
Exit Sub
|
||||
|
||||
ErrHandler:
|
||||
MsgBox "Error in " & MODULE_NAME & ": " & Err.Description, _
|
||||
vbCritical, "Error"
|
||||
Resume CleanUp
|
||||
End Sub
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
| Element | Convention | Example |
|
||||
|---------|-----------|---------|
|
||||
| Sub/Function | PascalCase | `GenerateMonthlyReport` |
|
||||
| Variable | camelCase | `lastRow`, `wsData` |
|
||||
| Constant | UPPER_SNAKE | `MAX_ROWS`, `REPORT_TITLE` |
|
||||
| Module | PascalCase | `ModReport`, `ModUtils` |
|
||||
| Worksheet variable | ws + Name | `wsData`, `wsSummary` |
|
||||
| Range variable | rng + Desc | `rngData`, `rngHeaders` |
|
||||
|
||||
### Variable Declaration Rules
|
||||
```vba
|
||||
' Always use explicit types
|
||||
Dim lastRow As Long ' Not Integer (row limit)
|
||||
Dim ws As Worksheet
|
||||
Dim rng As Range
|
||||
Dim cell As Range
|
||||
Dim i As Long
|
||||
Dim strValue As String
|
||||
Dim dblAmount As Double
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Find Last Row/Column (Robust)
|
||||
```vba
|
||||
' Last row with data in column A
|
||||
Dim lastRow As Long
|
||||
lastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
|
||||
|
||||
' Last column with data in row 1
|
||||
Dim lastCol As Long
|
||||
lastCol = ws.Cells(1, ws.Columns.Count).End(xlToLeft).Column
|
||||
|
||||
' Used range (less reliable but useful)
|
||||
Dim usedRows As Long
|
||||
usedRows = ws.UsedRange.Rows.Count
|
||||
```
|
||||
|
||||
### Loop Through Data
|
||||
```vba
|
||||
' Row loop
|
||||
Dim i As Long
|
||||
For i = 2 To lastRow ' Skip header
|
||||
If ws.Cells(i, 1).Value <> "" Then
|
||||
' Process row
|
||||
End If
|
||||
Next i
|
||||
|
||||
' For Each (range)
|
||||
Dim cell As Range
|
||||
For Each cell In ws.Range("A2:A" & lastRow)
|
||||
If Not IsEmpty(cell) Then
|
||||
' Process cell
|
||||
End If
|
||||
Next cell
|
||||
```
|
||||
|
||||
### Sheet Operations
|
||||
```vba
|
||||
' Reference sheet safely
|
||||
Dim ws As Worksheet
|
||||
On Error Resume Next
|
||||
Set ws = ThisWorkbook.Sheets("Data")
|
||||
On Error GoTo 0
|
||||
If ws Is Nothing Then
|
||||
MsgBox "Sheet 'Data' not found!", vbExclamation
|
||||
Exit Sub
|
||||
End If
|
||||
|
||||
' Create sheet if not exists
|
||||
Dim wsNew As Worksheet
|
||||
Dim sheetExists As Boolean
|
||||
For Each wsNew In ThisWorkbook.Sheets
|
||||
If wsNew.Name = "Summary" Then sheetExists = True
|
||||
Next wsNew
|
||||
If Not sheetExists Then
|
||||
Set wsNew = ThisWorkbook.Sheets.Add(After:=ThisWorkbook.Sheets(ThisWorkbook.Sheets.Count))
|
||||
wsNew.Name = "Summary"
|
||||
End If
|
||||
```
|
||||
|
||||
### User Interaction
|
||||
```vba
|
||||
' Simple input
|
||||
Dim userInput As String
|
||||
userInput = InputBox("Enter report month (YYYY-MM):", "Month Selection")
|
||||
If userInput = "" Then Exit Sub
|
||||
|
||||
' Confirmation
|
||||
If MsgBox("Generate report for " & userInput & "?", _
|
||||
vbYesNo + vbQuestion, "Confirm") = vbNo Then Exit Sub
|
||||
|
||||
' File picker
|
||||
Dim filePath As Variant
|
||||
filePath = Application.GetOpenFilename( _
|
||||
FileFilter:="Excel Files (*.xlsx;*.xlsm),*.xlsx;*.xlsm", _
|
||||
Title:="Select Source File")
|
||||
If filePath = False Then Exit Sub
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## VBA Injection via openpyxl
|
||||
|
||||
### Method 1: Preserve Existing VBA
|
||||
```python
|
||||
# Open with VBA preserved
|
||||
wb = load_workbook('source.xlsm', keep_vba=True)
|
||||
# Edit data/formatting as usual
|
||||
wb.save('output.xlsm') # VBA modules intact
|
||||
```
|
||||
|
||||
### Method 2: Copy VBA from Template
|
||||
```python
|
||||
# Use a template .xlsm that already has the VBA you need
|
||||
import shutil
|
||||
shutil.copy('template_with_macros.xlsm', 'output.xlsm')
|
||||
wb = load_workbook('output.xlsm', keep_vba=True)
|
||||
# Modify data
|
||||
wb.save('output.xlsm')
|
||||
```
|
||||
|
||||
### Method 3: Manual vbaProject.bin Injection
|
||||
```python
|
||||
# For advanced use: inject raw vbaProject.bin
|
||||
# 1. Create your VBA in Excel, save as .xlsm
|
||||
# 2. Extract vbaProject.bin from the .xlsm (it's a ZIP)
|
||||
# 3. Inject into new workbook
|
||||
|
||||
import zipfile
|
||||
import shutil
|
||||
|
||||
# Create the workbook first
|
||||
wb = Workbook()
|
||||
# ... add data ...
|
||||
wb.save('temp.xlsx')
|
||||
|
||||
# Convert to .xlsm by injecting VBA
|
||||
shutil.copy('temp.xlsx', 'output.xlsm')
|
||||
with zipfile.ZipFile('output.xlsm', 'a') as zf:
|
||||
zf.write('vbaProject.bin', 'xl/vbaProject.bin')
|
||||
|
||||
# Update [Content_Types].xml to register VBA
|
||||
# (This is fragile — Method 1 or 2 preferred)
|
||||
```
|
||||
|
||||
**Recommendation**: Method 1 (preserve) or Method 2 (template) are robust. Method 3 is fragile and should be last resort.
|
||||
|
||||
---
|
||||
|
||||
## Security Checklist
|
||||
|
||||
Before delivering any VBA-enabled file:
|
||||
|
||||
- [ ] No filesystem access outside workbook (no `Kill`, `FileCopy`, `MkDir` unless requested)
|
||||
- [ ] No network calls (`XMLHTTP`, `WinHttpRequest`) unless requested
|
||||
- [ ] No shell execution (`Shell`, `WScript.Shell`) unless requested
|
||||
- [ ] No registry access (`CreateObject("WScript.Shell").RegWrite`)
|
||||
- [ ] No auto-execution (`Auto_Open`, `Workbook_Open`) unless explicitly requested
|
||||
- [ ] Error handling in every Sub/Function
|
||||
- [ ] `ScreenUpdating` restored in cleanup
|
||||
- [ ] All variables explicitly declared (`Option Explicit`)
|
||||
- [ ] Module purpose documented in header comment
|
||||
|
||||
---
|
||||
|
||||
## Performance Guidelines
|
||||
|
||||
```vba
|
||||
' ALWAYS bracket bulk operations
|
||||
Application.ScreenUpdating = False
|
||||
Application.Calculation = xlCalculationManual
|
||||
Application.EnableEvents = False
|
||||
|
||||
' [Bulk operations here]
|
||||
|
||||
Application.EnableEvents = True
|
||||
Application.Calculation = xlCalculationAutomatic
|
||||
Application.ScreenUpdating = True
|
||||
```
|
||||
|
||||
### Array-Based Processing (for large data)
|
||||
```vba
|
||||
' Read range into array — much faster than cell-by-cell
|
||||
Dim data As Variant
|
||||
data = ws.Range("A1:Z" & lastRow).Value ' 2D array
|
||||
|
||||
' Process in memory
|
||||
Dim i As Long
|
||||
For i = LBound(data, 1) To UBound(data, 1)
|
||||
data(i, 3) = data(i, 1) * data(i, 2) ' Column C = A * B
|
||||
Next i
|
||||
|
||||
' Write back in one shot
|
||||
ws.Range("A1:Z" & lastRow).Value = data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging Support
|
||||
|
||||
When user reports VBA errors, include diagnostic code:
|
||||
|
||||
```vba
|
||||
' Debug logging to Immediate Window
|
||||
Debug.Print "Processing row " & i & ": " & ws.Cells(i, 1).Value
|
||||
|
||||
' Verbose error info
|
||||
ErrHandler:
|
||||
Debug.Print "ERROR in " & MODULE_NAME
|
||||
Debug.Print " Number: " & Err.Number
|
||||
Debug.Print " Description: " & Err.Description
|
||||
Debug.Print " Source: " & Err.Source
|
||||
```
|
||||
Reference in New Issue
Block a user