本页面将展示将新的工作台添加至 FreeCAD 界面的方法。工作台 是容纳 FreeCAD 命令的容器,可以使用 Python 或 C++ 编写,亦可二者并用,以兼顾 C++ 的速度与 Python 的灵活性。无论使用何种语言编写,工作台都将基于两个 Python 文件运行。工作台可能是 FreeCAD 发行版自带的(“内部工作台”),也可能是来自于在线仓库或 插件管理器(“外部工作台”)。内部工作台可能是使用 Python、C++ 或二者结合编写的,外部工作台必须基于 Python.
创建一个任意名称的文件夹,其中应包含一个 Init.py 文件,可有一个 InitGui.py 文件。将这个文件夹放置于用户 Mod 文件夹。当 FreeCAD 启动时会执行 Init.py 文件,如果 FreeCAD 是以 GUI 模式启动的,则在执行 Init.py 之后会立即执行 InitGui.py 文件。如此,FreeCAD 即可在启动时找到您的工作台,并在界面上加载。
用户 Mod 文件夹可在用户应用数据目录下找到。欲寻找用户应用数据目录,可在 Python 控制台 中输入App.getUserAppDataDir():
Mod 文件夹通常有如下结构:
/Mod/
+-- MyWorkbench/
+-- Init.py
+-- InitGui.py
您可以随意修改这些文件,一般的用法是这样的:
此处描述的文件结构及内容是创建新工作台的一般方法。当创建 Python 工作台时,也可以对文件结构作些许改动,如此创建的工作台可称为“命名空间工作台”,可使用 pip 安装。这两种结构都是有效的,因此如何选择取决于您的偏好。使用一般方法创建的工作台是在全局命名空间中的,而“命名空间工作台”有专属的命名空间。详见相关部分。
如果您用 Python 编写工作台,只需将您的 Init.py 和 InitGui.py 和其它 Python 文件放在一起即可。当使用 C++ 编写工作台时,您则需要小心行事。首先,需要遵循 FreeCAD 的一条基本原则:将 App 部分和 Gui 部分分离(App 部分可以独立运行在命令行模式下,而 Gui 部分只有在 FreeCAD 的 GUI 模式下才会载入。因此,在使用 C++ 编写工作台时,您很可能会创建 App 和 Gui 两个模块,Python 需要调用这两个模块。一个 FreeCAD 模块(App 模块或 Gui 模块)一定会包括一个模块初始化文件。典型的 AppMyModuleGui.cpp 文件如下:
extern "C" {
void MyModuleGuiExport initMyModuleGui()
{
if (!Gui::Application::Instance) {
PyErr_SetString(PyExc_ImportError, "Cannot load Gui module in console application.");
return;
}
try {
// import other modules this one depends on
Base::Interpreter().runString("import PartGui");
// run some python code in the console
Base::Interpreter().runString("print('welcome to my module!')");
}
catch(const Base::Exception& e) {
PyErr_SetString(PyExc_ImportError, e.what());
return;
}
(void) Py_InitModule("MyModuleGui", MyModuleGui_Import_methods); /* mod name, table ptr */
Base::Console().Log("Loading GUI of MyModule... done\n");
// initializes the FreeCAD commands (in another cpp file)
CreateMyModuleCommands();
// initializes workbench and object definitions
MyModuleGui::Workbench::init();
MyModuleGui::ViewProviderSomeCustomObject::init();
// add resources and reloads the translators
loadMyModuleResource();
}
}
"""FreeCAD init script of XXX module"""
# ***************************************************************************
# * Copyright (c) 2015 John Doe john@doe.com *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENSE text file. *
# * *
# * FreeCAD is distributed in the hope that it will be useful, *
# * but WITHOUT ANY WARRANTY; without even the implied warranty of *
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
# * GNU Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with FreeCAD; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************/
FreeCAD.addImportType("My own format (*.own)", "importOwn")
FreeCAD.addExportType("My own format (*.own)", "exportOwn")
print("I am executing some stuff here when FreeCAD starts!")
您的工作台可以使用任一种许可证,但注意,如果您想要将工作台整合入 FreeCAD 源码并随之一起发行,如上例所示,您需要使用 LGPL2+ 许可证。详见 许可证。
FreeCAD.addImportType() 和 addEXportType() 函数用于指定文件类型的名称、扩展名,以及负责导入或导出该类型文件的 Python 模块。在上述例子中,importOwn.py 模块负责处理 .own 文件。更多例子见 代码片段。
这是 InitGui.py 文件:
class MyWorkbench (Workbench):
MenuText = "My Workbench"
ToolTip = "A description of my workbench"
Icon = """paste here the contents of a 16x16 xpm icon"""
def Initialize(self):
"""This function is executed when the workbench is first activated.
It is executed once in a FreeCAD session followed by the Activated function.
"""
import MyModuleA, MyModuleB # import here all the needed files that create your FreeCAD commands
self.list = ["MyCommand1", "MyCommand2"] # a list of command names created in the line above
self.appendToolbar("My Commands", self.list) # creates a new toolbar with your commands
self.appendMenu("My New Menu", self.list) # creates a new menu
self.appendMenu(["An existing Menu", "My submenu"], self.list) # appends a submenu to an existing menu
def Activated(self):
"""This function is executed whenever the workbench is activated"""
return
def Deactivated(self):
"""This function is executed whenever the workbench is deactivated"""
return
def ContextMenu(self, recipient):
"""This function is executed whenever the user right-clicks on screen"""
# "recipient" will be either "view" or "tree"
self.appendContextMenu("My commands", self.list) # add commands to the context menu
def GetClassName(self):
# This function is mandatory if this is a full Python workbench
# This is not a template, the returned string should be exactly "Gui::PythonWorkbench"
return "Gui::PythonWorkbench"
Gui.addWorkbench(MyWorkbench())
除此之外,您可以按自己的想法修改这个文件。可以把整个工作台的代码都放在 InitGui.py 里。不过通常更方便的做法是把工作台的不同功能放在单独的文件中,这样文件会更小,也更容易阅读,然后在 InitGui.py 文件中导入这些文件。您可以按照自己的喜好组织这些文件,例如为添加的每个 FreeCAD 命令准备一个单独的文件。
您可以为自己的 Python 工作台添加一个“首选项”页面,这一页面会在 Qt 资源系统中查找一个具有特定名称的首选项图标。如果您的图标不在资源系统中,或者名称不正确,该图标就不会显示在首选项页面上。
添加工作台图标:
如果添加或更改了图标,必须重复以上步骤。
@kbwbe 为 A2Plus 工作台编写了一个优秀的脚本,用于编译资源。见下。
添加首选项页面:
其中“MyGroup”是左侧的首选项分组之一。FreeCAD 会自动在已知位置查找名为 preferences-mygroup.svg 的图标文件(可通过 FreeCADGui.addIconPath() 来扩展查找路径)
要分发您的 Python 工作台,可以直接把文件放到某个地方,然后让用户下载并手动放入他们的 Mod 目录;亦可将代码托管在在线的 git 仓库(目前支持 GitHub、GitLab、Framagit 和 Debian Salsa),并配置好让 插件管理器 自动安装。 关于如何将工作台加入 FreeCAD 官方插件列表的说明,请参见 FreeCAD Addons GitHub 仓库。使用插件管理器时,应包含一个 package.xml 元数据文件,该文件告诉插件管理器如何找到工作台图标,并显示描述、版本号等信息。它还可以用来指定该工作台依赖的其他工作台或 Python 包,阻止的包,或者需要替代的包。
关于如何快速创建基本的 package.xml 文件并将工作台添加到插件管理器,请见: 添加工作台到插件管理器。
也可以选择添加一个单独的元数据文件来描述你的 Python 依赖项。这个文件可以是名为 metadata.txt 的文件,用来描述工作台对其他插件、工作台或 Python 模块的外部依赖;也可以是一个 requirements.txt 文件,用来描述你的 Python 依赖。注意,如果使用 requirements.txt 文件,插件管理器仅会使用其中指定的包名来进行依赖关系解析,而不会支持 pip 的命令行选项、包含选项和版本信息。如果需要 pip 的这些功能,用户可以手动运行 pip 并使用该 requirements 文件。
metadata.txt 文件是纯文本格式,包含三行可选的内容:
workbenches=
pylibs=
optionalpylibs=
每一行应包含一个以逗号分隔的列表,列出该工作台所依赖的项目。依赖项可以是 FreeCAD 内部的工作台,例如 "FEM",也可以是外部插件,比如 "Curves". 所需和可选的 Python 库应使用它们的标准 Python 名称,就像用 pip install 安装时所使用的名称一样。例如:
workbenches=FEM,Curves
pylibs=ezdxf
optionalpylibs=metadata,git
也可以在插件中包含一个在卸载时执行的脚本文件,名为 uninstall.py,位于插件的顶层目录。当用户通过插件管理器卸载这一插件时,这个脚本会被执行。您可以使用这一脚本清理插件对用户系统作出的修改,比如可以使用该插件删除缓存文件等,确保插件卸载后不会留下残余。
为了确保插件能被插件管理器正确读取,可以启用“开发者模式”。在该模式下,插件管理器会检查所有可用插件,确认这些插件的元数据包含必要的元素。要启用此模式,请选择: 编辑 → 首选项... → 插件管理器 → 插件管理器选项 → 插件开发者模式,详情请参见 首选项编辑器。
如果您打算用 C++ 编写工作台,您可能也会想用 C++ 来编写工作台的定义(这并不是必需的,也可以只用 C++ 编写工具部分,工作台定义仍然用 Python)。如果使用 C++ 编写工作台定义,InitGui.py 文件会非常简单,可能只包含一行代码:
import MyModuleGui
MyModule 是完整的 C++ 工作台,包括命令和工作台定义。
用 C++ 编写工作台的方式与之类似。下面是一个典型的 Workbench.cpp 文件示例,适用于模块的 Gui 部分:
namespace MyModuleGui {
class MyModuleGuiExport Workbench : public Gui::StdWorkbench
{
TYPESYSTEM_HEADER();
public:
Workbench();
virtual ~Workbench();
virtual void activated();
virtual void deactivated();
protected:
Gui::ToolBarItem* setupToolBars() const;
Gui::MenuItem* setupMenuBar() const;
};
}
您也可以为 C++ 工作台添加首选项页面,步骤与 Python 工作台相似。
分发 C++ 工作台有两种方式:可以自行托管不同操作系统的预编译版本,亦可申请将您的代码合并到 FreeCAD 源代码中。如前所述,这需要遵守 LGPL2+ 许可证,并且必须先在 FreeCAD 论坛 向社区展示您的工作台,以接受审核。
FreeCAD 命令是 FreeCAD 界面的基本构建块。同一个命令可以以工具栏按钮或菜单项的形式出现。命令是一个简单的 Python 类,必须包含几个预定义的属性和函数,用来定义命令的名称、图标,以及激活命令时要执行的操作。
class My_Command_Class():
"""My new command"""
def GetResources(self):
return {"Pixmap" : "My_Command_Icon", # the name of a svg file available in the resources
"Accel" : "Shift+S", # a default shortcut (optional)
"MenuText": "My New Command",
"ToolTip" : "What my new command does"}
def Activated(self):
"""Do something here"""
return
def IsActive(self):
"""Here you can define if the command must be active or not (greyed) if certain conditions
are met or not. This function is optional."""
return True
FreeCADGui.addCommand("My_Command", My_Command_Class())
同样,您也可使用 C++ 编写命令,通常将其放在 Gui 模块中的 Commands.cpp 文件中。一个典型的 Commands.cpp 文件如下:
DEF_STD_CMD_A(CmdMyCommand);
CmdMyCommand::CmdMyCommand()
:Command("My_Command")
{
sAppModule = "MyModule";
sGroup = QT_TR_NOOP("MyModule");
sMenuText = QT_TR_NOOP("Runs my command...");
sToolTipText = QT_TR_NOOP("Describes what my command does");
sWhatsThis = QT_TR_NOOP("Describes what my command does");
sStatusTip = QT_TR_NOOP("Describes what my command does");
sPixmap = "some_svg_icon_from_my_resource";
}
void CmdMyCommand::activated(int iMsg)
{
openCommand("My Command");
doCommand(Doc,"print('Hello, world!')");
commitCommand();
updateActive();
}
bool CmdMyCommand::isActive(void)
{
if( getActiveGuiDocument() )
return true;
else
return false;
}
void CreateMyModuleCommands(void)
{
Gui::CommandManager &rcCmdMgr = Gui::Application::Instance->commandManager();
rcCmdMgr.addCommand(new CmdMyCommand());
}
A2Plus 工作台的 compileA2pResources.py 文件:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#***************************************************************************
#* *
#* Copyright (c) 2019 kbwbe *
#* *
#* Portions of code based on hamish's assembly 2 *
#* *
#* This program is free software; you can redistribute it and/or modify *
#* it under the terms of the GNU Lesser General Public License (LGPL) *
#* as published by the Free Software Foundation; either version 2 of *
#* the License, or (at your option) any later version. *
#* for detail see the LICENSE text file. *
#* *
#* This program is distributed in the hope that it will be useful, *
#* but WITHOUT ANY WARRANTY; without even the implied warranty of *
#* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
#* GNU Library General Public License for more details. *
#* *
#* You should have received a copy of the GNU Library General Public *
#* License along with this program; if not, write to the Free Software *
#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
#* USA *
#* *
#***************************************************************************
# This script compiles the A2plus icons for py2 and py3
# For Linux only
# Start this file in A2plus main directory
# Make sure pyside-rcc is installed
import os, glob
qrc_filename = 'temp.qrc'
if os.path.exists(qrc_filename):
os.remove(qrc_filename)
qrc = '''<RCC>
\t<qresource prefix="/">'''
for fn in glob.glob('./icons/*.svg'):
qrc = qrc + '\n\t\t<file>%s</file>' % fn
qrc = qrc + '''\n\t</qresource>
</RCC>'''
print(qrc)
f = open(qrc_filename,'w')
f.write(qrc)
f.close()
os.system(
'pyside-rcc -o a2p_Resources2.py {}'.format(qrc_filename))
os.system(
'pyside-rcc -py3 -o a2p_Resources3.py {}'.format(qrc_filename))
os.remove(qrc_filename)
The structure and file content for a workbench described above is the classic way of creating a new workbench. One can use a slight variation in the structure of files when making a new Python workbench, that alternative way is best described as a namespaced workbench, opening up the possibility to use pip to install the workbench. Both structures work, so it is more a question of preference when creating a new workbench. The style and structure for these workbenches reside in a dedicated namespace, instead of being available in the global namespace of FreeCAD.
(Add an explanation of dedicated namespace)
This structure is also based on the /Mod directory (see the workbench structure above) but differs in the use of subdirectories:
/Mod/
+-- MyWorkbench/
+-- freecad/
+-- MyWorkbench/
+-- __init.py__
+-- init_gui.py
+-- resources/
+-- icons/
+-- translations/
A starter kit to create the structure of an external Python workbench is available on GitHub: FreeCAD Workbench Starter Kit