Python GUI Cookbook —— 线程与网络

创建线程、队列和TCP/IP套接字

当进程被创建时,进程会自动创建一个主线程来运行该应用程序,被称为单线程应用程序。

对于我们的 Python GUI,单线程应用程序将导致 GUI 在调用长运行时间的任务时(例如点击了具有几秒钟睡眠时间的按钮)变冻结。为了保持 GUI 的响应,我们需要使用多线程。 我们也可以通过创建多个 GUI 实例来构成(正如在任务管理器中所见的)多个进程。

在设计上,进程之间彼此隔离、不共享公共数据。为了在独立的两个进程之间进行通信,需要使用先进的进程间通信(Inter Process Communication,IPC)技术。而另一方面,线程则共享公用数据、代码和文件,这使得同一进程中的线程间通信比使用 IPC 时更容易。

创建多线程

为了保持 GUI 响应,创建多线程时必要的。多线程运行在相同的计算机进程内存空间,不需要写复杂的 IPC 代码。

首先导入 threading 模块

1
2
3
4
5
6
7
8
9
from tkinter import ttk
from tkinter import scrolledtext
from tkinter import messagebox as msg
from tkinter import Spinbox
from tkinter import Menu
from time import sleep

from threading import Thread
[...]

在类中添加一个方法以创建线程

1
2
def method_in_a_thread(self):
print('Hi, How are you?')

调用该方法

1
2
3
4
5
6
app = App()

# Running methods in Threads
run_thread = Thread(target=app.method_in_a_thread)

app.win.mainloop()

现在运行代码 … … 好吧,什么都没发生。

开始一个线程

当我们在不使用线程时,调用一些有睡眠(sleep)的函数或方法的 GUI 会发生什么?

这里我们使用 sleep 来模拟实际应用中等待 web 服务器和数据库响应、大文件传输或复杂的计算等任务。

在按钮的调用方法中添加一个带有睡眠时间的循环,会导致 GUI 无响应。

1
2
3
4
5
6
7
def click_me(self):
self.action.configure(text='Hello '+self.name.get()+' '+
self.number_chosen.get())
# Non-threaded code with sleep freezes the GUI
for idx in range(10):
sleep(5)
self.scrol.insert(tk.INSERT, str(idx)+'\n')

不像常规的函数和方法,这里我们需要 start 一个方法才能开启它自己的线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[...]

def method_in_a_thread(self):
print('Hi, How are you?')

# Running methods in Threads
def create_thread(self):
self.run_thread = Thread(target=self.method_in_a_thread)
print(self.run_thread)
self.run_thread.start()


def click_me(self):
self.action.configure(text='Hello '+self.name.get()+' '+
self.number_chosen.get())
self.create_thread()
# # Non-threaded code with sleep freezes the GUI
# for idx in range(10):
# sleep(5)
# self.scrol.insert(tk.INSERT, str(idx)+'\n')

[...]

method_in_a_thread 中添加一些 sleep,来验证一下是不是真的解决了我们的问题:

1
2
3
4
5
6

def method_in_a_thread(self):
print('Hi, How are you?')
for idx in range(10):
sleep(5)
self.scrol.insert(tk.INSERT, str(idx)+'\n')

可以发现我们的 GUI 又能响应了。

停止线程

直觉上来说,如果有 start() 方法那么就会有相应的 stop() 方法,然而并没有。因此需要将线程作为后台任务运行(守护进程,daemon),当关闭主线程(GUI)时,所有的守护进程也将自动停止。

首先,通过对线程构造函数添加 args 参数,我们可以给线程的方法传入参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def method_in_a_thread(self, num_of_loops=10):
print('Hi, How are you?')
for idx in range(num_of_loops):
sleep(1)
self.scrol.insert(tk.INSERT, str(idx)+'\n')
sleep(1)
print('method_in_a_thread():', self.run_thread.isAlive())

# Running methods in Threads
def create_thread(self):
self.run_thread = Thread(target=self.method_in_a_thread, args=[8])
print(self.run_thread)
self.run_thread.start()
print('createThread():',self.run_thread.isAlive())

这时,如果我们提前结束 GUI,我们就会得到以下错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<Thread(Thread-1, initial)>
Hi, How are you?
createThread(): True
Exception in thread Thread-1:
Traceback (most recent call last):
File "C:\Python36\lib\threading.py", line 916, in _bootstrap_inner
self.run()
File "C:\Python36\lib\threading.py", line 864, in run
self._target(*self._args, **self._kwargs)
File "multiple_threads.py", line 169, in method_in_a_thread
self.scrol.insert(tk.INSERT, str(idx)+'\n')
File "C:\Python36\lib\tkinter\__init__.py", line 3266, in insert
self.tk.call((self._w, 'insert', index, chars) + args)
RuntimeError: main thread is not in main loop

