使用 Python 的 Tkinter模块 开发 IRC 客户端

617 查看

前段时间尝试用 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 地址