【Office Scripts】UserForm代替パターン:進捗表示はセルで、キャンセルは実行ボタンで実装する

Office Scriptsで進捗表示とキャンセルを実装する
目次

はじめに

前回、Power Automateと組み合わせることで「処理の完了通知・エラー通知」は実現できると解説しました。ただし、その方式では処理中のリアルタイム進捗表示途中キャンセルまでは代替できません。

今回は、この2つの課題を掘り下げます。進捗表示は前回同様ワークシート上のセルを仲介にした間接的な仕組みで実現しますが、キャンセルについては実際に検証した結果、スクリプトの実行ボタンに標準搭載されている「停止」機能を使うことで、VBAのキャンセルボタンに近い形の中断が実現できることが分かりました。第4回で作成した転記エンジンのコードをベースに、両方の実装方法と「どこまで実用に耐えるか」を検証します。

前回までのおさらい

実現したいことOffice Scripts単体Power Automate併用今回のセル連携パターン
完了・エラー通知
処理中の進捗表示△(間接的に可能)
処理中のキャンセル△(間接的に可能)✅(実行状態バーの「停止」ボタン)

先に結論を言うと: 進捗表示は依然として「本物のリアルタイムUI」ではなく、ワークシートのセルを介した間接的な表示にとどまります。一方でキャンセルについては、スクリプトを「ワークシートのボタン」から実行した場合に限り、実行中に表示される実行状態バーの「停止」ボタンで実際に処理を中断できることを検証で確認しました。VBAのキャンセルボタンに近い体験が得られますが、Power Automate経由の実行など、ボタンを使わないケースでは使えません。その場合の代替として、セルを使った協調的キャンセルも後述します。

実現方針の全体像

制御用シート(例:「制御」シート)を1枚追加
└─ A1セル:進捗状況(%・件数を書き込む)
メインループの中で定期的にA1へ書き込む

【キャンセル】2つの方法を用途に応じて使い分ける
① スクリプトの実行ボタン(推奨)
→ 実行状態バーの「停止」ボタンでネイティブに中断できる
② セルを使った協調的キャンセル(補足・代替手段)
→ B1セル(TRUE/FALSE)を定期的に読み取り、TRUEならループを中断する
→ Power Automate経由の実行など、①のボタンが使えない場合に使う

進捗表示は「書き込むだけ」というシンプルな仕組みです。キャンセルは、まず①のネイティブな停止ボタンを基本とし、それが使えない実行方法のときだけ②の協調的キャンセルで補うという位置づけになります。

準備:制御用シートを追加する

まず、ブックに「制御」という名前のシートを1枚追加し、以下のようにセルを用意します。

セル用途初期値
A1進捗表示(文字列)"待機中"
B1キャンセルフラグ(真偽値)FALSE

このシートは、貼付元・貼付先とは別の「状態確認専用シート」として使います。ユーザーはこのシートを開いておくことで進捗を確認できます。B1セルは、後述する協調的キャンセル(スクリプト実行ボタンが使えない場合の代替手段)を使うときにのみ必要です。ネイティブの停止ボタンだけを使う場合はA1セルの用意だけで構いません。

Step 1:進捗表示を実装する

第4回のメインループに、進捗書き込み処理を追加します。

function main(workbook: ExcelScript.Workbook) {

  // ~ 前回までの準備処理(省略)~
  const ctrlSheet = workbook.getWorksheet("制御");

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

    // ~ ID照合・転記処理(省略、前回記事と同じ)~

    lngHcount++;

    // ★ 進捗表示:1000件ごとに制御シートのA1へ書き込む
    if (tgetTmpR % 1000 === 0) {
      const percent = Math.round(((tgetTmpR - wRow + 1) / (workShEndR - wRow + 1)) * 100);
      ctrlSheet.getRange("A1").setValue(
        `${percent}%完了(${tgetTmpR - wRow + 1}/${workShEndR - wRow + 1}件・HIT ${lngHcount}件)`
      );
    }
  }

  ctrlSheet.getRange("A1").setValue(`完了!【データ更新件数: ${lngHcount} 件】`);
}