通过将线程转换为守护进程能解决这个问题。

1
2
3
4
5
6
7
# Running methods in Threads
def create_thread(self):
self.run_thread = Thread(target=self.method_in_a_thread, args=[8])
print(self.run_thread)
self.run_thread.setDaemon(True)
self.run_thread.start()
print('createThread():',self.run_thread.isAlive())

使用队列

当接收到要处理和显示的最终的结果时,使用多线程在队列中来完成分配的任务是非常有用的。数据以先入先出(FIFO)的方式工作。

queue 模块导入 Queue

1
2
3
4
5
6
7
8
9
10
11
12
import tkinter as tk
from tkinter import ttk
from tkinter import scrolledtext
from tkinter import messagebox as msg
from tkinter import Spinbox
from tkinter import Menu
from time import sleep

from threading import Thread
from queue import Queue

[...]

创建队列实例

1
2
3
4
5
6
7
def use_queues(self):
gui_queue = Queue()
print(gui_queue)
for idx in range(10):
gui_queue.put('Message from a queue: '+str(idx))
while True:
print(gui_queue.get())

在按钮点击事件中调用该方法

1
2
3
4
5
def click_me(self):
self.action.configure(text='Hello '+self.name.get()+' '+
self.number_chosen.get())
self.create_thread()
self.use_queues()

运行代码,会导致以下结果:

随着代码的运行,我们的 GUI 被冻结了。为了解决该问题,我们需要在它自己的线程中调用该方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Running methods in Threads
def create_thread(self):
self.run_thread = Thread(target=self.method_in_a_thread, args=[8])
print(self.run_thread)
self.run_thread.setDaemon(True)
self.run_thread.start()
# print('createThread():',self.run_thread.isAlive())

# start queue in its own thread
write_thread = Thread(target=self.use_queues, daemon=True)
write_thread.start()


def use_queues(self):
gui_queue = Queue()
print(gui_queue)
for idx in range(10):
gui_queue.put('Message from a queue: '+str(idx))
while True:
print(gui_queue.get())


def click_me(self):
self.action.configure(text='Hello '+self.name.get()+' '+
self.number_chosen.get())
self.create_thread()

在不同的模块之间传递队列

  • 模块化设计使得代码能够重用且提高了代码的可读性
  • 将 GUI widget 从表达业务逻辑的功能中分离出来

导入另外的模块

1
import Queues as bq

Queues.py

1
2
3
4
5
def write_to_scrol(inst):
print('hi from Queue', inst)
for idx in range(10):
inst.gui_queue.put('Message from a queue: ' + str(idx))
inst.create_thread(6)

修改代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
[...]

def __init__(self):
# Create instance
self.gui_queue = Queue()
self.win = tk.Tk()
# Add title
self.win.title("Python GUI")
self.create_widgets()

[...]

# Running methods in Threads
def create_thread(self, num=3):
self.run_thread = Thread(target=self.method_in_a_thread, args=[num])
print(self.run_thread)
self.run_thread.setDaemon(True)
self.run_thread.start()
# print('createThread():',self.run_thread.isAlive())

# start queue in its own thread
write_thread = Thread(target=self.use_queues, daemon=True)
write_thread.start()


def use_queues(self):
# gui_queue = Queue()
# print(gui_queue)
# for idx in range(10):
# gui_queue.put('Message from a queue: '+str(idx))
while True:
print(self.gui_queue.get())


def click_me(self):
print(self)
self.action.configure(text='Hello '+self.name.get()+' '+
self.number_chosen.get())
# self.create_thread()
bq.write_to_scrol(self)

[...]

使用对话框 widget 将文件复制到网络

  • 将文件从本地硬盘复制到网络位置
  • 使用内置对话框浏览硬盘
  • 让文本框只读、指定默认的进入位置

首先,在 Tab2 中添加两个按钮和文本框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# Create Manage Files Frame
mngFilesFrame = ttk.LabelFrame(tab2, text=' Manage Files: ')
mngFilesFrame.grid(column=0, row=1, sticky='WE', padx=10, pady=5)

# Add Widgets to Manage Files Frame
lb = ttk.Button(mngFilesFrame, text='Browse to File...',
command=self.getFileMame)
lb.grid(column=0, row=0, sticky=tk.W)

cb = ttk.Button(mngFilesFrame, text='Copy File to: ',
command=self.copyFile)
cb.grid(column=0, row=1, sticky=tk.W)

file = tk.StringVar()
self.entryLen = scrol_w
self.fileEntry = ttk.Entry(mngFilesFrame, width=self.entryLen,
textvariable=file)
self.fileEntry.grid(column=1, row=0, sticky=tk.W)

