Krita Python 插件编写教程

你可能已经编写了一些可以在 Python 脚本工具中运行的脚本,但你说不定还想进一步扩展其功能,并且让它随 Krita 进程自动启动。把脚本包装成插件不但用起来更方便,功能也更加强大。

尽管你可能已经对 Python 了如指掌,但要想 Krita 正确识别你的插件,学习一些细节知识还是很有必要的。本节将介绍如何创建 Krita 专用的几种 Python 脚本。

本文的范例是为那些对 Python 已经有了初步了解的人们准备的。这些范例和相关的 API 文档鼓励读者大胆地进行实验,而不是盲目地复制粘贴代码,所以请认真阅读它们。

创建 Krita 能够识别的插件

Krita 的脚本由两部分组成:脚本的文件夹和脚本的”.desktop” 文件。 脚本的文件夹保存了脚本的 Python 文件,而 desktop 文件则供 Krita 加载和注册该脚本使用。要让 Krita 加载你的脚本,两者必须同时被放置在 Krita 的资源文件夹的 pykrita 子文件夹里面 (不同操作系统的路径不同,请参见 资源管理 页面)。要显示资源文件夹,可在菜单栏打开 设置 ‣ 管理资源 对话框。点击 打开资源文件夹 按钮后,系统自带的资源管理器将会显示 Krita 的资源文件夹。具体操作可参考 API 文档 的“Auto starting scripts”一节。如果 Krita 的资源文件夹里没有 pykrita 子文件夹,请使用系统的资源管理器新建一个。

Krita 通过带有 .desktop 扩展名的文件识别插件,该文件包含了脚本的基本信息。

所以,你要为每个插件创建一个文件夹和一个 desktop 文件。

该 desktop 文件的内容格式如下:

[Desktop Entry]
Type=Service
ServiceTypes=Krita/PythonPlugin
X-KDE-Library=myplugin
X-Python-2-Compatible=false
X-Krita-Manual=myPluginManual.html
Name=My Own Plugin
Comment=Our very own plugin.
Type

此项应总是设为 Service (服务)。

ServiceTypes

对于 Krita 的 Python 插件,此项必须设为 Krita/PythonPlugin

X-KDE-Library

此项指定该插件的文件夹名称。

X-Python-2-Compatible

指定是否兼容 Python 2。例子中此项为 false (否),如果 Krita 是为 Python 2 而不是 3 进行编译 (cmake 配置参数为 -DENABLE_PYTHON_2=ON ),那么 false 意味着此插件不会在 Krita 里面显示。

X-Krita-Manual

此项为可选参数,它指向该插件配套的使用手册项目。它会被显示在 Python 插件管理器中。 HTML 格式的手册将显示为富文本 ,否则将显示为纯文本。

Name

插件在 Python 插件管理器中显示的名称。

Comment

插件在 Python 插件管理器中的描述。

Krita 的 Python 插件必须是 Python 模块,因此每个插件都要包含一个 __init__.py 脚本,该脚本包含下面这行代码:

from .myplugin import *

.myplugin 字串用来指定插件的主程序文件名。按照上面的介绍制作一个插件放到 Krita 的资源文件夹,然后重新启动 Krita,该插件便会被显示在“配置 Krita”对话框的“Python 插件管理器”页面。不过现在它会被显示为灰色,因为插件的文件夹里面并没有一个叫做 myplugin.py 的文件。如果把鼠标悬停在已禁用的插件上,你可以看到相关的错误信息。

备注

你需要自行启用你的插件。前往设置菜单,打开 配置 Krita 对话框,找到 Python 插件管理器页面,然后启用你的插件。

小结

总而言之,如果你想要创建一个叫做 myplugin 的脚本:

  • 在 Krita 的 resources/pykrita 子文件夹里面
    • 创建一个名为 myplugin 的文件夹

    • 创建一个名为 myplugin.desktop 的文件

  • myplugin 文件夹里面
    • 创建一个名为 __init__.py 的文件

    • 创建一个名为 myplugin.py 的文件

  • __init__.py 文件里加入这行代码:

from .myplugin import *
  • 在 desktop 文件里放置这段代码:

    [Desktop Entry]
    Type=Service
    ServiceTypes=Krita/PythonPlugin
    X-KDE-Library=myplugin
    X-Python-2-Compatible=false
    Name=My Own Plugin
    Comment=Our very own plugin.
    
  • myplugin/myplugin.py 文件中编写脚本代码。

创建一个扩展程序

扩展程序 是随 Krita 一起启动的简单 Python 脚本。它们通过 Extension (扩展) 程序类进行编写。一个最简单的扩展程序代码如下:

from krita import *

class MyExtension(Extension):

    def __init__(self, parent):
        # This is initialising the parent, always important when subclassing.
        super().__init__(parent)

    def setup(self):
        pass

    def createActions(self, window):
        pass

# And add the extension to Krita's list of extensions:
Krita.instance().addExtension(MyExtension(Krita.instance()))

