Как разработать плагин для приложения PySide?

Насколько я понимаю, Qt предоставляет 2 основных механизма плагинов:

  1. Плагины, расширяющие Qt "Qt Extensions"
  2. Плагины, расширяющие приложения, разработанные с помощью Qt

Я заинтересован в разработке плагинов для моего приложения (2).

Я использую PySide, но не могу найти никаких ресурсов о разработке плагинов для приложений с использованием PySide/PyQt.

Следуя документации по C++ Qt, я понимаю, что приложение должно использовать макрос Q_DECLARE_INTERFACE(), а плагины должны использовать оба макроса Q_INTERFACES() и Q_EXPORT_PLUGIN2(), но я не знаю код, который они представляют, чтобы попытаться перевести его на python, Или есть другой способ, который я пропустил?

Обновить:

Ближайшее решение, которое я смог найти, - это ответ Алекса Мартелли на аналогичный вопрос. Хотя, похоже, это сработает, я бы предпочел использовать официальный подход Qt, чтобы избежать кросс-платформенных проблем.

2 ответа

Решение

Я думаю, что система плагинов Qt предназначена для того, чтобы люди могли писать плагины C++, скомпилированные как двоичные файлы. Я даже не знаю, возможно ли теоретически написать плагины на Python, которые будут использовать подобный двоичный интерфейс C++.

Если вы хотите поддерживать плагины Python, вам лучше всего использовать одну из множества чистых плагиновых систем. Я написал приложение PySide, которое использует YAPSY для загрузки скриптов плагина. YAPSY - это очень простой, компактный плагин-модуль. Его очень легко включить непосредственно в приложение, так как он представляет собой один файл и имеет лицензию BSD, поэтому вы можете использовать его на коммерческой основе. Просто найдите его в Google. Я даже смог упаковать свое приложение, используя py2exe, и все еще сохранял возможность импортировать плагины исходного файла python из каталога плагинов.

Я поклонник подхода Roll Your Own.

Под плагином я имею в виду:

  • Плагин : модуль или пакет, загружаемый во время выполнения, который дополняет или изменяет поведение основного модуля.

Я вижу два требования к плагину:

  1. Может ли основной модуль загружать плагин во время выполнения?
  2. Доступны ли данные между основным модулем и плагином?

Предположения

Разработка системы плагинов очень субъективна. Необходимо принять множество дизайнерских решений, но Единого верного пути не существует. Обычные ограничения - это время, усилия и опыт. Имейте в виду, что необходимо делать предположения и что реализация часто определяет терминологию (например, «плагин» или «пакет»). Будьте добры и задокументируйте их как можно лучше.

Эта реализация плагина предполагает:

  • Плагин - это либо файл Python, либо каталог (например, «пакет плагина»).

  • Пакет плагина - это каталог со структурой:

            plugin_package/
        plugin_package.py  <-- entry point
        other_module.py    <-+
        some_subdir/         |- other files
            icon.png       <-+
    

    Обратите внимание, что пакет плагина не обязательно является пакетом Python. Независимо от того, является ли пакет плагина пакетом Python, зависит от того, как вы хотите обрабатывать импорт.

  • Имя плагина совпадает с именем файла точки входа плагина, а также с каталогом пакета плагина.

  • QMainWindow QApplication является основным источником данных для совместного использования из основного модуля.

1. Загрузка подключаемого модуля

Для загрузки подключаемого модуля требуются две части информации: путь к точке входа и имя подключаемого модуля. Способы их получения могут отличаться, и для их получения обычно требуется синтаксический анализ строки / пути. Для дальнейшего предположим:

  • plugin_path это абсолютный путь к точке входа плагина и,
  • plugin_name - это имя плагина (которое является именем модуля в соответствии с приведенными выше предположениями).

Python неоднократно реализовывал и повторно реализовывал механизм импорта. На момент написания статьи (с использованием Python 3.8) использовался следующий метод импорта:

      import importlib
from importlib.util import spec_from_loader, module_from_spec
from importlib.machinery import SourceFileLoader