logDir = tk.StringVar()
self.netwEntry = ttk.Entry(mngFilesFrame, width=self.entryLen,
textvariable=logDir)
self.netwEntry.grid(column=1, row=1, sticky=tk.W)

# Add some space around each label
for child in mngFilesFrame.winfo_children():
child.grid_configure(padx=6, pady=6)

下面使用 tkinter 内置的对话框

1
2
from tkinter import filedialog as fd
from os import path
1
2
3
4
def getFileMame(self):
print('hello from getFileMame')
fDir = path.dirname(__file__)
fname = fd.askopenfilename(parent=self.win, initialdir=fDir)

可以为文本框设置默认值

1
2
3
4
5
self.name_entered = ttk.Entry(mighty, width=24, 
textvariable=self.name)
self.name_entered.grid(column=0, row=1, sticky='W')
self.name_entered.delete(0, tk.END)
self.name_entered.insert(0, '< default name>')

获取模块完整路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Module level GLOBALS
fDir, _ = path.split(path.realpath(__file__))
netDir = fDir + '\\Backup'

class App():
def __init__(self):
# Create instance
self.gui_queue = Queue()
self.win = tk.Tk()
# Add title
self.win.title("Python GUI")
self.create_widgets()
self.defaultFileEntries()


def defaultFileEntries(self):
self.fileEntry.delete(0, tk.END)
self.fileEntry.insert(0, fDir)
if len(fDir) > self.entryLen:
self.fileEntry.config(width=len(fDir) + 3)
self.fileEntry.config(state='readonly')
self.netwEntry.delete(0, tk.END)
self.netwEntry.insert(0, netDir)
if len(netDir) > self.entryLen:
self.netwEntry.config(width=len(netDir) + 3)

Tab2 设置为默认显示页

1
2
# self.name_entered.focus()
tabControl.select(1)

本例可以用来在 backup 文件夹备份代码,如果不存在则使用 os.makedirs 创建

1
2
if not path.exists(netDir):
makedirs(netDir, exist_ok=True)

源代码点此查看

使用 TCP/IP 通过网络通信

创建 TCP_Server 模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from socketserver import BaseRequestHandler, TCPServer

class RequestHandler(BaseRequestHandler):
# override base class handle method

def handle(self):
print('Server connected to: ', self.client_address)
while True:
rsp =self.request.recv(512)
if not rsp: break
self.request.send(b'Server received: ' + rsp)


def start_server():
server = TCPServer(('', 24000), RequestHandler)
server.server_forever()

修改 Queues 模块

1
2
3
4
5
6
7
8
9
10
11
12
# Using TCP/IP
from socket import socket, AF_INET, SOCK_STREAM

def write_to_scrol(inst):
print('hi from Queue', inst)
sock = socket(AF_INET, SOCK_STREAM)
sock.connect(('localhost', 24000))
for idx in range(10):
sock.send(b'Message from a queue: ' + bytes(str(idx).encode()))
recv = sock.recv(8192).decode()
inst.gui_queue.put(recv)
inst.create_thread(6)

App 类的初始化过程中,让 TCP server 在自己的线程中开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
[...]

import Queues as bq
from TCP_Server import start_server

[...]

class App():
def __init__(self):
# Create instance
self.gui_queue = Queue()
self.win = tk.Tk()
# Add title
self.win.title("Python GUI")
self.create_widgets()
self.defaultFileEntries()
# Start TCP/IP server in its own thread
svrT = Thread(target=start_server, daemon=True)
svrT.start()
[...]
def use_queues(self):

while True:
q_item = self.gui_queue.get()
self.scrol.insert(tk.INSERT, q_item+'\n')
print(q_item)
[...]

使用 urlopen 从网站读取数据

创建新的 URL 模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from urllib.request import urlopen

link = 'http://python.org/'

def get_html():
try:
http_rsp = urlopen(link)
print(http_rsp)
html = http_rsp.read()
print(html)
html_decoded = html.decode()
print(html_decoded)
except Exception as ex:
print('*** Failed to get Html! ***\n\n' + str(ex))
else:
return html_decoded

if __name__ == '__main__':
get_html()

导入 URL 模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[...]

import Queues as bq
from TCP_Server import start_server
import URL as url

[...]

def click_me(self):
print(self)
self.action.configure(text='Hello '+self.name.get()+' '+
self.number_chosen.get())
# self.create_thread()
bq.write_to_scrol(self)
sleep(2)
html_data = url.get_html()
print(html_data)
self.scrol.insert(tk.INSERT, html_data)

源代码点此查看

参考文献

  • Python GUI Programming Cookbook - Second Edition by Burkhard A. Meier
GreatX wechat
订阅公众号,获取更多信息。