如何使用 SIP 制作两个或多个 C++/Qt 类的 python 模块?

Posted

技术标签:

【中文标题】如何使用 SIP 制作两个或多个 C++/Qt 类的 python 模块?【英文标题】:How can I make a python module of two or more C++/Qt classes using SIP? 【发布时间】:2018-12-05 19:34:24 【问题描述】:

我有两个 C++/Qt 类(A.h、A.cpp、B.h、B.cpp):

class A : public QObject

    // bla-bla
;
class B : public A

    // bla-bla
;

我想在 Python 类 A 和 B 中使用类似这样的东西:

import mymodule

class MyB(mymodule.B):
    pass

a = mymodule.A()

我可以用一个类制作一个模块并在 Python 中成功使用它,但我不明白如何处理两个或更多类。

这是我用于构建模块的文件如何查找一个类:

*.pro:

TEMPLATE = lib

CONFIG   += qt warn_on release

HEADERS  = A.h
SOURCES  = A.cpp
TARGET   = mymodule

DESTDIR  = /home/alex/tmp/lib

*.sip:

%Module A 0

%Import QtCore/QtCoremod.sip

class A : QObject

%TypeHeaderCode
#include "A.h"
%End

public:
  A();

// bla-bla
;

配置.py:

import os
import sipconfig
from PyQt4 import pyqtconfig

build_file = "A.sbf"

config = pyqtconfig.Configuration()

qt_sip_flags = config.pyqt_sip_flags

os.system(" ".join([config.sip_bin, "-c", ".", "-b", build_file, "-I", 
config.pyqt_sip_dir, qt_sip_flags, "A.sip"]))

installs = []
installs.append(["A.sip", os.path.join(config.default_sip_dir, "A")])

makefile = pyqtconfig.QtCoreModuleMakefile(
                      configuration=config,
                      build_file=build_file,
                      installs=installs)

makefile.LFLAGS.append("-L/home/alex/tmp/lib")
makefile.extra_libs = ["A"]

makefile.generate()

运行 Makefile 后,我有了可以在 Python 中使用的“A”模块。 如何在一个 python 模块中创建 2 个或多个类?

【问题讨论】:

【参考方案1】:

在开发模块的时候最好把项目组织起来,在这种情况下我会使用如下:

├── configure.py
├── examples
│   └── main.py
├── sip
│   ├── base.sip
│   ├── derived.sip
│   └── pyfoomodule.sip
└── src
    ├── base.cpp
    ├── base.h
    ├── derived.cpp
    ├── derived.h
    ├── foomodule_global.h
    └── FooModule.pro

base.sip 和 derived.sip 不创建模块,它只定义类:

base.sip

%Import QtCore/QtCoremod.sip
class Base: public QObject

%TypeHeaderCode
#include "base.h"
%End
public:
    Base(QObject *parent=nullptr);
    virtual QString doStuff();
;

派生的.sip

%Import QtCore/QtCoremod.sip
class Derived: public Base

%TypeHeaderCode
#include "derived.h"
%End
public:
    Derived(QObject *parent=nullptr);
    QString doStuff();
;

在 pyfoomodule.sip 中创建项目,包括其他 .sip

pyfoomodule.sip

%Module(name=PyFooModule, call_super_init=True, keyword_arguments="Optional")
%DefaultMetatype PyQt4.QtCore.pyqtWrapperType
%DefaultSupertype sip.simplewrapper
%Include base.sip
%Include derived.sip

我还创建了一个脚本,负责编译项目。

configure.py

from PyQt4.QtCore import PYQT_CONFIGURATION as pyqt_config
from distutils import sysconfig
import os, sipconfig, sys


class HostPythonConfiguration(object):
    def __init__(self):
        self.platform=sys.platform
        self.version=sys.hexversion>>8

        self.inc_dir=sysconfig.get_python_inc()
        self.venv_inc_dir=sysconfig.get_python_inc(prefix=sys.prefix)
        self.module_dir=sysconfig.get_python_lib(plat_specific=1)

        if sys.platform=='win32':
            self.data_dir=sys.prefix
            self.lib_dir=sys.prefix+'\\libs'
        else:
            self.data_dir=sys.prefix+'/share'
            self.lib_dir=sys.prefix+'/lib'

