【検証】生成AIはVBAをOffice Scriptsに変換できるか?Win32 APIの壁とAIへの正しい依頼方法

【VBA→Office Scripts】生成AIで変換してみた
目次

はじめに

前回までのシリーズでは、VBAとOffice Scriptsの違いや基本的な書き換えパターンを解説してきました。

今回は実際のVBAコードを生成AIに渡して、Office Scriptsへの変換を試みます。

題材は kurumico.com で公開している「スリープ対策VBA」のサンプルファイル。このVBAには keybd_event というWindows専用のAPIが使われており、変換の難易度が高いコードです。

検証を通じて見えてきたのは「AIに何を・どう伝えるか」によって変換品質が大きく変わるという事実。記事の後半では、再現性のある変換依頼テンプレートもご紹介します。

今回の題材:汎用転記エンジンVBAの仕様

今回使用するVBAは、表面上は「スリープ対策コード」ですが、その本体は動的な列マッピングで2シート間のデータを引き当てる汎用転記エンジンです。

シートの構造

このVBAが前提とするシート構造は以下のとおりです。

貼付元シート(マクロ実行シート)

内容
1行目設定情報(G1=データ開始行、K1=貼付先シート名)
2行目マッピング定義(▼=ID照合列、1・2・3…=転記列番号)
3行目見出し行
4行目〜データ本体

貼付先シート

内容
1行目設定情報(G1=データ開始行)
2行目マッピング定義(▼=ID照合列、1・2・3…=転記先列番号)
3行目見出し行
4行目〜転記先データ(IDのみ入力済み)

動作の仕組み

貼付先シートの▼列(ID)を1件ずつ取り出す
 ↓
貼付元シートの▼列から同じIDを検索(MATCH)
 ↓
2行目の数字(1, 2, 3…)で定義された列のデータを取得
 ↓
貼付先シートの対応する列へ転記

ポイント: 列の番号はシートごとに自由な位置に置けるため、シート構造が異なっていても自動でマッピングされます。たとえば貼付元の「商品名」がB列でも、貼付先では別の列に転記できます。

VBAコードの全体像

変換対象のVBAコードです。#If VBA7 による32/64ビット対応の宣言と、メインの処理ループが含まれます。

Option Explicit
#If VBA7 Then
Private Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)
Private Declare PtrSafe Sub keybd_event Lib "user32" ( _
    ByVal bVk As Byte, ByVal bScan As Byte, _
    ByVal dwFlags As Long, ByVal dwExtraInfo As LongPtr)
#Else
Private Declare Sub Sleep Lib "KERNEL32" (ByVal dwMilliseconds As Long)
Private Declare Sub keybd_event Lib "user32" (ByVal bVk As Byte, _
    ByVal bScan As Byte, ByVal dwFlags As Long, ByVal dwExtraInfo As Long)
#End If

Private Const KEYEVENTF_KEYUP = &H2

Sub Array_match()
    ' ~ メイン処理(省略)~
    ' ループ内でkeybd_eventを実行(スリープ防止)
    If tgetTmpR Mod 1000 = 0 Then
        keybd_event 0, 0, 0, 0
        keybd_event 0, 0, KEYEVENTF_KEYUP, 0
    End If
End Sub

VBAの起動方法:シートボタンによるマクロ実行

元のVBAはシート上に設置したボタン(フォームコントロール) を押すことで起動する設計になっていました。

シート上のボタン → クリック → Array_match() マクロが実行される

VBAではシート上にボタンを設置し、右クリック →「マクロの登録」で任意のSubプロシージャを割り当てるのが一般的な操作起動の方法です。ボタンはシートに常駐しているため、ファイルを開くたびに同じ操作でマクロを実行できます。

Office Scriptsでの代替方法

Office Scriptsも同様に、シート上のボタンにスクリプトを割り当てることができます。

手順は以下のとおりです。

  1. Excel on the web で「挿入」タブをクリック
  2. 「ボタン」を選択してシート上に配置
  3. 表示されるダイアログで割り当てるスクリプトを選択

これにより、VBAと同様にシートボタンクリックでスクリプトが起動します。

項目VBAOffice Scripts
ボタンの設置挿入 → フォームコントロール → ボタン挿入 → ボタン
割り当て方法右クリック → マクロの登録ボタン配置時にスクリプトを選択
実行環境デスクトップExcelExcel on the web
ボタンの保存ブックに保存されるブックに保存される

