65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 TKinter 的 Python OpenGL

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2016年1月22日

CPOL

14分钟阅读

viewsIcon

53160

downloadIcon

1620

为什么简单,如果我们能复杂呢?

背景

2003 年,我首次接触 3D 编程。那时我遇到了 Nehe 编程教程(如今在其网站上被视为过时内容)。

当时教程中的挑战是

  • C 语言:这没问题,毕竟用 C 编程总是一项令人愉快的挑战;
  • Windows 编程,在教程中被巧妙地简化了(但对于像我这样的初学者来说,它就像希腊语一样令人费解)
  • OpenGL 编程和 3D 编程的要求(实践中教授得很好);

之后,我了解到创建这类应用程序确实有更简化的方法。

使用 GLUT 等库,一个用于创建用户界面和数据输入的友好层,可以创建具有 3D 功能的应用程序。

一方面是灵活性,另一方面是引入的复杂性,很容易,但有局限性。

几年过去了,到 2008 年底,我开始学习 Python。

从那时起,当我遇到其他语言或平台的解决方案时,我就好奇如何在 Python 中用额外的难度来完成它

我必须只使用 Python 标准库来重做。

回到 3D 编程。

使用 Python,有许多用于创建 3D 应用程序的选项,例如 PyOpenGLVPythonpyglet 和新出现的 pyGLFW

但再次,我总是只希望使用 Python 及其标准库。

然而,在标准库中创建 GUI 的能力是使用 TkInter,它没有允许使用 OpenGL 来创建具有 3D 内容的应用程序的特性或组件。

就这样过了几年。

幸运的是,在最近几周,我发现我当时错了。

解决方案

时不时地,我寻找使用 Tkinter 中的 OpenGL 的解决方案。我从未找到易于集成的选项。当我找到接近我想要的,但它不是官方的或未更新以供使用。

然后有一天,我发现了 Lewis Van Winkle 的帖子《OpenGL with C and Tcl / Tk》。

他简明扼要地阐述了他更喜欢使用 Tcl / TK 来构建应用程序的图形界面,并让 C 和 OpenGL 完成 3D 的繁重工作。

并且他发现了如何在 Windows 上使用 C 编程在 Tcl / Tk 中轻松使用 OpenGL。

基本上,他传递了 Tk 的 frame 部件的 id,并将其配置为在 OpenGL 的上下文中(就像在 Windows 编程中对任何其他窗口所做的那样)使用。

最好的方式是不可能,对吗?这正是我想要的。

毕竟,从 C 搭配 Tcl / Tk 到 Python 和 TkInter 应该不会太难。

Python 是用 C 编写的,其标准库使用由 TKinter 包装的 Tcl / Tk。

而且确实不难。

然而,在做任何事情之前,我在 Visual Studio 中设置了一个项目来运行 Lewis 的原始应用程序,以便我能更好地理解他的代码是如何工作的。

研究原始代码

我与 Tcl / Tk 的接触一直很肤浅。直到学习并用 Python 开发了一点之后,我才去学习 TkInter,最终我才认识了这个语言。

我通过 Python 测试了完整的 Tcl 代码,以了解例如在 Tcl 中完全计算的三维实体,并在 Tk 中绘制(Tcl/Tk 的 wiki 中有这样的示例)。

然而,我仍然不知道 Lewis 的原始代码中有多少部分(使用 C 作为连接)我需要重写(用 Python 或 Tcl)。

从 Tcl 文件中找到的简单代码(行数少且复杂度低),我可以看到秘密实际上在于 C 源代码。

因此,我开始研究 C 代码,忽略 OpenGL 命令,只剩下 Tcl / Tk 函数调用。

char * p [ ] = {"OpenGL Test", "gui.tcl"};

Tk_Main (2, p, Init);

上面的几行让我最初推断,Tcl 文件是在编译时读取和解释的,在最终的可执行文件中不需要(但我错了,这个 TCL 文件必须与将生成可执行文件的目录在同一目录下)。

在尝试理解 Init 函数时,我发现了像 Tcl_CreateObjCommandTcl_CreateTimeHandler 这样的命令,它们可以由 Python 调用,从而调用 Tcl 解释器,或者可能是在 C 中创建一个扩展。

我开始阅读文档,了解这些命令的用途、它们的参数和返回值。

并开始怀疑,我最初的想法,关于创建一个 C 扩展或调用纯 Tcl 中的例程,实际上(一如既往地)采取了最复杂的方式来解决问题。

