02.python电子白板实现

配套视频教程地址

配套视频教程

代码

代码

client.py
新增监听鼠标按下,抬起,移动功能
根据用户选择的作图工具,生成画图消息,发送给服务器
注意,这里不直接画出图形,而是等到服务器将画图消息再次转发回来,再进行绘图

import time
from threading import Thread

from drawing_tools import DrawingTools
from graphical_widgets import ExternalWindows
from network import MConnection
from whiteboard import Whiteboard


class Client(Thread,DrawingTools):

    # Tracks whether left mouse is down
    left_but = "up"

    # x and y positions for drawing with pencil
    x_pos, y_pos = None, None

    # Tracks x & y when the mouse is clicked and released
    x1_line_pt, y1_line_pt, x2_line_pt, y2_line_pt = None, None, None, None

    # class variable to keep track of the list of people who you have allowed you to delete their stuff
    # And the people you gave permission to delete theirs

    # list of objects that the whiteboard can draw
    Objects = {'line': 'L', 'oval': 'O', 'circle': 'C', 'rectangle': 'R', 'square': 'S', 'erase': 'E', 'drag': 'DR'}

    def __init__(self):
        my_connexion = MConnection()

        DrawingTools.__init__(self,my_connexion)
        Thread.__init__(self)
        self.setDaemon(True)
        self._init_mouse_action()
        self.text = "WOW"


        # This part refers to the class that allows user to exchange messages between themselves

    # The run handles the messages
    # As it recognizes the type it assigns to the proper class that will handle it!
    def run(self):
        while True:
            try:
                msg = self.my_connexion.receive_message()
                if( msg[0] in ['O', 'C', 'L', 'R', 'S', 'E', 'D', 'Z', 'T', 'DR']):
                    self.draw_from_message(msg)
            except ValueError:
                pass
            except IndexError:
                pass
            except ConnectionResetError:
                ExternalWindows.show_error_box("Server down please save your work")
                self.save_and_load.save()
                self.myWhiteBoard.destroy()



    def _init_mouse_action(self):
        self.drawing_area.bind("", self.motion)
        self.drawing_area.bind("", self.left_but_down)
        self.drawing_area.bind("", self.left_but_up)

    # ---------- CATCH MOUSE UP ----------
    # Here when the mouse is pressed we register it's first position and change the state of the button
    # The x_pos and y_pos are used for drawing with the mouse, since for mouse drawing we need to update
    # the positions as we draw
    # ---------- CATCH MOUSE UP ----------
    def left_but_down(self, event=None):
        self.left_but = "down"
        # Set x & y when mouse is clicked
        self.x1_line_pt = event.x
        self.y1_line_pt = event.y

        self.x_pos = event.x
        self.y_pos = event.y

        if self.drawing_tool == "eraser":
            self.delete_item(event)

        # Get tag of current object clicked object for dragging function
        try:
            self.user_last_object_clicked = self.drawing_area.gettags('current')[0]
            self.last_object_clicked = self.drawing_area.gettags('current')[1]
        except IndexError:
            self.user_last_object_clicked = None
            self.last_object_clicked = None

    # ---------- CATCH MOUSE UP ----------
    # When the mouse is released we save the position of the release in x2_line_pt and y2_line_pt
    # Once we have the 4 coordinates required for the drawing we call the drawing function,
    # the drawing function depends on the tool selected by the user
    # The tool is selected by pressing the widget on the main screen
    # Here we finally draw when the mouse is released
    # When the mouse is released we call object draw that is responsible for drawing a object
    # Or the text draw responsible for drawing text
    def left_but_up(self, event=None):
        self.left_but = "up"

        # Reset the line
        self.x_pos = None
        self.y_pos = None

        # Set x & y when mouse is released
        self.x2_line_pt = event.x
        self.y2_line_pt = event.y

        # If mouse is released and line tool is selected
        # draw the line
        if self.drawing_tool in ['line', 'oval', 'rectangle', 'circle', 'square', 'drag']:
            self.obj_draw(self.drawing_tool)
        elif self.drawing_tool == "text":
            self.text_draw()

    # ---------- CATCH MOUSE MOVEMENT ----------
    # Every time the mouse moves we call this function
    # If the selected tool is the pencil, we call the function to draw with the pencil
    def motion(self, event=None):
        if self.drawing_tool == "pencil":
            self.pencil_draw(event)
        if self.drawing_tool == "eraser":
            self.delete_item(event)

    # ---------- DRAW PENCIL ----------
    # Here we draw with the pencil, this function send's it's own messages because it is drawn differently than
    # the other tools
    # That is the main reason why the message is send through here and not through the send message function
    # We take the x.pos and y.pos which are initially set by pressing the mouse
    # And draw a line between this position and the mouse position 0.02 seconds later
    # We them update x.pos and y.pos with the mouse current position
    # Send it all through a message so it can be draw by the others

    def pencil_draw(self, event=None):
        if self.left_but == "down":
            # Make sure x and y have a value
            # if self.x_pos is not None and self.y_pos is not None:
            # event.widget.create_line(self.x_pos, self.y_pos, event.x, event.y, smooth=TRUE)
            time.sleep(0.02)
            msg = ('D', self.x_pos, self.y_pos, event.x, event.y, self.color, self.my_connexion.ID)
            self.my_connexion.send_message(msg)

            self.x_pos = event.x
            self.y_pos = event.y

    # This section is for drawing the different objects
    # The function we call depends on the different drawing tool we are used
    # Since we always have to send the same four coordinates, we only change the type of object we are drawing
    def obj_draw(self, obj):
        if obj in self.Objects:
            self.send_item(self.Objects[obj])

    # ---------- DRAW TEXT ----------
    # Here we draw text!
    # Since drawing text is different than the other types of message we send this is also done here
    # For text the messages are longer, since they require to send the text we want to draw
    def text_draw(self):
        if None not in (self.x1_line_pt, self.y1_line_pt):
            # Show all fonts available
            self.text = ExternalWindows.return_text()
            msg = ('T', self.text, self.x1_line_pt, self.y1_line_pt, self.color, self.my_connexion.ID)
            self.my_connexion.send_message(msg)

    # ------- Sending Messages for drawing ------------------------------------------------------
    # In this function we prepare the messages that will be sent by the connexion class
    # The messages here are a tuple of format:
    # (A,B,C,D,E,F) where A is the letters refering to the type, B,C,D,E are coordinates E is the color and F is the user ID
    # For the drag message things are different, first the type, them the object clicked, them the changes in coordinates
    def send_item(self, msg_type):
        if msg_type in ['L', 'C', 'O', 'R', 'S']:
            msg = (msg_type, self.x1_line_pt, self.y1_line_pt, self.x2_line_pt, self.y2_line_pt,
                   self.color, self.my_connexion.ID)
            self.my_connexion.send_message(msg)
        if msg_type in ['DR']:
            msg = (msg_type, self.last_object_clicked, self.x2_line_pt - self.x1_line_pt,
                   self.y2_line_pt - self.y1_line_pt)
            self.my_connexion.send_message(msg)

    # DELETE STUFF ####################################
    # Here we find the canvas id of whatever we clicked on
    # From the canvas id we find the tag we globally assigned for all users referring to that object
    # Afterwards we send that ID as a message to delete the objects in all possible canvas
    def delete_item(self, event):
        if self.left_but == "down":
            indice = 0
            try:
                canvas_item_id = self.drawing_area.find_overlapping(event.x+2,event.y+2,event.x-2,event.y-2)
                canvas_item_id = (max(canvas_item_id),)
                indice = len(self.drawing_area.gettags(canvas_item_id))
            except ValueError:
                pass
            if indice == 3:
                user, global_id,whatever = self.drawing_area.gettags(canvas_item_id)
                self.my_connexion.send_message(('Z', global_id))
            elif indice == 2:
                user, global_id = self.drawing_area.gettags(canvas_item_id)
                self.my_connexion.send_message(('Z', global_id))


