はじめに
PowerPoint で 50 頁くらいの資料を作ったところ,目次が欲しいと言われました。資料を印刷することも考慮して下記のようにページ番号 SH. と内容の一覧表があり,さらに内容のほうのセルをクリックするとそのページにジャンプするようにして欲しいというものです。
| SH. | 内容 |
|---|---|
| 3 | 仕様案(お品書き) |
| 4 | 実装コード |
| 5 | 技術解説 |
| 6 | 結論 |
要するにプレゼンテーション内のハイパーリンクを貼るだけの簡単なお仕事です。ただ 10 ページなら手作業でリンクを貼れますが,50 ページとなるとやる気がしません。
PowerPoint の VBA マクロで自動リンク作成機能を作成しても良いのですが,マクロをアウトソーシングするのがマイブームで,WSH + JavaScript で作ることにしました。
仕様案(お品書き)
- 現在オープンしているプレゼンテーション,すなわちアクティブプレゼンテーションを対象とします。
- 現在選択している表を対象とします。※複数選択可能とします。
- 表全体を選択している場合,1列目がページ番号,2列目がページの内容として,2列目に指定したページへのハイパーリンクを貼ります。
- 表の一部(セル)を選択している場合は選択範囲内の1列目,2列目とします。1列しか選択しなかったり,3列以上選択した場合はエラーとします。
- 1列目が数字でない場合や,プレゼンテーションの最終ページ番号よりも大きい場合,ハイパーリンクを貼らずにスキップします。
- ついでにハイパーリンクを表示する機能,削除する機能も作ります。これは,どちらかというとテスト用ですね。
実装コード
気の短い人もいるでしょうから先に実装コードを示します。
実装コードはコチラ
//---------------------------------------------------------------------------
// PpMouseActivation 列挙型
//---------------------------------------------------------------------------
var ppMouseClick = 1; // マウスをクリックした時に実行
var ppMouseOver = 2; // マウスを通過(ホバー)した時に実行
//---------------------------------------------------------------------------
// PpSelectionType 列挙型
//---------------------------------------------------------------------------
var ppSelectionNone = 0; // なし
var ppSelectionSlides = 1; // スライド
var ppSelectionShapes = 2; // 図形
var ppSelectionText = 3; // テキスト
//---------------------------------------------------------------------------
// メイン関数の呼び出し
//---------------------------------------------------------------------------
var args = WScript.Arguments.Unnamed;
var ret = main(args);
try {
WScript.Quit(ret);
} catch(e) {
/* 何もしない */
}
//---------------------------------------------------------------------------
// メイン関数
//---------------------------------------------------------------------------
function main(args) {
//-----------------------------------------------------------------------
// ヘルプメッセージ
//-----------------------------------------------------------------------
if(args.Count == 0) {
var msg = [
"PowerPoint のテキストボックスのハイパーリンクユーティリティ",
"",
"PPTLINK(.JS) [コマンド]",
"",
"コマンド:",
" list: ハイパーリンク一覧を表示します。",
" remove: ハイパーリンクを削除します。",
" table: テーブルに目次を作成します。",
"",
"現在選択しているテキストボックスを対象とします。"
];
WScript.StdErr.WriteLine(msg.join("\n"));
return -1;
}
//-----------------------------------------------------------------------
// コマンドのチェック
//-----------------------------------------------------------------------
var func_table = {
list: command_list,
remove: command_remove,
table: command_table
};
var func = func_table[args(0).toLowerCase()];
if(func == null) {
WScript.StdErr.WriteLine("コマンド " + args(0) + " には対応していません!!");
return -1;
}
//-----------------------------------------------------------------------
// PowerPoint の起動
//-----------------------------------------------------------------------
var app = null;
try {
app = GetObject("", "PowerPoint.Application");
} catch(e) {
app = WScript.CreateObject("PowerPoint.Application");
}
if(app == null) {
WScript.StdErr.WriteLine("PowerPoint の起動に失敗しました!!");
return -1;
}
app.Visible = true;
//-----------------------------------------------------------------------
// アクティブプレゼンテーションのチェック
//-----------------------------------------------------------------------
if(app.Presentations.Count == 0) {
WScript.StdErr.WriteLine("アクティブプレゼンテーションが見つかりません!!");
return -1;
}
//-----------------------------------------------------------------------
// 現在選択しているオブジェクトのチェック ⇒ テキストボックス以外はエラー
//-----------------------------------------------------------------------
var sel = app.ActiveWindow.Selection;
switch(sel.Type) {
case ppSelectionShapes:
case ppSelectionText:
break;
default:
WScript.StdErr.WriteLine("テキストボックスが選択されていません!!");
return -1;
}
//-----------------------------------------------------------------------
// 現在選択しているテキストボックスに対して実行
//-----------------------------------------------------------------------
for(var i = 1; i <= sel.ShapeRange.Count; i++)
func(sel.ShapeRange(i));
return 0;
}
//---------------------------------------------------------------------------
// ハイパーリンク一覧の作成
//---------------------------------------------------------------------------
function get_list(shape) {
var list = [];
var check_range = function(range) {
var link = range.ActionSettings(ppMouseClick).Hyperlink;
if(link.Address == "" && link.SubAddress == "") return false;
list.push({ text:range.Text, link:link });
return true;
};
var check_shape = function(shape) {
if(!shape.HasTextFrame) return;
if(!shape.TextFrame.HasText) return;
var range = shape.TextFrame.TextRange;
if(check_range(range)) return;
for(var i = 1; i <= range.Runs().Count; i++)
check_range(range.Runs(i));
};
if(shape.HasTable) {
var table = shape.Table;
for(var i = 1; i <= table.Rows.Count; i++ )
for(var j = 1; j <= table.Columns.Count; j++ )
check_shape(table.Cell(i, j).Shape);
} else {
check_shape(shape);
}
return list;
}
//---------------------------------------------------------------------------
// コマンド list
//---------------------------------------------------------------------------
function command_list(shape) {
//-------------------------------------------------------------------
// ハイパーリンク一覧の作成
//-------------------------------------------------------------------
var list = get_list(shape);
if(list.length == 0) return;
//-------------------------------------------------------------------
// ハイパーリンク一覧表示
//-------------------------------------------------------------------
for(var i = 0; i < list.length; i++) {
var a = [
"\"" + list[i].text + "\"",
"\"" + list[i].link.Address + "\"",
"\"" + list[i].link.SubAddress + "\""
];
WScript.Echo(a.join(","));
};
WScript.Echo(list.length + " 個のハイパーリンクがあります。");
}
//---------------------------------------------------------------------------
// コマンド remove
//---------------------------------------------------------------------------
function command_remove(shape) {
//-------------------------------------------------------------------
// ハイパーリンク一覧の作成
//-------------------------------------------------------------------
var list = get_list(shape);
if(list.length == 0) return;
//-------------------------------------------------------------------
// ハイパーリンクの削除
//-------------------------------------------------------------------
for(var i = 0; i < list.length; i++) {
var a = [
"\"" + list[i].text + "\"",
"\"" + list[i].link.Address + "\"",
"\"" + list[i].link.SubAddress + "\""
];
WScript.Echo("削除: " + a.join(","));
list[i].link.Delete();
}
WScript.Echo(list.length + " 個のハイパーリンクを削除しました。");
}
//---------------------------------------------------------------------------
// コマンド table
//---------------------------------------------------------------------------
function command_table(shape) {
if(!shape.HasTable) return;
var table = shape.Table;
if(table.Columns.Count < 2) {
WScript.StdErr.WriteLine("二列以上ある表を選択して下さい!!")
return;
}
//-------------------------------------------------------------------
// 選択しているセル範囲を調べる
//-------------------------------------------------------------------
var row1 = 0, row2 = 0, col1 = 0, col2 = 0;
for(var i = 1; i <= table.Rows.Count; i++) {
for(var j = 1; j <= table.Columns.Count; j++) {
if(!table.Cell(i, j).Selected) continue;
if(row1 < 1) row1 = i;
if(col1 < 1) col1 = j;
if(row2 < i) row2 = i;
if(col2 < j) col2 = j;
}
}
if(row1 < 1) {
row1 = 1; row2 = table.Rows.Count;
col1 = 1; col2 = 2;
} else if(col2 - col1 != 1) {
WScript.StdErr.WriteLine("セルを二列選択して下さい!!")
return;
}
//-------------------------------------------------------------------
// ハイパーリンク一覧の作成
//-------------------------------------------------------------------
var app = shape.Application;
var last = app.ActivePresentation.Slides.Count;
var count = 0;
for(var i = row1; i <= row2; i++) {
var range1 = table.Cell(i, col1).Shape.TextFrame.TextRange;
var range2 = table.Cell(i, col2).Shape.TextFrame.TextRange;
var s = range1.Text
var m = s.match(/^\s*(\d+)\s*$/);
if(m == null) continue;
var n = parseInt(m[1]);
if(n < 1) continue;
if(n > last) continue;
var link = range2.ActionSettings(ppMouseClick).Hyperlink;
link.Address = "";
link.SubAddress = n;
var a = [
"\"" + range2.Text + "\"", n
];
WScript.Echo("追加: " + a.join(","));
count++;
}
WScript.Echo(count + " 個のハイパーリンクを作成しました。");
}
技術解説
0. コマンドのチェック
第一引数 args(0) でコマンドを与えます。これまではC言語のように switch ~ case で分岐していましたが,JavaScript っぽく連想配列を使うことにしました。
var func_table = {
list: command_list,
remove: command_remove,
table: command_table
};
var func = func_table[args(0).toLowerCase()];
if(func == null) {
WScript.StdErr.WriteLine("コマンド " + args(0) + " には対応していません!!");
return -1;
}
1. PowerPoint の起動
既に PowerPoint を起動していればそのアプリケーションオブジェクトを流用します。そうでなければ新たに起動します。最後に Visible プロパティを true にしておく必要があります。これを忘れると大量のゾンビプロセスを発生させることになります。
var app = null;
try {
app = GetObject("", "PowerPoint.Application");
} catch(e) {
app = WScript.CreateObject("PowerPoint.Application");
}
if(app == null) {
WScript.StdErr.WriteLine("PowerPoint の起動に失敗しました!!");
return -1;
}
app.Visible = true;
2. アクティブプレゼンテーションのチェック
アクティブプレゼンテーションの存在チェックです。現在オープンしているプレゼンテーションの数がゼロかどうかで判別するようにしました。まったくプレゼンテーションを開いていない状況で ActivePresentation を参照するとnull を返すのではなく例外を引き起こすので。
if(app.Presentations.Count == 0) {
WScript.StdErr.WriteLine("アクティブプレゼンテーションが見つかりません!!");
return -1;
}
3. 現在選択しているオブジェクトのチェック
現在選択しているオブジェクトのチェックです。テキストボックス以外はエラーとします。
var sel = app.ActiveWindow.Selection;
switch(sel.Type) {
case ppSelectionShapes:
case ppSelectionText:
break;
default:
WScript.StdErr.WriteLine("テキストボックスが選択されていません!!");
return -1;
}
4. コマンドの実行
現在選択しているテキストボックス(複数可)に対してコマンドを実行します。
for(var i = 1; i <= sel.ShapeRange.Count; i++)
func(sel.ShapeRange(i));
5. list コマンド
指定した図形オブジェクト shape のハイパーリンクを表示します。実はテキストボックスのハイパーリンクを取得するのは少々面倒なので get_list() を呼び出します。関数化したのは remove コマンドと共用する意味もあります。
function command_list(shape) {
var list = get_list(shape);
if(list.length == 0) return;
for(var i = 0; i < list.length; i++) {
var a = [
"\"" + list[i].text + "\"",
"\"" + list[i].link.Address + "\"",
"\"" + list[i].link.SubAddress + "\""
];
WScript.Echo(a.join(","));
};
WScript.Echo(list.length + " 個のハイパーリンクがあります。");
}
6. removeコマンド
指定した図形オブジェクト shape のハイパーリンクを削除します。Microsoft Office のコレクションを削除する場合,最後の要素から削除したり,あるいはいったん別の配列にオブジェクトをコピーしてから削除するなどの工夫が必要です。このため関数 get_list() を呼び出してハイパーリンクの配列を取得してから削除します。
function command_remove(shape) {
var list = get_list(shape);
if(list.length == 0) return;
for(var i = 0; i < list.length; i++) {
var a = [
"\"" + list[i].text + "\"",
"\"" + list[i].link.Address + "\"",
"\"" + list[i].link.SubAddress + "\""
];
WScript.Echo("削除: " + a.join(","));
list[i].link.Delete();
}
WScript.Echo(list.length + " 個のハイパーリンクを削除しました。");
}
7. ハイパーリンク一覧の取得
先ほど少々面倒だといったハイパーリンクを取得する関数です。
- 表の場合はセルごとに分割して取得する必要がありますが,これ自体はさほど難解ではありません。
- テキストボックスの文字列全体にリンクが貼られている場合もあれば,文字列の一部のみにリンクが貼られている場合もあります。テキストボックスの文字列全体にリンクが貼られている場合,分割された文字列にもリンクが存在するため,二重にカウントしないように注意する必要があります。一方,文字列の一部のみにリンクが貼られている場合,全体のリンクは存在しません。
function get_list(shape) {
var list = [];
var check_range = function(range) {
var link = range.ActionSettings(ppMouseClick).Hyperlink;
if(link.Address == "" && link.SubAddress == "") return false;
list.push({ text:range.Text, link:link });
return true;
};
var check_shape = function(shape) {
if(!shape.HasTextFrame) return;
if(!shape.TextFrame.HasText) return;
var range = shape.TextFrame.TextRange;
if(check_range(range)) return;
for(var i = 1; i <= range.Runs().Count; i++)
check_range(range.Runs(i));
};
if(shape.HasTable) {
var table = shape.Table;
for(var i = 1; i <= table.Rows.Count; i++ )
for(var j = 1; j <= table.Columns.Count; j++ )
check_shape(table.Cell(i, j).Shape);
} else {
check_shape(shape);
}
return list;
}
8. table コマンド
このコマンドだけは指定した図形が表(テーブル)以外の場合は何もしないでリターンします。
- 表全体を選択した場合,表の1列目がページ番号,2列目がページの内容として2列目にハイパーリンクを貼りますが,1列目の内容をパースして空白だったり,整数に変換できない場合は次行にスキップします。またページ番号が範囲外のときも次行にスキップします。※エラーとはしません。
- 表の一部を選択したとき,選択範囲の1列目をページ番号,2列目をページの内容と判断します。1列しか選択しなかったり,3列以上選択した場合はエラーとします。
-
PowerPoint には表の選択範囲を一発で取得できる便利な機能はありません。表のすべてのセルを検索して
Selectedプロパティが有効になっていれば現在選択されている範囲の一部ということになります。このため表全体を検索してSelectedプロパティが有効になっているセルの行番号と列番号の最小値と最大値をそれぞれ求めます。
| 3 | 仕様案(お品書き) | ||||
| 4 | 実装コード | ||||
| 5 | 技術解説 | ||||
| 6 | 結論 | ||||
function command_table(shape) {
if(!shape.HasTable) return;
var table = shape.Table;
if(table.Columns.Count < 2) {
WScript.StdErr.WriteLine("二列以上ある表を選択して下さい!!")
return;
}
var row1 = 0, row2 = 0, col1 = 0, col2 = 0;
for(var i = 1; i <= table.Rows.Count; i++) {
for(var j = 1; j <= table.Columns.Count; j++) {
if(!table.Cell(i, j).Selected) continue;
if(row1 < 1) row1 = i;
if(col1 < 1) col1 = j;
if(row2 < i) row2 = i;
if(col2 < j) col2 = j;
}
}
if(row1 < 1) {
row1 = 1; row2 = table.Rows.Count;
col1 = 1; col2 = 2;
} else if(col2 - col1 != 1) {
WScript.StdErr.WriteLine("セルを二列選択して下さい!!")
return;
}
var app = shape.Application;
var last = app.ActivePresentation.Slides.Count;
var count = 0;
for(var i = row1; i <= row2; i++) {
var range1 = table.Cell(i, col1).Shape.TextFrame.TextRange;
var range2 = table.Cell(i, col2).Shape.TextFrame.TextRange;
var s = range1.Text
var m = s.match(/^\s*(\d+)\s*$/);
if(m == null) continue;
var n = parseInt(m[1]);
if(n < 1) continue;
if(n > last) continue;
var link = range2.ActionSettings(ppMouseClick).Hyperlink;
link.Address = "";
link.SubAddress = n;
var a = [
"\"" + range2.Text + "\"", n
];
WScript.Echo("追加: " + a.join(","));
count++;
}
WScript.Echo(count + " 個のハイパーリンクを作成しました。");
}
実行例
引数なしで実行するとヘルプメッセージを表示します。
PowerPoint のテキストボックスのハイパーリンクユーティリティ
PPTLINK(.JS) [コマンド]
コマンド:
list: ハイパーリンク一覧を表示します。
remove: ハイパーリンクを削除します。
table: テーブルに目次を作成します。
現在選択しているテキストボックスを対象とします。
サンプルのプレゼンテーションです。
上記の表を選択して table コマンドを実行します。
c:\Qiita>pptlink table
追加: "仕様案(お品書き)",3
追加: "実装コード",4
追加: "技術解説",5
追加: "結論",6
4 個のハイパーリンクを作成しました。
実行すると下記のようにハイパーリンクが貼られます。先頭行と最終行にはリンクが貼られません。
list コマンドの実行結果を示します。表示内容は(おそらく)下記の意味だと思われます。
- ハイパーリンクを貼った文字列
- ハイパーリンク先のアドレス ※今回はローカルなので空文字
- ハイパーリンク先のサブアドレス
- スライドID
- ページ番号
- スライドのタイトル文字列
c:\Qiita>pptlink list
"仕様案(お品書き)","","258,3,[3] 仕様案(お品書き)"
"実装コード","","259,4,[4] 実装コード"
"技術解説","","260,5,[5] 技術解説"
"結論","","261,6,[6] 結論"
4 個のハイパーリンクがあります。
remove コマンドの実行結果を示します。
c:\Qiita>pptlink remove
削除: "仕様案(お品書き)","","258,3,[3] 仕様案(お品書き)"
削除: "実装コード","","259,4,[4] 実装コード"
削除: "技術解説","","260,5,[5] 技術解説"
削除: "結論","","261,6,[6] 結論"
4 個のハイパーリンクを削除しました。
結言というか感想
このスクリプトを次に使うのっていつになるのかなあ?
エラッタ
下記の不具合というか課題があることが分かっています。
- テキストボックスがグループ化されているとハイパーリンクを取得できません。