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