if __name__ == '__main__':
    c = Client()
    c.start()
    c.show_canvas()

drawing_tools.py
根据收到的服务器绘图命令进行绘图

import tkinter.font as font
import math
from whiteboard import Whiteboard


class DrawingTools(Whiteboard):
    # ----------DEFINING METHODS FOR DRAWING FROM MESSAGE----------------
    Colors = {'b': 'blue', 'r': 'red', 'g': 'green', 'o': 'orange', 'y': 'yellow', 'c': 'cyan', 'p': 'purple1',
              'd': 'black', 's': 'snow'}
    line_width = 2

    def __init__(self, connexion):
        Whiteboard.__init__(self, connexion)

    # ---------- DRAW FROM MESSAGE----------

    # Here we draw from the received message
    # All messages here are tuples with the format (A,B,C,D,F)
    # And A, the first index of the tuple, contains the type of message
    # B,C usually contain coordinates from creation of the message
    # D contains the user that has draw the message
    # F contains an unique id for each message, this allows us to find the object when needed
    def draw_from_message(self, msg):
        _type = msg[0]
        if _type == 'O':
            self._draw_oval_from_message(msg)
        elif _type == 'C':
            self._draw_circle_from_message(msg)
        elif _type == 'L':
            self._draw_line_from_message(msg)
        elif _type == 'R':
            self._draw_rectangle_from_message(msg)
        elif _type == 'S':
            self._draw_square_from_message(msg)
        elif _type == 'E':
            self._draw_erase_from_message(msg)
        elif _type == 'D':
            self._draw_from_message(msg)
        elif _type == 'Z':
            self._delete_from_message(msg)
        elif _type == 'T':
            self._draw_text_from_message(msg)
        elif _type == 'DR':
            self._drag_from_message(msg)

    # Here we draw an oval from the received message, taking the four received points
    # The points are the sections 1 to 4 of the message
    # The color is section 5
    # Section 6 is the user who draw this object and section 7 is the ID we bind to this object
    def _draw_oval_from_message(self, msg):

        a, b, c, d = float(msg[1]), float(msg[2]), float(msg[3]), float(msg[4])
        color = msg[5]
        item = self.drawing_area.create_oval(a, b, c, d, fill=self.Colors[color], width=0)
        self.drawing_area.itemconfig(item, tags=(msg[6], msg[7]))

    # Here we draw an oval from the received message, taking the four received points
    # The points are the sections 1 to 4 of the message
    # The color is section 5
    # Section 6 is the user who draw this object and section 7 is the ID we bind to this object
    def _draw_circle_from_message(self, msg):
        a, b, c, d = float(msg[1]), float(msg[2]), float(msg[3]), float(msg[4])
        color = msg[5]
        radius = math.sqrt((a-c)**2+(b-d)**2)/2
        x_center = (a + c) / 2
        y_center = (b + d) / 2
        item = self.drawing_area.create_oval(x_center-radius, y_center-radius, x_center+radius, y_center+radius,
                                             fill=self.Colors[color], width=0)
        self.drawing_area.itemconfig(item, tags=(msg[6], msg[7]))

    # ---------- DRAW LINE FROM MESSAGE----------
    # Here we draw a line from the received message, taking the four received points
    # The points are the sections 1 to 4 of the message
    # The color is section 5
    # Section 6 is the user who draw this object and section 7 is the ID we bind to this object
    def _draw_line_from_message(self, msg):
        a, b, c, d = float(msg[1]), float(msg[2]), float(msg[3]), float(msg[4])
        color = msg[5]
        item = self.drawing_area.create_line(a, b, c, d, fill=self.Colors[color], width=self.line_width)
        self.drawing_area.itemconfig(item, tags=(msg[6], msg[7]))

    # ---------- DRAW RECTANGLE FROM MESSAGE----------
    # Here we draw a rectangle from the received message, taking the four received points
    # The points are the sections 1 to 4 of the message
    # The color is section 5
    # Section 6 is the user who draw this object and section 7 is the ID we bind to this object
    def _draw_rectangle_from_message(self, msg):
        a, b, c, d = float(msg[1]), float(msg[2]), float(msg[3]), float(msg[4])
        color = msg[5]
        item = self.drawing_area.create_rectangle(a, b, c, d, fill=self.Colors[color], width=0)
        self.drawing_area.itemconfig(item, tags=(msg[6], msg[7]))

    # ---------- DRAW RECTANGLE FROM MESSAGE----------
    # Here we draw a rectangle from the received message, taking the four received points
    # The points are the sections 1 to 4 of the message
    # The color is section 5
    # Section 6 is the user who draw this object and section 7 is the ID we bind to this object
    def _draw_square_from_message(self, msg):
        a, b, c, d = float(msg[1]), float(msg[2]), float(msg[3]), float(msg[4])
        color = msg[5]
        radius = (c+d-a-b)/2
        item = self.drawing_area.create_rectangle(a, b, a+radius, b+radius, fill=self.Colors[color], width=0)
        self.drawing_area.itemconfig(item, tags=(msg[6], msg[7]))

    # ---------- ERASE ALL -------------------------
    # This message erases everything in the whiteboard!
    # This message also erases all logs in the server to avoid a memory leak
    def _draw_erase_from_message(self, msg):
        for s in msg:
            item = self.drawing_area.find_withtag(s)
            self.drawing_area.delete(item)

    # ---------- PENCIL    -------------------------
    # Here we have the pencil, it draws a line from the 4 points received
    # As with the others the points are sections 1 to 4 and color is section 5
    # Section 6 is the user who draw this object and section 7 is the ID we bind to this object
    def _draw_from_message(self, msg):
        a, b, c, d = float(msg[1]), float(msg[2]), float(msg[3]), float(msg[4])
        color = msg[5]
        # print('[Network]Received point: (%s, %s)' % (a, b))
        # print('[Canvas]Drawing lines')
        item = self.drawing_area.create_line(a, b, c, d, fill=self.Colors[color], width=self.line_width)
        self.drawing_area.itemconfig(item, tags=(msg[6], msg[7]))

    # ---------- TEXT    -------------------------
    # The text message is a little bit more complicated since the sections are more complex
    # Here we need to send the text plus a pair of coordinates we are putting the text into!
    # But the text can have spaces between them, that is why we use the join function here
    # And the coordinates are numbered as bellow, a and b refer to the two coordinates
    # color refers to the color of the text
    def _draw_text_from_message(self, msg):
        write, a, b, color = " ".join(msg[1:-5]), float(msg[-5]), float(msg[-4]), msg[-3]
        text_font = font.Font(family='Helvetica',
                              size=20, weight='bold', slant='italic')
        item = self.drawing_area.create_text(a, b, fill=self.Colors[color], font=text_font, text=write)
        self.drawing_area.itemconfig(item, tags=(msg[-2], msg[-1]))

    # -------------------- DRAG --------------------------------
    # Here we drag objects from a received drag message
    # The drag message structure is (DR, msg_identification, newposition1, newposition2, user)
    # So we use the part 2 and 3 to move, and the msg_identification to find the object on the board
    # All objects are tagged with their message identification, which allows us to find them this way
    def _drag_from_message(self, msg):
        a, b = msg[2], msg[3]
        item = self.drawing_area.find_withtag(msg[1])
        self.drawing_area.move(item, a, b)

    # -------------------- DELETE --------------------------------
    # Delete messages are from the format ("E", msg_identification)
    # So since every object is tagged with their message identification
    # We find them using this tag and delete it!
    def _delete_from_message(self, msg):
        item = self.drawing_area.find_withtag(msg[1])
        self.drawing_area.delete(item)