console.logは実行後にまとめて表示されるだけですが、ワークシートのセルへの書き込みは実行中に随時反映されるため、進捗の「見える化」としてはこちらの方が実用的です。

どこまでリアルタイムに見えるか

実行方法見え方
Excel Onlineの「Automate」タブから自分で実行自分の編集セッションで実行されるため、A1セルの更新がほぼそのまま画面に反映される
Power Automate経由で実行バックグラウンドセッションでの実行のため、ファイルを開いている別のユーザー・自分自身に共同編集の更新として見える場合があるが、反映タイミングは保証されない

Excel Onlineから直接実行するケース(VBAの「シートボタンを押して実行」に一番近い使い方)であれば、進捗表示の実用性は高いといえます。

もっと視覚的に見せる:文字バー・データバーの活用

ここまでは「◯%完了」という文字列表示でしたが、視覚的な%バーに近づける方法も2通りあります。

方式1:文字を繰り返して疑似バーを作る

const barLength = 20; // バー全体の長さ
const filled = Math.floor((percent / 100) * barLength);
const bar = "■".repeat(filled) + "□".repeat(barLength - filled);
ctrlSheet.getRange("A1").setValue(`${bar} ${percent}%`);

実装は簡単ですが、見た目はフォント依存です。等幅フォントでないと■の間隔がガタつくことがあり、セル幅を固定しておかないと折り返しで崩れる点にも注意してください。

方式2:Excelネイティブの「データバー」条件付き書式を使う

セルにあらかじめデータバー(条件付き書式)を設定しておき、スクリプト側は数値を書き込むだけにする方法です。実際に検証した構成では、1つのセルに%を書き込むのではなく、「HIT件数」「処理件数」「総件数」の3つの数値セルを縦に並べ、その範囲にデータバーを設定しました。

セル用途
A2ラベル文字列「HIT件数」
B2HIT件数の値
A3ラベル文字列「処理件数」
B3処理件数の値
A4ラベル文字列「総件数」
B4総件数の値(データバーの基準となる最大値)
// B2:B4の3セルに数値を書き込むだけ
ctrlSheet.getRange("B2").setValue(lngHcount);                          // HIT件数
ctrlSheet.getRange("B3").setValue(tgetTmpR - wRow + 1);                // 処理件数
ctrlSheet.getRange("B4").setValue(workShEndR - wRow + 1);              // 総件数

B2:B4の範囲にあらかじめデータバーを設定しておくと、総件数を基準にHIT件数・処理件数のバーが相対的な長さで描画され、「全体に対してどこまで進んでいるか」が一目で分かるようになります。この条件付き書式はaddConditionalFormat(ConditionalFormatType.dataBar)でスクリプトから設定することも可能です。

比較項目①文字の繰り返し②データバー(条件付き書式)
見た目の滑らかさフォント依存でガタつくことがあるExcelネイティブの滑らかなグラデーション
実装の手間文字列を組み立てるロジックが必要数値を書き込むだけ(書式は事前設定)
事前準備不要セルにデータバー書式を一度設定しておく必要あり
テキストとの併記バーと%を同じセルに表示しやすい数値と視覚バーが同じセルに重なる(%表示は別セルが必要な場合あり)

まとめると: VBAのプログレスバーそのものは再現できませんが、文字バーやデータバーを使えば「数値だけの表示」よりもかなり視覚的な進捗表示に近づけられます。

Step 2:スクリプトの実行ボタンを設置してネイティブに中断する

ここからがキャンセルの本題です。結論から言うと、スクリプトをワークシート上のボタンから実行すると、実行中に表示される「実行状態」バーに「停止」ボタンが表示され、これをクリックすると実際に処理を中断できます。VBAのキャンセルボタンにもっとも近い、ネイティブな中断手段です。

