C# で COM オブジェクトを作ってみる

# VBA を書くのはもういやだー(´；ω；`)

C# の味を覚えてしまった私には、もはや VBA で大きなコードを書くのは苦痛です。
VBA もインターフェースなどを駆使すれば綺麗なコードを維持できるけれど、ライブラリは貧弱だし何かと手間が掛かります。
せっかく C# で作った DLL があるのだから、使える環境なら積極的に使って楽しちゃおう。

※COM 自体は言語に依存しているわけではないので Ruby など様々な言語でも使えますよ。
※IEに限定すればクライアントサイドのJavaScriptでも。。。

# COM コンポーネントは登録しないと使えないよ

最初に手動で COM コンポーネントを登録する方法を説明します。
コマンドプロンプトから RegAsm.exe を使うんですけど、私の環境では以下の2カ所にありました。
C:\Windows\Microsoft.NET\Framework\v2.0.50727\RegAsm.exe
C:\Windows\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe

どっち使うんだろうなぁ・・・。新しい方使っておけばいいのかな？
わかる方いたらコメントください(o*｡_｡)oﾍﾟｺｯ

hoge.dll を登録する場合
登録：RegAsm.exe /codebase hoge.dll
解除：RegAsm.exe /u hoge.dll

開発環境で登録する場合には Visual Studio 付属のコマンドプロンプトを使えば RegAsm.exe へのパスも通っているので楽ちんです。
Visual Studio 2010
　スタートメニューのVisual Studio 2010 -> Visual Studio Tools -> Visual Studio コマンドプロンプト（2010）
Visual Studio 2013
　スタートメニューのVisual Studio 2013 -> Visual Studio ツール -> 開発者コマンド プロンプト for VS2013

# Visual Studio がビルド時に登録してくれるよ

COM の登録はレジストリの変更を伴うので管理者権限が必要ですから、Visual Studio を管理者権限で起動する必要があります。
スタートメニューから右クリして管理者権限で起動するか、以下の方法で常に管理者権限で起動するようにしてしまいましょう。

レジストリエディターを起動して以下のキーに管理者権限で実行したい実行ファイルのフルパスを登録します。
HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers

文字列値を作成して、「値の名前」に実行ファイルのフルパス、「値のデータ」に「RUNASADMIN」と入力します。
Visual Studio のプロセスである devenv.exe と、ソリューションファイルの関連付けに割り当てられている VSLauncher.exe を登録しておくといい感じですよ。

