C#でPowerPointを制御することになった不幸なあなたへ

私です:raising_hand:

# はじめに

とある仕事でパワポ（のスライドショー）を制御する羽目になり、初めはRuby（Win32OLE）で書いていたのですが、その後に別のプログラムをC#で書く必要があって、パワポ制御プログラムだけRubyなのもなと思い始めたのでC#に移植しました。せっかくなのでその知見を書きます。なお、C#はあまり詳しくないです。

# 基礎知識

どの言語を使うにしても、Windowsで他のアプリを制御すると言ったらOLEです。
OLEってタグで記事書いてる人いませんね。。。~~すでに滅びた技術なのかなOLE。~~
OLEとCOMはどちらが古くからある用語かは私もよく知りません。とりあえず以降ではCOMって方を使います。

# C#でのCOMオブジェクト制御

というわけで「C# OLE」でググりました。引っかかったのがこちらの方の記事。
https://fornext1119.hatenablog.com/entry/20120328/p3

一部引用（改行等は修正）

```c#
//ワークブックコレクションオブジェクトを生成する。
object excelBooks = excelApp.GetType().InvokeMember(
    "Workbooks", BindingFlags.GetProperty, null, excelApp, null
);

//Excelファイルのオープン
object excelBook = excelBooks.GetType().InvokeMember(
    "Open", BindingFlags.InvokeMethod, null, excelBooks,
    new object[]{
        strMacroPath,
        System.Type.Missing, System.Type.Missing, System.Type.Missing,
        System.Type.Missing, System.Type.Missing, System.Type.Missing,
        System.Type.Missing, System.Type.Missing, System.Type.Missing,
        System.Type.Missing, System.Type.Missing, System.Type.Missing
    }
);
```

え？まじすか？
というわけで少し前にこちらの記事は見つけていたのですが気乗りしないので移植は放置してました。

問題点は2つあります。

- COMオブジェクトのプロパティやメソッドは`InvokeMember`でアクセスする必要があり上記のようにわかりにくい書き方になる。
- 「指定しない引数」についても`Type.Missing`を渡す必要がある？[^1]

[^1]: Workbooks.Openのリファレンスは[こちら](https://docs.microsoft.com/ja-jp/office/vba/api/excel.workbooks.open)。よくよく数えてみると引数は15個ありますね。

# ヘルパークラス作りました

Rubyで書かれたものを移植するのにこんな人間に優しくない表記は嫌なので、せめてもということで以下のヘルパークラスを作りました。

```C#
public class OLEHelper {
    public static object createObject(string progID)
    {
        var t = Type.GetTypeFromProgID(progID);
        return Activator.CreateInstance(t);
    }

    public static void freeObject(object o)
    {
        Marshal.FinalReleaseComObject(o);
    }

    public static object getProperty(object o, string name)
    {
        return o.GetType().InvokeMember(name, BindingFlags.GetProperty, null, o, null);
    }

    public static void setPropery(object o, string name, object value)
    {
        o.GetType().InvokeMember(name, BindingFlags.SetProperty, null, o, new object[]{value});
    }

    // paramsは可変長引数
    public static object call(object o, string name, params object[] args)
    {
        return o.GetType().InvokeMember(name, BindingFlags.InvokeMethod, null, o, args);
    }
}
```

このヘルパークラスを使うとなんとかSAN値を下げずにパワポ制御プログラムが書けます。
コメントに書いてあるように結局メソッドを呼び出すときも必要な引数だけで大丈夫なようです。

```C#
class Program
{
    static void Main(string[] args)
    {
        var path = Path.GetFullPath(@".\test.pptx");

        var powerpoint = OLEHelper.createObject("PowerPoint.Application");
        var presentations = OLEHelper.getProperty(powerpoint, "Presentations");
        // Openの引数は4つあるが必要な数だけ渡すので大丈夫らしい
        var presentation = OLEHelper.call(presentations, "Open", path);
        // presentationsを解放しても開いたpresentationに影響はない
        OLEHelper.freeObject(presentations);

        var slideshowsettings = OLEHelper.getProperty(presentation, "SlideShowSettings");
        var slideshowwindow = OLEHelper.call(slideshowsettings, "Run");
        OLEHelper.freeObject(slideshowsettings);

        var slideshowview = OLEHelper.getProperty(slideshowwindow, "View");

        do {
            Thread.Sleep(5 * 1000);
            OLEHelper.call(slideshowview, "Next");
        } while ((int)OLEHelper.getProperty(slideshowview, "State") != 5);

        OLEHelper.call(powerpoint, "Quit");

        // オブジェクトを全部解放しないとウインドウが閉じられない
        OLEHelper.freeObject(slideshowview);
        OLEHelper.freeObject(slideshowwindow);
        OLEHelper.freeObject(presentation);
        OLEHelper.freeObject(powerpoint);
    }
}
```

もう一つ重要な点として、コメントにあるように「全てのCOMオブジェクト」を解放しないとパワポウインドウ、というかプロセスは「パワポ制御プログラム」が終了しても終わりません。
その点で言うと元ネタ記事のプログラムは全オブジェクトを解放してないと思うのでちゃんとExcelが終了するのかちょっと気になります（元ネタのプログラムは動かしてない）

素のobjectじゃなくてIDisposable実装したラッパークラス作ったりすると幸せになりそうな気がしますが、実際のプログラムではオブジェクトの生成と破棄が別メソッドに分かれているのでラッパークラスは作りませんでした。

# まとめ

- PowerPointなどOfficeを制御したかったらOLE。
- C#（というか.NET）では`InvokeMember`でプロパティやメソッドが呼び出せる。SAN値が下がるのでヘルパークラスかラッパークラスを作ること推奨。