当我(应用了基本的开发流程)时,这种印象变得越来越强烈。

  • 在 Visual Studio 中链接所需的依赖项,
  • 运行,
  • 出现错误,
  • 尝试解决,搜索它们,
  • 最终修复所有
    • (并返回到步骤 2,直到不再出现错误……)

当我完成这一切时,原始代码运行得很顺利。

我看到我不仅学到了一点 Tcl / Tk 在 C 中的用法,而且 Lewis 的解决方案在 Python 和 Tkinter 上的实现,可能比我最初的想法要简单。

然而,在我确定这一点之前(是创建一个 C 扩展,还是一个由 Python 调用,或者只是使用纯 Python/Tkinter 的 Tcl 例程),我遇到了另一个问题:我需要 Windows 和 OpenGL 的特定函数来调用,并尝试配置我的 TkInter Frame 部件以使用 OpenGL。

所以,我在 Python 标准库中找到了另一个惊喜。

Ctypes

自然地,在我的第一步之后,我想更多地了解 Python。曾经有一段时间我想知道是否可能并且容易地进行 Windows 编程,就像我在 Nehe 教程中看到的那样。

那时,我搜索并很快找到了 O'reilly 的书:《Python Programming on Win32》。我很快地翻阅了书页,出于好奇只看了示例。那时我把 3D 编程放在了一边。

