Jump to content

工作台的创建

From FreeCAD Documentation
This page is a translated version of the page Workbench creation and the translation is 83% complete.
Outdated translations are marked like this.
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()

  • 在 Linux 上,Mod 文件夹通常是 /home/<username>/.local/share/FreeCAD/Mod/0.20 and above 版本) 或者 /home/<username>/.FreeCAD/Mod/0.19 and below 版本);
  • 在 Windows 上是 %APPDATA%\FreeCAD\Mod\,通常就是 C:\Users\<username>\Appdata\Roaming\FreeCAD\Mod\
  • 在 macOS 上通常是 /Users/<username>/Library/Application Support/FreeCAD/Mod/

Mod 文件夹通常有如下结构:

/Mod/
 +-- MyWorkbench/
     +-- Init.py
     +-- InitGui.py

您可以随意修改这些文件,一般的用法是这样的:

  • 在 Init.py 文件中,加入 FreeCAD 在命令行状态下也可能用到的功能,例如文件导入/导出
  • 通常在 InitGui.py 文件中定义一个工作台。工作台包括其名称、图标及一系列 FreeCAD 命令(详下)。本文件也定义 FreeCAD 加载时需要执行的功能(为保证软件启动速度,越少越好)、工作台加载时需要执行的功能(也会是您的主要工作内容)以及取消选中该工作台时需要执行的功能(例如清理功能)。

此处描述的文件结构及内容是创建新工作台的一般方法。当创建 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 资源系统中查找一个具有特定名称的首选项图标。如果您的图标不在资源系统中,或者名称不正确,该图标就不会显示在首选项页面上。

添加工作台图标:

  • 首选项图标的名称格式必须为:“preferences-”+ 模块名称 + “.svg”,全部小写
  • 创建一个 qrc 文件,其中列出所有图标的名称
  • 在主 *.py 目录下,运行 pyside-rcc -o myResources.py myqrc.qrc
  • 在 InitGui.py 中,添加 import myResource(.py)
  • 将 myResources.py 和 myqrc.qrc 更新到代码仓库(git)中

如果添加或更改了图标,必须重复以上步骤。

@kbwbe 为 A2Plus 工作台编写了一个优秀的脚本,用于编译资源。见下。

添加首选项页面:

  • 需要先编译 Qt Designer 插件,该插件可以让你通过 Qt Designer 添加首选项设置
  • 在 Qt Designer 中创建一个空白的 widget(不包含按钮或其他控件)
  • 设计你的首选项页面,任何需要保存的设置(preferences),都必须使用插件中提供的 Gui::Pref* 类型控件
  • 在这些控件中,一定要填写 PrefName(首选项的值名称)和 PrefPath(例如 Mod/MyWorkbenchName),如此,首选项的值将会保存在 BaseApp/Preferences/Mod/MyWorkbenchName 路径下
  • 将 .ui 文件保存在你的工作台目录下,确保 CMake 可处理该文件
  • 在工作台代码的某一处,例如在 InitGui 文件的 Initialize 方法中(其他地方也可以),添加:FreeCADGui.addPreferencePage("/path/to/myUiFile.ui", "MyGroup")

其中“MyGroup”是左侧的首选项分组之一。FreeCAD 会自动在已知位置查找名为 preferences-mygroup.svg 的图标文件(可通过 FreeCADGui.addIconPath() 来扩展查找路径)

  • 确保 addPreferencePage() 方法只被调用一次,否则首选项页面会被重复添加

发行

要分发您的 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

相关链接