第三章:使用 Python 进行自动化和提高生产力
在本章中,我们将涵盖以下主题:
- 使用 Tkinter 创建图形用户界面
- 创建一个图形启动菜单应用程序
- 在应用程序中显示照片信息
- 自动整理您的照片
介绍
到目前为止,我们只专注于命令行应用程序;然而,树莓派不仅仅是命令行。通过使用图形用户界面(GUI),通常更容易从用户那里获取输入并以更简单的方式提供反馈。毕竟,我们一直在不断处理多个输入和输出,所以为什么在不必要的情况下限制自己只使用命令行的程序格式呢?
幸运的是,Python 可以支持这一点。与其他编程语言(如 Visual Basic 和 C/C++/C#)类似,这可以通过使用提供标准控件的预构建对象来实现。我们将使用一个名为Tkinter的模块,它提供了一系列良好的控件(也称为小部件)和工具,用于创建图形应用程序。
首先,我们将以encryptdecrypt.py
为例,演示可以编写和在各种方式中重复使用的有用模块。这是良好编码实践的一个例子。我们应该致力于编写可以进行彻底测试,然后在许多地方重复使用的代码。
接下来,我们将通过创建一个小型图形启动菜单应用程序来扩展我们之前的示例,以运行我们喜爱的应用程序。
然后,我们将探索在我们的应用程序中使用类来显示,然后
整理照片。
使用 Tkinter 创建图形用户界面
我们将创建一个基本的 GUI,允许用户输入信息,然后程序可以用来加密和解密它。
准备工作
您必须确保该文件放置在相同的目录中。
由于我们使用了 Tkinter(Python 的许多可用附加组件之一),我们需要确保它已安装。它应该默认安装在标准的 Raspbian 镜像上。我们可以通过从 Python 提示符导入它来确认它已安装,如下所示:
Python3
>>> import tkinter
如果未安装,将引发ImportError
异常,在这种情况下,您可以使用以下命令进行安装(使用Ctrl + Z退出 Python 提示符):
sudo apt-get install python3-tk
如果模块加载了,您可以使用以下命令来阅读有关模块的更多信息(完成阅读后使用Q退出):
>>>help(tkinter)
您还可以使用以下命令获取有关模块内所有类、函数和方法的信息:
>>>help(tkinter.Button)
以下dir
命令将列出在module
范围内的任何有效命令或变量:
>>>dir(tkinter.Button)
您将看到我们自己的模块将包含由三个引号标记的函数的信息;如果我们使用help
命令,这将显示出来。
命令行将无法显示本章中创建的图形显示,因此您将需要启动树莓派桌面(使用startx
命令),或者如果您是远程使用它。
确保您已启用X11 转发并且运行着X 服务器(参见第一章,使用树莓派 3 计算机入门)。
如何做…
我们将使用tkinter
模块为encryptdecrypt.py
脚本生成 GUI。
为了生成 GUI,我们将创建以下tkencryptdecrypt.py
脚本:
#!/usr/bin/python3 #tkencryptdecrypt.py import encryptdecrypt as ENC import tkinter as TK def encryptButton(): encryptvalue.set(ENC.encryptText(encryptvalue.get(), keyvalue.get())) def decryptButton(): encryptvalue.set(ENC.encryptText(encryptvalue.get(), -keyvalue.get())) #Define Tkinter application root=TK.Tk() root.title("Encrypt/Decrypt GUI") #Set control & test value encryptvalue = TK.StringVar() encryptvalue.set("My Message") keyvalue = TK.IntVar() keyvalue.set(20) prompt="Enter message to encrypt:" key="Key:" label1=TK.Label(root,text=prompt,width=len(prompt),bg='green') textEnter=TK.Entry(root,textvariable=encryptvalue, width=len(prompt)) encryptButton=TK.Button(root,text="Encrypt",command=encryptButton) decryptButton=TK.Button(root,text="Decrypt",command=decryptButton) label2=TK.Label(root,text=key,width=len(key)) keyEnter=TK.Entry(root,textvariable=keyvalue,width=8) #Set layout label1.grid(row=0,columnspan=2,sticky=TK.E+TK.W) textEnter.grid(row=1,columnspan=2,sticky=TK.E+TK.W) encryptButton.grid(row=2,column=0,sticky=TK.E) decryptButton.grid(row=2,column=1,sticky=TK.W) label2.grid(row=3,column=0,sticky=TK.E) keyEnter.grid(row=3,column=1,sticky=TK.W) TK.mainloop() #End
使用以下命令运行脚本:
python3 tkencryptdecrypt
它是如何工作的…
我们首先导入两个模块;第一个是我们自己的encryptdecrypt
模块,第二个是tkinter
模块。为了更容易看到哪些项目来自哪里,我们使用ENC
/TK
。如果您想避免额外的引用,您可以使用from import *
直接引用模块项目。
当我们点击加密和解密按钮时,将调用encryptButton()
和decryptButton()
函数;它们将在以下部分中解释。
使用Tk()
命令创建主 Tkinter 窗口,该命令返回所有小部件/控件可以放置的主窗口。
我们将定义六个控件如下:
Label
:这显示了加密消息的提示输入信息:Entry
:这提供了一个文本框来接收用户要加密的消息Button
:这是一个加密按钮,用于触发要加密的消息Button
:这是一个解密按钮,用于反转加密Label
:这显示了密钥:字段以提示用户输入加密密钥值Entry
:这提供了第二个文本框来接收加密密钥的值
这些控件将产生一个类似于以下截图所示的 GUI:
加密/解密消息的 GUI
让我们来看一下第一个label1
的定义:
label1=TK.Label(root,text=prompt,width=len(prompt),bg='green')
所有控件必须链接到应用程序窗口;因此,我们必须指定我们的 Tkinter 窗口root
。标签使用的文本由text
设置;在这种情况下,我们将其设置为一个名为prompt
的字符串,该字符串已经在之前定义了我们需要的文本。我们还设置width
以匹配消息的字符数(虽然不是必需的,但如果我们稍后向标签添加更多文本,它会提供更整洁的结果),最后,我们使用bg='green'
设置背景颜色。
接下来,我们为我们的消息定义文本Entry
框:
textEnter=TK.Entry(root,textvariable=encryptvalue, width=len(prompt))
我们将定义textvariable
——将一个变量链接到框的内容的一种有用的方式,这是一个特殊的字符串变量。我们可以直接使用textEnter.get()
访问text
,但我们将使用一个Tkinter StringVar()
对象来间接访问它。如果需要,这将允许我们将正在处理的数据与处理 GUI 布局的代码分开。enycrptvalue
变量在使用.set()
命令时会自动更新它所链接到的Entry
小部件(并且.get()
命令会从Entry
小部件获取最新的值)。
接下来,我们有两个Button
小部件,加密和解密,如下所示:
encryptButton=TK.Button(root,text="Encrypt",command=encryptButton) decryptButton=TK.Button(root,text="Decrypt",command=decryptButton)
在这种情况下,我们可以设置一个函数,当点击Button
小部件时调用该函数,方法是设置command
属性。我们可以定义两个函数,当每个按钮被点击时将被调用。在以下代码片段中,我们有encryptButton()
函数,它将设置控制第一个Entry
框内容的encryptvalue StringVar
。这个字符串被设置为我们通过调用ENC.encryptText()
得到的结果,我们要加密的消息(encryptvalue
的当前值)和keyvalue
变量。decrypt()
函数完全相同,只是我们将keyvalue
变量设置为负数以解密消息:
def encryptButton(): encryptvalue.set(ENC.encryptText(encryptvalue.get(), keyvalue.get()))
然后我们以类似的方式设置最终的Label
和Entry
小部件。请注意,如果需要,textvariable
也可以是整数(数值),但没有内置检查来确保只能输入数字。当使用.get()
命令时,会遇到ValueError
异常。
在我们定义了 Tkinter 窗口中要使用的所有小部件之后,我们必须设置布局。在 Tkinter 中有三种定义布局的方法:place、pack和grid。
place 布局允许我们使用精确的像素位置指定位置和大小。pack 布局按照它们被添加的顺序将项目放置在窗口中。grid 布局允许我们以特定的布局放置项目。建议尽量避免使用 place 布局,因为对一个项目进行任何小的更改都可能对窗口中所有其他项目的位置和大小产生连锁效应;其他布局通过确定它们相对于窗口中其他项目的位置来解决这个问题。
我们将按照以下截图中的布局放置这些项目:
加密/解密 GUI 的网格布局
使用以下代码设置 GUI 中前两个项目的位置:
label1.grid(row=0,columnspan=2,sticky= TK.E+TK.W) textEnter.grid(row=1,columnspan=2,sticky= TK.E+TK.W)
我们可以指定第一个Label
和Entry
框将跨越两列(columnspan=2
),并且我们可以设置粘性值以确保它们跨越整个宽度。这是通过设置TK.E
表示东边和TK.W
表示西边来实现的。如果需要在垂直方向上做同样的操作,我们会使用TK.N
表示北边和TK.S
表示南边。如果未指定column
值,grid
函数会默认为column=0
。其他项目也是类似定义的。
最后一步是调用TK.mainloop()
,这允许 Tkinter 运行;这允许监视按钮点击并调用与它们链接的函数。
创建图形应用程序-开始菜单
本示例显示了如何定义我们自己的 Tkinter 对象的变体,以生成自定义控件并动态构建菜单。我们还将简要介绍使用线程来允许其他任务继续运行,同时执行特定任务。
准备工作
要查看 GUI 显示,您需要一个显示树莓派桌面的显示器,或者您需要连接到另一台运行 X 服务器的计算机。
如何做…
- 要创建图形开始菜单应用程序,请创建以下
graphicmenu.py
脚本:
#!/usr/bin/python3 # graphicmenu.py import tkinter as tk from subprocess import call import threading #Define applications ["Display name","command"] leafpad = ["Leafpad","leafpad"] scratch = ["Scratch","scratch"] pistore = ["Pi Store","pistore"] app_list = [leafpad,scratch,pistore] APP_NAME = 0 APP_CMD = 1 class runApplictionThread(threading.Thread): def __init__(self,app_cmd): threading.Thread.__init__(self) self.cmd = app_cmd def run(self): #Run the command, if valid try: call(self.cmd) except: print ("Unable to run: %s" % self.cmd) class appButtons: def __init__(self,gui,app_index): #Add the buttons to window btn = tk.Button(gui, text=app_list[app_index][APP_NAME], width=30, command=self.startApp) btn.pack() self.app_cmd=app_list[app_index][APP_CMD] def startApp(self): print ("APP_CMD: %s" % self.app_cmd) runApplictionThread(self.app_cmd).start() root = tk.Tk() root.title("App Menu") prompt = ' Select an application ' label1 = tk.Label(root, text=prompt, width=len(prompt), bg='green') label1.pack() #Create menu buttons from app_list for index, app in enumerate(app_list): appButtons(root,index) #Run the tk window root.mainloop() #End
- 上面的代码产生了以下应用程序:
应用程序菜单 GUI
它是如何工作的…
我们创建 Tkinter 窗口与之前一样;但是,我们不是单独定义所有项目,而是为应用程序按钮创建一个特殊的类。
我们创建的类充当了appButtons
项目要包含的蓝图或规范。每个项目将包括一个app_cmd
的字符串值,一个名为startApp()
的函数和一个__init__()
函数。__init__()
函数是一个特殊函数(称为构造函数),当我们创建一个appButtons
项目时会调用它;它将允许我们创建任何所需的设置。
在这种情况下,__init__()
函数允许我们创建一个新的 Tkinter 按钮,其中文本设置为app_list
中的一个项目,当点击按钮时调用startApp()
函数。使用self
关键字是为了调用属于该项目的命令;这意味着每个按钮将调用一个具有访问该项目的本地数据的本地定义函数。
我们将self.app_cmd
的值设置为app_list
中的命令,并通过startApp()
函数准备好使用。现在我们创建startApp()
函数。如果我们直接在这里运行应用程序命令,Tkinter 窗口将会冻结,直到我们打开的应用程序再次关闭。为了避免这种情况,我们可以使用 Python 线程模块,它允许我们同时执行多个操作。
runApplicationThread()
类是使用threading.Thread
类作为模板创建的——这个类继承了threading.Thread
类的所有特性。和之前的类一样,我们也为这个类提供了__init__()
函数。我们首先调用继承类的__init__()
函数以确保它被正确设置,然后我们将app_cmd
的值存储在self.cmd
中。创建并初始化runApplicationThread()
函数后,调用start()
函数。这个函数是threading.Thread
的一部分,我们的类可以使用它。当调用start()
函数时,它将创建一个单独的应用程序线程(也就是说,模拟同时运行两个任务),允许 Tkinter 在执行类中的run()
函数时继续监视按钮点击。
因此,我们可以将代码放在run()
函数中来运行所需的应用程序(使用call(self.cmd)
)。
还有更多…
使 Python 特别强大的一个方面是它支持面向对象设计(OOD)中使用的编程技术。这是现代编程语言常用的一种技术,用来帮助将我们希望程序执行的任务转化为代码中有意义的构造和结构。OOD 的原则在于,我们认为大多数问题都由几个对象(GUI 窗口、按钮等)组成,它们相互交互以产生期望的结果。
在前一节中,我们发现可以使用类来创建可以多次重复使用的唯一对象。我们创建了一个appButton
类,它生成了一个具有该类所有功能的对象,包括其自己的app_cmd
版本,该版本将被startApp()
函数使用。appButton
类型的另一个对象将有其自己不相关的[app_cmd]
数据,其startApp()
函数将使用它。
你可以看到,类对于将一组相关的变量和函数集中在一个对象中非常有用,而且类将在一个地方保存它自己的数据。拥有同一类型(类)的多个对象,每个对象内部都有自己的函数和数据,会导致更好的程序结构。传统的方法是将所有信息保存在一个地方,然后来回发送每个项目以供各种函数处理;然而,在大型系统中,这可能变得繁琐。
下图显示了相关函数和数据的组织结构:
数据和函数
到目前为止,我们已经使用 Python 模块将程序的不同部分分开。
文件;这使我们能够在概念上将程序的不同部分分开(界面、编码器/解码器或类库,比如 Tkinter)。模块可以提供控制特定硬件的代码,定义互联网接口,或提供常用功能的类库;然而,它最重要的功能是控制接口(在导入项目时可用的函数、变量和类的集合)。一个良好实现的模块应该有一个清晰的接口,其重点是围绕它的使用方式,而不是它的实现方式。这使你能够创建多个可以轻松交换和更改的模块,因为它们共享相同的接口。在我们之前的例子中,想象一下,通过支持encryptText(input_text,key)
,要将encryptdecrypt
模块更改为另一个模块是多么容易。复杂的功能可以分解成更小、可管理的块,可以在多个应用程序中重复使用。
Python 一直在使用类和模块。每次你导入一个库,比如sys
或 Tkinter,或者使用value.str()
转换一个值,或者使用for...in
遍历一个列表,你都可以在不用担心细节的情况下使用它们。你不必在你写的每一行代码中都使用类或模块,但它们是你程序员工具箱中有用的工具,适合你正在做的事情时使用。
通过在本书的示例中使用类和模块,我们将了解它们如何使我们能够生成结构良好、易于测试和维护的代码。
在应用程序中显示照片信息
在这个例子中,我们将创建一个实用类来处理照片,其他应用程序(作为模块)可以使用它来访问照片元数据并轻松显示预览图像。
准备就绪
以下脚本使用了Python Image Library(PIL);Python 3 的兼容版本是Pillow。
Pillow 没有包含在 Raspbian 仓库中(由apt-get
使用);因此,我们需要使用名为PIP的Python 包管理器来安装 Pillow。
要为 Python 3 安装包,我们将使用 Python 3 版本的 PIP(这需要 50MB 的可用空间)。
以下命令可用于安装 PIP:
sudo apt-get update sudo apt-get install python3-pip
在使用 PIP 之前,请确保已安装libjpeg-dev
以允许 Pillow 处理 JPEG 文件。您可以使用以下命令执行此操作:
sudo apt-get install libjpeg-dev
现在您可以使用以下 PIP 命令安装 Pillow:
sudo pip-3.2 install pillow
PIP 还可以通过使用uninstall
而不是install
来轻松卸载软件包。
最后,您可以通过运行python3
来确认它已成功安装:
>>>import PIL >>>help(PIL)
您不应该收到任何错误,并且应该看到有关 PIL 及其用途的大量信息(按Q键完成)。按照以下方式检查安装的版本:
>>PIL.PILLOW_VERSION
您应该看到2.7.0
(或类似)。
通过使用以下命令安装 pip-2.x,PIP 也可以与 Python 2 一起使用:
sudo apt-get install python-pip
使用sudo pip install
安装的任何软件包都将仅为 Python 2 安装。
如何做…
要在应用程序中显示照片信息,请创建以下photohandler.py
脚本:
##!/usr/bin/python3 #photohandler.py from PIL import Image from PIL import ExifTags import datetime import os #set module values previewsize=240,240 defaultimagepreview="./preview.ppm" filedate_to_use="Exif DateTime" #Define expected inputs ARG_IMAGEFILE=1 ARG_LENGTH=2 class Photo: def __init__(self,filename): """Class constructor""" self.filename=filename self.filevalid=False self.exifvalid=False img=self.initImage() if self.filevalid==True: self.initExif(img) self.initDates() def initImage(self): """opens the image and confirms if valid, returns Image""" try: img=Image.open(self.filename) self.filevalid=True except IOError: print ("Target image not found/valid %s" % (self.filename)) img=None self.filevalid=False return img def initExif(self,image): """gets any Exif data from the photo""" try: self.exif_info={ ExifTags.TAGS[x]:y for x,y in image._getexif().items() if x in ExifTags.TAGS } self.exifvalid=True except AttributeError: print ("Image has no Exif Tags") self.exifvalid=False def initDates(self): """determines the date the photo was taken""" #Gather all the times available into YYYY-MM-DD format self.filedates={} if self.exifvalid: #Get the date info from Exif info exif_ids=["DateTime","DateTimeOriginal", "DateTimeDigitized"] for id in exif_ids: dateraw=self.exif_info[id] self.filedates["Exif "+id]= dateraw[:10].replace(":","-") modtimeraw = os.path.getmtime(self.filename) self.filedates["File ModTime"]="%s" % datetime.datetime.fromtimestamp(modtimeraw).date() createtimeraw = os.path.getctime(self.filename) self.filedates["File CreateTime"]="%s" % datetime.datetime.fromtimestamp(createtimeraw).date() def getDate(self): """returns the date the image was taken""" try: date = self.filedates[filedate_to_use] except KeyError: print ("Exif Date not found") date = self.filedates["File ModTime"] return date def previewPhoto(self): """creates a thumbnail image suitable for tk to display""" imageview=self.initImage() imageview=imageview.convert('RGB') imageview.thumbnail(previewsize,Image.ANTIALIAS) imageview.save(defaultimagepreview,format='ppm') return defaultimagepreview
前面的代码定义了我们的Photo
类;在*还有更多…*部分和下一个示例中运行它之前,它对我们没有用处。
它是如何工作的…
我们定义了一个名为Photo
的通用类;它包含有关自身的详细信息,并提供
用于访问可交换图像文件格式(EXIF)信息并生成的函数
一个预览图像。
在__init__()
函数中,我们为我们的类变量设置值,并调用self.initImage()
,它将使用 PIL 中的Image()
函数打开图像。然后我们调用self.initExif()
和self.initDates()
,并设置一个标志来指示文件是否有效。如果无效,Image()
函数将引发IOError
异常。
initExif()
函数使用 PIL 从img
对象中读取 EXIF 数据,如下面的代码片段所示:
self.exif_info={ ExifTags.TAGS[id]:y for id,y in image._getexif().items() if id in ExifTags.TAGS }
前面的代码是一系列复合语句,导致self.exif_info
被填充为标签名称及其相关值的字典。
ExifTag.TAGS
是一个包含可能的标签名称及其 ID 的列表的字典,如下面的代码片段所示:
ExifTag.TAGS={ 4096: 'RelatedImageFileFormat', 513: 'JpegIFOffset', 514: 'JpegIFByteCount', 40963: 'ExifImageHeight', ...etc...}
image._getexif()
函数返回一个包含图像相机设置的所有值的字典,每个值都与其相关的 ID 链接,如下面的代码片段所示:
Image._getexif()={ 256: 3264, 257: 2448, 37378: (281, 100), 36867: '2016:09:28 22:38:08', ...etc...}
for
循环将遍历图像的 EXIF 值字典中的每个项目,并检查其在ExifTags.TAGS
字典中的出现;结果将存储在self.exif_info
中。其代码如下:
self.exif_info={ 'YResolution': (72, 1), 'ResolutionUnit': 2, 'ExposureMode': 0, 'Flash': 24, ...etc...}
再次,如果没有异常,我们将设置一个标志来指示 EXIF 数据是有效的,或者如果没有 EXIF 数据,我们将引发AttributeError
异常。
initDates()
函数允许我们收集所有可能的文件日期和来自 EXIF 数据的日期,以便我们可以选择其中一个作为我们希望用于文件的日期。例如,它允许我们将所有图像重命名为标准日期格式的文件名。我们创建一个self.filedates
字典,其中包含从 EXIF 信息中提取的三个日期。然后添加文件系统日期(创建和修改),以防没有 EXIF 数据可用。os
模块允许我们使用os.path.getctime()
和os.path.getmtime()
来获取文件创建的时期值。它也可以是文件移动时的日期和时间-最后写入的文件修改时间(例如,通常指图片拍摄的日期)。时期值是自 1970 年 1 月 1 日以来的秒数,但我们可以使用datetime.datetime.fromtimestamp()
将其转换为年、月、日、小时和秒。添加date()
只是将其限制为年、月和日。
现在,如果Photo
类被另一个模块使用,并且我们希望知道拍摄的图像的日期,我们可以查看self.dates
字典并选择合适的日期。但是,这将要求程序员知道self.dates
值的排列方式,如果以后更改了它们的存储方式,将会破坏他们的程序。因此,建议我们通过访问函数访问类中的数据,以便实现独立于接口(这个过程称为封装)。我们提供一个在调用时返回日期的函数;程序员不需要知道它可能是五个可用日期中的一个,甚至不需要知道它们是作为时期值存储的。使用函数,我们可以确保接口保持不变,无论数据的存储或收集方式如何。
最后,我们希望Photo
类提供的最后一个函数是previewPhoto()
。此函数提供了一种生成小缩略图图像并将其保存为便携式像素图格式(PPM)文件的方法。正如我们将在一会儿发现的那样,Tkinter 允许我们将图像放在其Canvas
小部件上,但不幸的是,它不直接支持 JPEG,只支持 GIF 或 PPM。因此,我们只需将要显示的图像的小副本保存为 PPM 格式,然后让 Tkinter 在需要时将其加载到Canvas
上。
总之,我们创建的Photo
类如下:
操作 | 描述 |
__init__(self,filename) |
这是对象初始化程序。 |
initImage(self) |
这将返回img ,一个 PIL 类型的图像对象。 |
initExif(self,image) |
如果存在,这将提取所有的 EXIF 信息。 |
initDates(self) |
这将创建一个包含文件和照片信息中所有可用日期的字典。 |
getDate(self) |
这将返回照片拍摄/创建的日期的字符串。 |
previewPhoto(self) |
这将返回预览缩略图的文件名的字符串。 |
属性及其相应的描述如下:
属性 | 描述 |
self.filename |
照片的文件名。 |
self.filevalid |
如果文件成功打开,则设置为True 。 |
self.exifvalid |
如果照片包含 EXIF 信息,则设置为True 。 |
self.exif_info |
这包含照片的 EXIF 信息。 |
self.filedates |
这包含了文件和照片信息中可用日期的字典。 |
为了测试新类,我们将创建一些测试代码来确认一切是否按我们的预期工作;请参阅以下部分。
Python 物联网入门指南(二)(2)https://developer.aliyun.com/article/1507137