ボタンの設置手順

  1. スクリプトの編集画面を開き、「このスクリプトを共有」セクションまでスクロールする
  2. ブックに関連付ける」のトグルをオンにする
  3. ワークシートにボタンを追加」を選択する
  4. シート上に配置されたボタンをクリックすると、そのスクリプトが実行される
「ワークシートにボタンを追加する」

実行中に表示される「停止」ボタン

ボタンをクリックしてスクリプトが実行されると、画面上部に以下のような実行状態バーが表示されます。

ワークシートに追加された「ボタン」と実行状態バーの「停止」ボタン

この「停止」ボタンをクリックすると、実行中のスクリプトが実際に中断されます。第4回の転記エンジンのように大量データをループ処理するスクリプトでも、途中でこのボタンを押せばその場で止められることを確認しました。

注意点
  • この停止ボタンが使えるのは、スクリプトをワークシートのボタンから実行した場合です。Power Automate経由の実行(第5回)ではこの実行状態バー自体が表示されないため、使えません
  • 停止した時点までの書き込み(転記結果や進捗セルの値)はそのまま残ります。処理を再開する場合は、途中の行から再実行できるような設計(第4回で触れたN1セルによる開始行の上書き指定など)と組み合わせるとよいでしょう
  • 停止のタイミングは「停止ボタンをクリックした瞬間」に近く、次のチェックポイントまで待つ必要がある後述の協調的キャンセルよりも反応が速いです

Step 1のコードをそのままボタンに割り当てる

特別なコードの変更は不要です。付録に掲載している完全なコードをそのままスクリプトとして保存し、上記の手順でボタンを設置すれば、進捗表示とネイティブなキャンセルの両方が使える状態になります。

Step 3:セルを使った協調的キャンセル(補足・代替手段)

前述のとおり、通常はStep 2のネイティブな停止ボタンで十分です。ここから解説するセルを使ったキャンセルは、Power Automate経由の実行など、停止ボタンが表示されない実行方法のときの代替手段として押さえておいておきましょう。

同じループに、キャンセルフラグの確認処理を追加します。

for (let tgetTmpR = wRow; tgetTmpR <= workShEndR; tgetTmpR++) {

    // ★ キャンセル確認:1000件ごとにB1を読み取る
    if (tgetTmpR % 1000 === 0) {
      const cancelFlag = ctrlSheet.getRange("B1").getValue();
      if (cancelFlag === true) {
        ctrlSheet.getRange("A1").setValue(
          `中断しました(${tgetTmpR - wRow + 1}件処理・HIT ${lngHcount}件時点)`
        );
        console.log("ユーザーの指示により中断しました");
        return; // ここでスクリプトを終了する
      }
    }

    // ~ ID照合・転記処理(省略)~
  }

ユーザーが処理を中断したい場合、このコードでは「制御」シートのB1セルに直接TRUEを入力することで、次のチェックタイミングでループが終了します。

成立条件と限界

項目内容
反応タイミング即時ではなく、次のチェック(例:1000件ごと)まで待つ
フラグの設定方法Excel Online上で直接セルを編集、または別のスクリプト・Power Automateフローからセルを書き換える
VBAとの違いVBAのDoEventsは毎ループでイベント処理を挟めるが、Office Scriptsに相当する仕組みはなく、チェック頻度を上げるほどホスト呼び出し回数が増えて処理速度が落ちる

つまり、Step 2のネイティブな停止ボタンのような即時性はなく、次のチェックポイントまで待つ緩やかな中断という位置づけです。スクリプト実行ボタンが使える環境では、基本的にはStep 2を優先してください。

チェック頻度のトレードオフ

進捗表示・キャンセル確認の頻度(何件ごとにチェックするか)は、以下のトレードオフで決めます。

チェック頻度進捗の見え方・反応速度処理速度への影響
100件ごと細かく反映される・中断も早く反応セル読み書きの回数が増え、全体処理が遅くなる
1000件ごとある程度まとまった間隔で反映影響は小さい
10000件ごとほぼ気付けない影響はほぼない

