RootProCADにアドインを追加してみた

,

dxf/dwgが使える2次元CADのRootProCADは、アドインで機能拡張できるとの事なので、早速アドインを作成してみたら出来た。これで何でも出来そうな気がしてきた。

RootProCADのサイト画面

1)RootProCADを使っている理由

dxf/dwgファイルを解析する事が時々ありますので何か良さそうなtoolは無いかと探して居ました。dxfファイルならPythonの「ezdxf」で読み込めますので、以後はPythonの「networkX」などでグラフ解析が出来ます。dwgファイルの場合は「ODA File Converter」でdxfファイルに変換出来ます。

dxf/dwgのviewerは幾つかあると思いますが、目についたのが「RootProCAD free」でした。

早速ダウンロードして使ってみると、調査したいdwg/dxfファイルについては十分な性能でした。アドインで機能追加するにはfreeでは無く有料版が必要との事でしたが、ヘルプファイルを見る限り十分な拡張性が見込めました。また値段も1.9万程度なのでAutoCad互換のCADとしても格安でした。

昔々、インテリCADの会員になり、インテリCADがAutoCADと比べて表示が遅かったので20倍ほど高速化したりしていましたが、RootProCADもインテリCADの流れなのかな?とも思ってしまいました。

AutoCADと互換CADの歴史がIJCADのURLに有りました。面白いので一読を。

2)アドインの開発

2−1)VS2022を使ってとアドイン開発

RootProCADのアドインはc#/vb/vcで作成できる様ですが、近頃良く使っているc#で作成する事にします。

開発環境はVS2022を使用します。
VS2022を起動して、[ファイル] – [新規作成] – [プロジェクト] メニュー、または [ファイル] – [新しいプロジェクト] メニューをクリックすると[新しいプロジェクト] ダイアログボックスが表示されます。検索窓に「RootPro」を入れると以下の様にRootProのプロジェクトテンプレートが表示されますのでc#を選んで「次へ」ボタンを押して、新規のアドインプロジェクトを作成していきます。

プロジェクトが作成されるとVS2022で作業できる様になります。

自動的に作成されたフォルダーやファイルがプロジェクトにあります。

ファイル名重要な関数機能

AppAddIn.cs

 AppAddIn_Startup
アドインが読み込まれた時に実行される関数。コマンドの登録と、実行関数を指定する。

AppAddIn_Shutdown
アドインの終了時に実行される関数。コマンドの登録削除を行う。

2−2)アドインの機能

1。座標指定してその位置にJUMPする機能が見当たらなかったので、「座標JUMP機能」を作成する。(=まだ、RootProの初心者なので、機能があるのかも知れませんが、見つけれなかったので、面倒なのでアドインにした)
2。同様に文字列検索機能も追加=サンプルの文字列検索がVS2022では動かないので修正して動かす様にした。。。が、基本機能で文字列検索があった。。。が、基本機能の文字列検索は、移動するたびに画面のスケールが変わりzoom-outして行くので、詳細な情報を見て行くには不向きなので、「座標JUMP機能」と一緒に「文字列JUMP」機能も追加した。
3。ダイアログで作ったので、画面の拡大・縮小・移動などが出来ないため画面拡大・縮小・移動機能も追加した。

3)今回作成したアドイン

3−1)追加・修正したコード

1つのフォームを追加して以下の様なダイアログ(AxSearcheDialog)を作成しました。機能は2-2)で説明した様に、指定座標jump/文字列検索jump/画面拡大・縮小・移動です。

