Tkinter 通知板与套接字消息 - 奇怪的行为

1 投票
1 回答
1817 浏览
提问于 2025-04-17 22:35

我在这个论坛上已经使用了一段时间,但这是我第一次提问,因为我一直找不到解决我遇到的问题的好办法,而且我希望这个问题对其他人也有帮助。

我正在实现一个简单的通知板,也就是一个可以显示来自套接字连接的消息的窗口。这个通知板会把最新收到的消息用红色显示,旧的消息用蓝色显示,最多显示十条。当客户端发送的消息是'Q'时,连接会结束,通知板也会被销毁。

我使用了Tkinter、线程和套接字,但运行起来不太流畅(通知板刷新需要一些时间)。我想到了一些可能的问题:处理连接的线程没有关闭;更新窗口是通过销毁并重新创建顶层窗口来完成的。不幸的是,我不太明白这些问题是否是导致问题的根源。

这是客户端的代码,非常简单:

#!/usr/bin/env python

import socket

HOST = ''           # Symbolic name meaning the local host
PORT = 24073        # Arbitrary non-privileged port

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST,PORT))

while True:
    message = raw_input('Enter your command (Q=quit): ')
    s.send(message)
    reply = s.recv(1024)
    if reply=='Q':
        print 'Request to disconnect was received.'
        break
    else :
        print reply
s.close()

这是服务器的代码。服务器实现了一个类来处理通知板的特性,还有一个线程用于套接字连接,最后是包含mainloop()的主要部分。

#!/usr/bin/env python

import socket
import threading

from Tkinter import *
from datetime import datetime

### Class definition

class NoticationsBoard() :

    def __init__(self, title):
        self.messages = []
        self.toplevel = None
        self.title = title
        self.backgroundColor = 'black'
        self.textColor = 'blue'
        self.textColorFirst = 'red'
        self.boardSize = '200x250+0+0'
        self.listsize = 10

    def createBoard(self):
        self.toplevel = Toplevel()
        self.toplevel.title(self.title)
        self.toplevel.configure(background='black')
        self.toplevel.geometry(self.boardSize)

    def publish(self, message):
        self.addToList(message)
        self.displayList()

    def addToList(self, msg):
        if len(self.messages) == self.listsize:
            self.messages.pop(0)
        timestamp = datetime.utcnow().strftime('%H:%M:%S')
        newMessage = (msg, timestamp)
        self.messages.append(newMessage)

    def displayList(self):
        # Destroy and create the window (is it really necessary?)
        if self.toplevel is not None :
            self.toplevel.destroy()
        self.createBoard()
        # create labels for all the messages in the list
        index = 1
        for m, t in self.messages :
            color = self.textColor
            if index == len(self.messages) :
                color = self.textColorFirst
            label = Label(self.toplevel, text=m, height=0, width=100, fg=color, anchor=W)
            label.grid(row=0,column=1)
            label.configure(background=self.backgroundColor)
            label.pack(side='bottom')
            index = index +1

####### Run

def receiveMessages(newsboard) :
    print '===== Inside receiveMessages ======'
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    print 'Socket created'
    try:
        s.bind((HOST, PORT))
    except socket.error , msg:
        print 'Bind failed. Error code: ' + str(msg[0]) + 'Error message: ' + msg[1]
        sys.exit()
    print 'Socket bind complete'
    s.listen(1)
    print 'Socket now listening on port', PORT
    # Accept the connection once
    (conn, addr) = s.accept()
    print 'Connected with ' + addr[0] + ':' + str(addr[1])
    stored_data = ''
    while True:
        # RECEIVE DATA
        data = conn.recv(1024)
        # PROCESS DATA
        if data == 'Q' :
            print 'Client wants to disconnect.'
            reply = 'Q'
            conn.send(reply)
            break
        else :
            print data
            newsboard.publish(data)
            reply = 'Message received:' + data
            conn.send(reply)
    print 'Close connection.'
    conn.close()
    board.destroy()

HOST = ''   # Symbolic name meaning the local host
PORT = 24073    # Arbitrary non-privileged port

app = Tk()
app.title("GUI main")

board = NoticationsBoard('Notifications')    

t = threading.Thread(target=receiveMessages, args = (board,))
t.start()

app.update()    # Not sure what it does and if it is necessary
app.mainloop()

我使用的是Python 2.7.5。

最后,虽然这只是小事,我试图在每条消息左边显示时间戳,并用不同的颜色显示。看起来在同一个标签上显示不同颜色的文字是不可能的,所以我在for循环中创建了其他标签来显示时间戳。我尝试使用.grid(column=0).grid(column=1)把时间戳和消息标签并排显示,但它们并不是并排显示,而是上下排列,我还没弄明白为什么。

如你所见,我不是一个熟练的程序员,绝对是Python的新手……

感谢提前给我建议的人,希望这个问题对很多人都有帮助。

1 个回答

0