getValuesetValueのような個別セルへのアクセスは、範囲をまとめて読み書きするgetValuessetValuesに比べてホスト呼び出しのコストが高くなります。進捗・キャンセル用のセルアクセスも例外ではないため、データ件数や許容できる処理時間に応じてチェック件数を調整するのが現実的です。

チェック間隔を自動計算・手動指定できるようにする

固定値の1000をそのまま使うと、データが少ない場合は更新回数が少なすぎ、逆に非常に多い場合は更新頻度が高くなりすぎることがあります。そこで、総件数から自動計算しつつ、制御シートのC1セルで手動指定も可能にする方式に改善できます。

// チェック間隔を決定(1~1000件の範囲)
// ・制御シートのC1に数値が入っていればそれを優先(手動指定)
// ・未入力の場合はデータ量から自動計算(全体で約50回更新される間隔)
const totalRows = workShEndR - wRow + 1;
const manualInterval = ctrlSheet.getRange("C1").getValue();
const checkInterval = typeof manualInterval === "number" && manualInterval > 0
  ? Math.min(1000, Math.max(1, Math.round(manualInterval)))
  : Math.min(1000, Math.max(1, Math.ceil(totalRows / 50)));

判定も、絶対行番号(tgetTmpR)ではなく処理済み件数を基準にすると、間隔が小さい場合でも意図通りに動作します。

const processedCount = tgetTmpR - wRow + 1;
if (processedCount % checkInterval === 0) {
  // ここに進捗表示・キャンセル確認の処理
}
データ量自動計算されるチェック間隔
100件2件ごと(100÷50)
5,000件100件ごと
100,000件1000件ごと(上限でクランプ)
30件1件ごと(下限でクランプ)

データ量に応じて更新回数がほぼ一定になるため、「小さいデータでは頻繁に、大きいデータでは間隔を空けて」チェックが行われ、処理速度への影響を一定に保てます。このcheckIntervalprocessedCountは、Step 1の進捗表示だけでなくStep 3の協調的キャンセル(B1セルの確認)にも同じように適用できます。

できること・できないことの整理

実現したいこと今回の方法
処理件数・進捗率の可視化✅ 制御シートのセルで表示可能
キャンセルボタンのクリックによる即時停止✅ スクリプト実行ボタンの「停止」で可能(ボタン実行時のみ)
処理中のユーザーによる中断指示(ボタンが使えない場合)△ フラグセルへの入力で代替可能(即時ではない)
VBAのプログレスバーのような視覚的な%バー△ 文字バー・データバーで疑似再現可能(VBA本来のUIとは異なる)
Power Automate実行中の中断指示❌ 実行状態バー自体が表示されないため停止ボタンは使えない。フラグセルによる代替のみ

よくある質問・エラー対処

Q. 進捗セルを更新しても画面に反映されない

→ Power Automate経由で実行している場合、実行セッションと閲覧セッションが分かれているため、反映が遅れることがあります。確実に見たい場合は、Excel Onlineの「Automate」タブから自分のセッションで直接実行してください。

Q. B1にTRUEと入力しても処理が止まらない

→ チェック件数(今回の例では1000件ごと)に達するまでは判定されません。データ件数が少ない場合はチェック間隔を短くする(例:100件ごと)ことで反応を早められますが、処理速度とのトレードオフになります。

Q. 進捗表示用のシートは削除してもいい?

→ スクリプト実行時に「制御」シートが存在しないとエラーになります。削除する場合はコード内のシート名参照も含めて見直してください。

Q. 実行状態バーに「停止」ボタンが表示されない

→ スクリプトをワークシートのボタンから実行していない(コードエディタの「実行」から直接実行している、またはPower Automate経由で実行している)可能性があります。「停止」ボタンが使えるのは、スクリプトを共有して「ワークシートにボタンを追加」した状態で、そのボタンから実行した場合のみです。

Q. 「停止」ボタンを押すと、書き込み中のデータはどうなる?