server.py
将收到的绘图命令:
1.保存到logs列表里,以便转发给新上的客户端
2.转发某一客户端的绘图命令给所有客户端

import socket
import threading
import time

# Here we have the global variables
# The Clients consists of the list of thread objects clients
# The logs consists of all the messages send through the server, it is used to redraw when someone new connects
Clients = []
Logs = {}


# -------------------------------SERVER ----------------------------------------
# This is the Server Thread, it is responsible for listening to connexions
# It opens new connections as it is a thread constantly listening at the port for new requests
class Server:
    ID = 1

    def __init__(self, host, port):
        self.host = host
        self.port = port

        # Initialize network
        self.network = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.network.bind((self.host, self.port))
        self.network.listen(10)
        print("The Server Listens at {}".format(port))

        # Start the pinger
        threading.Thread(target=self.pinger).start()

    # Here we have the main listener
    # As somebody connects we send a small hello as confirmation
    # Also we give him an unique ID that will be able to differentiate them from other users
    # We send the server logs so the new user can redraw at the same state in the board
    # We send the list of connected users to construct the permission system
    def start(self):
        while True:
            connexion, infos_connexion = self.network.accept()
            print("Sucess at " + str(infos_connexion))
            connexion.send('HLO'.encode())
            time.sleep(0.1)

            # Send all ID's so user cannot repeat any id's
            msg = " "
            for client in Clients:
                msg = msg + " " + client.clientID
            connexion.sendall(msg.encode())
            time.sleep(0.1)

            # Here we start a thread to wait for the users nickname input
            # We do this so a server can wait for a nickname input and listen to new connections
            threading.Thread(target=self.wait_for_user_nickname, args=[connexion]).start()

    # This function was created just to wait for the users input nickname
    # Once it's done if sends the logs so the user can be up to date with the board
    # And finally it creates the Client Thread which will be responsible for listening to the user messages
    def wait_for_user_nickname(self, connexion):
        # Receive the chosen ID from user
        try:
            new_user_id = connexion.recv(1024).decode()

            for log in Logs:
                connexion.send(Logs[log])

            new_client = Client(connexion, new_user_id)
            new_client.load_users()
            Clients.append(new_client)
            Server.ID = Server.ID + 1
            new_client.start()
        except ConnectionResetError:
            pass
        except ConnectionAbortedError:
            pass


    # Function used by pinger
    # Sends a removal message to alert all users of the disconnection
    def announce_remove_user(self, disconnectedClient):
        msg = 'RE' + ' ' + str(disconnectedClient.clientID) + ' ' + 'Ø'
        msg = msg.encode('ISO-8859-1')
        print(threading.enumerate())
        for client in Clients:
            client.connexion.sendall(msg)

    # This is the pinger function, it is used to check how many users are currently connected
    # It pings all connections, if it receives a disconnection error, it does the following things:
    # 1.Sends a removal message to alert all users of the disconnection
    # 2.Removes client from list of clients to avoid sending messages to it again
    # 3.Sends the permission to delete the disconnected user stuff from the board!
    def pinger(self):
        while True:
            time.sleep(0.1)
            for client in Clients:
                try:
                    msg = "ß".encode('ISO-8859-1')
                    print('ß')
                    client.connexion.send(msg)
                except ConnectionResetError:
                    client.terminate()
                    Clients.remove(client)
                    self.announce_remove_user(client)
                except ConnectionAbortedError:
                    client.terminate()
                    Clients.remove(client)
                    self.announce_remove_user(client)


