欢迎光临
我们一直在努力

QT5 Python GUI 编程秘籍(三)

原文:zh.annas-archive.org/md5/261ce8c146dbb5cb015ee100187951c5

译者:飞龙

协议:CC BY-NC-SA 4.0

第九章:使用 wxPython 库扩展我们的 GUI

在本章中,我们将通过使用 wxPython 库来增强我们的 Python GUI。

  • 如何安装 wxPython 库

  • 如何在 wxPython 中创建我们的 GUI

  • 使用 wxPython 快速添加控件

  • 尝试将主 wxPython 应用嵌入到主 tkinter 应用中

  • 尝试将我们的 tkinter GUI 代码嵌入到 wxPython 中

  • 如何使用 Python 控制两个不同的 GUI 框架

  • 如何在两个连接的 GUI 之间进行通信

简介

在本章中,我们将介绍另一个目前不随 Python 一起提供的 Python GUI 工具包。它被称为 wxPython。

这个库有两个版本。原始版本被称为 Classic,而最新版本则被称为其开发项目的代号,Phoenix。

在这本书中,我们仅使用 Python 3 进行编程,并且由于新的凤凰项目旨在支持 Python 3,因此这是我们本章使用的 wxPython 版本。

首先,我们将创建一个简单的 wxPython 图形用户界面,然后我们将尝试将本书中开发的基于 tkinter 的 GUI 与新的 wxPython 库连接起来。

注意

wxPython 是 wxWidgets 的 Python 绑定。

wxPython 中的 w 代表 Windows 操作系统,而 x 代表基于 Unix 的操作系统,例如 Linux 和 OS X。

如果使用这两个 GUI 工具包同时操作不成功,我们将尝试使用 Python 来解决任何问题,然后我们将在 Python 中使用进程间通信(IPC)来确保我们的 Python 代码按预期工作。

如何安装 wxPython 库

wxPython 库并非随 Python 一起安装,因此,为了使用它,我们首先必须安装它。

这个菜谱将向我们展示在哪里以及如何找到正确的版本进行安装,以确保与所安装的 Python 版本和正在运行的操作系统相匹配。

注意事项

wxPython 第三方库已经存在超过 17 年,这表明它是一个健壮的库。

准备就绪

为了使用 wxPython 与 Python 3 兼容,我们必须安装 wxPython Phoenix 版本。

如何做到这一点…

在网上搜索 wxPython 时,我们可能会找到官方网站 www.wxpython.org。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_09_01.jpg

如果我们点击 MS Windows 的下载链接,我们可以看到几个 Windows 安装程序,所有这些安装程序都仅适用于 Python 2.x。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_09_02.jpg

要在 Python 3 中使用 wxPython,我们必须安装 wxPython/Phoenix 库。我们可以在快照构建链接中找到安装程序:

wxpython.org/Phoenix/snapshot-builds/

从这里,我们可以选择与我们的 Python 版本和操作系统版本相匹配的 wxPython/Phoenix 版本。我正在使用运行在 64 位 Windows 7 操作系统上的 Python 3.4。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_09_03.jpg

Python 轮子(.whl)安装包有一个编号方案。

对于我们来说,这个方案最重要的部分是我们正在安装适用于 Python 3.4(安装程序名称中的 cp34)和 Windows 64 位操作系统(安装程序名称中的 win_amd64 部分)的 wxPython/Phoenix 构建。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_09_04.jpg

在成功下载 wxPython/Phoenix 软件包后,我们现在可以导航到它所在的目录,并使用 pip 安装此软件包。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_09_05.jpg

我们在 Python 的site-packages文件夹中有一个名为wx的新文件夹。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_09_06.jpg

注意

wx 是 wxPython/Phoenix 库安装到的文件夹名称。我们将把这个模块导入到我们的 Python 代码中。

我们可以通过执行来自官方 wxPython/Phoenix 网站的简单演示脚本来验证我们的安装是否成功。官方网站的链接是 wxpython.org/Phoenix/docs/html/。

import wx
app = wx.App()
frame = wx.Frame(None, 1, "Hello World")
frame.Show()
app.MainLoop()

运行前面的 Python 3 脚本将使用 wxPython/Phoenix 创建以下 GUI。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_09_07.jpg

它是如何工作的…

在这个菜谱中,我们成功安装了适用于 Python 3 的正确版本的 wxPython 工具包,我们可以用它来使用 Python 3。我们找到了这个 GUI 工具包的 Phoenix 项目,它是当前活跃的开发线。Phoenix 将在适当的时候取代 Classic wxPython 工具包,并且特别针对与 Python 3 的良好兼容性。

在成功安装了 wxPython/Phoenix 工具包之后,我们只用五行代码就创建了图形用户界面。

注意事项

我们之前通过使用 tkinter 实现了相同的结果。

如何在 wxPython 中创建我们的 GUI

在这个菜谱中,我们将开始使用 wxPython GUI 工具包创建我们的 Python GUI。

我们将首先重新创建我们之前使用 tkinter 创建的几个小部件,tkinter 是 Python 自带的一个库。

然后,我们将探讨 wxPython GUI 工具包提供的一些小部件,这些小部件使用 tkinter 创建起来较为困难。

准备就绪

之前的配方向您展示了如何安装与您所使用的 Python 版本和操作系统相匹配的正确版本的 wxPython。

如何做到这一点…

开始探索 wxPython GUI 工具包的好地方是访问以下网址:wxpython.org/Phoenix/docs/html/gallery.html

这个网页展示了多个 wxPython 小部件。点击任何一个,我们就会被带到它们的文档页面,这是一个非常不错且实用的功能,可以快速了解 wxPython 控件。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_09_08.jpg

以下截图展示了 wxPython 按钮小部件的文档。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_09_09.jpg

我们可以非常快速地创建一个带有标题、菜单栏和状态栏的工作窗口。当鼠标悬停在菜单项上时,状态栏会显示该菜单项的文本。这可以通过编写以下代码实现:

# Import wxPython GUI toolkit
import wx

# Subclass wxPython frame
class GUI(wx.Frame):
def __init__(self, parent, title, size=(200,100)):
# Initialize super class
wx.Frame.__init__(self, parent, title=title, size=size)

# Change the frame background color
self.SetBackgroundColour('white')

# Create Status Bar
self.CreateStatusBar()

# Create the Menu
menu= wx.Menu()

# Add Menu Items to the Menu
menu.Append(wx.ID_ABOUT, "About", "wxPython GUI")
menu.AppendSeparator()
menu.Append(wx.ID_EXIT,"Exit"," Exit the GUI")

# Create the MenuBar
menuBar = wx.MenuBar()

# Give the MenuBar a Title
menuBar.Append(menu,"File")

# Connect the MenuBar to the frame
self.SetMenuBar(menuBar)

# Display the frame
self.Show()

# Create instance of wxPython application
app = wx.App()

# Call sub-classed wxPython GUI increasing default Window size
GUI(None, "Python GUI using wxPython", (300,150))

# Run the main GUI event loop
app.MainLoop()

这创建了一个以下 GUI,它使用 wxPython 库用 Python 编写。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_09_10.jpg

在之前的代码中,我们继承了wx.Frame。在下面的代码中,我们继承了wx.Panel并将wx.Frame传递给我们的类__init__()方法。

注意事项

在 wxPython 中,顶级 GUI 窗口被称为框架。没有框架就无法创建 wxPython GUI,并且框架必须作为 wxPython 应用程序的一部分来创建。

我们在我们的代码底部创建了应用和框架。

为了将小部件添加到我们的图形用户界面(GUI)中,我们必须将它们附加到一个面板上。面板的父级是框架(我们的顶级窗口),而我们放置到面板中的小部件的父级是面板。

以下代码向一个父级为框架的面板添加了一个多行文本框小部件。我们还向面板小部件添加了一个按钮小部件,当点击该按钮时,会在文本框中打印出一些文本。

这里是完整的代码:

import wx # Import wxPython GUI toolkit
class GUI(wx.Panel): # Subclass wxPython Panel
def __init__(self, parent):

# Initialize super class
wx.Panel.__init__(self, parent)

# Create Status Bar
parent.CreateStatusBar()

# Create the Menu
menu= wx.Menu()

# Add Menu Items to the Menu
menu.Append(wx.ID_ABOUT, "About", "wxPython GUI")
menu.AppendSeparator()
menu.Append(wx.ID_EXIT,"Exit"," Exit the GUI")

# Create the MenuBar
menuBar = wx.MenuBar()

# Give the Menu a Title
menuBar.Append(menu,"File")

# Connect the MenuBar to the frame
parent.SetMenuBar(menuBar)

# Create a Print Button
button = wx.Button(self, label="Print", pos=(0,60))

# Connect Button to Click Event method
self.Bind(wx.EVT_BUTTON, self.printButton, button)

# Create a Text Control widget
self.textBox = wx.TextCtrl(
self, size=(280,50), style=wx.TE_MULTILINE)

def printButton(self, event):
self.textBox.AppendText(
"The Print Button has been clicked!")

app = wx.App() # Create instance of wxPython application
frame = wx.Frame(None, title="Python GUI using wxPython", size=(300,180)) # Create frame
GUI(frame) # Pass frame into GUI
frame.Show() # Display the frame
app.MainLoop() # Run the main GUI event loop

运行前面的代码并点击我们的 wxPython 按钮小部件,将产生以下 GUI 输出:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_09_11.jpg

它是如何工作的…

在这个菜谱中,我们使用成熟的 wxPython GUI 工具包创建了我们自己的 GUI。仅用几行 Python 代码,我们就能够创建一个带有 最小化、最大化 和 退出 按钮的完整功能 GUI。我们添加了一个菜单栏、一个多行文本控件和一个按钮。我们还创建了一个状态栏,当选择菜单项时它会显示文本。我们将所有这些小部件放置在一个面板容器小部件中。

我们将打印按钮连接到了文本控件。

当鼠标悬停在菜单项上时,一些文本会在状态栏中显示。

使用 wxPython 快速添加控件

在这个菜谱中,我们将重新创建本书早期使用 tkinter 创建的 GUI,但这次我们将使用 wxPython 库。我们将看到使用 wxPython GUI 工具包创建自己的 Python GUI 是多么简单快捷。

我们不会重新创建在前面章节中创建的全部功能。例如,我们不会国际化我们的 wxPython GUI,也不会将其连接到 MySQL 数据库。我们将重新创建 GUI 的视觉方面,并添加一些功能。

注意事项

比较不同的库为我们提供了选择用于我们自己的 Python GUI 开发的工具包,我们可以在自己的 Python 代码中结合使用这些工具包中的几个。

准备就绪

确保您已安装 wxPython 模块以遵循此配方。

如何做到这一点…

首先,我们像之前使用 tkinter 一样创建我们的 Python OOP 类,但这次我们继承并扩展了 wx.Frame 类。为了清晰起见,我们不再将我们的类命名为 OOP,而是将其重命名为 MainFrame。

注意

在 wxPython 中,主要的 GUI 窗口被称为 Frame。

我们还创建了一个回调方法,当点击退出菜单项时关闭 GUI,并声明浅灰色元组作为我们 GUI 的背景颜色。

import wx
BACKGROUNDCOLOR = (240, 240, 240, 255)

class MainFrame(wx.Frame):
def __init__(self, *args, **kwargs):
wx.Frame.__init__(self, *args, **kwargs)

self.createWidgets()
self.Show()

def exitGUI(self, event): # callback
self.Destroy()

def createWidgets(self):
self.CreateStatusBar() # wxPython built-in method
self.createMenu()
self.createNotebook()

接下来,我们通过创建一个 wxPython Notebook 类的实例并将其分配为我们自定义的名为 Widgets 的类的父类,向我们的 GUI 添加一个标签控制。

notebook 类实例变量以 wx.Panel 作为其父类。