注意点: Office Scriptsのボタンは Excel on the web でのみ 動作します。同じファイルをデスクトップ版Excelで開いてもボタンからスクリプトは実行できません。

AIにVBAを渡すだけでは何が起きるか(失敗例)

まず「よくある依頼方法」で変換を試みます。

失敗するプロンプト例

以下のVBAコードをOffice Scriptsに変換してください。

[VBAコードをそのまま貼り付け]

AIが返してくる変換結果(問題あり)

この依頼で多くのAIが返してくるのは、以下のようなシンプルな列転記コードです。

// ❌ AIが誤解して生成したコード(シート構造を把握できていない)
function main(workbook: ExcelScript.Workbook) {
  const sheet1 = workbook.getWorksheet("Sheet1");
  const sheet2 = workbook.getWorksheet("Sheet2");

  const lastRow = sheet1.getUsedRange().getRowCount();
  for (let i = 1; i < lastRow; i++) {
    const id = sheet1.getRangeByIndexes(i, 0, 1, 1).getValue();
    sheet2.getRangeByIndexes(i, 0, 1, 1).setValue(id);
  }
}

何が問題か:

  • A列→A列の単純コピーになっている
  • 2行目のマッピング定義を読み取っていない
  • ▼列の自動検出がない
  • G1・K1の設定情報を参照していない
  • ID照合(MATCH)が実装されていない

AIはVBAコードのロジックだけを見て変換するため、そのVBAが前提とするシートの構造を把握できないのです。

keybd_eventについてはどう返すか

keybd_event については、どのAIも正直に変換不可と回答します。

keybd_event はWindowsのWin32 APIであり、Office Scriptsはブラウザのサンドボックス環境で動作するためOSレイヤーへのアクセスができません。この機能は変換できません。」

これは正しい回答です。ただし「変換できない=移行できない」ではありません。後述します。

シート構造を伝えた上で変換依頼する(成功例)

AIへの依頼にシート構造の説明を加えると、変換品質が大きく変わります。

正しいプロンプト例

以下のVBAコードをOffice Scriptsに変換してください。

【シートの構造】
・貼付元シート(マクロの実行シート)
– 1行目:設定情報
– G1:データ開始行番号(例:4)
– K1:貼付先のシート名
– 2行目:マッピング定義
– ▼:ID照合列を示すマーカー
– 数字(1, 2, 3…):転記する列の番号(データが入っている列に置く)
– 3行目:見出し行
– G1の値の行〜:データ本体

・貼付先シート
– 同様の構造(G1=データ開始行)
– 2行目に▼と数字でマッピング列を定義

【変換の条件】
– 変換できない機能(Win32 API等)は削除して理由をコメントで明記すること
– TypeScript形式で記述すること
– VBAとの対応関係がわかるようにコメントを入れること
– ヘルパー関数に分けて整理すること

【VBAコード】
[VBAコードを貼り付け]

変換後のOffice Scriptsコード

上記のプロンプトで生成されたコードが以下です。

