使用 Python 的 Tkinter模組 開發 IRC 客戶端

發表於2016-01-30

前段時間嘗試用 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 地址

相關文章