【问题标题】:Tkinter notification board with messages from socket - Weird behaviour带有来自套接字的消息的 Tkinter 通知板 - 奇怪的行为
【发布时间】:2014-03-16 23:15:09
【问题描述】:

我使用这个论坛有一段时间了,但这是我第一次提出问题,因为我无法找到解决困难的好方法,并且因为我希望这个问题对我有用也给其他人。

我正在实现一个简单的通知板,即显示来自套接字连接的消息的窗口。该板以红色打印收到的最后一条消息,以蓝色打印旧消息,最多十个。当客户端发送的消息为'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 的新手......

提前感谢谁会给我一些建议,希望这个问题对很多人有用。

【问题讨论】:

  • 可能是 stackoverflow.com/q/16745507/291641 的副本。从不同的线程调用 UI 方法可能会导致问题。按照建议,使用队列并从 UI 线程轮询队列应该可以解决问题。或者使用 Twisted 以面向事件的方式使用 1 个线程进行整个运行。
  • 感谢您的建议。我不确定如何避免从线程化的receiveMessages() 函数调用 UI 方法。我查看了您链接的问题,但我很难看到需要在哪里添加 queuepoll。至于 Twisted,一个面向事件的工具可能是一个好主意,但我想知道这是否过于简单,因为理论上这段代码应该执行的任务很简单。在您看来,事件驱动的实现会简化整个结构吗?
  • 快速更新:我正在从 Jacob Hallén (code.activestate.com/recipes/…) 编写的配方开始重写代码。一旦我得到它的工作,我会上传我的工作解决方案(这可能不是最好的,但还没有!)。

标签: python multithreading sockets tkinter


【解决方案1】:

好的,我似乎通过收集其他人的部分问题、建议和代码找到了解决方案。外观上可能差别不大。 在 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()

客户与问题中的相同。

我不确定我的代码是不是最优雅的(好吧,可以这么说……它不是!),但它似乎可以完成这项工作。尽管如此,我还是希望得到您的反馈,因为我确信我忽略了许多问题,并且事情本可以以更简单的方式完成。 :)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-09-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多