这段代码并不具备任何功能。一般来说我们会在 createActions 处为 Krita 添加功能,这样我们就可以通过 工具 菜单来访问该脚本了。

首先,让我们创建一个 操作 。我们可以用 Window.createAction() 来实现它。Krita 会为它创建的每个窗口调用 createActions ,并把需要使用的窗口对象传递过去。

示例如下:

def createActions(self, window):
    action = window.createAction("myAction", "My Script", "tools/scripts")
“myAction”

此项指定 Krita 用来查找该操作的唯一 ID。

“My Script”

此项将作为工具名称显示在工具菜单

重新启动 Krita 之后,你就会发现在工具菜单的脚本里面多了一个叫做“My Script”的操作了。不过它现在还不能够发挥作用,因为我们还没有把它连接到一个脚本。

现在让我们把它写成一个简单的导出文档脚本。把下面的代码添加到 extension 程序类,确保它位于“Krita.instance()”所在的那行代码前面:

def exportDocument(self):
    # Get the document:
    doc =  Krita.instance().activeDocument()
    # Saving a non-existent document causes crashes, so lets check for that first.
    if doc is not None:
        # This calls up the save dialog. The save dialog returns a tuple.
        fileName = QFileDialog.getSaveFileName()[0]
        # And export the document to the fileName location.
        # InfoObject is a dictionary with specific export options, but when we make an empty one Krita will use the export defaults.
        doc.exportImage(fileName, InfoObject())

然后为上面代码中调用的 QFileDialog 添加 import 功能:

from krita import *
from PyQt5.QtWidgets import QFileDialog

最后,把该操作连接到新建导出文档:

def createActions(self, window):
    action = window.createAction("myAction", "My Script")
    action.triggered.connect(self.exportDocument)

这便是一个 信号/信号槽连接 的范例,像 Krita 这样的 Qt 应用程序经常会用到这种连接。我们会在后面介绍如何编写我们自己的信号和信号槽。

重新启动 Krita 后,你刚刚编写的这个新操作应该就可以导出文档了。

创建可配置的键盘快捷键

虽然你的新操作已经可以发挥作用,但是它却并未在菜单栏的 设置 ‣ 配置 Krita ‣ 键盘快捷键 列表中列出。

Krita 出于某些考虑,只会将 .action 文件中包含的操作添加到键盘快捷键列表。为了将我们之前编写的操作添加到快捷键列表,我们需要如下编写 action 文件:

<?xml version="1.0" encoding="UTF-8"?>
<ActionCollection version="2" name="Scripts">
    <Actions category="Scripts">
        <text>My Scripts</text>

        <Action name="myAction">
        <icon></icon>
        <text>My Script</text>
        <whatsThis></whatsThis>
        <toolTip></toolTip>
        <iconText></iconText>
        <activationFlags>10000</activationFlags>
        <activationConditions>0</activationConditions>
        <shortcut>ctrl+alt+shift+p</shortcut>
        <isCheckable>false</isCheckable>
        <statusTip></statusTip>
        </Action>
    </Actions>
</ActionCollection>
<text>My Scripts</text>

此项会在脚本分类下面新建一个名为“My Scripts”的子分类,此操作的快捷键将被添加到该分类。

name

此项填写你在编写该扩展程序时给它命名的唯一 ID。

icon

此项指定一个可用的图标名称,仅在 KDE Plasma 下面显示,因为 Gnome 和 Windows 用户觉得在菜单里显示图标不美观。

text

指定在快捷键编辑器中显示的文字。

whatsThis

此项指定的文字会在 Qt 应用程序调用“这是什么”帮助信息时显示。

toolTip

指定在鼠标悬停时显示的工具提示文字。

iconText

指定在工具栏中显示时的替代文字。例如“缩放图像至新尺寸”可以被缩写为“调整图像大小”以节省工具栏空间。

activationFlags

此项决定该操作是否被禁用。

activationConditions

此项决定该操作被激活的条件 (例如仅在选区可编辑时激活)。可以参考 相关用例代码

shortcut

指定默认的快捷键。

isCheckable

指定它是否一个复选框。

statusTip

指定显示在状态栏的提示文字。

将该文件保存为 myplugin.action ,“myplugin”字串应与你的插件名称相同。要注意的是,action 文件不应该保存在资源文件夹的 pykrita 子文件夹,而应该保存在 actions 子文件夹。(把 Python 插件和 desktop 文件放在 share/pykrita 文件夹,把 action 文件放在 share/actions 文件夹) 重新启动 Krita。现在应该可以在快捷键列表找到该操作了。

创建工具面板

创建一个 面板 和创建一个扩展程序的做法差不多,面板在某些方面更容易编写,但需要用到更多的窗口部件。一个最简单的工具面板代码如下:

from PyQt5.QtWidgets import *
from krita import *

class MyDocker(DockWidget):

    def __init__(self):
        super().__init__()
        self.setWindowTitle("My Docker")

    def canvasChanged(self, canvas):
        pass