function main(workbook: ExcelScript.Workbook) {

  // 貼付元シート(実行シート)= VBAの prefSh
  const prefSh = workbook.getActiveWorksheet();
  // K1から貼付先シート名を取得 = VBAの sName
  const sName  = prefSh.getRange("K1").getValue() as string;
  const workSh = workbook.getWorksheet(sName);

  if (!workSh) {
    console.log(`シート "${sName}" が見つかりません`);
    return;
  }

  // ▼マーク列の自動検出 = VBAの WorksheetFunction.Match("▼", ...)
  const ptCol = findValueInRow(prefSh, 1, "▼");
  const wtCol = findValueInRow(workSh, 1, "▼");

  if (ptCol === -1 || wtCol === -1) {
    console.log("▼マークが見つかりません(2行目を確認してください)");
    return;
  }

  // マッピング列数の取得 = VBAの WorksheetFunction.Max(...)
  const pMCol = getMaxInRow(prefSh, 1);
  const wMCol = getMaxInRow(workSh, 1);

  if (pMCol !== wMCol) {
    console.log("引き当てる列の数が不整合のため中止します!");
    return;
  }

  // データ開始行の取得 = VBAの prefSh.Range("G1") / workSh.Range("G1")
  const pRow = prefSh.getRange("G1").getValue() as number;
  let   wRow = workSh.getRange("G1").getValue() as number;

  // 列マッピング配列の構築 = VBAの pCol() / wCol()
  const pCol: number[] = [];
  let pFlgCol = 0;
  for (let i = 1; i <= pMCol; i++) {
    const col = findValueInRow(prefSh, 1, i);
    pCol.push(col);
    if (i > 1 && col !== pCol[i - 2] + 1) pFlgCol = 1;
  }

  const wCol: number[] = [];
  let wFlgCol = 0;
  for (let i = 1; i <= wMCol; i++) {
    const col = findValueInRow(workSh, 1, i);
    wCol.push(col);
    if (i > 1 && col !== wCol[i - 2] + 1) wFlgCol = 1;
  }

  // 最終行の取得 = VBAの Cells(Rows.Count, col).End(xlUp).Row
  const workShEndR = getLastRow(workSh, wtCol);
  const prefShEndR = getLastRow(prefSh, ptCol);

  // ID照合用にターゲット列を配列として一括取得
  const tgetRng = prefSh
    .getRangeByIndexes(pRow - 1, ptCol, prefShEndR - pRow + 1, 1)
    .getValues();

  // 開始行の上書き指定(N1セル)= VBAの再開始行機能
  const n1Val = prefSh.getRange("N1").getValue();
  if (typeof n1Val === "number" && n1Val > wRow) wRow = n1Val;

  let lngHcount = 0;

  // メインループ = VBAの For tgetTmpR = wRow To workShEndR
  for (let tgetTmpR = wRow; tgetTmpR <= workShEndR; tgetTmpR++) {

    const tmpStr = workSh
      .getRangeByIndexes(tgetTmpR - 1, wtCol, 1, 1)
      .getValue();

    // ID照合 = VBAの WorksheetFunction.Match(tmpStr, tgetRng, 0)
    let matchRng = -1;
    for (let r = 0; r < tgetRng.length; r++) {
      if (tgetRng[r][0] === tmpStr) {
        matchRng = r + pRow;
        break;
      }
    }

    if (matchRng === -1) continue;

    // 転記元データの取得
    const myArray: (string | number | boolean)[] = [];
    if (pFlgCol === 1) {
      for (let i = 0; i < pMCol; i++) {
        myArray.push(
          prefSh.getRangeByIndexes(matchRng - 1, pCol[i], 1, 1)
                .getValue() as string | number | boolean
        );
      }
    } else {
      const vals = prefSh
        .getRangeByIndexes(matchRng - 1, pCol[0], 1, pMCol)
        .getValues()[0];
      myArray.push(...(vals as (string | number | boolean)[]));
    }

    // 貼付先への書き込み
    if (wFlgCol === 1) {
      for (let i = 0; i < wMCol; i++) {
        workSh.getRangeByIndexes(tgetTmpR - 1, wCol[i], 1, 1)
               .setValue(myArray[i]);
      }
    } else {
      workSh.getRangeByIndexes(tgetTmpR - 1, wCol[0], 1, pMCol)
             .setValues([myArray]);
    }

    lngHcount++;

    // ★ keybd_event(スリープ防止)→ 変換不可・削除
    //   理由:Win32 APIはOffice Scriptsから呼び出せない
    //   ただしOffice ScriptsはクラウドでPC自体がスリープしないため不要
    // ★ UserForm(プログレスバー)→ 変換不可・削除
    //   理由:Office ScriptsにUI部品なし
    // ★ キャンセルボタン / DoEvents → 変換不可・削除
    //   理由:イベント駆動モデルがない
    if (tgetTmpR % 1000 === 0) {
      console.log(`処理中... ${tgetTmpR} / ${workShEndR} 件`);  // プログレスバーの代替
    }
  }

  console.log(`完了!【データ更新件数: ${lngHcount} 件】`);
}

// ── ヘルパー関数 ──────────────────────────────────

