QCADに指定座標への移動機能を追加

,

QCADに指定座標への移動機能を追加してみました。JavaScriptでメニュー登録、追加機能実行=座標移動ダイアログ表示して、指定座標への画面移動や、拡大・縮小を行う機能です。資料が古かったり、サンプルスクリプトが少なかったりと色々ドタバタしましたが、一応動作しています。

1。「M雑多」「QcadAdd」「座標移動とズーム」を実行

2。座標ジャンプダイアログが表示される。

3。移動する座標をカンマ区切りで指定します。指定後「この座標へジャンプ」ボタンを押すと、画面が移動します。

4。拡大、縮小ボタンで画面表示サイズを変更できます。

以後に、実際のスクリプト作成の方法やメニュー配置方法を説明します。

対象環境: QCAD Professional 3.32.6 / Qt 6.x / macOS arm64


1. ファイル構成の原則

QCAD の AddOn スキャン(AddOn.getAddOns())は以下の規則でスクリプトを自動検出する。

ディレクトリ名/ディレクトリ名.js

✅ 正しい構成

scripts/Misc/
  QcadAdd/
    QcadAdd.js            ← AddOnとして認識される(ディレクトリ名と一致)
    MoveAndZoom/
      MoveAndZoom.js      ← AddOnとして認識される(ディレクトリ名と一致)

❌ 動作しない構成

scripts/Misc/
  QcadAdd/
    MoveAndZoom.js        ← ディレクトリ直下のファイルは無視される

2. QCAD本体へのスクリプト配置

QCAD Pro の scripts フォルダは以下のパスにある:

/Applications/QCAD-Pro.app/Contents/Resources/scripts/

readme.txt より: このフォルダに配置したスクリプトはプラグインより優先される。

シンボリックリンクで開発フォルダを接続する方法

cd /Applications/QCAD-Pro.app/Contents/Resources/scripts/Misc/
ln -s /path/to/MyProject/QcadAdd QcadAdd

これにより開発フォルダを直接 QCAD に認識させることができる。


3. 必須ファイルの構成