我不太确定我是否在这本书中第一次了解到 ctypes(我不这么认为,因为现在回顾,Mark Hammond 创建的扩展名为 pywin32(并且更新良好,2014 年有近期构建)。

总之,重点是我再次搜索 Python 中的 Windows 编程(如果可能,使用 OpenGL),以实现 Lewis 的解决方案,并找到了两个很棒的示例。

所以,我承认。我没有深入研究 Ctypes 的学习,因为它属于标准 API(从 python 2.5 开始官方支持),并且在阅读了一些文档后,足以让我相信它将是可采用的解决方案(如果我只想使用标准库,也许是唯一的)。

我专注于上面找到的代码,并使用我的 PyCharm IDE,尝试使用 Python 3.5.1 64 位来运行它们。

分析代码

Thomas 的代码只需要更新打印函数,并且我注释掉了两个 assert。

代码执行了,但出现了一些异常。

OSError: exception: access violation reading 0x00000000781BEE10

DefWindowProc 函数中(hWnd,message,wParam,lParam),它处理收到的 Windows 消息。

另一个问题:即使我点击了标准的窗口终止图标,应用程序也无法正常结束。

需要终止正在运行的应用程序,点击 PyCharm 上的终止按钮。

搜索后,我找不到任何可以解释应该是什么或者如何解决这个异常的内容。

因此,我继续分析 Darius 的代码。

它也执行成功了,但出现了新的异常。

ctypes.ArgumentError: argument 4: <class 'OverflowError'>: int too long to convert

再次在 DefWindowProcW(hwnd, message, wParam, lParam) 中。

我看到了一些有趣的东西。

该代码的一些执行结果只显示了带有居中文本(在代码中定义)的窗口轮廓,背景完全透明。

经过更多研究,它们都无法让我清楚这些异常可能是什么。

然而,在其中一次阅读中,我意识到有些人谈论了 32 位和 64 位架构之间的差异。

我想到一个可能的原因是我使用的 Python 版本。

老实说,我不太想接受这可能是答案,但是……

我切换到 Python 3.5.1 32 位来确认,成功了!

两个代码都无异常或错误地运行了。

我选择了 Thomas 的代码,因为它包含了使用 OpenGL 所需的设置。

通过分析代码,我了解到(通过文档)Ctypes 已经提供了一个名为 WinDLL 的对象。

我只需进行一个简单的调用即可轻松访问 Windows 的主要 DLL。

_user32 = WinDLL('user32')

然后在 _user32 变量中拥有 user32.dll 文件提供的所有资源。

由于我需要访问所有 OpenGL 资源,我只需进行类似的调用。

_opengl32 = WinDLL('opengl32')

这样就完成了。

配置运行 OpenGL 的窗口还需要访问 DeviceContext

Device Context 是从窗口的 ID(在 Windows 中是称为 HWND 的数据类型)中提取的。

如 Lewis 的代码所示,这个 ID 可以从 Tk frame 部件获得。

原始(Lewis)

// Grab the HWND from Tk Frame (defined in tcl source)
Tcl_GetIntFromObj (interp, objv [1] (int *) & hWnd);

// Setup OpenGL
dc = GetDC (hWnd)

使用 ctypes 的 Python

hdc = winapi._user32.getDC(<id / window hWnd>)

其中 winapi 是 Thomas 定义的一个 Python 模块,包含 Windows 编程所需的结构。

要访问 OpenGL 命令,只需包含前缀即可。

winapi._opengl32

其中 _opengl32 是 winapi 模块中定义的变量,就像上面一样。

因此,一个基本的 OpenGL 命令。

glOrtho(...)

变成

winapi._opengl32.glOrtho(...)

这个前缀肯定不是必需的,但我倾向于保留 Thomas 创建的模块中定义的结构,仅用于此初始时刻。

我认为创建一个包装器会简化(这很可能是第三方应用程序所做的)。

现在有了功能性的基本代码理解,我将 Lewis 的代码添加到了 Thomas 的代码中,以进行更多的测试。

进行了一些调整,又出现了两个困难。

  • 访问 OpenGL 的常量;
  • 以及为 OpenGL 应用程序预期的数据类型进行转换;

为了使其运行,有必要用其数值或十六进制值替换 Lewis 代码中使用的 OpenGL 常量(它们都直接从 opengl.h 查询和复制)。

这个 hack 是因为据我所知,无法从 DLL 中提取常量定义的值。

第二个困难:因为我们正在访问 OpenGL 功能,所以使用的数据类型与 C 语言相同,只是用另一种名称(非常简单的名称)重写了。自然,这些数据类型与 Python 类型不同,因为它们被设计得更接近硬件。

在这种情况下,(再一次)强大的 ctypes 提供了类来帮助转换为这些 OpenGL 所期望的数据类型。

再次以 glOrtho 函数为例,我们可以看到它在 opengl 头文件中的引用。

其签名定义为

void WINGDIAPI APIENTRY glOrtho (GLdouble left, GLdouble right , GLdouble bottom, GLdouble top, GLdouble zNear, GLdouble zFar);

这个函数有六个参数,全部是 GLdouble 数据类型,这与 C 语言的 double 数据类型相匹配。

因此,为了将整数或浮点 Python 类型转换为 double,我使用了 c_double 类。

很快,最终代码中用于此测试的行变成了。

winapi._opengl32.glOrtho(
        winapi.c_double(prm1),
        winapi.c_double(prm2),
        winapi.c_double(-2),
        winapi.c_double(2),
        winapi.c_double(-2),
        winapi.c_double(2)
)

因此,进行了必要的调整,一切都完美运行了。

现在,有了所有这些,我就可以制作出我期望的最终代码了。

Application

我的最终应用程序的最终结构是。

  • 项目文件夹 (TkInterOgl)
    • 辅助文件夹(可能是库)(tko)
      • Python 模块:gdi_hdr.py
      • Python 模块:ogl_hdr.py
      • Python 模块:tk_win.py
    • Python 模块(入口点):main.py

辅助模块的结构是为了映射我使用的命令,遵循 Thomas 在其 winapi 模块中定义的相同思想。这些包装由于 Ctypes 的存在而成为可能。

这种包装简化了调用并最大限度地减少了手动转换。

我指定了将由参数接收的数据类型,但也定义了期望的数据类型。如下面的 OpenGL 函数。

其原始签名是

WINGDIAPI void APIENTRY glBegin (GLEnum mode);

而它的包装是

glBegin = _libGL.glBegin
glBegin.restype = None
glBegin.argtypes = [GLEnum]

成员 restype 定义了返回数据类型(None 对应 C 中的 void 数据类型)。

成员 argtypes 期望一个空列表(如果函数不需要参数)或一个数据类型列表(根据每个期望的参数)。

在上面的例子中,原始函数只有一个参数:GLEnum 数据类型。

查阅 OpenGL 头文件,我发现 GLEnum 类型实际上与 C 语言中的 unsigned int 相同。

所以 GLEnum 在我们的例子中预先定义为。

GLEnum = c_uint

其中 c_uint 是 ctypes 中为转换无符号整数数据类型值定义的类。

回到最终应用程序的结构,gdi_hdr.py 模块映射了 Windows 编程所需的函数和常量(PIXELFORMATDESCRIPTOR、GetDC、ChoosePixelFormat 等……)。它是 winapi 模块的摘要。

ogl_hdr.py 模块就像 OpenGL 头文件的 Python 版本(但只包装了我测试应用程序所需的最低限度)。

这些定义都基于 opengl.h 头文件,或者查阅 MSDN 参考以查找 Windows 编程中使用的函数签名。

至于标准编码格式:gdi_hdr.py 模块是根据 PEP8 定义的(我尽量遵循)。

在 ogl_hdr.py 模块中,我选择保留 OpenGL 头文件定义的标准编码格式,考虑到那些已经熟悉的人,并且因为它也出现在其他框架(three.js、JOGL 等)中。

最后,对于 tk_win.py 模块,我设置了一个继承 TkInter Frame 部件的类的定义(它映射了原始的 TK Frame 部件),包含一些方法。

这个类具有以下方法。

  • _cfg_tkogl:
    • 从 frame 部件检索 Device Context 并配置它以接收 OpenGL 命令;
    • 调用 set_ortho_view 方法;
    • 并将应用程序配置为在 10ms 后运行 render_loop 方法。
  • on_resize
    • 当应用程序窗口的大小被调整时,由开发者定义的要执行的操作;
  • _render_loop
    • 用于动画效果的循环;
    • 调用 render_scene 方法;
    • swap buffers
  • render_scene
    • 由开发者定义的。这是包含将构建 3D 效果的 OpenGL 命令的地方;
  • set_ortho_view:。
    • 由开发者定义的用于观察 3D 空间的视图操作。

这个类被设计成被继承并在开发者应用程序中定义。

最后,入口点(main.py)是一个包含类定义示例的 Python 模块。

这个例子包含 Lewis 制作的原始 OpenGL 代码示例,它成就了这一切。

结果(有奖励)

关注点

  • 首先,我认为目标已成功实现。我可以在 Python Tkinter 中使用 OpenGL,而无需第三方资源;
  • 我相信我对 PyOpenGL 等项目的工具有了简短(非常简短)的认识。并非偶然,当我映射 OpenGL 函数时,我需要解决一个困难。我查阅了 PyOpenGL 参考资料,看看是否能在他们的定义中找到解决方案(我找到了)。那时我意识到,看到这些库提供了多少资源,以及这些资源是如何仅用 Python 来创建的,还有 C 扩展会有多大用处,是多么有趣;
  • Ctypes 是一个需要认真研究的库。它确实是 Python 连接操作系统现有资源的桥梁(可能有点明显,但对我来说最初不是);
  • 一些解决方案可以多么简单(以及找到它们可能需要多少时间,恕我直言);

奖励

  • 该应用程序也在 Linux 上运行。
    • 我使用了 Xubuntu 15.10 32 位(使用 minimal iso 安装在 virtual box 中);
    • 我需要安装:
      • libgl1-mesa-dev
      • libglu1-mesa-dev-devlibx11
      • libx11-dev
      • 我使用了 pyenv 来安装 Python 3.5.1;
    • 您必须手动设置 libGL.so (ogl_hdr.py) 和 X11lib.so (x11_gdi.py) 的路径,因为这些文件在其他 Linux 发行版上可能位于不同的位置。
    • Linux 上 OpenGL 的窗口配置似乎要简单得多。
    • 我注意到与 Windows 版本相比,动画速度略慢,但这可能是由于 virtualbox 驱动程序以及它是一个虚拟化系统;
  • 基于本文的未来项目
    • 使用 TkInter 构建应用程序以理解 OpenGL 功能;
    • 创建我的粒子引擎 😄;
    • 移植使用 OpenGL 的简单应用程序;
  • 以及一些需要回答的问题。
    • 向 Python 开发者询问 OpenGL 头文件的初步映射的最自然的方式是什么?
    • 或者一个改进 Tk 与 OpenGL 集成的扩展?(我希望看到这些功能成为标准库的一部分);
    • 这种替代方法允许使用 OpenGL 的程度有多大?
      • 是否允许使用着色器?
  • 最后但同样重要的是,大大提高我的英语水平(书面和口语)。😀
  • 示例代码可在我的 github 存储库中找到:pytkogl

 

致谢

感谢

感谢您分享您的知识和代码,使这一切成为可能。

历史

2016-01-25 - 添加致谢;

2016-01-24 - 添加 github 链接到示例代码存储库;修复截图图像和文本格式;

2016-01-22 - 更正单引号和将 _opengl32 改为 _libGL(在最终应用程序中使用)。

2016-01-21 - 在 CodeProject 提交向导中发布文章;

2016-01-11 - 使用 Google Docs 工具翻译版本;

2015-12-26 - 在我的 Google Docs 中创建巴西葡萄牙语文章;

© . All rights reserved.