Krita.instance().addDockWidgetFactory(DockWidgetFactory("myDocker", DockWidgetFactoryBase.DockRight, MyDocker))

代码中的 setWindowTitle (设置窗口标题) 一项用于指定该工具面板在 Krita 的工具面板列表中显示的名称。setWindowTitle 一项不可省略,但你无需用它来做任何事情,所以是“pass”。

在 addDockWidgetFactory 里面:

“myDocker”

把此项替换为你的工具面板的唯一 ID,Krita 使用该 ID 来跟踪此面板。

DockWidgetFactoryBase.DockRight

此项指定面板的位置,可以是DockTornOffDockTopDockBottomDockRightDockLeftDockMinimized

MyDocker

把此项替换为你想要添加的工具面板的程序类名称。

如果我们把之前编写的导出文档扩展程序添加到这个工具面板,用户要怎样才能激活它呢?答案当然就是用“按钮”了!要添加一个按钮,我们需要进行一些 Qt GUI 编程。

Krita 默认使用 PyQt,但它的文档非常差劲。这大概是因为常规的 Qt 文档非常完善,而且也把 PyQt 作为程序类包含在内了,所以 PyQt 项目就懒得写自己的文档了。例如 PyQt 文档的 QWidget 章节 看上去就是 常规 Qt 文档 给该程序类编写的文档的翻版。

不管怎样,我们首先要做的是创建一个 QWidget ,这并不特别复杂,在 setWindowTitle 下面添加:

mainWidget = QWidget(self)
self.setWidget(mainWidget)

然后创建一个按钮:

buttonExportDocument = QPushButton("Export Document", mainWidget)

要把这个按钮连接到我们编写的功能,首先要文档里查找它的信号。 QPushButton 并不具备它自己特有的信号,但 Qt 文档提到它从 QAbstractButton 那里继承了 4 个信号,所以我们可以使用那些信号。在这个例子里我们将使用 clicked 信号。

buttonExportDocument.clicked.connect(self.exportDocument)

重新启动 Krita 之后我们就有了一个工具面板,面板上还有一个按钮。点击按钮就会调用我们编写的导出功能。

不过这个按钮在界面里的位置有点不舒服。这是因为我们的 mainWidget 部件没有布局。让我们完成这一步:

mainWidget.setLayout(QVBoxLayout())
mainWidget.layout().addWidget(buttonExportDocument)

Qt 有好几种 布局方式 ,但 QHBoxLayout 和 QVBoxLayout 最容易使用,它们只是按水平和垂直位置排列窗口部件。

重新启动 Krita 之后,按钮的位置应该就好看的多了。

创建 PyQt 信号和信号槽

我们在刚才的范例中已经使用过 PyQt 信号和信号槽,但有时候你也需要创建自己的信号和信号槽。不过 因为 PyQt 的文档不知所云 ,而且信号和信号槽的创建方式又跟 C++ 下的 Qt 很不一样,因此我们在此展开说明一下:

你使用 PyQt 制作的所有 Python 功能都可以被看作是信号槽。这意味着它们可以接受 Action.triggered 或者 QPushButton.clicked 这样的信号。但 QCheckBox 有一个开关信号,它发送的是布尔值。我们要怎样才能使功能的信号槽接受布尔值呢?

首先,你要为自定义信号槽指定合适的 import 功能:

from PyQt5.QtCore import pyqtSlot

(当然,如果 from PyQt5.QtCore import * 已经在 import 的列表里,你就没必要再做一遍了。)

然后,你要在该功能前面添加一个 PyQt 信号槽定义:

@pyqtSlot(bool)
def myFunction(self, enabled):
    enabledString = "disabled"
    if (enabled == True):
        enabledString = "enabled"
    print("The checkbox is"+enabledString)

接下来,在你创建了复选框之后,你可以使用类似下面的这些功能:myCheckbox.toggled.connect(self.myFunction).

与之类似,要创建自定义 PyQt 信号,可以如下进行:

# signal name is added to the member variables of the class
signal_name = pyqtSignal(bool, name='signalName')

def emitMySignal(self):
    # And this is how you trigger the signal to be emitted.
    self.signal_name.emit(True)

记得指定合适的 import 功能:

from PyQt5.QtCore import pyqtSignal

在使用非标准 Python 对象的信息和信息槽时,你要把它们的名称放到引号里面。

关于单元测试的提醒

如果你打算为你的插件编写单元测试,可参考 mock krita module 页面。

结语

以上便是给 Krita 创建一个 Python 插件所必须的全部技术细节。虽然本教程既没有介绍如何解析像素数据,也没有介绍处理文档时的最佳做法,但只要你有一些 Python 的编程经验,创建一个插件应该难不倒你。

还是那句话,认真阅读为 Krita、Qt 和 Python 准备的 API 文档,大胆探索你可以用它们来做些什么,我们相信你一定能够把 Krita 的 Python 功能运用自如,开拓出一片新天地。