loader = SourceFileLoader(plugin_name, plugin_path)
spec = spec_from_loader(plugin_name, loader)
plugin_module = module_from_spec(spec)
spec.loader.exec_module(plugin_module)

# Required for reloading a module
sys.modules[plugin_name] = plugin_module

# # This is how to reload a module
# importlib.reload(plugin_module)

Вероятно, неплохо было бы включить обработку ошибок и запись загруженных модулей (например, сохранив их в dict). Эти детали исключены здесь для краткости.

2. Обмен данными

Существует два направления обмена данными:

  • Может ли основной модуль получить доступ к данным подключаемого модуля?
  • Может ли подключаемый модуль получить доступ к данным основного модуля?

Основной модуль может получить доступ к данным плагина бесплатно после импорта (по определению). Просто получите доступ к модулю плагина __dict__:

      # Accessing plugin data from the main module
some_data = plugin_module.__dict__.get('data')

Доступ к данным основного модуля из плагина - более сложная проблема.

Как указано выше, я обычно рассматриваю QMainWindow как синоним понятия конечного пользователя о «приложении». Это основной виджет, с которым взаимодействуют пользователи, и поэтому он обычно содержит большую часть данных или имеет легкий доступ к ним. Проблема заключается в том, чтобы поделиться экземпляром QMainWindow .

Решение для совместного использования данных экземпляра QMainWindow состоит в том, чтобы сделать их одноэлементными. Это заставляет любого QMainWindow быть в основные пользователи окна взаимодействуют с. Есть несколько способов создания синглтонов в Python. Двумя наиболее распространенными способами, вероятно, являются использование метаклассов или модулей. Наибольший успех у меня получился при использовании модульного одноэлементного подхода.

Разбейте код QMainWindow на отдельный модуль Python. На уровне модуля создайте, но не инициализируйте QMainWindow. Создайте экземпляр уровня модуля, чтобы другие модули могли получить доступ к экземпляру как к атрибуту модуля. Не инициализируйте его, потому что для init требуется QApplication (и потому что модуль главного окна не является точкой входа в приложение).

      # main_window.py
from PySide2 import QtWidgets

class MyMainWindow(QtWidgets.QMainWindow):

    def __init__(self):
        super().__init__()

main_window_instance = MyMainWindow.__new__(MyMainWindow)

Используйте отдельный модуль, например, для точки входа в приложение. Импортируйте экземпляр главного окна, создайте QApplication и инициализируйте главное окно.

      # main.py
import sys

# Importing the instance makes it a module singleton
from main_window import main_window_instance
from PySide2 import QtWidgets

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    main_window_instance.__init__()
    main_window_instance.show()
    sys.exit(app.exec_())

Импорт экземпляра главного окна - вот что делает его одноэлементным. Создание QApplication здесь не является строго необходимым (это тоже синглтон), но мне кажется более понятным.

Теперь, когда плагины загружаются во время выполнения, они могут импортировать main_window_instance. Поскольку main_windowМодуль уже загружен точкой входа, это главное окно, используемое основным модулем (а не новый экземпляр). Теперь доступ к данным из основного модуля можно получить из подключаемого модуля.

      # plugin.py

# The plugin can now access the main module's data
from main_window import main_window_instance

main_window_instance.setWindowTitle('Changed from plugin')

Комментарии

Минимальная установка требует трех файлов:, и. В main_window.py определяет и создает главное окно, main.py делает его одноэлементным и инициализирует его, а plugin.py импортирует и использует экземпляр.

Многие детали были упущены в надежде, что основные компоненты будут очевидны. В идеале документации Qt и Python должно хватить, чтобы заполнить пробелы ...

Есть и другие соображения, например, как распространять плагины и управлять ими. Плагины могут размещаться удаленно, упаковываться в (zip) архивы, объединяться в соответствующие пакеты Python и т. Д. Плагины могут быть настолько простыми (или сложными), насколько вы хотите. Надеюсь, этот ответ дает достаточно вдохновения для того, как вы хотите, чтобы ваша система плагинов выглядела.

Другие вопросы по тегам