この技術情報では、以前にも ".NET API でコマンドを作る" でカスタムコマンドの作成方法をお伝えしました。しかし実際に開発をしてみますと、カスタムコマンド定義の数とともにコード行数も増え、その数が10を超える頃には非常に読みにくいコードになってきます。

そこで本稿ではカスタムコマンドの処理部分を別クラスに切り出し、コマンドクラスの見通しをよくする方法についてお伝えしたいと思います。

なおサンプルコードは VisualStudio2005 の C# で作成し、 AutoCAD2007/2008/2009 で動作確認を行いました。

コマンド処理クラスの作成

カスタムコマンドの処理はそれぞれに独立していますから、各処理ごとにクラスとして切り出してやれば見通しのよいコードになるはずです。これはまさに、オブジェクト指向でいうところのカプセル化です。

しかし、コマンドごとにインスタンスを生成して、そこからメソッドを呼び出して……という手順は少々煩雑です。そこで、この処理をさらにまとめてしまおうというのが今回のコンセプトです。

今回は "エンティティを作成する" で示したコードを基にして、コマンド処理クラスを利用する形に改造してみました。まずは下記のサンプルコードをビルドし、その成果物を [NETLOAD] してください。次に [CREATE_LINE] コマンドを発行すると、(0,0,0)-(100,100,0) を結ぶ線分が描画されます。これは "エンティティを作成する" で示したものと同じです。

// Command.cs
public class Command
{
    [CommandMethod( "CREATE_LINE" )]
    public void CreateLine()
    {
        // 1. コマンド処理メソッドを呼び出す
        CreateLineCommandProcessor.Run();
    }
}
// CreateLineCommandProcessor.cs
class CreateLineCommandProcessor
{
    public static void Run()
    {
        // 2. コマンド処理クラスのインスタンスを生成する
        CreateLineCommandProcessor com_lib = new CreateLineCommandProcessor();

        // 3. コマンド処理のメインルーチンを呼び出す
        com_lib.RunCreateLine();
    }

    // インスタンスを生成されないよう、コンストラクタを private にしておく
    private CreateLineCommandProcessor()
    {
    }

    // コマンド処理のメインルーチン
    private void RunCreateLine()
    {
        Transaction trans = null;
        try
        {
            Document active_doc = Application.DocumentManager.MdiActiveDocument;
            Database active_db = active_doc.Database;
            trans = active_db.TransactionManager.StartTransaction();

            // 4. エンティティを作成する
            Point3d from_pos = Point3d.Origin;
            Point3d to_pos = new Point3d( 100, 100, 0 );
            Line line = new Line( from_pos, to_pos );

            // 5. モデルスペースを書き込みモードで取得する
            ObjectId blk_tbl_id = active_db.BlockTableId;
            BlockTable blk_tbl =
                (BlockTable)trans.GetObject( blk_tbl_id, OpenMode.ForRead );
            ObjectId model_space_id = blk_tbl[ BlockTableRecord.ModelSpace ];
            BlockTableRecord model_space =
                (BlockTableRecord)trans.GetObject( model_space_id, OpenMode.ForWrite );

            // 6. モデルスペースにエンティティを登録する
            model_space.AppendEntity( line );
            trans.AddNewlyCreatedDBObject( line, true );

            trans.Commit();
        }
        catch( System.Exception ex )
        {
            Editor edit = Application.DocumentManager.MdiActiveDocument.Editor;
            edit.WriteMessage( "\n{0}", ex.Message );
        }
        finally
        {
            // 7. トランザクションを破棄する
            if( trans != null )
            {
                trans.Dispose();
            }
        }
    }
}

環境によっては、コンパイル時に "この参照を解決できませんでした" という警告が出ます。この場合 "acdbmgd.dll" と "acmgd.dll" を削除し、 AutoCAD のインストールディレクトリから参照しなおしてください。

サンプルコード解説

先述の通り、今回の最大の目的はコマンドクラスの見通しをよくすることです。アプリケーションの規模が大きくなってくると、すべての起点であるはずのコマンドクラスの見通しが悪くなり、コードを追いかけることが困難になってきます。従って、コマンドクラスを簡略化することは、非常に重要な課題です。

そこでコマンドごとに処理を担当するクラスを作成し、カスタムコマンドメソッドからそのクラスに処理を委譲するようにしてみました。そのクラスは static メソッドを一つだけ公開しています。そしてその中で自分自身を生成し、各種処理を行うという構造です(*1)

Run メソッド

コマンド処理クラスのメンバのうち、外部に唯一公開されているメソッドです。static メソッドですから、外部からはどこにでもあるごく普通のメソッドかのように利用することができます。

その役割は、自分自身のインスタンスを生成し、メインルーチンになるメソッドを呼び出すだけです。

コンストラクタ

コマンド処理クラスが外部に向けて公開しているメンバは上記の Run() だけですから、他者にインスタンスを生成させることは止めなければなりません。そこでコンストラクタを private とし、自分自身しかインスタンスを生成できないようにしました(*2)。Run() は自分自身が定義しているメソッドですから、問題なくインスタンスを生成できます。

メインルーチン

ここから先はごく普通のメソッドです。今回はべた書きにしましたが、処理単位ごとにメソッドに切り出すなど、一般的なアプリケーション作成で使用するテクニックをそのまま適用できます(*3)

(MINERVA 深津貴成)
  1. ^ 弊社ではこの構造を "関数クラス" と呼んでいます。ワンショットだけど規模の大きな処理をするのに重宝する構造ですね。AutoCAD のコマンド処理をするには、実にうってつけです。
  2. ^ 考えてみると、誰もインスタンスを生成することのできない、非常に不気味なクラスです。しかし文法上、なんの問題もありません。
  3. ^ そして何より、メンバ変数を使うことができるので非常に便利です。