# -----------------------------------CLIENTS -------------------------------------
# This is the client thread, it is responsible for dealing with the messages from all different clients
# There is one thread for every connected client, this allows us to deal with them all at the same time
class Client():
    MessageID = 0

    def __init__(self, connexion, clientID):
        self.connexion = connexion
        self.clientID = clientID
        self._run = True

    def load_users(self):
        for client in Clients:
            msg = 'A' + ' ' + str(client.clientID) + ' ' + 'Ø'
            self.connexion.send(msg.encode('ISO-8859-1'))
            msg = 'A' + ' ' + str(self.clientID) + ' ' + 'Ø'
            client.connexion.send(msg.encode('ISO-8859-1'))

    def terminate(self):
        self._run = False

    def start(self):
        while self._run:
            try:
                # Here we start by reading the messages
                # Split according to the protocol
                msg = ""
                while True:
                    data = self.connexion.recv(1).decode('ISO-8859-1')
                    if data == "Ø":
                        break
                    msg = msg + data

                splitted_msg = msg.split()

                # Z is used to indicate message deletion so let's echo with a different function
                # Deletion messages are treated differently from normal messages
                # We don't keep track of them, and they must erase their log from the server
                # So we call a different function to deal with them
                if splitted_msg[0] == 'Z' or splitted_msg[0] == 'E':
                    self.echoes_delete(msg,splitted_msg)
                    continue
                # Here we have the drag messages
                elif splitted_msg[0] == 'DR':
                    self.update_position_in_logs(splitted_msg)
                    self.echoesAct3(msg)
                    continue

                elif splitted_msg[0] in ['O', 'C', 'L', 'R', 'S', 'E', 'D', 'Z', 'T']:
                    self.echoes(msg)

            # We pass the Connection Reset Error since the pinger will deal with it more effectivelly
            except ConnectionResetError:
                pass
            except ConnectionAbortedError:
                pass

    # Main echoes function!
    # This is responsible for echoing the message between the clients
    def echoesAct3(self, msg):
        msg = msg + " Ø"
        msg = msg.encode('ISO-8859-1')
        for client in Clients:
            client.connexion.sendall(msg)

    # Here we echo messages to all members of the network
    # Keep a dictionary of the messages to send it to new users
    # Update the message number for every message send
    def echoes(self, msg):
        msg = msg + " " + "m" + str(Client.MessageID)
        # We need to keep logs of all drawing messages to redraw them on new arriving clients
        Logs["m" + str(Client.MessageID)] = msg.encode('ISO-8859-1') + " Ø".encode('ISO-8859-1')
        Client.MessageID = Client.MessageID + 1
        # We do not want to log some types of messages. For instance like permission messages
        self.echoesAct3(msg)

    # Here we echo delete messages
    # We need to remove them from the message log
    # And finally echo the message to all members of the server
    def echoes_delete(self, msg, splitMsg):
        try:
            Logs.pop(splitMsg[1])
        except KeyError:
            pass
        self.echoesAct3(msg)

    # Here we update the position of a draged object in the server
    def update_position_in_logs(self, splitMsg):

        # We retrieve the original message
        original_message = Logs[splitMsg[1]]
        original_message = original_message[:-1]
        original_message = original_message.decode('ISO-8859-1')
        original_message = original_message.split()

        # Them add to the coordinates according to the drag!
        # The position of the coordinates of each message is different for different types of message
        # This requires us to alternate the coordinates differently according to the type
        if original_message[0] in ['L', 'C', 'O', 'R', 'S', 'D']:
            original_message[1] = str(int(original_message[1]) + int(splitMsg[2]))
            original_message[3] = str(int(original_message[3]) + int(splitMsg[2]))
            original_message[2] = str(int(original_message[2]) + int(splitMsg[3]))
            original_message[4] = str(int(original_message[4]) + int(splitMsg[3]))
            original_message = " ".join(original_message)
        elif original_message[0] in ['T']:
            original_message[2] = str(int(original_message[2]) + int(splitMsg[2]))
            original_message[3] = str(int(original_message[3]) + int(splitMsg[3]))
            original_message = " ".join(original_message)

            # Rewrite the log
        original_message = original_message + " Ø"
        Logs[splitMsg[1]] = original_message.encode('ISO-8859-1')

if __name__ == "__main__":
    host = ''
    port = 5000
    server = Server(host, port)
    server.start()

whiteboard.py

    #################################GETIING THE TEXT##################################################################
    # This part gets text from the user before printing it!
    # It refers to the text functionality of the text button widget on the top
    def get_text_from_user(self):
        self.drawing_tool = 'text'
        ExternalWindows.get_text_from_user()


    # ----------------------------- Erase All Function -----------------------------------------------------------------
    # Since this is an extra functionality i will explain it more extensively
    # This function finds every object tagged with the user nickname (user ID)
    # And also every single object tagged with an user which is in his list of permissions
    # Since every user is in it's own list of permissions, we only need to check the list of permissions
    # Disconnected users loose their privileges!
    # Them it sends a delete message for every one of them!
    def erase_all(self):
        A = self.drawing_area.find_all()
        for a in A:
            a = self.drawing_area.gettags(a)
            msg = ("E",a[1])
            self.my_connexion.send_message(msg)

你可能感兴趣的:(02.python电子白板实现)