→ 停止した時点までにセルへ書き込まれた内容(転記結果・進捗セルの値)はそのまま残ります。中途半端な状態でデータが残るため、再実行する際は処理済みの範囲と未処理の範囲を混同しないよう、テスト環境で挙動を確認してから本番データに使うことをおすすめします。

まとめ

  • Office ScriptsにはUserForm相当のAPIがないため、進捗表示はワークシートのセルを仲介にした間接的な方法でのみ代替できる
  • 進捗表示はセルへの書き込みで実現できるが、Excel Onlineから直接実行した場合に最も実用的に機能する
  • 「視覚的な%バー」は文字の繰り返しや、HIT件数・処理件数・総件数の3セルにデータバー(条件付き書式)を組み合わせることでかなり近づけられる
  • キャンセルは、スクリプトをワークシートのボタンから実行すれば、実行状態バーの「停止」ボタンでネイティブに即時中断できる(VBAのキャンセルボタンにもっとも近い方法)
  • Power Automate経由の実行など、停止ボタンが表示されない場合に限り、セルのフラグを定期的に読み取る「協調的キャンセル」を代替手段として使う
  • 協調的キャンセルはチェック頻度を上げるほど反応は良くなるが、処理速度とのトレードオフがある

関連記事

付録:Step 1(進捗表示)の完全なコード

本文中のStep 1のコードは説明のため一部を省略していました。第4回の転記エンジン全体に、進捗表示(A1)・データバー用の数値書き込み(B2:B4)・チェック間隔の自動計算/手動指定(C1)を組み込んだ、コピペでそのまま使える完全版は以下のとおりです(貼付元シートの取得は、第5回で確認したgetActiveWorksheet()の落とし穴を避けるためgetWorksheet("貼付元")で直接指定しています)。

function main(workbook: ExcelScript.Workbook) {

  const ctrlSheet = workbook.getWorksheet("制御");

  // 貼付元シート(実行シート)= VBAの prefSh
  // ★ getActiveWorksheet()は実行時のアクティブシートに依存するため使わない(第5回参照)
  const prefSh = workbook.getWorksheet("貼付元");
  // 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;

  // チェック間隔を決定(1~1000件の範囲)
  // ・制御シートのC1に数値が入っていればそれを優先(手動指定)
  // ・未入力の場合はデータ量から自動計算(全体で約50回更新される間隔)
  const totalRows = workShEndR - wRow + 1;
  const manualInterval = ctrlSheet.getRange("C1").getValue();
  const checkInterval = typeof manualInterval === "number" && manualInterval > 0
    ? Math.min(1000, Math.max(1, Math.round(manualInterval)))
    : Math.min(1000, Math.max(1, Math.ceil(totalRows / 50)));

  // メインループ = 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++;

    // ★ 進捗表示:checkInterval件ごとに制御シートのA1・B2:B4へ書き込む
    const processedCount = tgetTmpR - wRow + 1;
    if (processedCount % checkInterval === 0) {
      const percent = Math.round((processedCount / totalRows) * 100);
      ctrlSheet.getRange("A1").setValue(
        `${percent}%完了(${processedCount}/${totalRows}件・HIT ${lngHcount}件)`
      );

      // B2:B4の3セルに数値を書き込むだけ(データバーで視覚的に表示)
      ctrlSheet.getRange("B2").setValue(lngHcount);        // HIT件数
      ctrlSheet.getRange("B3").setValue(processedCount);   // 処理件数
      ctrlSheet.getRange("B4").setValue(totalRows);        // 総件数
    }
  }

  ctrlSheet.getRange("A1").setValue(`完了!【データ更新件数: ${lngHcount} 件】`);
  ctrlSheet.getRange("B2").setValue(lngHcount);
  ctrlSheet.getRange("B3").setValue(totalRows);
  ctrlSheet.getRange("B4").setValue(totalRows);
}

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

// 行内で値を検索して列インデックスを返す(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;
}

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

この記事のサンプルはありません!

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

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