Workbench creation/zh-cn

This page describes a proven way to create workbenches, but some years ago there was a suggestion how to improve the structure of the Python based external workbenches. There seems to be some kind of agreement that this structure should set a new standard but it hasn't found its way into this wiki yet (except two links in the Related section). Another paragraph is created to describe the new standard. See [discussion]

简介

本页面将展示将新的工作台添加至 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 安装。这两种结构都是有效的,因此如何选择取决于您的偏好。使用一般方法创建的工作台是在全局命名空间中的,而“命名空间工作台”有专属的命名空间。详见相关部分。

C++ 工作台的结构

如果您用 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();
    }
}

Init.py 文件

"""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 文件。更多例子见 代码片段

Python 工作台

这是 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++ 来编写工作台的定义(这并不是必需的,也可以只用 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 命令是 FreeCAD 界面的基本构建块。同一个命令可以以工具栏按钮或菜单项的形式出现。命令是一个简单的 Python 类,必须包含几个预定义的属性和函数,用来定义命令的名称、图标,以及激活命令时要执行的操作。

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++ 命令定义

同样,您也可使用 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)

New Standard External Workbench

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)

Structure

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/

Starter Kit

A starter kit to create the structure of an external Python workbench is available on GitHub: FreeCAD Workbench Starter Kit

相关链接