def createNotebook(self):
panel = wx.Panel(self)
notebook = wx.Notebook(panel)
widgets = Widgets(notebook) # Custom class explained below
notebook.AddPage(widgets, "Widgets")
notebook.SetBackgroundColour(BACKGROUNDCOLOR)
# layout
boxSizer = wx.BoxSizer()
boxSizer.Add(notebook, 1, wx.EXPAND)
panel.SetSizerAndFit(boxSizer)

注意事项

在 wxPython 中,标签控件被命名为Notebook,就像在 tkinter 中一样。

每个Notebook小部件都需要有一个父级,并且为了在 wxPython 中的Notebook中布局小部件,我们使用不同类型的布局管理器。

注意事项

wxPython 的 sizer 是类似于 tkinter 的网格布局管理器的布局管理器。

接下来,我们在笔记本页面中添加控件。我们通过创建一个继承自wx.Panel的单独类来实现这一点。

class Widgets(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.createWidgetsFrame()
self.addWidgets()
self.layoutWidgets()

我们通过将 GUI 代码拆分成小方法来模块化它,遵循 Python 面向对象编程的最佳实践,这使得我们的代码易于管理和理解。

#——————————————————
def createWidgetsFrame(self):
self.panel = wx.Panel(self)
staticBox = wx.StaticBox( self.panel, 1, "Widgets Frame" )
self.statBoxSizerV = wx.StaticBoxSizer(staticBox,
wx.VERTICAL)
#—————————————————–
def layoutWidgets(self):
boxSizerV = wx.BoxSizer( wx.VERTICAL )
boxSizerV.Add( self.statBoxSizerV, 1, wx.ALL )
self.panel.SetSizer( boxSizerV )
boxSizerV.SetSizeHints( self.panel )

#——————————————————
def addWidgets(self):
self.addCheckBoxes()
self.addRadioButtons()
self.addStaticBoxWithLabels()

注意事项

当使用 wxPython StaticBox 小部件时,为了成功布局它们,我们使用 StaticBoxSizer 和常规的 BoxSizer 的组合。wxPython 的 StaticBox 小部件与 tkinter 的 LabelFrame 小部件非常相似。

在 tkinter 中,将一个StaticBox嵌入另一个StaticBox是直接的,但使用 wxPython 则稍微不那么直观。使其工作的一种方法如下所示:

def addStaticBoxWithLabels(self):
boxSizerH = wx.BoxSizer(wx.HORIZONTAL)
staticBox = wx.StaticBox( self.panel, 1,
"Labels within a Frame" )
staticBoxSizerV = wx.StaticBoxSizer( staticBox, wx.VERTICAL )
boxSizerV = wx.BoxSizer( wx.VERTICAL )
staticText1 = wx.StaticText( self.panel, 1,
"Choose a number:" )
boxSizerV.Add( staticText1, 0, wx.ALL)
staticText2 = wx.StaticText( self.panel, 1,"Label 2")
boxSizerV.Add( staticText2, 0, wx.ALL )
#——————————————————
staticBoxSizerV.Add( boxSizerV, 0, wx.ALL )
boxSizerH.Add(staticBoxSizerV)
#——————————————————
boxSizerH.Add(wx.TextCtrl(self.panel))
# Add local boxSizer to main frame
self.statBoxSizerV.Add( boxSizerH, 1, wx.ALL )

首先,我们创建一个水平 BoxSizer。接下来,我们创建一个垂直 StaticBoxSizer,因为我们想在这个框架中将两个标签以垂直布局排列。

为了将另一个小部件安排在嵌入的StaticBox右侧,我们必须将嵌入的StaticBox及其子控件以及下一个小部件都分配给水平BoxSizer,然后将这个BoxSizer(现在包含我们的嵌入StaticBox和我们的其他小部件)分配给主StaticBox。

这听起来是不是很困惑?

您必须亲自尝试这些尺寸调整器,以了解如何使用它们。从本菜谱的代码开始,注释掉一些代码,或者修改一些 x 和 y 坐标,以观察效果。

也可以阅读官方的 wxPython 文档来获取更多信息。

注意事项

重要的是要知道在代码中添加到不同尺寸调整器的位置,以便实现我们想要的布局。

为了在第一个StaticBox下方创建第二个,我们创建单独的StaticBoxSizers并将它们分配到同一个面板。

class Widgets(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
self.panel = wx.Panel(self)
self.createWidgetsFrame()
self.createManageFilesFrame()
self.addWidgets()
self.addFileWidgets()
self.layoutWidgets()

#———————————————————-
def createWidgetsFrame(self):
staticBox = wx.StaticBox(
self.panel, 1, "Widgets Frame", size=(285, 1) )
self.statBoxSizerV = wx.StaticBoxSizer(
staticBox, wx.VERTICAL)

#———————————————————-
def createManageFilesFrame(self):
staticBox = wx.StaticBox(
self.panel, 1, "Manage Files", size=(285, 1) )
self.statBoxSizerMgrV = wx.StaticBoxSizer(
staticBox, wx.VERTICAL)

#———————————————————-
def layoutWidgets(self):
boxSizerV = wx.BoxSizer( wx.VERTICAL )
boxSizerV.Add( self.statBoxSizerV, 1, wx.ALL )
boxSizerV.Add( self.statBoxSizerMgrV, 1, wx.ALL )

self.panel.SetSizer( boxSizerV )
boxSizerV.SetSizeHints( self.panel )

#———————————————————-
def addFileWidgets(self):
boxSizerH = wx.BoxSizer(wx.HORIZONTAL)
boxSizerH.Add(wx.Button(
self.panel, label='Browse to File…'))
boxSizerH.Add(wx.TextCtrl(
self.panel, size=(174, 1), value= "Z:\\\\" ))

boxSizerH1 = wx.BoxSizer(wx.HORIZONTAL)
boxSizerH1.Add(wx.Button(
self.panel, label='Copy File To: '))
boxSizerH1.Add(wx.TextCtrl(
self.panel, size=(174, 1), value= "Z:\\\\Backup" ))

boxSizerV = wx.BoxSizer(wx.VERTICAL)
boxSizerV.Add(boxSizerH)
boxSizerV.Add(boxSizerH1)

self.statBoxSizerMgrV.Add( boxSizerV, 1, wx.ALL )

以下代码实例化了主事件循环,该循环运行我们的 wxPython 图形用户界面程序。

#======================
# Start GUI
#======================
app = wx.App()
MainFrame(None, title="Python GUI using wxPython", size=(350,450))
app.MainLoop()

我们使用 wxPython 构建的 GUI 最终效果如下:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_09_12.jpg

它是如何工作的…

我们在几个类中设计和布局我们的 wxPython GUI。

一旦我们在 Python 模块的底部部分完成这个操作,我们就创建一个 wxPython 应用程序的实例。接下来,我们实例化我们的 wxPython GUI 代码。

之后,我们调用主 GUI 事件循环,该循环执行此应用程序进程内运行的所有 Python 代码。这显示了我们的 wxPython GUI。

注意事项

无论我们在创建应用程序和调用其主事件循环之间放置什么代码,它都将成为我们的 wxPython GUI。

真正熟悉 wxPython 库及其 API 可能需要一些时间,但一旦我们了解了如何使用它,这个库就变得非常有趣,并且是构建我们自己的 Python 图形用户界面(GUIs)的强大工具。wxPython 还有一个可以与之配合使用的可视化设计工具:www.cae.tntech.edu/help/programming/wxdesigner-getting-started/view

这个菜谱使用面向对象编程(OOP)来学习如何使用 wxPython 图形用户界面(GUI)工具包。

尝试将主 wxPython 应用嵌入到主 tkinter 应用中

现在我们已经使用 Python 内置的 tkinter 库以及 wxWidgets 库的 wxPython 包装器创建了相同的 GUI,我们确实需要将这些技术创建的 GUI 结合起来。

注意事项

wxPython 和 tkinter 库各自都有其优势。在诸如 stackoverflow.com/ 这样的在线论坛中,我们经常看到类似的问题,比如:哪一个更好?我应该使用哪个 GUI 工具包?这表明我们必须做出一个“非此即彼”的选择。我们不必做出这样的决定。

在这样做的主要挑战之一是,每个 GUI 工具包都必须有自己的事件循环。

在这个菜谱中,我们将尝试通过从我们的 tkinter GUI 中调用它来嵌入一个简单的 wxPython GUI。

准备就绪

我们将重用我们在第一章中构建的 tkinter GUI,即创建 GUI 表单和添加小部件。

如何做到这一点…

我们从一个看起来是这样的简单 tkinter GUI 开始:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_09_13.jpg

接下来,我们将尝试调用一个简单的 wxPython 图形用户界面(GUI),这是我们在这个章节之前的一个菜谱中创建的。

这是用简单、非面向对象的方式完成此操作的完整代码:

#===========================================================
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext

win = tk.Tk()

win.title("Python GUI")
aLabel = ttk.Label(win, text="A Label")
aLabel.grid(column=0, row=0)
ttk.Label(win, text="Enter a name:").grid(column=0, row=0)
name = tk.StringVar()
nameEntered = ttk.Entry(win, width=12, textvariable=name)
nameEntered.grid(column=0, row=1)
ttk.Label(win, text="Choose a number:").grid(column=1, row=0)
number = tk.StringVar()
numberChosen = ttk.Combobox(win, width=12, textvariable=number)
numberChosen['values'] = (1, 2, 4, 42, 100)
numberChosen.grid(column=1, row=1)
numberChosen.current(0)
scrolW = 30
scrolH = 3
scr = scrolledtext.ScrolledText(win, width=scrolW, height=scrolH, wrap=tk.WORD)
scr.grid(column=0, sticky='WE', columnspan=3)
nameEntered.focus()

#===========================================================
def wxPythonApp():
import wx
app = wx.App()
frame = wx.Frame(None, 1, "wxPython GUI", size=(200,150))
frame.SetBackgroundColour('white')
frame.CreateStatusBar()
menu= wx.Menu()
menu.Append(wx.ID_ABOUT, "About", "wxPython GUI")
menuBar = wx.MenuBar()
menuBar.Append(menu,"File")
frame.SetMenuBar(menuBar)
frame.Show()
app.MainLoop()

action = ttk.Button(win, text="Call wxPython GUI", command= wxPythonApp )
action.grid(column=2, row=1)

#======================
# Start GUI
#======================
win.mainloop()

运行前面的代码,在点击 tkinter 的Button控件后,将从一个 tkinter GUI 启动一个 wxPython GUI。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_09_14.jpg

它是如何工作的…

重要的是,我们将整个 wxPython 代码放入了一个名为 def wxPythonApp() 的独立函数中。

在按钮点击事件的回调函数中,我们只需调用此代码。

注意事项

需要注意的一点是,在继续使用 tkinter GUI 之前,我们必须关闭 wxPython GUI。

尝试将我们的 tkinter GUI 代码嵌入到 wxPython 中

在这个菜谱中,我们将与之前的菜谱相反的方向,尝试从 wxPython GUI 内部调用我们的 tkinter GUI 代码。

准备就绪

我们将重用本章之前的一个菜谱中创建的一些 wxPython GUI 代码。

如何做到这一点…

我们将从简单的 wxPython 图形用户界面(GUI)开始,它看起来像这样:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_09_15.jpg

接下来,我们将尝试调用一个简单的 tkinter GUI。

这是用简单、非面向对象的方式完成此操作的完整代码:

#=============================================================
def tkinterApp():
import tkinter as tk
from tkinter import ttk
win = tk.Tk()
win.title("Python GUI")
aLabel = ttk.Label(win, text="A Label")
aLabel.grid(column=0, row=0)
ttk.Label(win, text="Enter a name:").grid(column=0, row=0)
name = tk.StringVar()
nameEntered = ttk.Entry(win, width=12, textvariable=name)
nameEntered.grid(column=0, row=1)
nameEntered.focus()
def buttonCallback():
action.configure(text='Hello ' + name.get())
action = ttk.Button(win, text="Print", command=buttonCallback)
action.grid(column=2, row=1)
win.mainloop()

#=============================================================
import wx
app = wx.App()
frame = wx.Frame(None, 1, "wxPython GUI", size=(200,180))
frame.SetBackgroundColour('white')
frame.CreateStatusBar()
menu= wx.Menu()
menu.Append(wx.ID_ABOUT, "About", "wxPython GUI")
menuBar = wx.MenuBar()
menuBar.Append(menu,"File")
frame.SetMenuBar(menuBar)
textBox = wx.TextCtrl(frame, size=(180,50), style=wx.TE_MULTILINE)

def tkinterEmbed(event):
tkinterApp()

button = wx.Button(frame, label="Call tkinter GUI", pos=(0,60))
frame.Bind(wx.EVT_BUTTON, tkinterEmbed, button)
frame.Show()

#======================
# Start wxPython GUI
#======================
app.MainLoop()

运行前面的代码,在点击 wxPython 的 Button 控件后,将启动一个 tkinter GUI。然后我们可以将文本输入到 tkinter 的文本框中。通过点击其按钮,按钮文本会更新为相应的名字。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_09_16.jpg

在启动 tkinter 事件循环后,wxPython GUI 仍然可以响应,因为我们可以在 tkinter GUI 运行时输入到 TextCtrl 小部件中。

注意事项

在之前的配方中,我们无法使用我们的 tkinter GUI,直到我们关闭了 wxPython GUI。意识到这种差异可以帮助我们做出设计决策,如果我们想要结合这两种 Python GUI 技术的话。

我们也可以通过多次点击 wxPython GUI 按钮来创建几个 tkinter GUI 实例。然而,当任何 tkinter GUI 还在运行时,我们无法关闭 wxPython GUI。我们必须先关闭它们。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_09_17.jpg

它是如何工作的…

在这个菜谱中,我们与之前的菜谱相反,首先使用 wxPython 创建了一个 GUI,然后,在它内部,创建了几个使用 tkinter 构建的 GUI 实例。

当一个或多个 tkinter GUI 运行时,wxPython GUI 仍然保持响应。然而,点击 tkinter 按钮只在其第一次点击时更新其按钮文本。

如何使用 Python 控制两个不同的 GUI 框架

在这个菜谱中,我们将探讨从 Python 控制 tkinter 和 wxPython GUI 框架的方法。在前一章中,我们已经使用了 Python 线程模块来保持我们的 GUI 响应,因此在这里我们将尝试使用相同的方法。

我们将看到事情并不总是以直观的方式运作。

然而,当我们从其中调用 wxPython GUI 的实例时,我们将改进我们的 tkinter GUI,使其不再无响应。

准备就绪

这个菜谱将扩展本章中之前的一个菜谱,其中我们尝试将一个主要的 wxPython GUI 嵌入我们的 tkinter GUI。

如何做到这一点…

当我们从 tkinter GUI 创建了一个 wxPython GUI 实例后,我们就不能再使用 tkinter GUI 控件,直到关闭 wxPython GUI 的那个实例。现在让我们来改进这一点。

我们第一次尝试可能是使用来自 tkinter 按钮回调函数的线程。

例如,我们的代码可能看起来像这样:

def wxPythonApp():
import wx
app = wx.App()
frame = wx.Frame(None, 1, "wxPython GUI", size=(200,150))
frame.SetBackgroundColour('white')
frame.CreateStatusBar()
menu= wx.Menu()
menu.Append(wx.ID_ABOUT, "About", "wxPython GUI")
menuBar = wx.MenuBar()
menuBar.Append(menu,"File")
frame.SetMenuBar(menuBar)
frame.Show()
app.MainLoop()

def tryRunInThread():
runT = Thread(target=wxPythonApp)
runT.setDaemon(True)
runT.start()
print(runT)
print('createThread():', runT.isAlive())

action = ttk.Button(win, text="Call wxPython GUI", command=tryRunInThread)

首先,这似乎是有效的,这很直观,因为 tkinter 控件不再被禁用,我们可以通过点击按钮创建几个 wxPython GUI 的实例。我们还可以在其他的 tkinter 小部件中输入和选择内容。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_09_18.jpg

然而,一旦我们尝试关闭 GUIs,我们就会从 wxWidgets 那里收到一个错误,并且我们的 Python 可执行文件会崩溃。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_09_19.jpg

为了避免这种情况,我们不必尝试在单独的线程中运行整个 wxPython 应用程序,而是可以将代码修改为仅让 wxPython 的 app.MainLoop 在线程中运行。

def wxPythonApp():
import wx
app = wx.App()
frame = wx.Frame(None, 1, "wxPython GUI", size=(200,150))
frame.SetBackgroundColour('white')
frame.CreateStatusBar()
menu= wx.Menu()
menu.Append(wx.ID_ABOUT, "About", "wxPython GUI")
menuBar = wx.MenuBar()
menuBar.Append(menu,"File")
frame.SetMenuBar(menuBar)
frame.Show()

runT = Thread(target=app.MainLoop)
runT.setDaemon(True)
runT.start()
print(runT)
print('createThread():', runT.isAlive())

action = ttk.Button(win, text="Call wxPython GUI", command=wxPythonApp)
action.grid(column=2, row=1)

它是如何工作的…

我们最初尝试在一个线程中运行整个 wxPython GUI 应用程序,但这样做并没有成功,因为 wxPython 的主事件循环期望成为应用程序的主线程。

我们通过仅在一个线程中运行 wxPython 的 app.MainLoop 来解决这个问题,这让它误以为自己是主线程。

这种方法的一个副作用是,我们不能再单独关闭所有的 wxPython GUI 实例。至少有一个实例只有在关闭创建线程作为守护进程的 wxPython GUI 时才会关闭。

我并不完全清楚这是为什么。直观上,人们可能会期望能够关闭所有守护线程,而无需等待创建它们的父线程先关闭。

这可能是因为在我们主线程仍在运行时,引用计数器没有被设置为零。

在实用层面上,这是它目前的工作方式。

如何在两个连接的 GUI 之间进行通信

在之前的菜谱中,我们找到了连接 wxPython GUI 和 tkinter GUI 的方法,可以从一个调用另一个,反之亦然。

虽然这两个图形用户界面(GUIs)同时成功运行,但它们实际上并没有相互通信,因为它们只是互相启动。

在这个菜谱中,我们将探讨如何让这两个图形用户界面相互通信。

准备就绪

阅读之前的某个食谱可能是为这个食谱做准备的一个好方法。

在这个菜谱中,我们将使用与上一个菜谱略有修改的 GUI 代码,但大部分基本的 GUI 构建代码是相同的。

如何做到这一点…

在之前的菜谱中,我们面临的主要挑战之一是如何结合两种旨在成为应用程序唯一 GUI 工具包的 GUI 技术。我们发现了很多简单的方法来实现它们的结合。

我们将再次从 tkinter GUI 主事件循环中启动 wxPython GUI,并在 tkinter 进程内部启动其自己的线程来运行 wxPython GUI。

为了做到这一点,我们将使用一个共享的全局多进程 Python 队列。

注意事项

虽然在这个菜谱中通常最好避免使用全局数据,但它们是一个实用的解决方案,并且 Python 的全局变量实际上仅在它们被声明的模块中是全局的。

这里是使两个 GUI 在某种程度上相互通信的 Python 代码。为了节省空间,这并非纯面向对象编程(OOP)代码。

我们也没有展示所有小部件的创建代码。该代码与之前的食谱相同。

# Ch09_Communicate.py
import tkinter as tk
from tkinter import ttk
from threading import Thread

win = tk.Tk()
win.title("Python GUI")

from multiprocessing import Queue
sharedQueue = Queue()
dataInQueue = False

def putDataIntoQueue(data):
global dataInQueue
dataInQueue = True
sharedQueue.put(data)

def readDataFromQueue():
global dataInQueue
dataInQueue = False
return sharedQueue.get()
#===========================================================
import wx
class GUI(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent)
parent.CreateStatusBar()
button = wx.Button(self, label="Print", pos=(0,60))
self.Bind(wx.EVT_BUTTON, self.writeToSharedQueue, button)

#——————————————————–
def writeToSharedQueue(self, event):
self.textBox.AppendText(
"The Print Button has been clicked!\\n")
putDataIntoQueue('Hi from wxPython via Shared Queue.\\n')
if dataInQueue:
data = readDataFromQueue()
self.textBox.AppendText(data)

text.insert('0.0', data) # insert data into GUI

#============================================================
def wxPythonApp():
app = wx.App()
frame = wx.Frame(
None, title="Python GUI using wxPython", size=(300,180))
GUI(frame)
frame.Show()
runT = Thread(target=app.MainLoop)
runT.setDaemon(True)
runT.start()
print(runT)
print('createThread():', runT.isAlive())
#============================================================
action = ttk.Button(win, text="Call wxPython GUI", command=wxPythonApp)
action.grid(column=2, row=1)

#======================
# Start GUI
#======================
win.mainloop()

首先运行前面的代码会创建程序中的 tkinter 部分,当我们点击这个 GUI 中的按钮时,它将运行 wxPython GUI。这两个 GUI 仍然像之前一样同时运行,但这次,两个 GUI 之间增加了一个额外的通信层级。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_09_20.jpg

前一个截图的左侧显示了 tkinter 图形用户界面,通过点击调用 wxPython GUI按钮,我们可以调用 wxPython GUI 的一个实例。我们可以通过多次点击按钮来创建多个实例。

注意事项

所有创建的 GUI 都保持响应。它们不会崩溃也不会冻结。

点击任何 wxPython GUI 实例上的 打印 按钮,会在其自身的 TextCtrl 小部件中写入一句话,然后还会写入另一行到自身以及 tkinter GUI 中。您需要向上滚动才能在 wxPython GUI 中看到第一句话。

注意事项

这种工作方式是通过使用模块级队列和 tkinter 的 Text 小部件

需要注意的一个重要元素是,我们创建了一个线程来运行 wxPython 的 app.MainLoop,就像我们在之前的菜谱中做的那样。

def wxPythonApp():
app = wx.App()
frame = wx.Frame(
None, title="Python GUI using wxPython", size=(300,180))
GUI(frame)
frame.Show()
runT = Thread(target=app.MainLoop)
runT.setDaemon(True)
runT.start()

我们创建了一个从 wx.Panel 继承的类,并将其命名为 GUI。然后,我们在前面的代码中实例化了这个类的实例。

在这个类中,我们创建了一个按钮点击事件回调方法,然后调用上面编写的程序代码。正因为如此,这个类可以访问这些函数并且可以向共享队列写入。

#——————————————————
def writeToSharedQueue(self, event):
self.textBox.AppendText(
"The Print Button has been clicked!\\n")
putDataIntoQueue('Hi from wxPython via Shared Queue.\\n')
if dataInQueue:
data = readDataFromQueue()
self.textBox.AppendText(data)
text.insert('0.0', data) # insert data into tkinter

我们首先检查数据是否已经在先前方法中放置到共享队列中,如果是这样,我们随后将公共数据打印到两个 GUI 上。

注意事项

putDataIntoQueue() 这一行将数据放入队列,而 readDataFromQueue() 则从队列中读取数据,并将其保存在 data 变量中。

text.insert('0.0', data) 是将此数据写入 tkinter GUI 的 打印 按钮的 wxPython 回调方法的代码行。

以下是在代码中被调用并使代码工作的过程函数(不是方法,因为它们没有绑定)。

from multiprocessing import Queue
sharedQueue = Queue()
dataInQueue = False

def putDataIntoQueue(data):
global dataInQueue
dataInQueue = True
sharedQueue.put(data)

def readDataFromQueue():
global dataInQueue
dataInQueue = False
return sharedQueue.get()

我们使用一个简单的布尔标志dataInQueue来通信,当数据在队列中可用时。

它是如何工作的…

在这个菜谱中,我们成功地将我们以前以类似方式创建的两个 GUI 结合起来,但之前是独立的,彼此之间没有交流。然而,在这个菜谱中,我们通过让一个 GUI 启动另一个 GUI,并通过简单的 Python 多进程队列机制,使它们能够相互通信,将数据从共享队列写入两个 GUI 中。

有许多非常先进和复杂的技术可用于连接不同的进程、线程、线程池、锁、管道、TCP/IP 连接等等。

在 Python 的精神中,我们找到了一个对我们来说简单有效的解决方案。一旦我们的代码变得复杂,我们可能需要重构它,但这是一个良好的开端。

第十章. 使用 PyOpenGL 和 PyGLet 创建惊人的 3D GUIs

在本章中,我们将创建令人惊叹的 Python GUI,它能够显示真正的三维图像,这些图像可以围绕自身旋转,以便我们从各个角度观察它们。

  • PyOpenGL 将我们的 GUI 进行转换

  • 我们的 3D 图形用户界面!

  • 使用位图使我们的 GUI 看起来更美观

  • PyGLet 比 PyOpenGL 更容易转换我们的 GUI

  • 我们的用户界面色彩绚丽

  • 使用 tkinter 创建幻灯片

简介

在本章中,我们将通过赋予它真正的三维能力来转换我们的 GUI。我们将使用两个 Python 第三方库。PyOpenGL 是 OpenGL 标准的 Python 绑定,它是一个所有主要操作系统都内置的图形库。这使得生成的控件具有原生外观和感觉。

Pyglet 是 OpenGL 库的另一个 Python 绑定,但它也可以创建 GUI 应用程序,这使得使用 Pyglet 进行编码比使用 PyOpenGL 更为简便。

PyOpenGL 转换我们的 GUI

在这个菜谱中,我们将成功创建一个 Python GUI,它实际上会导入 PyOpenGL 模块并正常工作!

为了做到这一点,我们需要克服一些初始挑战。

这个菜谱将展示一种经过验证确实有效的方法。如果你在自己的实验中遇到困难,请记住托马斯·爱迪生所说的著名话语。

注意事项

托马斯·爱迪生,白炽灯泡的发明者,回答了一位记者关于爱迪生失败的问题。爱迪生回答说:

“我没有失败,我只是找到了一万种行不通的方法。”

首先,我们必须安装 PyOpenGL 扩展模块。

在成功安装与我们的操作系统架构相匹配的 PyOpenGL 模块后,我们将创建一些示例代码。

准备就绪

我们将安装 PyOpenGL 包。在这本书中,我们使用的是 Windows 7 64 位操作系统和 Python 3.4。下面跟随的下载截图是为了这个配置。

我们还将使用 wxPython。如果您尚未安装 wxPython,您可以阅读前一章中关于如何安装 wxPython 以及如何使用此 GUI 框架的一些食谱。

注意事项

我们正在使用 wxPython Phoenix 版本,这是最新的版本,并计划在未来取代原始的 Classic wxPython 版本。

如何做到这一点…

为了使用 PyOpenGL,我们首先需要安装它。以下网址是官方的 Python 软件包安装网站:

pypi.python.org/pypi/PyOpenGL/3.0.2#downloads

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_10_01.jpg

这看起来是正确的安装方式,但结果证明,它不适用于 Windows 7 64 位操作系统和 Python 3.4.3 64 位。

在前一章的食谱中提到了一个更好的地方来查找 Python 安装包。你可能已经熟悉它了。我们下载与我们的操作系统和 Python 版本相匹配的包。它带有新的 .whl 格式,因此我们首先需要安装 Python 轮子包。

注意事项

如何安装 Python 轮子包在之前的菜谱中有描述。

使用pip命令通过PyOpenGL-3.1.1a1-cp34-none-win_amd64.whl文件安装 PyOpenGL 既成功又安装了我们所需的全部 64 位模块。

将 <your full path> 替换为你下载 wheel 安装程序的完整路径。

pip install <your full path> PyOpenGL3.1.1a1cp34nonewin_amd64.whl

当我们现在尝试导入一些 PyOpenGL 模块时,它工作正常,如下代码示例所示:

# Ch10_import_OpenGL.py
import wx
from wx import glcanvas
from OpenGL.GL import *
from OpenGL.GLUT import *

所有这些代码所做的只是导入几个 OpenGL Python 模块。它没有做其他任何事情,但是当我们运行我们的 Python 模块时,我们没有得到任何错误。

这证明了我们已经成功地将 OpenGL 绑定安装到了 Python 中。

现在我们的开发环境已经成功搭建完成,我们可以使用 wxPython 来尝试它。

注意事项

许多在线示例仅限于使用 Python 2.x,以及使用 wxPython 的经典版本。我们正在使用 Python 3 和 Phoenix。

使用基于 wxPython 示例代码创建了一个工作的 3D 立方体。相比之下,运行圆锥示例没有成功,但这个示例让我们走上了正确的道路。

这里是网址:

wiki.wxpython.org/GLCanvas%20update

这里是对代码的一些修改:

import wx
from wx import glcanvas
from OpenGL.GL import *
from OpenGL.GLUT import *

class MyCanvasBase(glcanvas.GLCanvas):
def __init__(self, parent):
glcanvas.GLCanvas.__init__(self, parent, 1)

# This context was missing from the code
self.context = glcanvas.GLContext(self) # <- added

def OnPaint(self, event):
dc = wx.PaintDC(self)
# We have to pass in a context ——
# self.SetCurrent() # commented out
self.SetCurrent(self.context) # <- changed

我们现在可以创建以下 GUI:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_10_02.jpg

在 wxPython 的古典版本中,SetCurrent()函数不需要上下文。以下是在网上搜索时可能会找到的一些代码。

def OnPaint(self, event):

dc = wx.PaintDC(self)
self.SetCurrent()
if not self.init:
self.InitGL()
self.init = True
self.OnDraw()

在使用 wxPython Phoenix 时,前面的代码无法正常工作。我们可以在网上查找 Phoenix 的正确语法。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_10_03.jpg

它是如何工作的…

在这个菜谱中,我们第一次使用 PyOpenGL Python 绑定库体验了 OpenGL。虽然 OpenGL 能够在真正的 3D 中创建真正惊人的图像,但在过程中我们遇到了一些挑战,然后找到了解决这些挑战的方法,使它得以工作。

注意

我们正在用 Python 编写代码,创建 3D 图像!

我们的三维 GUI!

在这个菜谱中,我们将使用 wxPython 创建自己的图形用户界面。我们正在重用一些来自 wxPython 演示示例的代码,我们将这些代码简化到仅显示 3D OpenGL 所需的最小代码量。

注意事项

OpenGL 是一个非常庞大的库。我们不会对这个库进行详细的解释。如果你想要进一步学习 OpenGL,有很多书籍和在线文档可供参考。它有自己的着色语言。

准备就绪

阅读之前的食谱可能是为这个食谱做的良好准备。

如何做到这一点…

由于整个 Python 代码在这里有点长,我们将只展示一小部分代码。

整个代码可在网上找到,这个 Python 模块被称为:

# Ch10_wxPython_OpenGL_GUI
import wx
from wx import glcanvas
from OpenGL.GL import *
from OpenGL.GLUT import *

#—————————————————
class CanvasBase(glcanvas.GLCanvas):
def __init__(self, parent):
glcanvas.GLCanvas.__init__(self, parent, 1)
self.context = glcanvas.GLContext(self)
self.init = False

# Cube 3D start rotation
self.last_X = self.x = 30
self.last_Y = self.y = 30

self.Bind(wx.EVT_SIZE, self.sizeCallback)
self.Bind(wx.EVT_PAINT, self.paintCallback)
self.Bind(wx.EVT_LEFT_DOWN, self.mouseDownCallback)
self.Bind(wx.EVT_LEFT_UP, self.mouseUpCallback)
self.Bind(wx.EVT_MOTION, self.mouseMotionCallback)

def sizeCallback(self, event):
wx.CallAfter(self.setViewport)
event.Skip()

def setViewport(self):
self.size = self.GetClientSize()
self.SetCurrent(self.context)
glViewport(0, 0, self.size.width, self.size.height)

def paintCallback(self, event):
wx.PaintDC(self)
self.SetCurrent(self.context)
if not self.init:
self.initGL()
self.init = True
self.onDraw()

def mouseDownCallback(self, event):
self.CaptureMouse()
self.x, self.y = self.last_X, self.last_Y = event.GetPosition()

def mouseUpCallback(self, evt):
self.ReleaseMouse()

def mouseMotionCallback(self, evt):
if evt.Dragging() and evt.LeftIsDown():
self.last_X, self.last_Y = self.x, self.y
self.x, self.y = evt.GetPosition()
self.Refresh(False)

#—————————————————–
class CubeCanvas(CanvasBase):
def initGL(self):
# set viewing projection
glMatrixMode(GL_PROJECTION)
glFrustum(0.5, 0.5, 0.5, 0.5, 1.0, 3.0)

# position viewer
glMatrixMode(GL_MODELVIEW)
glTranslatef(0.0, 0.0, 2.0)

# position object
glRotatef(self.y, 1.0, 0.0, 0.0)
glRotatef(self.x, 0.0, 1.0, 0.0)

glEnable(GL_DEPTH_TEST)
glEnable(GL_LIGHTING)
glEnable(GL_LIGHT0)

def onDraw(self):
# clear color and depth buffers
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

# draw six faces of a cube
glBegin(GL_QUADS)
glNormal3f( 0.0, 0.0, 1.0)
glVertex3f( 0.5, 0.5, 0.5)
glVertex3f(0.5, 0.5, 0.5)
glVertex3f(0.5,0.5, 0.5)
glVertex3f( 0.5,0.5, 0.5)

glNormal3f( 0.0, 0.0,1.0)
glVertex3f(0.5,0.5,0.5)

#===========================================================
app = wx.App()
frame = wx.Frame(None, title="Python GUI using wxPython", size=(300,230))
GUI(frame)
frame.Show()
app.MainLoop()

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_10_04.jpg

前面的截图展示了我们的 wxPython 图形用户界面。当我们点击按钮控件时,以下第二个窗口就会出现。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_10_05.jpg

注意

我们现在可以使用鼠标来旋转立方体,以便看到它的六个面。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_10_06.jpg

我们还可以最大化这个窗口,坐标将会缩放,我们可以在这么大的窗口中旋转这个立方体!

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_10_07.jpg

这个立方体也可以是一艘《星际迷航》太空船!

如果这是我们想要发展的,我们只需成为这个技术的高级程序员即可。

注意事项

许多视频游戏正在使用 OpenGL 进行开发。

它是如何工作的…

我们首先创建了一个常规的 wxPython 图形用户界面,并在其上放置了一个按钮控件。点击此按钮将调用导入的 OpenGL 3D 库。所使用的代码是 wxPython 示例代码的一部分,我们稍作修改以使其与 Phoenix 兼容。

注意

这个配方将我们自己的 GUI 粘合到这个库上。

OpenGL 是一个如此庞大且令人印象深刻的库。这个菜谱展示了如何在 Python 中创建一个工作示例的技巧。

注意事项

通常,一个工作示例就足以让我们开始我们的旅程。

使用位图使我们的 GUI 看起来更美观

这份食谱灵感来源于一个曾经工作过的 wxPython IDE 构建框架。

它不适用于 Python 3 和 wxPython Phoenix,但代码非常酷。

我们将重用该项目提供的代码中的大量位图图像。

在时间耗尽之前,你可以在 GitHub 上 fork 谷歌的代码。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_10_08.jpg

准备就绪

我们将继续在本食谱中使用 wxPython,因此阅读前一章的部分内容可能有助于为制作本食谱做准备。

如何做到这一点…

在对 gui2py 代码进行逆向工程并对该代码进行其他修改后,我们可能会得到以下窗口小部件,它显示了一个漂亮的、拼贴式的背景。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_10_09.jpg

当然,我们在重构之前提到的网站上的代码时丢失了很多小部件,但这也确实给了我们一个酷炫的背景,点击退出按钮仍然有效。

下一步是弄清楚如何将代码中的有趣部分整合到我们自己的图形用户界面中。

我们通过在之前菜谱的 GUI 中添加以下代码来实现这一点。

#———————————————————-
class GUI(wx.Panel): # Subclass wxPython Panel
def __init__(self, parent):
wx.Panel.__init__(self, parent)

imageFile = 'Tile.bmp'
self.bmp = wx.Bitmap(imageFile)
# react to a resize event and redraw image
parent.Bind(wx.EVT_SIZE, self.canvasCallback)

def canvasCallback(self, event=None):
# create the device context
dc = wx.ClientDC(self)
brushBMP = wx.Brush(self.bmp)
dc.SetBrush(brushBMP)
width, height = self.GetClientSize()
dc.DrawRectangle(0, 0, width, height)

注意事项

我们必须绑定到父级,而不是自身,否则我们的位图将不会显示。

现在运行我们的改进代码,将位图作为我们 GUI 的背景。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_10_10.jpg

点击按钮仍然调用我们的 OpenGL 3D 绘图,所以我们没有丢失任何功能。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_10_11.jpg

它是如何工作的…

在这个菜谱中,我们通过使用位图作为背景来增强了我们的图形用户界面。我们将位图图像平铺,当我们调整 GUI 窗口大小时,位图会自动调整自身以填充我们在设备上下文中绘制的 Canvas 整个区域。

注意事项

前面的 wxPython 代码可以加载不同的图像文件格式。

PyGLet 比 PyOpenGL 更容易转换我们的 GUI

在这个菜谱中,我们将使用 PyGLet 图形用户界面开发框架来创建我们的 GUI。

PyGLet 比 PyOpenGL 更易于使用,因为它自带了 GUI 事件循环,所以我们不需要使用 tkinter 或 wxPython 来创建我们的 GUI。

如何做到这一点…

为了使用 Pyglet,我们首先必须安装这个第三方 Python 插件。

使用pip命令,我们可以轻松地安装库,一个成功的安装在我们 Python 的site-packages文件夹中看起来是这样的:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_10_12.jpg

在线文档位于当前版本的此网站:

pyglet.readthedocs.org/en/pyglet-1.2-maintenance/

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_10_13.jpg

首次使用 Pyglet 库的经验可能看起来是这样的:

import pyglet

window = pyglet.window.Window()
label = pyglet.text.Label('PyGLet GUI',
font_size=42,
x=window.width//2, y=window.height//2,
anchor_x='center', anchor_y='center')

@window.event
def on_draw():
window.clear()
label.draw()

pyglet.app.run()

上述代码来自官方 pyglet.org 网站,并生成以下功能齐全的 GUI:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_10_14.jpg

它是如何工作的…

在这个菜谱中,我们使用了另一个封装了 OpenGL 库的第三方 Python 模块。

这个库自带事件循环处理能力,这使得我们无需依赖另一个库来创建一个运行的 Python GUI。

我们已经探索了展示我们如何安装和使用这个出色的 GUI 库的官方网站。

我们的用户界面色彩绚丽

在这个菜谱中,我们将扩展之前菜谱中使用的 Pyglet 编写的 GUI,将其转变为真正的 3D。

我们还将为它添加一些炫酷的颜色。这个食谱灵感来源于《OpenGL SuperBible》系列书籍中的某些示例代码。它创建了一个非常多彩的立方体,我们可以使用键盘的上、下、左、右按钮在三维空间中旋转它。

我们对示例代码进行了轻微改进,使得在按下任意一个键时图片会转动,而不是需要按下并释放键。

准备就绪

之前的配方解释了如何安装 PyGLet,并为您介绍了这个库。如果您还没有这样做,浏览那一章可能是个不错的主意。

注意事项

在在线文档中,PyGLet 通常全部使用小写字母拼写。虽然这可能是一种 Python 风格的做法,但我们将类名首字母大写,并且对于变量、方法和函数名,我们使用小写字母来开始每个名称。

在这本书中,除非为了澄清代码,我们不会使用下划线。

如何做到这一点…

以下代码创建了下方显示的 3 维彩色立方体。这次,我们将使用键盘方向键来旋转图像,而不是鼠标。

import pyglet
from pyglet.gl import *
from pyglet.window import key
from OpenGL.GLUT import *

WINDOW = 400
INCREMENT = 5

class Window(pyglet.window.Window):

# Cube 3D start rotation
xRotation = yRotation = 30

def __init__(self, width, height, title=''):
super(Window, self).__init__(width, height, title)
glClearColor(0, 0, 0, 1)
glEnable(GL_DEPTH_TEST)

def on_draw(self):
# Clear the current GL Window
self.clear()

# Push Matrix onto stack
glPushMatrix()

glRotatef(self.xRotation, 1, 0, 0)
glRotatef(self.yRotation, 0, 1, 0)

# Draw the six sides of the cube
glBegin(GL_QUADS)

# White
glColor3ub(255, 255, 255)
glVertex3f(50,50,50)

# Yellow
glColor3ub(255, 255, 0)
glVertex3f(50,50,50)

# Red
glColor3ub(255, 0, 0)
glVertex3f(50,50,50)
glVertex3f(50,50,50)

# Blue
glColor3f(0, 0, 1)
glVertex3f(50,50,50)

# <… more color defines for cube faces>

glEnd()

# Pop Matrix off stack
glPopMatrix()

def on_resize(self, width, height):
# set the Viewport
glViewport(0, 0, width, height)

# using Projection mode
glMatrixMode(GL_PROJECTION)
glLoadIdentity()

aspectRatio = width / height
gluPerspective(35, aspectRatio, 1, 1000)

glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
glTranslatef(0, 0, 400)

def on_text_motion(self, motion):
if motion == key.UP:
self.xRotation -= INCREMENT
elif motion == key.DOWN:
self.xRotation += INCREMENT
elif motion == key.LEFT:
self.yRotation -= INCREMENT
elif motion == key.RIGHT:
self.yRotation += INCREMENT

if __name__ == '__main__':
Window(WINDOW, WINDOW, 'Pyglet Colored Cube')
pyglet.app.run()

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_10_15.jpg

使用键盘方向键,我们可以旋转 3D 立方体。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_10_16.jpg

它是如何工作的…

在这个菜谱中,我们使用了 pyglet 来创建一个彩色的立方体,我们可以通过键盘的方向键在三维空间中旋转它。

我们为立方体的六个面定义了多种颜色,并使用 pyglet 创建了主窗口框架。

代码与本章中之前的配方类似,其中我们使用了 wxPython 库来创建一个立方体。这样做的原因是,在底层,wxPython 和 pyglet 都使用了 OpenGL 库。

使用 tkinter 创建幻灯片

在这个菜谱中,我们将使用纯 Python 创建一个不错的可工作幻灯片 GUI。

我们将看到 Python 核心内置函数的限制,然后我们将探索另一个名为 Pillow 的第三方模块,该模块扩展了 tkinter 内置的功能,特别是在图像处理方面。

虽然 Pillow 这个名字一开始听起来可能有点奇怪,但实际上它背后有着丰富的历史。

注意事项

本书仅使用 Python 3.4 及以上版本。

我们不会回到 Python 2。

吉多已经表达了他有意打破向后兼容性的决定,并决定 Python 3 是 Python 编程的未来。

对于 GUI 和图像,Python 2 的老版本有一个名为 PIL 的非常强大的模块,代表 Python Image Library。这个库包含大量功能,在 Python 3 非常成功创建后的几年里,这些功能尚未被翻译成 Python 3。

许多开发者仍然选择使用 Python 2 而不是按照 Python 的仁慈独裁者所设计的未来版本,因为 Python 2 仍然拥有更多的库可用。

这有点令人难过。

幸运的是,已经创建了一个新的图像库,它可以与 Python 3 一起使用,并且它被命名为 PIL 加上一些东西。

注意事项

Pillow 与 Python 2 PIL 库不兼容。

准备就绪

在本食谱的第一部分,我们将使用纯 Python。为了提高代码质量,我们将使用 pip 功能安装另一个 Python 模块。因此,尽管你很可能熟悉 pip,但了解如何使用它可能是有用的。

如何做到这一点…

首先,我们将使用纯 Python 创建一个工作界面,该界面可以在窗口框架内随机排列幻灯片。

这里是工作代码,以下是运行此代码的结果截图:

from tkinter import Tk, PhotoImage, Label
from itertools import cycle
from os import listdir

class SlideShow(Tk):
# inherit GUI framework extending tkinter
def __init__(self, msShowTimeBetweenSlides=1500):
# initialize tkinter super class
Tk.__init__(self)

# time each slide will be shown
self.showTime = msShowTimeBetweenSlides

# look for images in current working directory
listOfSlides = [slide for slide in listdir() if slide.endswith('gif')]

# cycle slides to show on the tkinter Label
self.iterableCycle = cycle((PhotoImage(file=slide), slide) for slide in listOfSlides)

# create tkinter Label widget which can display images
self.slidesLabel = Label(self)

# create the Frame widget
self.slidesLabel.pack()

def slidesCallback(self):
# get next slide from iterable cycle
currentInstance, nameOfSlide = next(self.iterableCycle)

# assign next slide to Label widget
self.slidesLabel.config(image=currentInstance)

# update Window title with current slide
self.title(nameOfSlide)

# recursively repeat the Show
self.after(self.showTime, self.slidesCallback)

#=================================
# Start GUI
#=================================
win = SlideShow()
win.after(0, win.slidesCallback())
win.mainloop()

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_10_21.jpg

这里是展开幻灯片中的另一个时刻。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_10_22.jpg

虽然幻灯片滑动确实令人印象深刻,但纯 Python tkinter GUIs 的内置功能不支持非常流行的.jpg格式,因此我们不得不求助于另一个 Python 库。

为了使用 Pillow,我们首先必须使用pip命令进行安装。

成功安装后的样子如下:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_10_23.jpg

Pillow 支持.jpg格式,为了使用它,我们不得不稍微改变我们的语法。

使用 Pillow 是一个高级主题,本书的这一版不会涉及。

它是如何工作的…

Python 是一个非常出色的工具,在这份食谱中,我们探讨了多种使用和扩展它的方法。

注意事项

当手指指向月亮时,那不是月亮本身,而只是一个指示者。

第十一章:最佳实践

在本章中,我们将探讨与我们的 Python 图形用户界面相关的最佳实践。

  • 避免意大利面式代码

  • 使用 __init__ 连接模块

  • 混合面向对象编程(OOP)和瀑布式开发

  • 使用代码命名约定

  • 何时不使用面向对象编程

  • 如何成功使用设计模式

  • 避免复杂性

简介

在本章中,我们将探讨不同的最佳实践,这些实践可以帮助我们以高效的方式构建我们的图形用户界面(GUI),并保持其可维护性和可扩展性。

这些最佳实践也将帮助我们调试我们的 GUI,使其达到我们想要的样子。

避免意大利面式代码

在这个菜谱中,我们将探讨创建 spaghetti 代码的典型方法,然后我们将看到一种避免此类代码的更好方法。

注意事项

意大利面代码是指功能相互交织在一起的代码。

准备就绪

我们将使用内置的 Python 库 tkinter 创建一个新且简单的 GUI。

如何做到这一点…

在网上搜索并阅读了文档之后,我们可能首先会编写以下代码来创建我们的图形用户界面:

# Spaghetti Code #############################
def PRINTME(me):print(me)
import tkinter
x=y=z=1
PRINTME(z)
from tkinter import *
scrolW=30;scrolH=6
win=tkinter.Tk()
if x:chVarUn=tkinter.IntVar()
from tkinter import ttk
WE='WE'
import tkinter.scrolledtext
outputFrame=tkinter.ttk.LabelFrame(win,text=' Type into the scrolled text control: ')
scr=tkinter.scrolledtext.ScrolledText(outputFrame,width=scrolW,height=scrolH,wrap=tkinter.WORD)
e='E'
scr.grid(column=1,row=1,sticky=WE)
outputFrame.grid(column=0,row=2,sticky=e,padx=8)
lFrame=None
if y:chck2=tkinter.Checkbutton(lFrame,text="Enabled",variable=chVarUn)
wE='WE'
if y==x:PRINTME(x)
lFrame=tkinter.ttk.LabelFrame(win,text="Spaghetti")
chck2.grid(column=1,row=4,sticky=tkinter.W,columnspan=3)
PRINTME(z)
lFrame.grid(column=0,row=0,sticky=wE,padx=10,pady=10)
chck2.select()
try: win.mainloop()
except:PRINTME(x)
chck2.deselect()
if y==x:PRINTME(x)
# End Pasta #############################

运行前面的代码将产生以下图形用户界面:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_01.jpg

这并不是我们想要的图形用户界面。我们希望它看起来更像是这样:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_02.jpg

虽然这段意大利面代码创建了一个图形用户界面,但由于代码中存在很多混乱,这段代码非常难以调试。

以下为生成所需图形用户界面的代码:

#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext

#======================
# Create instance
#======================
win = tk.Tk()

#======================
# Add a title
#======================
win.title("Python GUI")

#=========================
# Disable resizing the GUI
#=========================
win.resizable(0,0)

#=============================================================
# Adding a LabelFrame, Textbox (Entry) and Combobox
#=============================================================
lFrame = ttk.LabelFrame(win, text="Python GUI Programming Cookbook")
lFrame.grid(column=0, row=0, sticky='WE', padx=10, pady=10)

#=============================================================
# Using a scrolled Text control
#=============================================================
outputFrame = ttk.LabelFrame(win, text=' Type into the scrolled text control: ')
outputFrame.grid(column=0, row=2, sticky='E', padx=8)
scrolW = 30
scrolH = 6
scr = scrolledtext.ScrolledText(outputFrame, width=scrolW, height=scrolH, wrap=tk.WORD)
scr.grid(column=1, row=0, sticky='WE')

#=============================================================
# Creating a checkbutton
#=============================================================
chVarUn = tk.IntVar()
check2 = tk.Checkbutton(lFrame, text="Enabled", variable=chVarUn)
check2.deselect()
check2.grid(column=1, row=4, sticky=tk.W, columnspan=3)

#======================
# Start GUI
#======================
win.mainloop()

它是如何工作的…

在这个菜谱中,我们比较了面条代码和良好代码。良好代码相对于面条代码具有许多优势。

它有清晰的注释部分。

意大利面代码:

def PRINTME(me):print(me)
import tkinter
x=y=z=1
PRINTME(z)
from tkinter import *

好的代码:

#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk

它具有一种自然流动,遵循小部件在 GUI 主表单中的布局方式。

在面条代码中,底部的 LabelFrame 在顶部 LabelFrame 之前被创建,并且它与一个导入语句和一些小部件创建混合在一起。

意大利面代码:

import tkinter.scrolledtext
outputFrame=tkinter.ttk.LabelFrame(win,text=' Type into the scrolled text control: ')
scr=tkinter.scrolledtext.ScrolledText(outputFrame,width=scrolW,height=scrolH,wrap=tkinter.WORD)
e='E'
scr.grid(column=1,row=1,sticky=WE)
outputFrame.grid(column=0,row=2,sticky=e,padx=8)
lFrame=None
if y:chck2=tkinter.Checkbutton(lFrame,text="Enabled",variable=chVarUn)
wE='WE'
if y==x:PRINTME(x)
lFrame=tkinter.ttk.LabelFrame(win,text="Spaghetti")

好的代码:

#=============================================================
# Adding a LabelFrame, Textbox (Entry) and Combobox
#=============================================================
lFrame = ttk.LabelFrame(win, text="Python GUI Programming Cookbook")
lFrame.grid(column=0, row=0, sticky='WE', padx=10, pady=10)

#=============================================================
# Using a scrolled Text control
#=============================================================
outputFrame = ttk.LabelFrame(win, text=' Type into the scrolled text control: ')
outputFrame.grid(column=0, row=2, sticky='E', padx=8)

它不包含不必要的变量赋值,也没有一个print函数,这个函数并不能完成阅读代码时人们期望它所做的调试工作。

意大利面代码:

def PRINTME(me):print(me)
x=y=z=1
e='E'
WE='WE'
scr.grid(column=1,row=1,sticky=WE)
wE='WE'
if y==x:PRINTME(x)
lFrame.grid(column=0,row=0,sticky=wE,padx=10,pady=10)
PRINTME(z)
try: win.mainloop()
except:PRINTME(x)
chck2.deselect()
if y==x:PRINTME(x)

好的代码:

没有以上任何一种。

import 语句仅导入所需的模块。它们不会在代码中造成混乱。没有重复的 import 语句。没有 import * 语句。

意大利面代码:

import tkinter 1
x=y=z=1
PRINTME(z)
from tkinter import *
scrolW=30;scrolH=6
win=tkinter.Tk()
if x:chVarUn=tkinter.IntVar()
from tkinter import ttk
WE='WE'
import tkinter.scrolledtext

好的代码:

import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext

选择的变量名非常有意义。没有不必要的if语句使用数字1代替True。

意大利面代码:

x=y=z=1
if x:chVarUn=tkinter.IntVar()
wE='WE'

好的代码:

#=============================================================
# Using a scrolled Text control
#=============================================================
outputFrame = ttk.LabelFrame(win, text=' Type into the scrolled text control: ')
outputFrame.grid(column=0, row=2, sticky='E', padx=8)
scrolW = 30
scrolH = 6
scr = scrolledtext.ScrolledText(outputFrame, width=scrolW, height=scrolH, wrap=tk.WORD)
scr.grid(column=1, row=0, sticky='WE')

我们没有丢失预期的窗口标题,并且我们的复选按钮最终位于正确的位置。我们还使围绕复选按钮的LabelFrame可见。

意大利面代码:

我们丢失了窗口标题,并且没有显示顶部的LabelFrame。检查按钮最终出现在了错误的位置。

优秀的代码:

#======================
# Create instance
#======================
win = tk.Tk()

#======================
# Add a title
#======================
win.title("Python GUI")

#=============================================================
# Adding a LabelFrame, Textbox (Entry) and Combobox
#=============================================================
lFrame = ttk.LabelFrame(win, text="Python GUI Programming Cookbook")
lFrame.grid(column=0, row=0, sticky='WE', padx=10, pady=10)

#=============================================================
# Creating a checkbutton
#=============================================================
chVarUn = tk.IntVar()
check2 = tk.Checkbutton(lFrame, text="Enabled", variable=chVarUn)
check2.deselect()
check2.grid(column=1, row=4, sticky=tk.W, columnspan=3)

#======================
# Start GUI
#======================
win.mainloop()

使用 __init__ 连接模块

当我们使用 Eclipse IDE 的 PyDev 插件创建一个新的 Python 项目时,它会自动创建一个 __init__.py 模块。如果我们不使用 Eclipse,我们也可以手动创建它。

注意事项

__init__.py 模块通常为空,其大小为 0 千字节。

我们可以使用这个通常为空的模块,通过向其中输入代码来连接不同的 Python 模块。本食谱将展示如何进行这一操作。

准备就绪

我们将创建一个与之前菜谱中创建的类似的新的图形用户界面。

如何做到这一点…

随着我们的项目规模越来越大,我们自然会将其拆分成几个 Python 模块。使用像 Eclipse 这样的现代 IDE,查找位于不同子文件夹中、要么在需要导入它的代码之上要么之下的模块,竟然出奇地复杂。

克服这一限制的一个实用方法是使用__init__.py模块。

注意事项

在 Eclipse 中,我们可以将 Eclipse 内部项目环境设置为特定的文件夹,我们的 Python 代码将会找到它。但 outside of Eclipse,例如从命令窗口运行时,Python 模块导入机制有时会出现不匹配,导致代码无法运行。

这里是空的 __init__.py 模块的截图,当在 Eclipse 代码编辑器中打开时,它显示的不是 __init__ 这个名字,而是它所属的 PyDev 包的名字。“1” 位于代码编辑器的左侧是行号,而不是在这个模块中编写的任何代码。这个空的 __init__.py 模块中绝对没有任何代码。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_03.jpg

此文件为空,但它确实存在。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_04.jpg

当我们运行以下代码并点击clickMe 按钮,我们会得到代码下方显示的结果。这是一个尚未使用__init__.py模块的常规 Python 模块。

注意事项

__init__.py 模块与 Python 类的 __init__(self) 方法并不相同。

# Ch11_GUI__init.py
#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk

#======================
# Create instance
#======================
win = tk.Tk()

#======================
# Add a title
#======================
win.title("Python GUI")

#=============================================================
# Adding a LabelFrame and a Button
#=============================================================
lFrame = ttk.LabelFrame(win, text="Python GUI Programming Cookbook")
lFrame.grid(column=0, row=0, sticky='WE', padx=10, pady=10)

def clickMe():
from tkinter import messagebox
messagebox.showinfo('Message Box', 'Hi from same Level.')

button = ttk.Button(lFrame, text="Click Me ", command=clickMe)
button.grid(column=1, row=0, sticky=tk.S)

#======================
# Start GUI
#======================
win.mainloop()

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_05.jpg

在前面的代码中,我们创建了以下函数,该函数导入 Python 的消息框,然后使用它来显示消息框窗口:

def clickMe():
from tkinter import messagebox
messagebox.showinfo('Message Box', 'Hi from same Level.')

当我们将clickMe()消息框代码移动到嵌套的目录文件夹中,并尝试将其导入到我们的 GUI 模块时,我们遇到了一些问题。

我们在 Python 模块所在的下方创建了三个子文件夹。然后我们将clickMe()消息框代码放入了一个新的 Python 模块中,我们将其命名为MessageBox.py。这个模块位于Folder3中,距离我们的 Python 模块有三层。

我们需要导入这个MessageBox.py模块,以便使用该模块包含的clickMe()函数。

首先,它似乎可以正常工作,因为我们从 Eclipse IDE 没有收到任何错误或警告,看起来我们可以像导入其他模块一样导入这个新的嵌套模块。

我们正在使用 Python 的相对导入语法:

from .Folder1.Folder2.Folder3.MessageBox import clickme

这可以在以下屏幕截图中看到:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_06.jpg

我们已经删除了本地的clickMe()函数,现在我们的回调应该使用导入的clickMe()函数,但它并没有按预期工作。当我们运行代码时,没有得到预期的弹出窗口,而是出现了一个导入系统错误:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_07.jpg

我们可以通过访问 PyDev 项目的属性,并将我们自己的新函数所在的子文件夹添加为外部库,在 Eclipse 中将其添加为外部库。这看起来可能不太直观,但它确实有效。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_08.jpg

当我们现在注释掉文件夹结构,而是直接从嵌套了三层的模块中导入函数时,代码按预期工作。

#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk
# from .Folder1.Folder2.Folder3.MessageBox import clickMe
from MessageBox import clickMe

此函数在消息框中显示不同的文本:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_09.jpg

实现相同结果的一个更好的方法是使用 Python 内置的 __init__.py 模块。

删除了之前特定的 Eclipse 外部库依赖后,我们现在可以直接使用此模块。

注意事项

我们将代码放入此模块,如果我们在程序中导入__init__.py模块,那么在 Python 3.4.3 版本中,这段代码将先于我们其他所有代码执行。

忽略 PyDev 未解析导入(带叉号的红色圆圈)错误。这个 import 是必要的;它使我们的代码运行,并使整个 Python 导入机制正常工作。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_10.jpg

在将__init__.py模块导入到我们的程序之后,我们就可以使用它了。第一个测试它是否工作的是在这个模块中编写一个打印语句。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_11.jpg

通过添加以下代码,我们可以通过编程方式找出我们的位置:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_12.jpg

现在,我们可以在本地的__init__.py模块中初始化 Python 搜索路径,只需将以下代码添加到相同的__init__.py模块中:

print('hi from GUI init\\n')
from sys import path
from pprint import pprint
#=======================================================
# Required setup for the PYTONPATH in order to find
# all package folders
#=======================================================
from site import addsitedir
from os import getcwd, chdir, pardir
for _ in range(10):
curFull = getcwd()
curDir = curFull.split('\\\\')[1]
if 'B04829_Ch11_Code' == curDir:
addsitedir(curFull)
addsitedir(curFull + '\\\\Folder1\\\\Folder2\\\\Folder3\\\\')
break
chdir(pardir)
pprint(path)

当我们现在运行我们的 GUI 代码时,我们得到相同的预期窗口,但我们已经移除了对 Eclipse PYTHONPATH变量的依赖。

现在我们可以在 Eclipse PyDev 插件之外成功运行相同的代码。

注意

我们的代码变得更加 Pythonic。

它是如何工作的…

在这个菜谱中,我们发现了一个使用免费随 Eclipse IDE 提供的出色且免费的 PyDev 插件的局限性。

我们首先在 Eclipse IDE 中找到了一种解决方案,然后通过变得 Pythonic,我们摆脱了这一 IDE 的依赖。

注意事项

使用纯 Python 通常是最佳选择。

混合面向对象编程(OOP)和瀑布式开发

Python 是一种面向对象的编程语言,但并不总是使用面向对象编程(OOP)有意义。对于简单的脚本任务,传统的瀑布式编码风格仍然适用。

在这个菜谱中,我们将创建一个新的图形用户界面(GUI),它结合了传统的逐行编码风格和更现代的面向对象编程(OOP)风格。

我们将创建一个面向对象的类,当我们将鼠标悬停在用瀑布式创建的 Python GUI 中的小部件上时,它将显示一个工具提示。

注意事项

下降式和瀑布式编码风格是相同的。这意味着我们必须在物理上将代码放置在代码之上,然后才能从下面的代码中调用它。在这个范式下,当我们执行代码时,代码实际上是从程序顶部逐行下降到程序底部的。

准备就绪

在这个菜谱中,我们将使用 tkinter 创建一个 GUI,这与本书第一章中创建的 GUI 类似。

如何做到这一点…

在 Python 中,我们可以通过使用self关键字将函数绑定到类中,将其转换为方法。这是 Python 真正美妙的功能之一,它使我们能够创建可理解和可维护的大型系统。

有时候,当我们只编写简短的脚本时,面向对象编程(OOP)并不适用,因为我们发现自己需要在很多变量前加上self关键字,而当代码不需要这样大时,它就会变得不必要地庞大。

让我们先使用 tkinter 创建一个 Python GUI,并以瀑布式进行编码。

以下代码创建 GUI:

#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox

#======================
# Create instance
#======================
win = tk.Tk()

#======================
# Add a title
#======================
win.title("Python GUI")

#=========================
# Disable resizing the GUI
#=========================
win.resizable(0,0)

#=============================================================
# Adding a LabelFrame, Textbox (Entry) and Combobox
#=============================================================
lFrame = ttk.LabelFrame(win, text="Python GUI Programming Cookbook")
lFrame.grid(column=0, row=0, sticky='WE', padx=10, pady=10)

#=============================================================
# Labels
#=============================================================
ttk.Label(lFrame, text="Enter a name:").grid(column=0, row=0)
ttk.Label(lFrame, text="Choose a number:").grid(column=1, row=0, sticky=tk.W)

#=============================================================
# Buttons click command
#=============================================================
def clickMe(name, number):
messagebox.showinfo('Information Message Box', 'Hello '+name+
', your number is: ' + number)

#=============================================================
# Creating several controls in a loop
#=============================================================
names = ['name0', 'name1', 'name2']
nameEntries = ['nameEntry0', 'nameEntry1', 'nameEntry2']

numbers = ['number0', 'number1', 'number2']
numberEntries = ['numberEntry0', 'numberEntry1', 'numberEntry2']

buttons = []

for idx in range(3):
names[idx] = tk.StringVar()
nameEntries[idx] = ttk.Entry(lFrame, width=12, textvariable=names[idx])
nameEntries[idx].grid(column=0, row=idx+1)
nameEntries[idx].delete(0, tk.END)
nameEntries[idx].insert(0, '<name>')

numbers[idx] = tk.StringVar()
numberEntries[idx] = ttk.Combobox(lFrame, width=14, textvariable=numbers[idx])
numberEntries[idx]['values'] = (1+idx, 2+idx, 4+idx, 42+idx, 100+idx)
numberEntries[idx].grid(column=1, row=idx+1)
numberEntries[idx].current(0)

button = ttk.Button(lFrame, text="Click Me "+str(idx+1), command=lambda idx=idx: clickMe(names[idx].get(), numbers[idx].get()))
button.grid(column=2, row=idx+1, sticky=tk.W)
buttons.append(button)
#======================
# Start GUI
#======================
win.mainloop()

当我们运行代码时,我们会得到图形用户界面,它看起来是这样的:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_13.jpg

我们可以通过添加工具提示来改进我们的 Python 图形用户界面。实现这一点的最佳方式是将创建工具提示功能的代码从我们的 GUI 中分离出来。

我们通过创建一个具有提示功能的不同类来实现这一点,然后我们在创建我们 GUI 的同一 Python 模块中创建这个类的实例。

使用 Python,我们无需将我们的ToolTip类放入一个单独的模块中。我们只需将它放置在过程代码之上,然后从该代码下方调用它。

代码现在看起来是这样的:

#======================
# imports
#======================
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox

#———————————————–
class ToolTip(object):
def __init__(self, widget):
self.widget = widget
self.tipwindow = None
self.id = None
self.x = self.y = 0

#———————————————–
def createToolTip(widget, text):
toolTip = ToolTip(widget)
def enter(event): toolTip.showtip(text)
def leave(event): toolTip.hidetip()
widget.bind('<Enter>', enter)
widget.bind('<Leave>', leave)

#———————————————–
# further down the module we call the createToolTip function
#———————————————–

for idx in range(3):
names[idx] = tk.StringVar()
nameEntries[idx] = ttk.Entry(
lFrame, width=12, textvariable=names[idx])
nameEntries[idx].grid(column=0, row=idx+1)
nameEntries[idx].delete(0, tk.END)
nameEntries[idx].insert(0, '<name>')

numbers[idx] = tk.StringVar()
numberEntries[idx] = ttk.Combobox(
lFrame, width=14, textvariable=numbers[idx])
numberEntries[idx]['values'] = (
1+idx, 2+idx, 4+idx, 42+idx, 100+idx)
numberEntries[idx].grid(column=1, row=idx+1)
numberEntries[idx].current(0)

button = ttk.Button(
lFrame, text="Click Me "+str(idx+1), command=lambda idx=idx: clickMe(names[idx].get(), numbers[idx].get()))
button.grid(column=2, row=idx+1, sticky=tk.W)
buttons.append(button)

#———————————————–
# Add Tooltips to more widgets
createToolTip(nameEntries[idx], 'This is an Entry widget.')
createToolTip(
numberEntries[idx], 'This is a DropDown widget.')
createToolTip(buttons[idx], 'This is a Button widget.')
#———————————————–

运行代码会在我们鼠标悬停在组件上时为我们的组件创建工具提示。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_14.jpg

它是如何工作的…

在这个菜谱中,我们以过程式的方式创建了一个 Python GUI,后来,我们在模块顶部添加了一个类。

我们可以非常容易地在同一个 Python 模块中混合使用过程式和面向对象编程。

使用代码命名约定

本书之前提供的食谱尚未使用结构化的代码命名约定。本食谱将向您展示遵循代码命名方案的价值,因为它有助于我们找到想要扩展的代码,同时也提醒我们关于程序设计的考虑。

准备就绪

在这个菜谱中,我们将查看本书第一章中的 Python 模块名称,并将它们与更好的命名约定进行比较。

如何做到这一点…

在本书的第一章中,我们创建了我们的第一个 Python 图形用户界面。我们通过递增不同的代码模块名称的序号来改进我们的 GUI。

它看起来是这样的:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_15.jpg

虽然这是一种典型的编码方式,但它并没有提供太多意义。当我们开发时编写 Python 代码,很容易增加数字。

后来,当我们回到这段代码时,我们并不清楚哪个 Python 模块提供了哪些功能,有时,我们最近更新的模块并不如早期版本好。

注意

明确的命名规范确实有帮助。

我们可以将第一章中的模块名称创建 GUI 表单和添加小部件与第八章中的名称国际化与测试进行比较,后者要更有意义。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_16.jpg

虽然并不完美,但为不同的 Python 模块选定的名称表明了每个模块的责任所在。当我们想要添加更多单元测试时,可以清楚地知道它们位于哪个模块中。

以下是如何使用代码命名规范在 Python 中创建 GUI 的另一个示例:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_17.jpg

注意

将单词 PRODUCT 替换为你目前正在工作的产品名称。

整个应用程序是一个图形用户界面。所有部分都是相互连接的。DEBUG.py模块仅用于调试我们的代码。与所有其他模块相比,调用 GUI 的主要函数名称是反向的。它以Gui开头,以.pyw扩展名结尾。

这是唯一具有此扩展名的 Python 模块。

从这个命名规范来看,如果你对 Python 足够熟悉,那么很明显,为了运行这个图形用户界面,你需要双击 Gui_PRODUCT.pyw 模块。

所有其他 Python 模块都包含向 GUI 提供功能以及执行底层业务逻辑以实现 GUI 所针对目的的功能。

它是如何工作的…

Python 代码模块的命名规范对我们保持高效和记住原始设计非常有帮助。当我们需要调试和修复缺陷或添加新功能时,它们是我们首先查阅的资源。

注意事项

通过数字递增模块名称并不十分有意义,最终还会浪费开发时间。

另一方面,Python 变量的命名更偏向自由形式。Python 会推断类型,因此我们不必指定一个变量将是 <list> 类型(它可能不是,或者实际上,在代码的后续部分,它可能变成另一种类型)。

变量命名的良好建议是使它们具有描述性,同时也不宜过度缩写。

如果我们想要指出某个变量被设计为类型<list>,那么使用完整的单词list而不是lst会更加直观。

对于number和num来说,情况类似。

虽然给变量起非常描述性的名字是个好主意,但有时名字可能会变得过长。在苹果的 Objective-C 语言中,一些变量和函数的名字非常极端:thisIsAMethodThatDoesThisAndThatAndAlsoThatIfYouPassInNIntegers:1:2:3

注意

在命名变量、方法和函数时,请使用常识。

何时不使用面向对象编程(OOP)

Python 自带面向对象编程的能力,但与此同时,我们也可以编写不需要使用 OOP 的脚本。

对于某些任务,面向对象编程(OOP)并不适用。

这个菜谱将展示何时不使用面向对象编程(OOP)。

准备就绪

在这个菜谱中,我们将创建一个类似于之前菜谱的 Python GUI。我们将比较面向对象(OOP)代码与非面向对象编程的替代方式。

如何做到这一点…

让我们先使用面向对象的方法创建一个新的 GUI。下面的代码将创建显示在代码下面的 GUI:

import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
from tkinter import Menu

class OOP():
def __init__(self):
self.win = tk.Tk()
self.win.title("Python GUI")
self.createWidgets()

def createWidgets(self):
tabControl = ttk.Notebook(self.win)
tab1 = ttk.Frame(tabControl)
tabControl.add(tab1, text='Tab 1')
tabControl.pack(expand=1, fill="both")
self.monty = ttk.LabelFrame(tab1, text=' Monty Python ')
self.monty.grid(column=0, row=0, padx=8, pady=4)

ttk.Label(self.monty, text="Enter a name:").grid(
column=0, row=0, sticky='W')
self.name = tk.StringVar()
nameEntered = ttk.Entry(
self.monty, width=12, textvariable=self.name)
nameEntered.grid(column=0, row=1, sticky='W')

self.action = ttk.Button(self.monty, text="Click Me!")
self.action.grid(column=2, row=1)

ttk.Label(self.monty,
text="Choose a number:").grid(column=1, row=0)
number = tk.StringVar()
numberChosen = ttk.Combobox(self.monty,
width=12, textvariable=number)
numberChosen['values'] = (42)
numberChosen.grid(column=1, row=1)
numberChosen.current(0)

scrolW = 30; scrolH = 3
self.scr = scrolledtext.ScrolledText(
self.monty, width=scrolW, height=scrolH, wrap=tk.WORD)
self.scr.grid(column=0, row=3, sticky='WE', columnspan=3)

menuBar = Menu(tab1)
self.win.config(menu=menuBar)
fileMenu = Menu(menuBar, tearoff=0)
menuBar.add_cascade(label="File", menu=fileMenu)
helpMenu = Menu(menuBar, tearoff=0)
menuBar.add_cascade(label="Help", menu=helpMenu)

nameEntered.focus()
#==========================
oop = OOP()
oop.win.mainloop()

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_18.jpg

我们可以通过稍微重构我们的代码,在不使用面向对象编程(OOP)方法的情况下实现相同的图形用户界面(GUI)。首先,我们移除OOP类及其__init__方法。

接下来,我们将所有方法移动到左侧,并移除self类引用,这样它们就变成了未绑定函数。

我们还移除了之前代码中所有的self引用。然后,我们将createWidgets函数调用移至函数声明之下。我们将其放置在mainloop调用之上。

最后,我们实现了相同的 GUI,但没有使用面向对象编程(OOP)。

重构后的代码如下所示:

import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
from tkinter import Menu

def createWidgets():
tabControl = ttk.Notebook(win)
tab1 = ttk.Frame(tabControl)
tabControl.add(tab1, text='Tab 1')
tabControl.pack(expand=1, fill="both")
monty = ttk.LabelFrame(tab1, text=' Monty Python ')
monty.grid(column=0, row=0, padx=8, pady=4)

ttk.Label(monty, text="Enter a name:").grid(
column=0, row=0, sticky='W')
name = tk.StringVar()
nameEntered = ttk.Entry(monty, width=12, textvariable=name)
nameEntered.grid(column=0, row=1, sticky='W')

action = ttk.Button(monty, text="Click Me!")
action.grid(column=2, row=1)

ttk.Label(monty, text="Choose a number:").grid(
column=1, row=0)
number = tk.StringVar()
numberChosen = ttk.Combobox(
monty, width=12, textvariable=number)
numberChosen['values'] = (42)
numberChosen.grid(column=1, row=1)
numberChosen.current(0)

scrolW = 30; scrolH = 3
scr = scrolledtext.ScrolledText(
monty, width=scrolW, height=scrolH, wrap=tk.WORD)
scr.grid(column=0, row=3, sticky='WE', columnspan=3)

menuBar = Menu(tab1)
win.config(menu=menuBar)
fileMenu = Menu(menuBar, tearoff=0)
menuBar.add_cascade(label="File", menu=fileMenu)
helpMenu = Menu(menuBar, tearoff=0)
menuBar.add_cascade(label="Help", menu=helpMenu)

nameEntered.focus()
#======================
win = tk.Tk()
win.title("Python GUI")
createWidgets()
win.mainloop()

它是如何工作的…

Python 使我们能够在合理的情况下使用面向对象编程(OOP)。其他语言如 Java 和 C#则强制我们始终使用面向对象编程的编码方法。在本食谱中,我们探讨了一种不适合使用面向对象编程的情况。

注意事项

如果代码库不断增长,面向对象(OOP)方法将更具可扩展性,但如果确定只需要这一种代码,那么就没有必要采用面向对象(OOP)方法。

如何成功使用设计模式

在这个菜谱中,我们将通过使用工厂设计模式来创建我们的 Python GUI 小部件。

在之前的菜谱中,我们手动逐个创建小部件,或者通过循环动态创建。

使用工厂设计模式,我们将使用工厂来创建我们的小部件。

准备就绪

我们将创建一个具有三个按钮的 Python 图形用户界面,每个按钮都有不同的样式。

如何做到这一点…

在我们的 Python GUI 模块顶部,紧接在导入语句下方,我们创建了几个类:

import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
from tkinter import Menu

class ButtonFactory():
def createButton(self, type_):
return buttonTypes[type_]()

class ButtonBase():
relief ='flat'
foreground ='white'
def getButtonConfig(self):
return self.relief, self.foreground

class ButtonRidge(ButtonBase):
relief ='ridge'
foreground ='red'

class ButtonSunken(ButtonBase):
relief ='sunken'
foreground ='blue'

class ButtonGroove(ButtonBase):
relief ='groove'
foreground ='green'

buttonTypes = [ButtonRidge, ButtonSunken, ButtonGroove]

class OOP():
def __init__(self):
self.win = tk.Tk()
self.win.title("Python GUI")
self.createWidgets()

我们创建了一个基类,我们的不同按钮样式类都继承自这个基类,并且每个子类都覆盖了relief和foreground配置属性。所有子类都从该基类继承getButtonConfig方法。此方法返回一个元组。

我们还创建了一个按钮工厂类和一个列表,用于存储我们按钮子类的名称。我们将这个列表命名为buttonTypes,因为我们的工厂将创建不同类型的按钮。

在模块的下方,我们使用相同的buttonTypes列表创建按钮小部件。

def createButtons(self):

factory = ButtonFactory()

# Button 1
rel = factory.createButton(0).getButtonConfig()[0]
fg = factory.createButton(0).getButtonConfig()[1]
action = tk.Button(self.monty,
text="Button "+str(0+1), relief=rel, foreground=fg)
action.grid(column=0, row=1)

# Button 2
rel = factory.createButton(1).getButtonConfig()[0]
fg = factory.createButton(1).getButtonConfig()[1]
action = tk.Button(self.monty,
text="Button "+str(1+1), relief=rel, foreground=fg)
action.grid(column=1, row=1)

# Button 3
rel = factory.createButton(2).getButtonConfig()[0]
fg = factory.createButton(2).getButtonConfig()[1]
action = tk.Button(self.monty,
text="Button "+str(2+1), relief=rel, foreground=fg)
action.grid(column=2, row=1)

首先,我们创建一个按钮工厂的实例,然后我们使用我们的工厂来创建我们的按钮。

注意事项

buttonTypes 列表中的项是我们子类的名称。

我们调用createButton方法,然后立即调用基类的getButtonConfig方法,并使用点符号获取配置属性。

当我们运行整个代码时,我们得到以下 Python tkinter 图形用户界面:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_19.jpg

我们可以看到,我们的 Python GUI 工厂确实创建了不同的按钮,每个按钮都有不同的样式。它们在文字颜色和浮雕属性上有所不同。

它是如何工作的…

在这个示例中,我们使用了工厂设计模式来创建具有不同样式的几个小部件。我们可以轻松地使用这种设计模式来创建整个 GUI。

设计模式是我们软件开发工具箱中一个非常激动人心的工具。

避免复杂性

在这个菜谱中,我们将扩展我们的 Python 图形用户界面,并学习处理软件开发工作中不断增长的复杂性的方法。

我们的合作者和客户都喜欢我们用 Python 创建的图形用户界面,并不断要求添加更多功能到我们的 GUI 中。

这增加了复杂性,并且很容易毁掉我们原本良好的设计。

准备就绪

我们将创建一个新的 Python GUI,类似于之前食谱中的那些,并且将以小部件的形式添加许多新功能。

如何做到这一点…

我们将从一个具有两个标签页的 Python 图形用户界面开始,其外观如下:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_20.jpg

我们收到的第一个新功能请求是向Tab 1添加功能,该功能清除scrolledtext小部件。

简单得很。我们只需在标签 1中添加另一个按钮。

# Adding another Button
self.action = ttk.Button(.
self.monty, text="Clear Text", command=self.clearScrol)
self.action.grid(column=2, row=2)

我们还必须创建回调方法以添加所需的功能,我们将在类的顶部定义该方法,并在创建我们的小部件的方法之外定义它。

# Button callback
def clickMe(self):
self.action.configure(text='Hello ' + self.name.get())

# Button callback Clear Text
def clearScrol(self):
self.scr.delete('1.0', tk.END)

现在我们的 GUI 有一个新的按钮,当我们点击它时,我们将ScrolledText小部件的文本清空。

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_21.jpg

为了添加这个功能,我们不得不在同一个 Python 模块的两个地方添加代码。

我们在createWidgets方法(未显示)中插入了新的按钮,然后创建了一个新的回调方法,当我们的新按钮被点击时,它会调用这个方法。我们将这段代码放置在第一个按钮的回调方法下方。

我们下一个功能请求是添加更多功能。业务逻辑封装在另一个 Python 模块中。我们通过在Tab 1中添加三个更多按钮来调用这个新功能。我们使用循环来完成这个操作。

# Adding more Feature Buttons
for idx in range(3):
b = ttk.Button(
self.monty, text="Feature" + str(idx+1))
b.grid(column=idx, row=4)

我们的用户界面现在看起来是这样的:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_22.jpg

接下来,我们的客户要求更多功能,我们采用同样的方法。我们的图形用户界面现在看起来如下:

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_23.jpg

注意

这还不错。当我们收到关于另外 50 个新功能的新的功能请求时,我们开始怀疑我们的方法是否仍然是最佳使用方法……

处理我们 GUI 必须处理的日益增加的复杂性的一个方法是通过添加标签页。通过添加更多标签页并将相关功能放入各自的标签页中,我们能够掌控复杂性,并使我们的 GUI 更加直观。

这里是创建我们新的Tab 3的代码,在其下方是我们的新 Python GUI:

# Tab Control 3 —————————————–
tab3 = ttk.Frame(tabControl) # Add a tab
tabControl.add(tab3, text='Tab 3') # Make tab visible

monty3 = ttk.LabelFrame(tab3, text=' New Features ')
monty3.grid(column=0, row=0, padx=8, pady=4)

# Adding more Feature Buttons
startRow = 4
for idx in range(24):
if idx < 2:
colIdx = idx
col = colIdx
else:
col += 1
if not idx % 3:
startRow += 1
col = 0

b = ttk.Button(monty3, text="Feature " + str(idx+1))
b.grid(column=col, row=startRow)

# Add some space around each label
for child in monty3.winfo_children():
child.grid_configure(padx=8)

https://github.com/OpenDocCN/freelearn-python-zh/raw/master/docs/qt5-py-gui-prog-cb/img/B04829_11_24.jpg

它是如何工作的…

在这个菜谱中,我们向我们的 GUI 添加了几个新的小部件,以便为我们的 Python GUI 添加更多功能。我们看到了随着越来越多的新功能请求,我们的精美 GUI 设计很容易陷入一种状态,使得如何使用 GUI 变得不那么清晰。

注意事项

突然,小玩意儿们统治了世界…

我们看到了如何通过模块化我们的 GUI 来处理复杂性,即将大型功能拆分成更小的部分,并使用标签将它们安排在功能相关区域。

虽然复杂性有许多方面,但模块化和重构代码通常是处理软件代码复杂性的一个非常有效的方法。

注意事项

在编程中,有时我们会遇到一道墙,陷入困境。我们不断地撞击这堵墙,但没有任何进展。

有时候我们感觉好像想要放弃。

然而,奇迹确实会发生……

如果我们持续不断地撞击这堵墙,在某个时刻,墙将会倒塌,道路就会畅通。

在那个时刻,我们可以在软件宇宙中留下积极的印记。

赞(0)
未经允许不得转载:171主机测评 » QT5 Python GUI 编程秘籍(三)
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址