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. よくあるハマりポイントと解決策
① beginEvent で EAction.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();
};

コメントを残す