![2014-08-29_161157.png](https://qiita-image-store.s3.amazonaws.com/0/52104/1d2e4789-c42d-8ee2-88f4-a294945c9f81.png)

ずっと前に遊んだゲームの体験版のパスが登録されてた(ﾟдﾟ)
アンインストール済みのゲームなのに残ってるのは気持ち悪いわ・・・

# プロジェクトの設定

新規作成 -> プロジェクトからクラスライブラリを選択します。
プロジェクトのプロパティを開いてアプリケーション -> アセンブリ情報画面の「アセンブリを COM 参照可能にする」をチェック。

![2014-08-30_041355.png](https://qiita-image-store.s3.amazonaws.com/0/52104/98b12979-581f-26e7-93a8-35f9007758e2.png)

ビルド の「COM 相互運用機能の登録」をチェック。

![2014-08-30_050028.png](https://qiita-image-store.s3.amazonaws.com/0/52104/b6d27bab-fc48-70b7-aff0-153ccd24cd0f.png)

# COM コンポーネントを作ってみよう(ﾟ∀ﾟ)

１から作るのはめんどくさいので、前回の記事の[自然順で文字列をソートするクラス](http://qiita.com/tomochan154/items/1a3048f2cd9755233b4f)を使って説明します。


### 公開するインターフェースを定義する

まずは COM として公開したいメソッドやプロパティをインターフェースにまとめます。

```csharp
[Guid("C7BE210D-6460-443c-A34D-01727732E56C")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface INaturalComparer
{
    [Description("昇順か降順を指定してね。")]
    NaturalSortOrder SortOrder { get; set; }

    [Description("比較おぷしょん。")]
    NaturalComparerOptions Options { get; set; }

    [Description("オブジェクトの大小関係を比較します。")]
    int Compare(string s1, string s2);
}
```

インターフェースには Guid 属性と InterfaceType 属性を付けます。
Guidの値についてはあとで説明します。
InterfaceType は事前バインディングと遅延バインディングのどちらに対応するか、もしくは両方に対応するかなどを決定します。
ComInterfaceType.InterfaceIsDual は既定値で両方にします。
詳細は [MSDN](http://msdn.microsoft.com/ja-jp/library/tz0x1301(v=vs.110).aspx) で確認してください。

インターフェースのメンバーには Description 属性を付けると、COM コンポーネントを利用するほうの開発環境で説明文が表示されます。
VBA だとメソッドの説明は表示されますが、プロパティの説明は表示されません。
COM の仕様でメソッドにしか説明が付かないのか、VBA がプロパティに対応していないのかは分かりませんでした。

![2014-08-30_071625.png](https://qiita-image-store.s3.amazonaws.com/0/52104/b9c23dec-a9b8-5ebf-7320-012de22fa76c.png)

### クラスにインターフェースを割り当てる

```csharp
[Guid("50884865-710F-47B8-8398-3E51E8E0C01B")]
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(INaturalComparer))]
public class NaturalComparer : INaturalComparer, IComparer<string>, IComparer
{
    // 実装
}
```

このようにクラスの属性を設定します。
ComDefaultInterface には先ほど定義した INaturalComparer インターフェースの型情報を指定します。
それ以外の属性については説明が面倒なのでググレカスでお願いします＞＜

### インターフェースで参照している Enum を COM に公開する

```csharp
[ComVisible(true)]
public enum NaturalSortOrder : int
{
    // ...
}

[Flags()]
[ComVisible(true)]
public enum NaturalComparerOptions
{
    // ...
}
```

ComVisible 属性に true を指定することで COM で参照可能になります。
COM に公開するインターフェースを実装しているクラスや Enum にはすべて ComVisible 属性を付けてください。

# GUID を作成する

GUID は他の人と同じ値を使うと衝突してどちらかのクラスが機能しなくなってしまいます。
なのでこの記事のソースからコピペして使ったり、ネットで拾ってくるのはダメ！絶対。
Visual Studio のメニューのツール -> GUID の作成から簡単に作れます。

![2014-08-30_043732.png](https://qiita-image-store.s3.amazonaws.com/0/52104/4ba1262c-3e3f-55e8-c4b4-a46220f80fe7.png)


# 出来上がった COM オブジェクトを VBA で使ってみる

Excel を起動して VBA のエディタを表示します。
参照設定から、先ほどのプロジェクト名と同じ名前を探してみます。
そういえば、プロジェクト名なににしたっけなぁ・・・。

![2014-08-30_072635.png](https://qiita-image-store.s3.amazonaws.com/0/52104/bf1cb4f5-17f3-13ac-30d8-2997533cf48c.png)

ClassLibrary1 でした(ﾟдﾟ)
みなさんはちゃんとした名前を付けてくださいね

そしたら標準モジュールで NaturalComparer クラスを New して使ってみる。

```vbnet:標準モジュール
Sub test()
    Dim comp As New NaturalComparer
    Debug.Print comp.Compare("hoge1", "hoge10")  '-1
    Debug.Print comp.Compare("hoge2", "hoge10")  '-1
    Debug.Print comp.Compare("hoge11", "hoge10") ' 1
End Sub
```

OKですね。

# イベントも実装してみよう

ちょっと微妙な例ですけど、NaturalComparer クラスで１文字ずつ比較する処理の中で、等価と判断したときにイベントを発生させて追加の比較処理を実装出来るようにしてみます。

```csharp
[Guid("DDD17585-5DF0-4A5D-9090-21FFEFC0B9F1")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface INaturalComparerEvent
{
    [Description("追加の比較処理をするときにつかってね。")]
    int ExCompare(string a, string b);
}

[Guid("50884865-710F-47B8-8398-3E51E8E0C01B")]
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(INaturalComparer))]
[ComSourceInterfaces(typeof(INaturalComparerEvent))]
public class NaturalComparer : INaturalComparer, IComparer<string>, IComparer
{
    [ComVisible(false)]
    public delegate int ExCompareEventHandler(string a, string b);
    public event ExCompareEventHandler ExCompare;

    protected virtual int LocalCompare(string s1, string s2)
    {
        ...
        // 両方の文字が同じときはカスタム比較するよ
        if (ExCompare != null)
        {
            // イベント発生
            diff = ExCompare(s1[p1].ToString(), s2[p2].ToString());
            if (diff != 0)
            {
                return diff;
            }
        }
        ...
    }
}
```

このようにイベントは別のインターフェースとして定義して ComSourceInterfaces 属性でクラスに割り当てます。
イベントの実装はオーソドックスにデリゲートを定義してイベントを公開するだけです。
ただしデリゲートは ComVisible 属性で非公開にしておかないと、意味のない空っぽの ExCompareEventHandler クラスが公開されてしまいますので注意です。

![2014-08-30_065606.png](https://qiita-image-store.s3.amazonaws.com/0/52104/9c5b9649-eefc-6542-d90c-6ab33b50d09b.png)


### VBA から利用してみる

```vbnet:Class1クラスモジュール
Private WithEvents comp As NaturalComparer

Private Sub Class_Initialize()
    Set comp = New NaturalComparer
End Sub

Sub Compare(a As String, b As String)
    Call comp.Compare(a, b)
End Sub

Private Function comp_ExCompare(ByVal a As String, ByVal b As String) As Long
    Debug.Print a
    comp_ExCompare = 0
End Function
```

```vbnet:標準モジュール
Sub test()
    Dim cls As New Class1
    cls.Compare "hoge1", "hoge10"
End Sub
```

```:イミディェイト
h
o
g
e
```

# さいごに

でもさ～、これどんなときに使うの？(´・ω・`)
って思う人が結構いるかもですよね。
私の場合は、主に Excel とクラウドを連携させるためとかで Excel のアドインを C# で作ったりします。
Excel は私のお客さんの間では大人気なので、結構使う機会が多いんですよ。
マクロで C# 使えるようになったらいいのになぁ～