// 行内で値を検索して列インデックスを返す(0-indexed)
// = VBAの WorksheetFunction.Match の代替
function findValueInRow(
  sheet: ExcelScript.Worksheet,
  rowIndex: number,
  value: string | number
): number {
  const colCount = sheet.getUsedRange().getColumnCount();
  const rowValues = sheet
    .getRangeByIndexes(rowIndex, 0, 1, colCount)
    .getValues()[0];
  return rowValues.indexOf(value);
}

// 行の数値の最大値を返す
// = VBAの WorksheetFunction.Max の代替
function getMaxInRow(sheet: ExcelScript.Worksheet, rowIndex: number): number {
  const colCount = sheet.getUsedRange().getColumnCount();
  const rowValues = sheet
    .getRangeByIndexes(rowIndex, 0, 1, colCount)
    .getValues()[0];
  const nums = rowValues.filter(v => typeof v === "number") as number[];
  return nums.length > 0 ? Math.max(...nums) : 0;
}

// 最終行を返す(1-indexed)
// = VBAの Cells(Rows.Count, col).End(xlUp).Row の代替
function getLastRow(sheet: ExcelScript.Worksheet, colIndex: number): number {
  const rowCount = sheet.getUsedRange().getRowCount();
  const colValues = sheet
    .getRangeByIndexes(0, colIndex, rowCount, 1)
    .getValues();
  for (let r = rowCount - 1; r >= 0; r--) {
    if (colValues[r][0] !== "" && colValues[r][0] !== null) return r + 1;
  }
  return 1;
}

実際にExcel on the webで動かしてみた

上記のコードをExcel on the webのOffice Scriptsエディタに貼り付けて実行しました。

テスト環境

貼付元シート

行\列ABCGK
1行目(説明ラベル)4貼付先
2行目12
3行目ID(見出し)商品名単価
4行目ID001りんご100
5行目ID002バナナ200
6行目ID003みかん150

貼付先シート(実行前)

行\列C(▼)D(1)E(2)
4行目ID001(空)(空)
5行目ID003(空)(空)
6行目ID002(空)(空)

IDは順不同で配置しています。

実行結果

コンソールに以下が出力されました。

完了!【データ更新件数: 3 件】

貼付先シート(実行後)

行\列C(▼)D(1:商品名)E(2:単価)
4行目ID001りんご100
5行目ID003みかん150
6行目ID002バナナ200

IDが順不同でも、ID照合によって正しい行に転記されています。✅

変換できた機能・できなかった機能の整理

✅ 変換できた機能