<AppAddIn.csの修正部分> 
   public partial class AppAddIn
    {
        private void AppAddIn_Startup(object sender, EventArgs e)
        {
            MessageBox.Show("アドイン(AxSearchDialog)
             を読み込みました。");
            //コマンドの追加
            CommandManager.AddMacroCommand("座標へJUMP/文字検索JUMP", 
             MacroCommand);
        }
        private void AppAddIn_Shutdown(object sender, EventArgs e)
        {
            //コマンドの削除
            CommandManager.RemoveMacroCommand(MacroCommand);
        }

        /// <summary>
        /// 文字列の検索・置換フォームの表示
        /// </summary>
        private void MacroCommand()
        {
            //フォームの作成
            AxSearcheDialog form = new AxSearcheDialog(this);

            //フォームを表示する(モーダル)
            form.ShowDialog();

            form.Dispose();
        }


        #region VSTA generated code
        private void InternalStartup()
        {
            this.Startup += new 
            System.EventHandler(AppAddIn_Startup);
            this.Shutdown += new 
            System.EventHandler(AppAddIn_Shutdown);
        }
        #endregion
    }
<追加したAxSearcheDialog.cs>   
 public partial class AxSearcheDialog : Form
    {

        AppAddIn AppAddIn;
        List<(Shape,String)> ResultShapes = 
             new List<(Shape,String)>();

        /// <summary>
        /// コンストラクタ
        /// </summary>  
        public AxSearcheDialog(AppAddIn app)
        {
            this.AppAddIn = app;
            InitializeComponent();

            //初期化
            //検索対象を全てチェックする
            figCB.Checked = true;
            atrNameCB.Checked = true;
            atrValueCB.Checked = true;
            partsNameCB.Checked = true;
        }

        /// <summary>
        /// 座標指定(x,y)画面移動を行うボタンが押された
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void xyJumpB_Click(object sender, EventArgs e)
        {
            if (xyTextTB.Text.Length > 2)
            {
                string[] dats = xyTextTB.Text.Split(',');
                if (dats.Length != 2)
                {
                    MessageBox.Show("入力値が不正です");
                    return;
                }
                bool xflg = double.TryParse(dats[0], out double x);
                bool yflg = double.TryParse(dats[1], out double y);
                if (!xflg || !yflg)
                {
                    MessageBox.Show("入力値が不正です");
                    return;
                }
                //
                viewPan(x, y);
            }
        }

        private void AxSearchDialog_Load(object sender, EventArgs e)
        {

        }

        /// <summary>
        /// クローズボタンが押された
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void closeB_Click(object sender, EventArgs e)
        {
            Close();
        }

        private void checkedListBox1_SelectedIndexChanged
             (object sender, EventArgs e)
        {

        }

        /// <summary>
        /// 文字列検索ボタンが押された
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>

        private void strSearchB_Click(object sender, EventArgs e)
        {
            //コントロールから値の取得
            Boolean isCase = this.caseStrCB.Checked;
            Boolean isTextShape = this.figCB.Checked;
            Boolean isAttributeName = this.atrNameCB.Checked;
            Boolean isAttributeValue = this.atrValueCB.Checked;
            Boolean isPartsName = this.partsNameCB.Checked;
            //検索文字列の取得
            String searchString = this.strTextTB.Text;     

            if (searchString.Length < 1)
            {
                MessageBox.Show("検索文字列が指定されていません");
                return;
            }
            //ドキュメントの取得
            Document doc = this.AppAddIn.ActiveDocument;			
            if (doc == null)
                return;
            //部分図の取得
            Drawing drawing = doc.CurrentDrawing;		

            Shape shape = null;

            // 結果のリストをクリアします
            ResultShapes.Clear();

            //図形を列挙する
            while ((shape = drawing.Shapes.GetNextShape(shape))
                     != null)
            {

                //検索を実行します
                if (isTextShape)            //テキストの検索を行う?
                {
                    (bool flg, String str) = 
                          TextShapeSearch(shape, 
                               searchString, isCase);
                    if (flg)
                    {
                        ResultShapes.Add((shape,str));
                    }
                }
                //属性の検索を行う
                if (isAttributeName || isAttributeValue)     
                {
                    (bool flg, String str) = 
                            AttributeSearch(shape, searchString,
                                   isCase, isAttributeName, 
                                   isAttributeValue);
                    if (flg)
                    {
                        ResultShapes.Add((shape, str));
                    }
                }
                if (isPartsName)        //部品の検索を行う?
                {
                    //部品(insert)なら
                    if ((shape is DrawComponentShape))      
                    {
                        DrawComponentShape parts = 
                              (DrawComponentShape)shape;
                        MasterDrawComponent insertBlock = 
                               parts.Master;
                        String name = insertBlock.Name;
                        int index = name.IndexOf(searchString, 0, 
                            isCase ? StringComparison.Ordinal : 
                            StringComparison.OrdinalIgnoreCase);

                        if (index != -1)    //見つかった 
                        {
                            ResultShapes.Add(
                                 (shape, $"部品:{name}"));
                        }
                    }
                }
            }
            //
            strSerchMesL.Text = 
                     ResultShapes.Count + " 個の図形が見つかりました";
            //
            MessageBox.Show(strSerchMesL.Text);
            //
            if (ResultShapes.Count < 1)
            {
                resultJmpUD.Maximum = 0;
                resultJmpUD.Minimum = 0;
                resultJmpUD.Value = 0;
                return;
            }   
            resultJmpUD.Maximum = ResultShapes.Count;
            resultJmpUD.Minimum = 1;
            resultJmpUD.Value = 1;
            resultJump(1);
        }
        /// <summary>
        /// テキスト図形の文字列を検索
        /// </summary>
        /// <param name="shape">検索する文字列</param>
        /// <param name="searchString">検索文字列</param>
        /// <param name="isCase">大文字・小文字を区別するか?</param>
        /// <returns></returns>
        private (Boolean,String) TextShapeSearch(Shape shape, 
                    String searchString, Boolean isCase)
        {

            //大文字、小文字を無視するか?
            StringComparison option = 
                isCase ? StringComparison.Ordinal : 
                StringComparison.OrdinalIgnoreCase;

            if (!(shape is TextShape))      //テキスト型でない?
                return (false,null);

            TextShape textShape = ((TextShape)shape);
            String toText = textShape.Text;

            //文字列を検索
            int index = toText.IndexOf(searchString, 0, option);
            if (index == -1)    //見つかりませんでした
                return (false, null);

            return (true,$"図形:{toText}");

        }

        /// <summary>
        /// 属性文字列を検索する
        /// </summary>
        /// <param name="shape">調べる図形</param>
        /// <param name="searchString">検索文字列</param>
        /// <param name="isCase">大文字・小文字を区別するか?</param>
        /// <param name="isName">属性名の検索を行うか?</param>
        /// <param name="isValue">属性値の検索を行うか?</param>
        /// <returns></returns>
        private (Boolean, String) AttributeSearch(Shape shape, 
                   String searchString, Boolean isCase, 
                   Boolean isName, Boolean isValue)
        {
            //大文字、小文字を無視するか?
            StringComparison option = 
                  isCase ? StringComparison.Ordinal : 
                           StringComparison.OrdinalIgnoreCase;

            for (int i = 0; i < shape.Attributes.Count; i++)
            {
                if (isName)
                {
                    String name = shape.Attributes[i].Name;
                    int index = 
                        name.IndexOf(searchString, 0, option);
                    if (index != -1)    //見つかった 
                    {
                        return (true, $"属性名:{name}");
                    }
                }

                if (isValue)
                {
                    String value = shape.Attributes[i].Value;
                    int index = 
                         value.IndexOf(searchString, 0, option);
                    if (index != -1)    //見つかった 
                    {
                        return (true, $"属性値:{value}");
                    }
                }

            }

            //見つかりませんでした。
            return (false, null);
        }

        /// <summary>
        /// 検索結果のup/downスピンボタンが押された
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void resultJmpUD_ValueChanged(object sender, 
                      EventArgs e)
        {
            int no  = (int)resultJmpUD.Value;
            resultJump(no);
        }

        /// <summary>
        /// 検索番号を指定して、その図形にジャンプします
        /// </summary>
        /// <param name="no"></param>
        private void resultJump(int no)
        {
            if (no < 1 || no > ResultShapes.Count)
            {
                return;
            }
            (Shape shp, String str) = ResultShapes[no - 1];
            //ドキュメントの取得
            Document doc = this.AppAddIn.ActiveDocument;			
            //図形を選択状態にします
            doc.SelectionManager.SelectShape(shp, 
                     SelectionCombineMode.Replace);
            //
            curSearchL.Text = "No." + no + " ( " + str +" )";
            //
            Point2d[] ext = shp.GetBoundingBox(); 
            double scx = (ext[0].X + ext[1].X) / 2;
            double scy = (ext[0].Y + ext[1].Y) / 2;
            viewPan(scx, scy);

        }

        /// <summary>
        /// 座標を指定して、ビューを移動します
        /// </summary>
        /// <param name="x"></param>
        /// <param name="y"></param>
        void viewPan(double x, double y)
        {
            /ビューの取得
            DrawingView view = 
                   (DrawingView)this.AppAddIn.ActiveDrawingView;  /
            //
            double cx = (view.ViewTopRightPoint.X + 
                         view.ViewBottomLeftPoint.X) / 2;
            double cy = (view.ViewTopRightPoint.Y + 
                         view.ViewBottomLeftPoint.Y) / 2;
            //
            double mvx = cx - x;
            double mvy = cy - y;
            //
            Point2d nBtomLeft = 
                new Point2d(view.ViewBottomLeftPoint.X - mvx,
                view.ViewBottomLeftPoint.Y - mvy);

            Point2d nTopRight = 
                new Point2d(view.ViewTopRightPoint.X - mvx,
                view.ViewTopRightPoint.Y - mvy);
            //
            view.ZoomWindow(nBtomLeft, nTopRight);
            //
            view.Update();	//ビューの更新
        }

        /// <summary>
        /// 表示倍率を指定して、画面を拡大・縮小します
        /// </summary>
        /// <param name="zoom"></param>
        void viewZoom(double zoom)
        {
            //ビューの取得
            DrawingView view = 
                   (DrawingView)this.AppAddIn.ActiveDrawingView;  
            //
            view.ZoomScale *= zoom;
            //
            view.Update();	//ビューの更新
        }

        /// <summary>
        /// 左右、上下にビューを1/4シフトします
        /// </summary>
        /// <param name="sx"></param>
        /// <param name="sy"></param>
        void viewQShift(int sx, int sy)
        {
            //ビューの取得
            DrawingView view = 
                (DrawingView)this.AppAddIn.ActiveDrawingView;  
                                                                            
            double cx = (view.ViewTopRightPoint.X +
                         view.ViewBottomLeftPoint.X) / 2;
            double cy = (view.ViewTopRightPoint.Y + 
                         view.ViewBottomLeftPoint.Y) / 2;
            //
            double qx = (view.ViewTopRightPoint.X - 
                         view.ViewBottomLeftPoint.X) / 4;
            double qy = (view.ViewTopRightPoint.Y - 
                         view.ViewBottomLeftPoint.Y) / 4;
            //
            double mvx = qx*sx;
            double mvy = qy*sy;
            //
            Point2d nBtomLeft = 
                new Point2d(view.ViewBottomLeftPoint.X - mvx,
                view.ViewBottomLeftPoint.Y - mvy);

            Point2d nTopRight = 
          new Point2d(view.ViewTopRightPoint.X - mvx,
                view.ViewTopRightPoint.Y - mvy);
            //
            view.ZoomWindow(nBtomLeft, nTopRight);
            //
            view.Update();	//ビューの更新
        }

        /// <summary>
        /// 画面の縮小ボタンが押された
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void zoomOutB_Click(object sender, EventArgs e)
        {
            viewZoom(0.8);
        }

        /// <summary>
        /// 画面の拡大ボタンが押された
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void zoomInB_Click(object sender, EventArgs e)
        {
            viewZoom(1.25);
        }

        /// <summary>
        /// 画面の左シフトボタンが押された
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>

        private void viewLeftB_Click(object sender, EventArgs e)
        {
            viewQShift(1, 0);
        }

        /// <summary>
        /// 画面の右シフトボタンが押された
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void viewRightB_Click(object sender, EventArgs e)
        {
            viewQShift(-1, 0);
        }

        /// <summary>
        /// 画面の上シフトボタンが押された
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>

        private void viewUpB_Click(object sender, EventArgs e)
        {
            viewQShift(0, -1);
        }

        /// <summary>
        /// 画面の下シフトボタンが押された
        /// </summary>s
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void viewDownB_Click(object sender, EventArgs e)
        {
            viewQShift(0, 1);
        }

        /// <summary>
        /// 画面の左上シフトボタンが押された
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void viewLeftUpB_Click(object sender, EventArgs e)
        {
            viewQShift(1, -1);
        }

        /// <summary>
        /// 画面の左下シフトボタンが押された
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>

        private void viewLeftDownB_Click(object sender, EventArgs e)
        {
            viewQShift(1, 1);
        }

        /// <summary>
        /// 画面の右上シフトボタンが押された
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param> 

        private void viewRightUpB_Click(object sender, EventArgs e)
        {
            viewQShift(-1, -1);
        }

        /// <summary>
        /// 画面の右下シフトボタンが押された
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void viewRightDownB_Click(object sender, EventArgs e)
        {
            viewQShift(-1, 1);
        }
    }

3−2)コンパイルと実行