好的,我好像找到了解决办法,主要是参考了其他人的问题、建议和代码。外观上可能有一些小差别。
在图形界面(GUI)部分,最明显的变化是我预先加载了所有的标签,然后只修改文本。
在多线程的部分,这个完全变了。请看下面的代码。

#!/usr/local/bin/python

try:
    import Tkinter
except ImportError:
    import tkinter as Tkinter
import time
import threading
import random
import Queue
import socket
import sys
from datetime import datetime

class GuiPart:
    def __init__(self, master, queue):
        self.queue = queue
        # GUI stuff
        self.labelArray = []
        self.messages = []
        # Set up the GUI
        self.master = master
        self.backgroundColor = 'black'
        self.listsize = 10
        master.config(bg=self.backgroundColor)
        self.board = Tkinter.LabelFrame(self.master, text='Notification Board',     bg='Black', fg='Yellow', labelanchor='n', width=170)
        self.initLabels()
        self.board.pack()

    def initLabels(self) :
        self.textColorTime = 'cyan'
        self.textColorMessage = 'orange'
        colorTime = 'blue'
        colorMessage = 'red'
        for i in range(0,self.listsize):
            la = Tkinter.Label(self.board, height=0, width=10, bg=self.backgroundColor, fg=colorTime, anchor=Tkinter.W)
            lb = Tkinter.Label(self.board, height=0, width=160, bg=self.backgroundColor, fg=colorMessage)
            la.grid(row=i,column=0, sticky=Tkinter.W)
            lb.grid(row=i,column=1, sticky=Tkinter.W)
            self.labelArray.append((la, lb))
            colorTime = self.textColorTime
            colorMessage = self.textColorMessage
            self.initList()
        self.displayList()

    def initList(self):
        for i in range(0, self.listsize):
            t = ''
            m = ''
            self.messages.append((t,m))

    def processIncoming(self):
        while self.queue.qsize():
            try:
                msg = self.queue.get(0)
                self.processMessage(msg)
            except Queue.Empty:
                pass

    def processMessage(self, message):
        timestamp = datetime.utcnow().strftime('%H:%M:%S')
        self.publish(timestamp, message)

    def publish(self, msg1, msg2):
        self.addToList(msg1, msg2)
        self.displayList()

    def addToList(self, msg1, msg2):
        if len(self.messages) == self.listsize:
            self.messages.pop(0)
        if (msg1 == None):
            msg1 = datetime.utcnow().strftime('%H:%M:%S')
        newMessage = (msg1, msg2)
        self.messages.append(newMessage)

    def displayList(self):
        index = self.listsize -1
        for t, m in self.messages :
            la, lb = self.labelArray[index]
            la.config(text=t)
            lb.config(text=m)
            index = index -1

    def destroy(self):
        self.master.destroy()

class ThreadedClient:

    def __init__(self, master):
        self.master = master
        # Create the queue
        self.queue = Queue.Queue()
        # Define connection parameters
        self.conn = None
        self.bindError = False
        # Set up the GUI part
        self.gui = GuiPart(master, self.queue)
        # Set up the thread to do asynchronous I/O
        self.running = True
        self.commThread = threading.Thread(target=self.workerThreadReceive)
        self.commThread.daemon = True
        self.commThread.start()
        # Start the periodic call in the GUI to check if the queue contains anything
        self.periodicCall()

    def periodicCall(self):
        if not self.running:
            self.killApplication()
        else :
            self.gui.processIncoming()
            self.master.after(100, self.periodicCall)

    def workerThreadReceive(self):
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        try :
            s.bind((HOST, PORT))
        except socket.error as msg :
            print 'Bind failed. Error code: ' + str(msg[0]) + ' Error message: ' + str(msg[1])
            self.running = False
            self.bindError = True
            return
        s.listen(1)
        (self.conn, self.addr) = s.accept()
        while self.running :
            data = self.conn.recv(1024)
            if data == 'Q' :
                self.conn.sendall('Q')
                self.running = False
            else :
                self.queue.put(data)
                reply = 'ACK'
                self.conn.sendall(reply)
        if self.conn is not None:
            self.conn.close()

    def killApplication(self):
        self.running = False
        if (self.conn is None) and (not self.bindError) :
            sfake = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sfake.connect((HOST,PORT))
            sfake.sendall('Q')
            sfake.close()
        self.gui.destroy()
        sys.exit()


HOST = ''       # Symbolic name meaning the local host
PORT = 24073    # Arbitrary non-privileged port

root = Tkinter.Tk()

client = ThreadedClient(root)

root.protocol("WM_DELETE_WINDOW", client.killApplication)
root.mainloop()

客户端的部分和问题中提到的是一样的。

我不确定我的代码是否是最优雅的(好吧,老实说,肯定不是!),但看起来是能完成任务的。不过,我还是想听听你们的反馈,因为我知道我可能忽略了很多问题,还有很多地方可以做得更简单。:)

撰写回答