VBAの機能Office Scriptsでの実装
WorksheetFunction.Match配列の線形検索(indexOf
WorksheetFunction.MaxMath.max()
Cells(Rows.Count, col).End(xlUp).RowgetUsedRange()からの逆順検索
Range().Value(一括読み書き)getValues() / setValues()
Forループ・配列処理標準TypeScript
On Error Resume Nexttry/catch
ActiveSheet / Worksheets(name)getActiveWorksheet() / getWorksheet()

❌ 変換できなかった機能

VBAの機能変換できない理由代替・備考
keybd_event(スリープ防止)Win32 API:OSレイヤー不可クラウド実行のため不要
Sleep(待機)Win32 API同上
UserForm・プログレスバーUI部品なしconsole.logで代替
DoEvents・キャンセルボタンイベント駆動なし代替手段なし
Application.ScreenUpdating対応APIなし不要(クラウドは常に最適)
MsgBoxダイアログなしconsole.logのみ
#If VBA7 Then(条件コンパイル)不要Office Scriptsは64bit固定

UserForm(プログレスバー・キャンセルボタン)が失われる影響

今回のVBAで最も「使い勝手の差」として現れるのが、UserFormの消滅です。

元のVBAでは処理中に以下のUIが表示されていました。

' ループ内で毎回更新されるプログレスバー
With UserForm1
    .Show vbModeless                        ' モードレスで表示(裏で処理継続)
    .ProgressBar1.Value = tgetTmpR          ' バーの進捗を更新
    .Label1.Caption = percent & "%完了【処理件数: " & _
                      tgetTmpR & " / " & workShEndR & " 】" & _
                      "【HIT件数:" & lngHcount & "件】"
End With

' キャンセルボタンが押されたら中断
If UserForm1.blCancel = True Then
    Unload UserForm1
    MsgBox "処理を中断しました。"
    End
End If

これによりユーザーは処理中に以下を把握できていました。

VBAで表示されていた情報Office Scriptsでの状況
進捗バー(視覚的な%表示)❌ 表示不可
処理件数 / 総件数(リアルタイム)❌ リアルタイム更新不可
HIT件数(照合成功数)❌ リアルタイム更新不可
キャンセルボタン(途中中断)❌ 中断手段なし
処理完了後のMsgBox(時間・件数)❌ ダイアログ表示不可

Office Scriptsで代替できるのは console.log のみです。

// 1000件ごとにコンソールへ出力(プログレスバーの代替)
if (tgetTmpR % 1000 === 0) {
  console.log(`処理中... ${tgetTmpR} / ${workShEndR} 件`);
}

// 完了後の出力(MsgBoxの代替)
console.log(`完了!【データ更新件数: ${lngHcount} 件】`);

ただし console.log はスクリプトエディタの「出力」タブにのみ表示され、処理中にリアルタイムで更新されるわけではありません。スクリプトが完了して初めてまとめて表示されます。

なぜUserFormは変換できないのか

Office Scriptsは TypeScript をベースとしたサーバーサイドの処理エンジンとして動作します。ウィンドウやダイアログを表示する仕組み(UI スレッド)を持たないため、VBAの UserFormMsgBoxDoEvents に相当するAPIが存在しません。

処理の見える化が必要な場合は、Power Automate と組み合わせることで対応できます(次回以降で解説予定)。

「変換できない=移行できない」ではない

keybd_event によるスリープ防止は変換できませんでしたが、Office Scriptsではそもそもこの問題が発生しません。

VBAのスリープ問題はPCがスリープ状態になるために起きます。Office ScriptsはMicrosoftのクラウドサーバー上で実行されるため、PCの電源状態に関係なく処理が継続します。

つまり削除された機能が不要になったのであり、機能が失われたわけではありません。

まとめ:AIへの変換依頼チェックリスト

  • AIにVBAコードだけ渡しても、シート構造を理解した変換はできない
  • シートの行構造・特殊列の意味・設定セルの用途を事前に説明することで変換品質が上がる
  • Win32 APIは変換不可だが、クラウド実行環境ではそもそも不要になるケースがある
  • UserForm・プログレスバー・キャンセルボタンは代替手段がなく、設計の見直しが必要
  • 変換依頼時は「変換できない場合の対応方針」も明示するとよい

変換依頼プロンプトのテンプレート:

以下のVBAコードをOffice Scriptsに変換してください。

【シートの構造】
(シートの行・列構成を具体的に説明)

【変換の条件】
– 変換できない機能は削除し、理由をコメントで明記すること
– TypeScript形式で記述すること
– VBAとの対応関係がわかるようにコメントを入れること
– ヘルパー関数に分けて整理すること

【VBAコード】
(コードを貼り付け)

関連記事

サンプルファイルのダウンロード

この記事で使用したテスト用Excelファイルはこちらからダウンロードできます。

サンプルファイルにはスクリプトは登録されていません。 ダウンロードしたExcelファイルを開き、「自動化」タブ >「新しいスクリプト」を開き、「変換後のOffice Scriptsコード」をコピーして貼り付けて保存してください。

Office Scripts 自体はExcelファイル(.xlsx)に内包されるものではなく、ユーザーのOneDrive上に保存される仕組みです。

想定される質問・エラー対処

Q. 「▼マークが見つかりません」と表示される

→ アクティブシートが「貼付元」になっているか確認してください。貼付先シートで実行すると、2行目の構成が異なるためエラーになります。

Q. 「シート “〇〇” が見つかりません」と表示される

→ K1セルに入力した貼付先シート名と、実際のシート名(タブ名)が一致しているか確認してください。全角・半角・スペースの違いでもエラーになります。

Q. 転記件数が0件になる

→ 以下を確認してください。

  • 貼付元と貼付先の2行目にマッピング番号(1, 2…)が正しく入っているか
  • マッピング番号はデータが入っている列に置く(別の空き列ではなく)
  • G1のデータ開始行番号が正しいか(見出し行の次の行番号)

当サイトの記事で使用したVBAなどのサンプルをDLできます

ダウンロードページへは下のカードをクリックすればジャンプできます。
よろしければご利用ください!

よかったらシェアしてね!
  • URLをコピーしました!
目次