ソースが出来たらコンパイル(ビルド)します。正常にコンパイルできると自動的にRootProCADのアドインフォルダーにdllが配置されます。

例)。。\ドキュメント\RootPro CAD\AddIns\RcadAxSerchDialog

VS2022で「開始」ボタンを押すとRootProCADが起動します。

アドインを使うには、「アドイン」ー「アドインの管理」でアドインマネージャーを起動して、作ったアドインを「有効化」する必要があります。

「有効化」を押してアドインを使用できる様にします。

有効になると初期化メッセージが表示されます。

4)動作状況

アドインを実行すると、「座標jump・文字列検索jump」ダイアログが出るので、例では「CLE」を検査した。4個のデータが検索できたので、最初の位置に画面が移動して、対応図形を自動選択する。スピンボタンで検索結果を切り替える=以下の例は、2番目の結果表示画面です。

スピンボタンで検索結果を切り替える。以下の画面は、3番目の表示結果です。

5)その他

5-1)GitHub Copilot

今回はVS2022を久しぶりに使いましたが、VSCODEと同様にGitHub Copilotが使えました。画面の中心計算や、シフト量計算のXを書くと、Yは自動で出てきたりと助けられました。この様な決まりきった計算式を今までは何度となく書いて居た物ですが、本当に便利な世の中ですね。

5-2)可能性

RootProCADのAPIが良くわからないので、最初は戸惑いましたがサンプルソースを幾つか見る事により何となく「お作法」が解ったので、後はいつもの様に作成するだけでした。もう少し慣れれば結構な事が出来そうな感じです。

近頃某CATV設計CADデータを見る事があったのですが、30年前と同じ様に非常に疑問のあるデータ構図でした(=進歩して居ない?)。いっそRootProCADベースで作り直してやるかなと思うこの頃です。


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

PAGE TOP