用python写GUI相比C++好处多多:语法简洁灵活,不用编译,自动垃圾回收,等等。很让我这个c++程序员眼红,自然要好好研究一下。
如果用纯python的方式很简单,qt或者wxwidget这个两个GUI库的python版本都已经很成熟,直接拿来用就行了。但在一些核心逻辑是由c来完成的项目里,就需要结合c和python来做GUI程序。c+python做GUI程序是有点麻烦的。通常采用的是extending方式,也就是用c++写python的扩展模块,提供一系列接口来供python调用。但是这样的方式有一些缺点,一是c模块的编译比较麻烦,二是如果已有的c++代码不是很规范,要归纳出一套易用的接口是不太容易的。
所以就希望用python嵌入(embedding)c的方式:主程序还是c程序,将python写的界面嵌入到c程序中,在事件触发时python方面只要调用一下相应的c++函数就可以完成逻辑的处理。省去了编译python模块的麻烦。而且主程序是c写的,编译出二进制的可执行程序比直接给一个python脚本去执行给用户的体验也要好一点。。。研究了几天(本人是python新手)总算是总结出一套用Cpp + python写GUI界面的方法。
本文假定你已经能够熟练使用c/c++语言。并且至少能用python写一些简单的GUI程序。如果你希望编译运行本文给出的列子,你的环境需要配置好python,pyside,boost.python(这玩意的编译很烦人囧)
这里使用boost.python来实现python的嵌入。其实用python的C API也不是不可以,但是在导出python模块的时候boost要方便许多,可以少打很多字。。并且boost.python也极大的简化了python的嵌入,不用再去关心python的C API里面恼人的索引计数和难看的错误处理(使用异常机制)。
ok,罗嗦了一大堆,下面正式开始。首先,我们需要用python来写一个GUI界面,这是我的代码:
MainFrame.py:
import
sys
from
PySide.QtCore
import
*
from
PySide.QtGui
import
*
if
(__name__
=
=
'__main__'
):
app
=
QApplication(sys.argv)
hellobt
=
QPushButton(
'Say Hello'
);
hellobt.show()
sys.exit(app.exec_())
|
这里用的是pyside。当然用什么gui库无所谓wxpython,pyqt都可以,这里只要完成一个最基础的只有一个按钮的UI就好。至于怎么写GUI这个不是我们的讨论范围。
如果你的机器上已经安装好了pyside环境的话,用python解释器运行这个文件,你会可见这样一个很搓的GUI程序。。
那么我们需要做的工作是让这个python写的界面在我们的C++代码里面跑起来。下面就是我们的main函数所在的main.cpp:
#include <boost/python.hpp>
int
main()
{
using
namespace
boost::python;
// 初始化python环境
Py_Initialize();
try
{
// 导入main模块并获得main下的命名空间
object main_module = import(
"__main__"
);
object main_namespace = main_module.attr(
"__dict__"
);
// 在main的命名空间中运行我们的python界面
exec_file(
"MainFrame.py"
, main_namespace);
}
catch
(...)
{
PyErr_Print();
}
return
0;
}
|
用你的编译器搞出一个可执行程序来,把我们一开始的MainFrame.py弄到程序的工作目录下,运行,我们就又看到了那个很搓的只有一个按钮的GUI。main.cpp的代码很简单,通过注释你应该能知道它干了些什么。要是对具体接口的使用还有疑问推荐你去翻翻boost.python的教材和手册。
好了,现在我们确实是把python嵌入进来了。但是我们的python和c++还是无法互相通信和调用。所以c++需要提供一些函数供python去调用。下面我们就来在main.cpp里声明一些c++提供给python的接口和模块。
#include <boost/python.hpp>
#include <iostream>
using
namespace
boost::python;
void
hello()
{
std::cout<<
"Hello from C :)"
<<std::endl;
}
BOOST_PYTHON_MODULE(emb)
{
def(
"hello"
, hello);
}
int
main()
{
Py_Initialize();
try
{
initemb();
object main_module = import(
"__main__"
);
object main_namespace = main_module.attr(
"__dict__"
);
exec_file(
"MainFrame.py"
, main_namespace);
}
catch
(...)
{
PyErr_Print();
}
return
0;
}
|
BOOST_PYTHON_MODULE这个宏需要解释一下。这个宏是由boost.python提供的,用来定义一个python模块,它块比C API提供的方式要简洁好多。BOOST_PYTHON_MODULE(XXX)即定义了一个名为XXX的python模块。
BOOST_PYTHON_MODULE会自动生成一个initXXX的模块初始化函数,在你的python代码import这个模块之前,你需要在c代码中调用此函数以初始化这个本不存在的模块。紧跟BOOST_PYTHON_MODULE后面的代码块里定义了具体的导出函数,这里只定义了一个。这里的def函数很智能,不管你提供的函数参数个数类型,返回值类型是什么样的,都只需要这样的一句话:def("fun", fun);前面的是python中用到的名字,后面是c++中的函数名。
可调用的c函数有了,接下来就应该在python中把这个函数关联到触发事件中去了。下面是修改过的MainFrame.py:
import
sys
from
PySide.QtCore
import
*
from
PySide.QtGui
import
*
import
emb
if
(__name__
=
=
'__main__'
):
app
=
QApplication(sys.argv)
hellobt
=
QPushButton(
'Say Hello'
)
hellobt.clicked.connect(emb.hello)
hellobt.show()
sys.exit(app.exec_())
|
可以看到,emb模块的使用就跟普通的python模块一样简单,hellobt.clicked.connect(emb.hello) 这句代码将我们的hello函数和按钮的点击信号连接了起来。大功告成,编译完程序之后可以看到,到点击一下按钮之后,控制台窗口中会出现c++代码中打印的hello字符。
改进:目前为止,boost.python的表现很完美,BOOST_PYTHON_MODULE很好用。
但当你继续深入认识下这个宏之后你会发现它不像看起来的那么完美:它没法让你把模块的声明和定义分离开来。这样一来我们所有的模块定义都只能在main.cpp里面完成。。。
boost.python这样的设计在用它进行extending式的使用时不会产生问题,因为extend python的程序中你不需要手动调用initXXX(),而在我们进行python嵌入的时候这个宏就不那么方便了。当然如果你能够忍受把模块的定义和main函数写在一个文件里面,这样的做法的是没问题的。但是如果我们需要定义的函数很多,模块很多,main.cpp中就会充斥着很多本来不该它有的东西。
这对于我们这样的完美主义者当然是不能忍受的。而boost.python又没有提供分离模块声明很定义的方法,我们只好自己解决。查询了boost.python的手册和源码后,我们知道BOOST_PYTHON_MODULE实际上生成的initXXX的声明是这样的:
extern "C" void initXXX();
知道了这个就简单了,我们只要定义一下的这一组宏就可以实现模块声明与实现的分离:PyModuleUtil.h
#include <boost/python.hpp>
// Macro to declare void intiXXX() function
#define PY_MODULE_DECLARE(MODULENAME) extern "C" void init##MODULENAME();
#define PY_MODULE_IMPLEMENT(x) BOOST_PYTHON_MODULE(x)
#define PY_DEF boost::python::def
|
void
hello();
PY_MODULE_DECLARE(emb)
|
cpp文件中我们这样写:
void
hello()
{
std::cout<<
"Hello from c :)"
<<std::endl;
}
PY_MODULE_IMPLEMENT(emb)
{
PY_DEF(
"hello"
, hello);
}
|
在main.cpp中,我们只需要把模块头文件包含进来,并手动调用一下模块的初始化就行。
写界面需要的机制前文所述基本就可以满足了。虽然特殊的时候,我们需要在c代码中调用python中的函数来控制界面(这是不推荐的,逻辑对界面可见,而界面应该对逻辑不可见)。可以使用boost.python提供的exec函数来进行,用法与exec_file相似,只是第一个参数直接给出要运行的语句的字符串而不是文件名。还有一个方式是获取python中对应函数的object对象并调用。具体方法请翻阅boost.python的文档。
由于自己在qt和python方面都是新手。所以如果有错误或者实现不恰当的地方还望指正。
参考:
Extending and Embedding the Python Interpreter
Boost.Python doc