02.python电子白板实现
2020-01-02 本文已影响0人
__豆约翰__
配套视频教程地址
代码
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("<Motion>", self.motion)
self.drawing_area.bind("<ButtonPress-1>", self.left_but_down)
self.drawing_area.bind("<ButtonRelease-1>", 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)