3-1. モジュール定義ファイル(例: QcadAdd/QcadAdd.js

メニュー階層を定義する。Misc の下にサブメニューを作成する場合:

include("scripts/Misc/Misc.js");

function QcadAdd(guiAction) {
    Misc.call(this, guiAction);
}

QcadAdd.prototype = new Misc();
QcadAdd.includeBasePath = includeBasePath;

QcadAdd.getMenu = function() {
    var menu = EAction.getSubMenu(
        Misc.getMenu(),
        99000,          // グループソート順(大きいほど後)
        100,            // ソート順
        QcadAdd.getTitle(),
        "QcadAddMenu"   // ウィジェット名(メニュー項目登録時に使用)
    );
    return menu;
};

QcadAdd.getTitle = function() { return "QcadAdd"; };
QcadAdd.prototype.getTitle = function() { return QcadAdd.getTitle(); };

QcadAdd.init = function(basePath) {
    QcadAdd.getMenu();  // メニューを作成するだけでよい
};

3-2. アクションファイル(例: QcadAdd/MoveAndZoom/MoveAndZoom.js

include("scripts/EAction.js");

function MoveAndZoom(guiAction) {
    EAction.call(this, guiAction);
}

MoveAndZoom.prototype = new EAction();

// ★ 必須: 親クラスの beginEvent を必ず呼ぶ
MoveAndZoom.prototype.beginEvent = function() {
    EAction.prototype.beginEvent.call(this);  // ← これがないとメニューから動かない
    // 処理...
    this.terminate();
};

MoveAndZoom.init = function(basePath) {
    var action = new RGuiAction("メニュー表示名", RMainWindowQt.getMainWindow());
    action.setRequiresDocument(true);
    action.setScriptFile(basePath + "/MoveAndZoom.js");
    action.setGroupSortOrder(99000);
    action.setSortOrder(0);
    action.setWidgetNames(["QcadAddMenu"]);  // ← モジュール定義のウィジェット名と一致させる
};

4. よくあるハマりポイントと解決策

beginEventEAction.prototype.beginEvent.call(this) が抜けている

  • 症状: メニューをクリックしても何も起きない(サイレント失敗)
  • 原因: 親クラスの初期化が行われず、QCAD 内部状態が未設定のまま中断する
  • 解決: 必ず先頭行に EAction.prototype.beginEvent.call(this); を追加する

getDocumentInterface is not defined

  • 症状: メニューから実行するとエラー、Script Console からは動く
  • 原因: getDocumentInterface()library.js のグローバル関数。AddOn コンテキストでは未定義になる場合がある
  • 解決: 以下に置き換える
// ❌ 動かない場合がある
var di = getDocumentInterface();

// ✅ 常に動作する
var di = RMainWindowQt.getMainWindow().getDocumentInterface();

// ✅ beginEvent 内では this から取得できる
var di = this.getDocumentInterface();

QcadAdd.js が存在しないとフォルダごと無視される

  • 症状: メニューに何も表示されない
  • 原因: AddOn スキャンは ディレクトリ名/ディレクトリ名.js がない場合そのディレクトリを無視する
  • 解決: 必ずモジュール定義ファイル(QcadAdd/QcadAdd.js)を作成する

setMenuPath はメニューを作成しない

  • 症状: action.setMenuPath("Misc/QcadAdd") を設定しても表示されない
  • 原因: setMenuPath は既存メニューへの配置指定。事前に getSubMenu でメニューを作成する必要がある
  • 解決: モジュール定義ファイルで getMenu() / getSubMenu() を使いメニューを先に作成し、setWidgetNames(["QcadAddMenu"]) で参照する

RGuiAction の第2引数に null を渡さない

// ❌
var action = new RGuiAction("名前", null);

// ✅
var action = new RGuiAction("名前", RMainWindowQt.getMainWindow());

5. ダイアログをモードレス(非ブロッキング)にする方法

dialog.exec() はモーダル(QCAD の通常操作がブロックされる)。
モードレスにするには以下のようにする:

var appWin = RMainWindowQt.getMainWindow();

// Qt.Tool: メインウィンドウに追従、常に手前に表示
var dialog = new QDialog(appWin, Qt.Tool);

// GC防止のためstaticプロパティに保持(必須)
MyAction._dialog = dialog;

// 多重起動防止
if (MyAction._dialog && !MyAction._dialog.isHidden()) {
    MyAction._dialog.raise();
    MyAction._dialog.activateWindow();
    return;
}

// exec() の代わりに show() を使う
dialog.show();  // ← ブロックせず即座に返る

// 閉じるボタン
btnClose.clicked.connect(function() { dialog.close(); });

注意: show() 後に関数が終了しても _dialog に参照を保持していないと
GC(ガベージコレクション)でダイアログが即座に破棄される。


6. スクリプト更新後の反映方法

方法 操作 備考
QCAD再起動 アプリを終了して再起動 確実
AddOnリスト再スキャン Tools → Run Script で -rescan 新規ファイルを認識させるとき
Script Console から再ロード include("scripts/Misc/QcadAdd/MoveAndZoom/MoveAndZoom.js") 既存クラスの更新のみ

7. デバッグのコツ

// beginEvent 内でステータスバーにメッセージを出す
EAction.handleUserMessage("MyAction: 起動中...");

// エラーを可視化する
try {
    // 処理
} catch(e) {
    EAction.handleUserWarning("エラー: " + e.message);
    print("ERROR: " + e);  // Script Consoleに出力
}

Script Console(Tools → Script Console)では print() の出力を確認できる。


8. 参考: 完全なディレクトリ構成例

/Applications/QCAD-Pro.app/Contents/Resources/scripts/Misc/
  QcadAdd/              ← シンボリックリンクでも可
    QcadAdd.js          ← モジュール定義(メニュー作成)
    MoveAndZoom/
      MoveAndZoom.js    ← アクション本体
    AnotherTool/
      AnotherTool.js    ← 追加アクション

9.完全なスクリプト例

// MoveAndZoom/MoveAndZoom.js
include("scripts/EAction.js");

function MoveAndZoom(guiAction) {
    EAction.call(this, guiAction);
}

MoveAndZoom.prototype = new EAction();

MoveAndZoom.prototype.beginEvent = function() {
    try {
        EAction.prototype.beginEvent.call(this);
        EAction.handleUserMessage("MoveAndZoom: 起動中...");
        var di = this.getDocumentInterface();
        MoveAndZoom.showDialog(di);
    } catch(e) {
        EAction.handleUserWarning("MoveAndZoom エラー: " + e.message);
        print("MoveAndZoom.beginEvent ERROR: " + e);
    }
    this.terminate();
};

MoveAndZoom.showDialog = function(di) {
    // 既に開いている場合は前面に出すだけ
    if (MoveAndZoom._dialog && !MoveAndZoom._dialog.isHidden()) {
        MoveAndZoom._dialog.raise();
        MoveAndZoom._dialog.activateWindow();
        return;
    }

    if (!di) {
        di = RMainWindowQt.getMainWindow().getDocumentInterface();
    }
    var appWin = RMainWindowQt.getMainWindow();
    if (!di) return;

    var dialog = new QDialog(appWin, Qt.Tool);
    MoveAndZoom._dialog = dialog; // GC防止 & 多重起動チェック用
    dialog.setWindowTitle("座標ジャンプ");
    dialog.setMinimumWidth(500);

    var layout = new QVBoxLayout(dialog);
    layout.addWidget(new QLabel("座標 (X, Y) を貼り付け:"));
    var editCoords = new QLineEdit(dialog);
    editCoords.setPlaceholderText("37971.89, -18162.86");
    layout.addWidget(editCoords);

    var statusEdit = new QTextEdit(dialog);
    statusEdit.setReadOnly(true);
    statusEdit.setMaximumHeight(100);
    statusEdit.setPlainText("待機中...");
    layout.addWidget(statusEdit);

    var btnMove = new QPushButton("この座標へジャンプ (現在の倍率)", dialog);
    btnMove.setMinimumHeight(45);
    layout.addWidget(btnMove);

    var zoomLayout = new QHBoxLayout();
    var btnZoomIn  = new QPushButton("拡大 (+)", dialog);
    var btnZoomOut = new QPushButton("縮小 (-)", dialog);
    btnZoomIn.setMinimumHeight(35);
    btnZoomOut.setMinimumHeight(35);
    zoomLayout.addWidget(btnZoomIn);
    zoomLayout.addWidget(btnZoomOut);
    layout.addLayout(zoomLayout);

    var btnClose = new QPushButton("閉じる", dialog);
    layout.addWidget(btnClose);

    // --- ジャンプ処理 ---
    btnMove.clicked.connect(function() {
        try {
            var rawText = (typeof editCoords.text === "function") ? editCoords.text() : editCoords.text;
            var parts = rawText.split(/[,\s\t\n]+/).filter(function(e) { return e.length > 0; });

            if (parts.length >= 2) {
                var x = parseFloat(parts[0]);
                var y = parseFloat(parts[1]);

                if (!isNaN(x) && !isNaN(y)) {
                    var currentDi = RMainWindowQt.getMainWindow().getDocumentInterface();
                    var doc = currentDi.getDocument();
                    var target = new RVector(x, y);

                    // 1. 選択解除
                    if (typeof currentDi.deselectAll === "function") {
                        currentDi.deselectAll();
                    }

                    // 2. 点の作成
                    var op = new RAddObjectsOperation();
                    var point = new RPointEntity(doc, new RPointData(target));
                    point.setSelected(true);
                    op.addObject(point);
                    currentDi.applyOperation(op);

                    // 3. 現在の表示サイズを取得して同じ倍率のまま中心移動
                    var view = currentDi.getLastKnownViewWithFocus();
                    if (!isNull(view) && typeof view.zoomTo === "function") {
                        var vw = view.getWidth();
                        var vh = view.getHeight();
                        var factor = view.getFactor(); // pixels per model unit
                        var halfW = (vw / 2) / factor;
                        var halfH = (vh / 2) / factor;
                        var box = new RBox(
                            new RVector(x - halfW, y - halfH),
                            new RVector(x + halfW, y + halfH)
                        );
                        view.zoomTo(box, 0);
                        statusEdit.setPlainText("成功: (" + x + ", " + y + ") へジャンプしました。");
                    } else if (typeof currentDi.zoomToSelection === "function") {
                        currentDi.zoomToSelection();
                        statusEdit.setPlainText("成功 [zoomToSelection]: (" + x + ", " + y + ")");
                    } else {
                        statusEdit.setPlainText("警告: ズーム手段が見つかりません。");
                    }

                    currentDi.regenerateViews();
                    if (typeof currentDi.repaintViews === "function") {
                        currentDi.repaintViews();
                    }
                }
            }
        } catch (err) {
            statusEdit.setPlainText("エラー:\n" + err.name + "\n" + err.message);
        }
    });

    // --- ZoomIn / ZoomOut ---
    function doZoom(isIn) {
        try {
            var zdi   = RMainWindowQt.getMainWindow().getDocumentInterface();
            var zview = zdi.getLastKnownViewWithFocus();
            if (!isNull(zview)) {
                if (isIn) {
                    zview.zoomIn();
                } else {
                    zview.zoomOut();
                }
                if (typeof zdi.repaintViews === "function") zdi.repaintViews();
                statusEdit.setPlainText(isIn ? "拡大しました。" : "縮小しました。");
            } else {
                statusEdit.setPlainText("警告: ビューが取得できません。");
            }
        } catch(e) {
            statusEdit.setPlainText("ズームエラー: " + e.message);
        }
    }

    btnZoomIn.clicked.connect(function()  { doZoom(true);  });
    btnZoomOut.clicked.connect(function() { doZoom(false); });

    btnClose.clicked.connect(function() { dialog.close(); });
    dialog.show();
};

MoveAndZoom.init = function(basePath) {
    var action = new RGuiAction("座標移動とズーム", RMainWindowQt.getMainWindow());
    action.setRequiresDocument(true);
    action.setScriptFile(basePath + "/MoveAndZoom.js");
    action.setGroupSortOrder(99000);
    action.setSortOrder(0);
    action.setWidgetNames(["QcadAddMenu"]);
};
// QcadAdd.js
include("scripts/Misc/Misc.js");

function QcadAdd(guiAction) {
    Misc.call(this, guiAction);
}

QcadAdd.prototype = new Misc();
QcadAdd.includeBasePath = includeBasePath;

QcadAdd.getMenu = function() {
    var menu = EAction.getSubMenu(
        Misc.getMenu(),
        99000, 100,
        QcadAdd.getTitle(),
        "QcadAddMenu"
    );
    return menu;
};

QcadAdd.getTitle = function() {
    return "QcadAdd";
};

QcadAdd.prototype.getTitle = function() {
    return QcadAdd.getTitle();
};

QcadAdd.init = function(basePath) {
    QcadAdd.getMenu();
};

コメントを残す

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

PAGE TOP