class TargetQtConfiguration(object):
    def __init__(self, qmake):
        pipe=os.popen(' '.join([qmake, '-query']))

        for l in pipe:
            l=l.strip()

            tokens=l.split(':', 1)
            if isinstance(tokens, list):
                if len(tokens) != 2:
                    error("Unexpected output from qmake: '%s'\n" % l)

                name,value=tokens
            else:
                name=tokens
                value=None

            name=name.replace('/', '_')
            setattr(self, name, value)

        pipe.close()        

if __name__=="__main__":
    from argparse import ArgumentParser

    parser=ArgumentParser(description="Configure PyAnalogClock module.")
    parser.add_argument(
        '-q', '--qmake',
        dest="qmake",
        type=str,
        default="qmake-qt4",
        help="Path to qmake executable"
    )
    parser.add_argument(
        '-s', '--sip-extras',
        dest="sip_extras",
        type=str,
        default="",
        help="Extra arguments to sip"
    )
    args=parser.parse_args()

    qmake_exe=args.qmake
    if not qmake_exe.endswith('qmake-qt4'):
        qmake_exe=os.path.join(qmake_exe,'qmake')

    if os.system(' '.join([qmake_exe, '-v']))!=0:

        if sys.platform=='win32':
            print("Make sure you have a working Qt qmake on your PATH.")
        else:
            print(
                "Use the --qmake argument to explicitly specify a "
                "working Qt qmake."
            )
        exit(1)

    sip_args=args.sip_extras

    pyconfig=HostPythonConfiguration()
    py_sip_dir=os.path.join(pyconfig.data_dir, 'sip', 'PyQt4')
    sip_inc_dir=pyconfig.venv_inc_dir

    qtconfig=TargetQtConfiguration(qmake_exe)

    inc_dir=os.path.abspath(os.path.join(".","src"))
    lib_dir=inc_dir

    sip_files_dir=os.path.abspath(os.path.join(".","sip"))
    output_dir =os.path.abspath(os.path.join(".", "modules"))
    build_file="pyfoomodule.sbf"
    build_path = os.path.join(output_dir, build_file)

    if not os.path.exists(output_dir): os.mkdir(output_dir)
    sip_file = os.path.join(sip_files_dir, "pyfoomodule.sip")

    config=sipconfig.Configuration()    

    cmd=" ".join([
        config.sip_bin,
        pyqt_config['sip_flags'],
        sip_args,
        '-I', sip_files_dir,
        '-I', py_sip_dir,
        '-I', config.sip_inc_dir,
        '-I', inc_dir,
        "-c", output_dir,
        "-b", build_path,
        "-w",
        "-o",
        sip_file,
    ])

    print(cmd)
    if os.system(cmd)!=0: sys.exit(1)

    installs = []
    installs.append([os.path.join(sip_files_dir, "pyfoomodule.sip"), 
        os.path.join(config.default_sip_dir, "PyFooModule")])

    makefile=sipconfig.SIPModuleMakefile(
        config,
        build_file,
        dir=output_dir,
        installs=installs
    )

    makefile.extra_defines+=['MYMODULE_LIBRARY','QT_CORE_LIB', 'QT_GUI_LIB']
    makefile.extra_include_dirs+=[os.path.abspath(inc_dir), qtconfig.QT_INSTALL_HEADERS]
    makefile.extra_lib_dirs+=[qtconfig.QT_INSTALL_LIBS, os.path.join('..','src')]
    makefile.extra_libs+=['FooModule']

    if sys.platform=='darwin':
        makefile.extra_cxxflags+=['-F'+qtconfig.QT_INSTALL_LIBS]        
        makefile.extra_include_dirs+=[
            os.path.join(qtconfig.QT_INSTALL_LIBS,'QtCore.framework','Headers'),
            os.path.join(qtconfig.QT_INSTALL_LIBS,'QtGui.framework','Headers')
        ]

        makefile.extra_lflags+=[      
            '-F'+qtconfig.QT_INSTALL_LIBS,
            "-framework QtGui",
            "-framework QtCore",
            "-framework DiskArbitration",
            "-framework IOKit",
            "-framework OpenGL",
            "-framework AGL",
        ]

    else:
        makefile.extra_include_dirs+=[
            os.path.join(qtconfig.QT_INSTALL_HEADERS, "QtCore"),
            os.path.join(qtconfig.QT_INSTALL_HEADERS, "QtGui"),
        ]
        makefile.extra_lib_dirs+=[os.path.join('..','src','release')]
        # makefile.extra_libs+=['Qt4Core','Qt4Gui']

    makefile.generate()

    sipconfig.ParentMakefile(
        configuration = config,
        subdirs = ["src", output_dir],
    ).generate()

    os.chdir("src")    
    qmake_cmd=qmake_exe
    if sys.platform=="win32": qmake_cmd+=" -spec win32-msvc2010"
    print()
    print(qmake_cmd)
    os.system(qmake_cmd)
    sys.exit()

