菜单栏与 Ribbon 扩展
大约 9 分钟
菜单栏与 Ribbon 扩展
本章详细介绍如何通过插件扩展 WebCAD 的菜单栏和 Ribbon 工具栏。
菜单栏扩展
默认菜单项 ID
WebCAD 默认提供以下菜单项:
| ID | 名称 | 说明 |
|---|---|---|
file | 文件 | 新建、打开、保存、导出等 |
create | 创建 | 绘图命令(直线、圆、多段线等) |
edit | 编辑 | 编辑命令(移动、复制、旋转等) |
view | 显示 | 视图控制、显示顺序、图层管理 |
insert | 插入 | 块和图像的插入与管理 |
dim | 标注 | 尺寸标注 |
property | 属性 | 实体属性 |
tool | 工具 | 工具、测量、统计 |
appearance | 外观 | 界面外观设置 |
help | 帮助 | 帮助和关于 |
添加菜单项到现有菜单
import type { Plugin, PluginContext } from 'vjcad';
const plugin: Plugin = {
manifest: { id: 'my-plugin', name: '我的插件', version: '1.0.0' },
async onActivate(context: PluginContext): Promise<void> {
// 注册命令
context.registerCommand('MYCMD', '我的命令', MyCommand);
context.registerIcon('MYCMD', '<svg>...</svg>');
// 添加到工具菜单
context.addMenuItem('tool', {
command: 'MYCMD',
shortcut: 'Ctrl+M' // 可选:快捷键提示
});
// 添加到编辑菜单,指定位置
context.addMenuItem('edit', {
command: 'MYCMD2',
after: 'COPY' // 在 COPY 命令之后插入
});
}
};创建新的主菜单
当需要添加一组相关命令时,可以创建新的主菜单:
async onActivate(context: PluginContext): Promise<void> {
// 注册命令和图标
context.registerCommand('TOOL1', '工具1', Tool1Command);
context.registerCommand('TOOL2', '工具2', Tool2Command);
context.registerIcon('TOOL1', '<svg>...</svg>');
context.registerIcon('TOOL2', '<svg>...</svg>');
// 创建新主菜单(如果不存在)
// 多个插件可以安全调用,只有第一个会创建
context.addMenu({
id: 'my-tools', // 唯一ID
label: '我的工具', // 显示名称
after: 'tool' // 在工具菜单后面(可选)
});
// 添加菜单项
context.addMenuItem('my-tools', { command: 'TOOL1' });
context.addMenuItem('my-tools', { command: 'TOOL2' });
}
async onDeactivate(context: PluginContext): Promise<void> {
// 移除菜单项
context.removeMenuItem('my-tools', 'TOOL1');
context.removeMenuItem('my-tools', 'TOOL2');
// 移除主菜单(如果是本插件创建的)
context.removeMenu('my-tools');
}菜单项配置
interface MenuItemConfig {
/** 命令名称(必填) */
command: string;
/** 快捷键提示(可选) */
shortcut?: string;
/** 在指定命令之前插入(可选) */
before?: string;
/** 在指定命令之后插入(可选) */
after?: string;
}
interface MenuConfig2 {
/** 菜单唯一ID(必填) */
id: string;
/** 显示名称(必填) */
label: string;
/** 在指定菜单之后插入(可选) */
after?: string;
}菜单图标
菜单项会自动使用与命令同名的图标。确保在添加菜单项前注册图标:
// 图标名称需与命令名一致
context.registerIcon('MYCMD', '<svg viewBox="0 0 24 24">...</svg>');
context.registerCommand('MYCMD', '我的命令', MyCommand);
context.addMenuItem('tool', { command: 'MYCMD' }); // 自动使用 MYCMD 图标Ribbon 工具栏扩展
默认 Ribbon 标签页
| ID | 名称 | 说明 |
|---|---|---|
default | 默认 | 常用绘图、修改、编辑等工具 |
tools | 工具 | 组、清理、特性、图像、测量等 |
plugins | 插件 | 插件管理 |
recent | 最近命令 | 动态显示最近执行的命令 |
默认 Ribbon 组(按标签页)
default 标签页:
| 组 ID | 名称 | 主要命令 |
|---|---|---|
draw | 绘图 | LINE, PLINE, ARC, CIRCLE, RECTANG, ELLIPSE, HATCH, SPLINE... |
modify | 修改 | MOVE, ROTATE, TRIM, COPY, MIRROR, FILLET, STRETCH, EXTEND, OFFSET... |
edit | 编辑 | UNDO, REDO, COPYCLIP, CUTCLIP, PASTECLIP... |
annotation | 注释 | TEXT, MTEXT, DIMLINEAR... |
layer | 图层特性 | LAYER, MAKELAYER, LAYALLON... |
navigation | 导航 | ZOOM, ZOOMEXTENTS, REGENALL... |
block | 块 | INSERT, BLOCK, QBLOCK... |
tools 标签页:
| 组 ID | 名称 | 主要命令 |
|---|---|---|
group | 组 | GROUP, UNGROUP |
purge | 清理 | PURGE, PURGEBLOCK, PURGEIMAGE... |
properties | 特性 | PROPERTIES, MATCHPROP... |
image | 图像 | IMAGE, IMAGEADJUST, IMAGECLIP... |
measure | 测量 | DIST, MEASUREANGLE, ID |
stats | 统计 | ENTITYSTATS, COUNTBLOCK... |
script | 脚本 | EXECSTR, EXECJS |
help | 帮助 | ABOUT, GRAPHICSINFO |
plugins 标签页:
| 组 ID | 名称 | 主要命令 |
|---|---|---|
plugin-manager | 插件管理 | PLUGINS |
添加按钮到现有组
import type { RibbonButtonConfig } from 'vjcad';
async onActivate(context: PluginContext): Promise<void> {
context.registerCommand('MYTOOL', '我的工具', MyToolCommand);
context.registerIcon('mytool', '<svg>...</svg>');
// 添加按钮到 default 标签页的 draw 组
const button: RibbonButtonConfig = {
icon: 'mytool', // 图标ID
cmd: 'MYTOOL', // 命令名
prompt: '我的绘图工具', // 工具提示
label: '工具', // 紧凑模式显示文本(可选)
type: 'small' // 按钮类型(可选)
};
context.addRibbonButton('default', 'draw', button, 'primary');
// 或添加到更多按钮区域
// context.addRibbonButton('default', 'draw', button, 'more');
}
async onDeactivate(context: PluginContext): Promise<void> {
context.removeRibbonButton('default', 'draw', 'MYTOOL');
}添加组到现有标签页
import type { RibbonGroupConfig } from 'vjcad';
async onActivate(context: PluginContext): Promise<void> {
// 注册命令和图标
context.registerCommand('CMD1', '命令1', Cmd1);
context.registerCommand('CMD2', '命令2', Cmd2);
context.registerIcon('cmd1', '<svg>...</svg>');
context.registerIcon('cmd2', '<svg>...</svg>');
// 添加组到 tools 标签页
const group: RibbonGroupConfig = {
id: 'my-group',
label: '我的工具',
pinnable: true, // 是否支持固定
displayMode: 'large', // 显示模式
primaryButtons: [
{ icon: 'cmd1', cmd: 'CMD1', prompt: '命令1', type: 'large' },
{ icon: 'cmd2', cmd: 'CMD2', prompt: '命令2', type: 'large' }
],
moreButtons: [
{ icon: 'cmd3', cmd: 'CMD3', prompt: '命令3' }
]
};
context.addRibbonGroup('tools', group);
}
async onDeactivate(context: PluginContext): Promise<void> {
context.removeRibbonGroup('tools', 'my-group');
}创建新的 Ribbon 标签页
import type { RibbonTabConfig } from 'vjcad';
async onActivate(context: PluginContext): Promise<void> {
// 注册命令和图标...
// 创建新标签页(如果不存在)
// 多个插件可以安全调用同一 ID,只有第一个会创建
context.addRibbonTab({
id: 'my-plugin-tab',
label: '我的插件',
groups: [] // 初始为空,稍后添加组
}, 'tools'); // 在 tools 标签页后插入(可选)
// 添加组到新标签页
context.addRibbonGroup('my-plugin-tab', {
id: 'main-group',
label: '主要功能',
pinnable: true,
primaryButtons: [
{ icon: 'cmd1', cmd: 'CMD1', prompt: '命令1', type: 'large' }
]
});
}
async onDeactivate(context: PluginContext): Promise<void> {
context.removeRibbonGroup('my-plugin-tab', 'main-group');
context.removeRibbonTab('my-plugin-tab');
}Ribbon 配置接口
/** 按钮类型 */
type RibbonButtonType = 'large' | 'small' | 'icon-only' | 'list';
/** 组显示模式 */
type RibbonGroupDisplayMode =
| 'large' // 大图标模式(图标+文字垂直排列)
| 'compact' // 紧凑模式(小图标+文字)
| 'small-icons' // 纯小图标模式(无文字,两行排列)
| 'layer' // 图层模式(特殊的图层列表)
| 'recent'; // 最近命令模式
/** 按钮配置 */
interface RibbonButtonConfig {
icon: string; // 图标ID
cmd: string; // 命令名
prompt?: string; // 工具提示
label?: string; // 紧凑模式显示文本
type?: RibbonButtonType; // 按钮类型
items?: RibbonButtonConfig[]; // 下拉列表子项(type='list'时)
}
/** 组配置 */
interface RibbonGroupConfig {
id: string; // 组唯一ID
label: string; // 组名称
pinnable?: boolean; // 是否支持固定
displayMode?: RibbonGroupDisplayMode; // 显示模式
primaryButtons: RibbonButtonConfig[]; // 主要按钮
moreButtons?: RibbonButtonConfig[]; // 更多按钮
}
/** 标签页配置 */
interface RibbonTabConfig {
id: string; // 标签页唯一ID
label: string; // 标签页名称
groups: RibbonGroupConfig[]; // 组列表
}菜单栏与 Ribbon 定制(MainView 构造参数)
除了通过插件的 PluginContext API 逐项扩展外,还可以在创建 MainView 时通过构造参数一次性定制菜单栏和 Ribbon 工具栏。支持全量替换和增量修改两种模式。
在线演示{target="_blank"}
菜单栏定制(MenuBarCustomConfig)
通过 MainViewConfig.menuBar 传入:
interface MenuBarCustomConfig {
/** 全量替换:提供完整菜单定义列表,忽略所有默认菜单 */
menus?: MenuDefinition[];
/** 增量模式:要移除的主菜单 ID 列表 */
removeMenus?: string[];
/** 增量模式:要新增的主菜单 */
addMenus?: AddMenuDefinition[];
/** 增量模式:对已有菜单的修改(key 为菜单 ID) */
modifyMenus?: Record<string, MenuModification>;
}当 menus 存在时视为全量模式,忽略增量字段。
菜单项定义
type MenuItemDef =
| { type: 'command'; command: string; label?: string; icon?: string;
shortcut?: string; column?: number }
| { type: 'separator'; column?: number }
| { type: 'submenu'; label: string; icon?: string; column?: number;
children: MenuItemDef[] };增量修改接口
interface MenuModification {
removeItems?: string[];
addItems?: Array<MenuItemDef & { before?: string; after?: string }>;
}
interface AddMenuDefinition {
id: string;
label: string;
items: MenuItemDef[];
before?: string;
after?: string;
}增量模式示例
const cadView = new MainView({
// ...
menuBar: {
removeMenus: ['help', 'appearance'],
addMenus: [{
id: 'custom-menu',
label: '自定义菜单',
after: 'insert',
items: [
{ type: 'command', command: 'DRAW_STAR' },
{ type: 'command', command: 'DRAW_GRID' },
{ type: 'separator' },
{ type: 'command', command: 'REGEN' },
]
}],
modifyMenus: {
file: {
removeItems: ['SWITCHWORKSPACE'],
addItems: [
{ type: 'command', command: 'DRAW_STAR', after: 'EXPORTPNG' }
]
}
}
}
});全量替换示例
const cadView = new MainView({
// ...
menuBar: {
menus: [
{ id: 'my-file', label: '文件', items: [
{ type: 'command', command: 'NEW' },
{ type: 'command', command: 'OPEN' },
{ type: 'separator' },
{ type: 'command', command: 'SAVELOCAL', shortcut: 'Ctrl+S' },
]},
{ id: 'my-draw', label: '绘图', items: [
{ type: 'command', command: 'LINE' },
{ type: 'command', command: 'CIRCLE' },
]},
]
}
});Ribbon 定制(RibbonCustomConfig)
通过 MainViewConfig.ribbon 传入:
interface RibbonCustomConfig {
/** 全量替换:完整的 Ribbon 配置 */
config?: RibbonConfig;
/** 增量模式:要移除的标签页 ID 列表 */
removeTabs?: string[];
/** 增量模式:要新增的标签页 */
addTabs?: AddRibbonTabDefinition[];
/** 增量模式:对已有标签页的修改(key 为标签页 ID) */
modifyTabs?: Record<string, RibbonTabModification>;
}当 config 存在时视为全量模式,忽略增量字段。
增量修改接口
interface RibbonTabModification {
removeGroups?: string[];
addGroups?: Array<RibbonGroupConfig & { before?: string; after?: string }>;
modifyGroups?: Record<string, RibbonGroupModification>;
}
interface RibbonGroupModification {
removePrimaryButtons?: string[];
removeMoreButtons?: string[];
addPrimaryButtons?: Array<RibbonButtonConfig & { before?: string; after?: string }>;
addMoreButtons?: Array<RibbonButtonConfig & { before?: string; after?: string }>;
}
interface AddRibbonTabDefinition {
tab: RibbonTabConfig;
before?: string;
after?: string;
}增量模式示例
const cadView = new MainView({
// ...
ribbon: {
removeTabs: ['recent'],
addTabs: [{
tab: {
id: 'custom-tab',
label: '自定义',
groups: [{
id: 'custom-draw',
label: '自定义绘图',
primaryButtons: [
{ icon: 'line', cmd: 'DRAW_STAR', prompt: '绘制五角星', label: '五角星' },
{ icon: 'rectang', cmd: 'DRAW_GRID', prompt: '绘制网格', label: '网格' },
]
}]
},
after: 'default'
}],
modifyTabs: {
'default': {
modifyGroups: {
draw: {
addPrimaryButtons: [
{ icon: 'circle', cmd: 'DRAW_STAR', prompt: '五角星',
label: '★', after: 'CIRCLE' }
]
}
}
}
}
}
});插件 API 与 MainView 定制的区别
| 插件 API(PluginContext) | MainView 构造参数 | |
|---|---|---|
| 时机 | 运行时动态添加/移除 | 初始化时一次性配置 |
| 粒度 | 逐项操作(addMenuItem 等) | 批量配置(全量或增量) |
| 清理 | 需在 onDeactivate 中手动清理 | 随 MainView 生命周期自动管理 |
| 适用场景 | 插件开发 | 应用级定制 |
完整示例:快速选择插件
以下是一个完整的插件示例,展示如何同时扩展菜单栏和 Ribbon:
import type { Plugin, PluginManifest, PluginContext } from 'vjcad';
// 命令类
class QuickSelectCommand {
async main(): Promise<void> {
// 打开快速选择面板...
}
}
// 图标 SVG
const ICON_QSELECT = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor">
<rect x="3" y="3" width="18" height="18" rx="2" stroke-width="2"/>
<path d="M9 12l2 2 4-4" stroke-width="2"/>
</svg>`;
// 插件清单
const manifest: PluginManifest = {
id: 'quick-select',
name: '快速选择',
version: '1.0.0',
author: 'WebCAD Team',
description: '快速选择和过滤图元'
};
// 插件定义
const plugin: Plugin = {
manifest,
async onActivate(context: PluginContext): Promise<void> {
// 1. 注册命令
context.registerCommand('QSELECT', '快速选择', QuickSelectCommand);
// 2. 注册图标(名称与命令一致,用于菜单显示)
context.registerIcon('QSELECT', ICON_QSELECT);
// 小写版本用于 Ribbon
context.registerIcon('qselect', ICON_QSELECT);
// 3. 添加到"插件"主菜单(如果不存在则创建)
context.addMenu({
id: 'plugins',
label: '插件',
after: 'tool'
});
context.addMenuItem('plugins', { command: 'QSELECT' });
// 4. 添加到"插件" Ribbon 标签页(如果不存在则创建)
context.addRibbonTab({
id: 'plugins',
label: '插件',
groups: []
});
// 5. 添加 Ribbon 组
context.addRibbonGroup('plugins', {
id: 'quick-select',
label: '快速选择',
pinnable: true,
primaryButtons: [
{
icon: 'qselect',
cmd: 'QSELECT',
prompt: '快速选择图元',
type: 'large'
}
]
});
console.log('[快速选择] 插件已激活');
},
async onDeactivate(context: PluginContext): Promise<void> {
// 清理资源(按添加的逆序)
context.removeRibbonGroup('plugins', 'quick-select');
context.removeMenuItem('plugins', 'QSELECT');
context.unregisterCommand('QSELECT');
console.log('[快速选择] 插件已停用');
}
};
export default plugin;API 参考
菜单相关
| 方法 | 说明 |
|---|---|
addMenu(config) | 添加主菜单(已存在则跳过) |
removeMenu(menuId) | 移除主菜单 |
addMenuItem(menuId, config) | 添加菜单项 |
removeMenuItem(menuId, command) | 移除菜单项 |
Ribbon 相关
| 方法 | 说明 |
|---|---|
addRibbonTab(tab, afterTabId?) | 添加标签页(已存在则跳过) |
removeRibbonTab(tabId) | 移除标签页 |
addRibbonGroup(tabId, group) | 添加组到标签页 |
removeRibbonGroup(tabId, groupId) | 移除组 |
addRibbonButton(tabId, groupId, btn, type) | 添加按钮到组 |
removeRibbonButton(tabId, groupId, cmd) | 移除按钮 |
图标相关
| 方法 | 说明 |
|---|---|
registerIcon(name, svg) | 注册 SVG 图标 |
最佳实践
命名规范:
- 命令名使用大写(如
MYCMD) - 图标名使用小写(如
mycmd) - 菜单/组/标签页 ID 使用 kebab-case(如
my-plugin)
- 命令名使用大写(如
资源清理:
- 在
onDeactivate中清理所有注册的资源 - 按照添加的逆序进行清理
- 在
共享资源:
- 使用统一的 ID(如
plugins)让多个插件共享同一菜单/标签页 addMenu和addRibbonTab会自动检测是否已存在
- 使用统一的 ID(如
图标一致性:
- 菜单项自动使用与命令同名的图标
- Ribbon 按钮需要明确指定图标 ID