前段时间尝试用 Python 做了一个在线多聊天室的服务器程序,通过 shell 登陆。开发环境:MAC OS 10.10,Python 2.7.9。经过测试,发现了一些问题:
- 无法支持中文聊天
- 消息输入、输出使用同一窗口,其他人发送的消息会冲乱当前正在输入的内容
- windows 的 shell 好像不支持消息输错回退
于是决定做一个 GUI 的客户端。
Python 的 GUI 模块很多,选择 Tkinter(以下简称Tk) 是因为 Python 自带、而且几个操作系统都支持。
客户端的开发有两个步骤:界面开发 以及 与服务器对接。
界面开发
需要有三个界面,分别用来输入昵称、通过按钮选择聊天室、进行聊天。
运行 Tk 先要创建一个根窗口,就像一面空墙,需要进行装饰。可以通过geometry指定窗口大小、位置。
from Tkinter import * #导入模块
root = Tk() #创建一个根窗口
root.mainloop() #进入窗口的主循环,否则无法显示界面
居中
root.geometry(self, newGeometry=None) # 通过 widthxheight+x+y (宽x高+左上角X轴坐标+左上角Y轴坐标)的方式,设置一个新的 geometry
root.geometry('%sx%s+%s+%s' %
(
root.winfo_width() , # 窗口宽度
root.winfo_height() , # 窗口高度
(root.winfo_screenwidth() - root.winfo_width())/2, # (屏幕宽度 - 窗口宽度)/2
(root.winfo_screenheight() - root.winfo_height())/2 # (屏幕高度 - 窗口高度)/2
))
然后根据需要创建窗口里的组件,包括规定组件的大小、颜色,最后按照一定的位置摆放这些组件。
Tk 提供了很多组件,用来实现各种功能,包括 输入框(Entry)、按钮(Button)、显示文本的标签(Label)、滚动条(Scrollbar)、字符串列表框(Listbox) 等。
每个组件都有一些参数可以配置,常用的配置方法有两种:
- widgetclass(master, option=value, …)。组件(放在哪个窗口, 参数=值, …),第一个参数指定了放置到哪一个窗口,可以是根窗口,或是框架控件(Frame) 或者
- widgetclass.config(option=value, …)
创建标签,显示文本
inputText = Label(self) #创建一个标签,用于显示文本信息
inputText["text"] = "欢迎,请输入昵称:" #标签的文本内容
inputText.pack(side="top") #指定将标签在窗口中向上放置
获取输入框的内容 创建名为 server_ip 的 StringVar(),和 Entry 的 textvariable参数进行绑定,输入的内容通过 server_ip.get() 获取。输入框还可以用 server_ip.set('127.0.0.1') 设置默认值。
server_ip = StringVar()
server_ip.set('127.0.0.1')
input_ip = Entry(self, textvariable=server_ip)
input_ip["width"] = 5
input_ip.pack(side="left", ipadx=30, padx=5)
ip = server_ip.get()
组件的函数调用,有 直接绑定函数 和 间接绑定事件 两种方式。
当需要指定按钮按下时,执行什么方法/函数,可以使用command参数绑定函数
QUIT = Button(root)
QUIT["text"] = "QUIT"
QUIT["fg"] = "red"
QUIT["command"] = root.quit # 结束 Tkinter 所有组件
QUIT.pack(side="left")
def quit():
pass
组件也可以bind绑定触发事件(键盘、鼠标),并指定 事件的行为。比如,为输入框绑定回车事件,指定调用 send_message函数 对输入的内容进行处理。使得回车就可以发送消息,而不用点击按钮。
frame_l_m = Frame(self) #创建一个框架控件
message_input = StringVar()
message_send = Entry(frame_l_m, textvariable=message_input)
message_send["width"] = 70
message_send.bind('', send_message)
message_send.pack(fill=X)
frame_l_m.pack()
def send_message():
pass
显示消息的窗口(我选择使用 Listbox 实现) 带有 滚动条,需要两步:
1. 用 Listbox 的 yscrollcommand参数,调用 scrollbar 的 set 方法
2. 设置 scrollbar 的command参数为 Listbox 的 yview(纵向滚动条)或 xview(横向) 方法
对于其他需要和滚动条绑定的组件都需要做以上两个设置。
另外,滚动条默认在顶端,如果希望能够自动下拉到聊天窗口的最底端,显示最新的消息,可以使用 Listbox 的 yview_moveto 方法,指定值为1.0。
注意,scrollbar 的位置是由 Listbox 确定的,所以应该找 Listbox 的方法,而不是 scrollbar 的方法
frame_l_t = Frame(self) #可以是 根窗口,或框架组件
scrollbar = Scrollbar(frame_l_t)
chatText = Listbox(frame_l_t, width=70, height=18, yscrollcommand=scrollbar.set)
chatText.yview_moveto(1.0)
scrollbar.config(command=chatText.yview)
scrollbar.pack(side="right", fill=Y)
chatText.pack(side="left")
将 输入框的内容 移到 显示消息的组件,并清空 输入框的内容。这需要用到 Listbox 的 insert方法 和 Entry 的 delete方法。 insert,指定从END(最后),插入消息 send_mesg。 delete,指定删除从 最开始0到END最后。
frame_l_t = Frame(self)
frame_l_m = Frame(self)
scrollbar = Scrollbar(frame_l_t)
chatText = Listbox(frame_l_t, width=70, height=18, yscrollcommand=scrollbar.set) # 消息显示组件
scrollbar.config(command=chatText.yview)
scrollbar.pack(side="right", fill=Y)
chatText.pack(side="left")
frame_l_t.pack()
message_input = StringVar()
message_send = Entry(frame_l_m, textvariable=message_input) # 消息输入组件
message_send["width"] = 70
message_send.bind('', send_message)
message_send.pack(fill=X)
frame_l_m.pack()
def send_message(self, event):
send_mesg = message_input.get()
chatText.insert(END, send_mesg) # 在消息显示组件显示
chatText.yview_moveto(1.0) # 将滚动条拉至最低
message_send.delete(0, END) # 从输入框删除
最后就是放置组件了,位置的管理有三种方式:pack(块)、grid(单元格)、place(位置)。
如果不配置管理方式,窗口/组件不会显示。
pack:
较简单,也最常用,如同拼七巧板,简单地将 组件\框架控件 作为一个方块进行堆砌。默认将组块从上到下放置。可以使用参数 fill、expand、side 进行控制。
fill表示如何组件填充方向,有三个值可选,X横向Y纵向BOTH横向和纵向 填充。但不会使用窗口中多出的空间。
expand设置是否使用窗口多出的空间,默认是0不使用,如果是非零值,通常使用1,将会对窗口未使用的部分进行填充,填充方向根据 fill 决定。
side确定组件摆放顺序,只使用TOP(默认),从上向下依次放置,LEFT,从左到右依次放置,也可以使用BOTTOM或RIGHT。但是,简单通过这样的方式摆放组件,并不一定会得到想要的效果,可以用Frame作为子窗口,对部分组件进行安放,然后再放置Frame。
grid:
适合摆放复杂的界面。由于pack的放置不一定满意,除了用Frame优化外,还可以使用grid进行放置。不需要指定窗口尺寸,grid会自动检测组件大小决定。用法类似描述Excel单元格坐标,参数row行,column列(默认为0),stickycolumnspanrowspan
row,将组件放入指定的某一行,数值从0开始。如果不指定,默认放在第一个空行。
column,将组件放入指定的某一列,数值从0开始,默认为0。
sticky,组件默认在单元格中居中对齐,可以通过对该参数设置N,S,E,W中的一个或多个值,改变在单元格中的对齐方式。
columnspan,组件可以占用不止一列单元格的空间
rowspan,组件也可以占用不止一行单元格的空间
place 最复杂、精细的控制,这里就不说明了……
界面的跳转 需要实现显示下一个窗口/界面的同时,关闭现有的窗口/界面。在当前窗口/界面的类中,定义方法,先创建并显示新的窗口/界面,然后使用当前窗口/界面的 destroy 方法,关闭 当前窗口/界面 以及 当前窗口/界面中所有的组件。
一定要先创建新窗口,再关闭现有的窗口
root = Tk()
app = Chat(master=root)
class Chat(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
self.pack() # 用来管理和显示组件,默认 side = "top"
def room_pm(self):
root = Tk() # 创建新窗口
app = Room(master=root, name="pm")
self.master.destroy() # 与 quit 不同,只销毁 当前组件 和 其子组件
Tkinter 的中文输入遇到过一点问题:中文输入法无法在输入框输入中文,只能打出拼音,但是可以将中文复制粘贴进去、标签、按钮、字符串列表框 等组建也可以显示中文。搜索后知道,是 MAC 自带的 Tkinter版本过低,下载新版本安装一下就解决了。但并不是要安装最新版本,具体解决过程
服务器对接
因为客户端是通过命令行 telnet 登陆服务器,而 Python 自带 telnetlib模块,可以实现 telnet 功能。
from telnetlib import *
host = "127.0.0.1"
port = 5000
server = Telnet(host, port)
客户端 简单、特定消息 的 发送和接受 通过 telnetlib 的 write 和 read_until 方法。
server.write("/back" + "\r\n") # 在服务器端,`\r\n`表示换行(回车)
server.write(send_mesg.encode("utf-8")+"\r\n") # 发送中文
server.read_until("More helps use: /help", 1) # 接收消息,直到收到指定字符串为止。也可以指定等待的秒数,接收目前收到的信息。
server.read_until("!")
进入聊天室后,由于需要同时进行 循环显示窗口、不断侦探/接收来自服务器的聊天消息 两个任务。
我选择在聊天室实例中,通过创建线程,调用 receiveMessage 方法接收聊天消息。用 telnetlib模块的 get_socket()方法,获得 socket对象,并通过这个对象,调用 recv方法 与服务器通信,接收消息。
import thread
class Room(Frame):
def __init__(self, master=None, name=None):
...
def receiveMessage(self):
socket = server.get_socket()
while 1:
clientMsg = socket.recv(4096)
if not clientMsg:
continue
else:
self.chatText.insert(END, clientMsg)
self.chatText.yview_moveto(1.0)
def startNewThread(self):
thread.start_new_thread(self.receiveMessage, ())
但 Tkinter 一直报错:
RuntimeError: main thread is not in main loop
因为 Tkinter 并非是真正的可以实现 多线程,还有很多问题。
三个解决方案:
1. 官方的方案:将 Tk 代码放入主线程,并将 现在线程 的代码放入 工作线程
2. 使用第三方库,例如twisted
3. 使用 mkTkinter。官方对 Tkinter 多线程 问题的修复版本。直接从官网下载单文件模块即可。
我选择了使用 mkTkinter 替换 Tkinter。只需从官网下载mtTkinter,放在同一目录就可以了,方法名称同 Tkinter一样。
# from Tkinter import *
from mtTkinter import *
解决方案资料来源
Tkinter 参考资料
项目的 github 地址