然后执行以下命令:

python configure.py --qmake /path/of/qmake
make 
sudo make install

最后是如何使用它的示例:

from PyQt4 import QtCore
from PyFooModule import Base, Derived

class PyDerived(Base):
    def doStuff(self):
        return "PyDerived"

if __name__ == '__main__':
    print("==============================")
    b = Base()
    pd = PyDerived()
    d = Derived()
    print("b.doStuff(): ", b.doStuff())
    print("pd.doStuff(): ", pd.doStuff())
    print("d.doStuff(): ", d.doStuff())
    print("==============================")
    print("Base is subclass of QObject: ", issubclass(Base, QtCore.QObject))
    print("PyDerived is subclass of Base: ", issubclass(PyDerived, Base))
    print("Derived is subclass of Base: ", issubclass(Derived, Base))
    print("==============================")

输出:

==============================
b.doStuff():  Base
pd.doStuff():  PyDerived
d.doStuff():  Derived
==============================
Base is subclass of QObject:  True
PyDerived is subclass of Base:  True
Derived is subclass of Base:  True
==============================

完整的例子可以在here找到。

【讨论】:

我有一个问题.. 'make install' mymodule.so 复制到 /usr/lib/python3/dist-packages/mymodule.so 和 mymodule.sip 复制到 /usr/share/sip /mymodule/mymodule.sip 当我尝试从 Python 脚本导入 mymodule 时出现错误:Traceback(最近一次调用最后一次):文件“”,第 1 行,在 ImportError: libmymodule.so.1: cannot打开共享对象文件:没有这样的文件或目录。如果我将 libmymodule.so.1 复制到 /usr/lib/python3/dist-packages 我可以导入 mymodule 而不会出错。我做错了什么? 我的项目除了FooModule.sp和PyFooModule.so没有生成任何mymodule,你有没有先尝试我的代码? 您必须有 2 组二进制文件:C ++ 的二进制文件,在我的示例中是 libFooModule.so、libFooModule.so.1、libFooModule.so.1.0 和 libFooModule.so.1.0.0安装在 /usr/lib 和另一个由 SIP 创建的名为 PyFooModule.so 的情况下,除了 .sip 文件之外,还安装在 /usr/lib/python3.7/site-packages/ 中 好的,我明白了,我以为我只需要一个库,它是从 *.sip 文件生成的。工作也需要libFooModule.so.1。 @Alex SIP 生成的.so 是Qt 项目生成的.so 的封装,所以SIP 的.so 依赖于Qt 生成的.so。您可以使用 ldd 进行验证。

以上是关于如何使用 SIP 制作两个或多个 C++/Qt 类的 python 模块?的主要内容,如果未能解决你的问题,请参考以下文章

QT Designer config

如何仅使用音频编解码器制作 iOS VOIP/SIP 应用程序 [关闭]

如何在 QT 中制作二维数组?

SIP基本场景分析

sip协议呼叫流程详解

qt多个线程调用同一个类怎么处理