在Python中,socket模块允许我们使用TCP和UDP协议来实现网络通信。在前面第17章【网络编程】中已经详细介绍了如何使用TCP和UDP协议来实现网络通信。本节教程中将通过UDP通信协议,使用Python编写一个基于socket模块的多功能聊天程序,包含Tkinter编写的GUI图形化聊天界面,主要功能包括账号注册和登录,用户成功登录聊天室后可以查看在线用户,并和聊天室内的其他在线用户聊天,包含私聊和群聊,并运用了Python的threading多线程库,利用线程提高聊天室的效率。
1、聊天程序功能介绍
登录和注册功能:在tkinter界面中,用户可以输入用户名和密码,点击按钮进行登录或注册。注册成功后,会通过Python的SQLite3模块,在工作目录下创建一个chat.db数据库文件,用于保存用户的账号和密码。SQLite3是Python中一个轻量级的嵌入式数据库,无需安装独立服务,导入SQLite3模块即可使用数据库功能,所有数据存储在一个以“.db”为后缀名的文件中。登录过程,如果勾选“记住密码”,则会在目录下自动创建user.json文件,用于保存登录信息,下次打开程序时将自动输入保存的账号和密码。
显示在线聊天用户:服务器端维护一个在线用户列表,并将其发送给所有的客户端。客户端收到更新后,在GUI聊天程序上实时更新在线聊天用户列表。
群聊和私聊功能:当用户在tkinter的文本框中输入消息并点击“群发”按钮时,客户端通过socket将消息发送到服务器。服务器接收到消息后,转发给所有在线的客户端。如果想要“私聊”在线某个用户,需提前在右边的在线聊天用户列表中选择“私聊”对象才能发送“私聊”信息。点击发送“私聊”信息后,只有两个私聊的对象才能看到各自的私聊信息,其他在线聊天用户无法看到私聊信息。
多线程:为了确保聊天用户界面的响应性,通常需要在客户端和服务器端使用多线程。主线程负责处理GUI事件,而另一个线程处理socket通信,这样就不会因为等待网络I/O而阻塞用户界面。
2、聊天程序实现思路
我们的目标是通过Tkinter的GUI图形界面开发一个多功能聊天运用程序。聊天程序分为两部分代码,一部分是服务器端(server.py),另一部分是客户端(client.py),服务器端通过接收用户数据并转发给所有客户端,客户端则同时进行发送和接收信息,以实现群聊和私聊功能。
启动聊天程序,先运行服务器端server.py代码,再运行客户端的client.py代码。如果要模拟多个用户登录到聊天室,可以打开Pyhint编辑器,在代码框中输入client.py代码后,多次点击“运行”按钮,就可以模拟多个用户登录聊天程序,或者client.py代码文件多复制几份,再分别运行即可。
3、聊天界面演示
(1)服务器端界面
(2)登录界面
(3)注册界面
(4)聊天界面
4、代码实现
(1)下载背景图片
bg-image.jpg:
首先,下载以上图片并命名为“bg-image.jpg",再打开第2章【安装和运行Python】教程里面介绍的Pyhint编辑器,把“bg-image.jpg"文件存放到Pyhint\Learn-Python\test文件夹下面。
(2)服务器server.py文件
在Pyhint\Learn-Python\test文件夹下面新建一个server.py文件,输入以下代码后保存。然后,使用Pyhint编辑器打开server.py文件并点击“运行”按钮,程序会弹出“聊天服务器正在运行窗口”,最后等待客户端的连接请求(注意:不要关闭该聊天服务器窗口)。
server.py:
动手练一练:
import json, time
from socket import *
from tkinter import messagebox
import tkinter as tk
# 聊天服务器设计
def MainServer():
# 通过UDP协议创建一个socket对象
sock = socket(AF_INET, SOCK_DGRAM)
# 绑定ip地址和端口,建议不要用80、8080等其它程序默认的端口
sock_address = ('127.0.0.1', 9999)
sock.bind(sock_address) # 绑定地址和端口
print('开启UDP服务器 %s:%s...', sock_address[0], sock_address[1])
# 创建一个空字典
user = {}
print('----------UDP服务器已启动-----------')
print('绑定UDP' + str(sock_address))
print('等待客户端数据...')
while True:
try:
# 收到客户端的消息分别存放在data和adres两个变量里
data, adres = sock.recvfrom(1024)
json_data = json.loads(data.decode('utf-8'))
print(json_data)
if json_data['message_type']=="init_message":
# address不等于adres时执行下面的代码
if json_data['content'] not in user:
user[json_data['content']]=adres
user_list=[i for i in user.keys()]
json_data['online_user'] = f'{user_list}'
json_str = json.dumps(json_data, ensure_ascii=False)
for address in user.values():
# 发送data和address到客户端
sock.sendto(json_str.encode('utf-8'), address)
print(json_data['content'] + '进入了聊天室')
print(f'当前在线用户{user_list}')
elif json_data['message_type']=="leave_message":
# address不等于adres时执行下面的代码
if json_data['content'] in user:
user.pop(json_data['content'])
user_list = [i for i in user.keys()]
for address in user.values():
# 发送data和address到客户端
sock.sendto(data, address)
print(json_data['content']+'离开了聊天室')
print(f'当前在线用户{user_list}')
continue
elif json_data['chat_type'] == "normal":
if json_data['message_type'] != "file":
for address in user.values():
if address != adres:
# 发送data和address到客户端
sock.sendto(data, address)
elif json_data['chat_type'] == "private":
recv_user = json_data['recv_user']
send_user = json_data['send_user']
if json_data['message_type'] != "file-data":
# 发送data和address到客户端
sock.sendto(data, user[recv_user])
else:
filename = json_data['file_name']
data_size = int(json_data['file_length'])
print('文件大小为' + str(data_size))
recvd_size = 0
data_total = b''
j = 0
while not recvd_size == data_size:
j = j + 1
if data_size - recvd_size > 1024:
data, adres = sock.recvfrom(1024)
recvd_size += len(data)
print('第' + str(j) + '次收到文件数据')
else: # 最后一片
data, adres = sock.recvfrom(1024)
recvd_size = data_size
print('第' + str(j) + '次收到文件数据')
data_total += data
fhead = len(data_total)
information = {}
information["chat_type"] = "private"
information["message_type"] = "file-data"
information["file_length"] = str(fhead)
information["file_name"] = json_data["file_name"]
information["send_user"] = json_data['send_user']
information["recv_user"] = json_data['recv_user']
information["content"] = ''
jsondata = json.dumps(information, ensure_ascii=False)
sock.sendto(jsondata.encode('utf-8'), user[recv_user])
print('开始发送文件数据...')
for i in range(len(data_total) // 1024 + 1):
# 防止数据发送太快,服务器来不及接收出错
time.sleep(0.0000000001)
# 判断是否到最后
if 1024 * (i + 1) > len(data_total):
# 最后一次剩下的数据传给对方
sock.sendto(data_total[1024 * i:], user[recv_user])
print('第' + str(i+1) + '次发送文件数据')
else:
sock.sendto(data_total[1024 * i:1024 * (i + 1)], user[recv_user])
print('第' + str(i+1) + '次发送文件数据')
now_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
print('%s: "%s" 文件发送完成! from %s:%s [目标:%s] at %s' % (send_user, filename, adres[0], adres[1], user[recv_user], now_time))
except ConnectionResetError:
print('连接出现意外错误')
# 当前文件作为脚本直接执行
if __name__ == "__main__":
# 聊天服务器窗口界面的实现
app = tk.Tk()
app.geometry("300x100")
app.title("聊天服务器窗口")
app.resizable(0, 0)
server_label = tk.Label(app, text="聊天服务器正在运行中,请勿关闭", width=40)
server_label.grid(column=0, row=0, pady=5, padx=2)
try:
messagebox.showinfo("完成", "聊天服务器已开启!")
MainServer()
except Exception as e:
messagebox.showerror("出现错误", str(e))
(3)客户端client.py文件
接着,同样在Pyhint\Learn-Python\test文件夹下面新建一个客户端client.py文件,输入以下代码后保存。然后,使用Pyhint编辑器打开client.py文件并点击“运行”按钮,程序就会弹出“登录”窗口,如果要模拟多个用户登录到聊天室,可以多次点击“运行”按钮,就可以模拟多个用户登录聊天程序,或者client.py代码文件多复制几份,再分别运行即可。
client.py:
动手练一练:
# 聊天程序客户端代码
import os, sys, json, sqlite3, time, threading, ctypes
from tkinter import scrolledtext
from tkinter.ttk import Treeview
import tkinter as tk
from tkinter import messagebox
from PIL import Image, ImageTk
from socket import *
# 获取当前脚本文件所在的目录
default_directory = os.path.dirname(os.path.abspath(__file__))
# 将当前脚本文件所在的目录设置为工作目录
os.chdir(default_directory)
# 创建登录窗口页面
class UserLogin(object):
def __init__(self, Register,Chat,master=None):
# 定义内部变量app
self.app = master
self.app.title('登录窗口')
self.Register=Register
self.Chat=Chat
# 设置登录窗口居中
screen_width = self.app.winfo_screenwidth()
screen_height = self.app.winfo_screenheight()
app_width = 450
app_height = 400
x = (screen_width - app_width) / 2
y = (screen_height - app_height) / 2
self.app.geometry("%dx%d+%d+%d" % (app_width, app_height, (x + 160), y))
# 设置窗口不可放大或缩小
self.app.resizable(0, 0)
# 告诉操作系统使用程序自身的dpi适配
ctypes.windll.shcore.SetProcessDpiAwareness(1)
# 获取屏幕的缩放因子
ScaleFactor = ctypes.windll.shcore.GetScaleFactorForDevice(0)
# 设置程序缩放
self.app.tk.call('tk', 'scaling', ScaleFactor / 75)
self.makelogin()
# 登录窗口布局
def makelogin(self):
self.frame2 = tk.Frame(self.app)
self.frame2.pack()
self.frame1 = tk.Frame(self.app)
self.frame1.pack(pady=10)
# 加载背景图片
self.benner_image = 'bg-image.jpg'
self.pic = Image.open(self.benner_image)
self.login_benner = ImageTk.PhotoImage(self.pic)
self.imageLabel = tk.Label(self.frame2, image=self.login_benner)
self.imageLabel.pack()
# 创建用户和密码标签
self.label_user = tk.Label(self.frame1, text="用户名:")
self.label_user.grid(row=0, column=0, pady=10)
self.label_password = tk.Label(self.frame1, text="密 码:")
self.label_password.grid(row=1, column=0)
# 创建用户名输入框
self.var_user_name = tk.StringVar()
self.entry_name = tk.Entry(self.frame1, textvariable=self.var_user_name)
self.entry_name.grid(row=0, column=1)
self.entry_name.focus_set() # 获得焦点
# 创建密码输入框
self.var_user_password = tk.StringVar()
self.entry_password = tk.Entry(self.frame1, textvariable=self.var_user_password, show="*")
self.entry_password.grid(row=1, column=1)
self.saved_message()
self.frame3 = tk.Frame(self.app)
self.frame3.pack()
self.rd_login = tk.IntVar()
self.rd_password = tk.IntVar()
self.checkboxLogin = tk.Checkbutton(self.frame3, text="自动登录", variable=self.rd_login)
self.checkboxpassword = tk.Checkbutton(self.frame3, text="记住密码", variable=self.rd_password)
self.la = tk.Label(self.frame3, width=5)
self.la.grid(row=0, column=0)
self.checkboxLogin.grid(row=0, column=1)
self.checkboxpassword.grid(row=0, column=2)
# 登录按钮绑定键盘的回车键
self.app.bind('<Return>', self.check_login)
self.login_button = tk.Button(self.frame3, text=" 登录 ", command=lambda: self.check_login())
self.login_button.grid(row=1, column=1, pady=5)
self.quit_button = tk.Button(self.frame3, text=" 退出 ", command=sys.exit)
self.quit_button.grid(row=1, column=2)
# 底部布局
self.frame4 = tk.Frame(self.app)
self.frame4.pack(side='bottom')
self.register_button = tk.Button(self.frame4, text=" 注册账号", relief=tk.FLAT, command=self.close_login)
self.register_button.pack(side='left', anchor='s')
self.la2 = tk.Label(self.frame4, width=50)
self.la2.pack()
self.tsLabel2 = tk.Label(self.frame4, text="聊天登录界面版本:V1.0", fg="red")
self.tsLabel2.pack(side='right', anchor='s', pady=5)
# 如果登录时勾选“记住密码”,则会在目录下自动创建一个user.json文件
# user.json文件用于保存登录信息,下次打开程序时将自动输入保存的账号和密码。
def red_msg(self):
if self.rd_password.get() == 1:
# 创建新的JSON
new_user = {
'username': self.var_user_name.get(),
'password': self.var_user_password.get()
}
with open('user.json', 'w') as wp:
json.dump(new_user, wp)
def saved_message(self):
self.saved_name = ''
self.saved_password = ''
if os.path.exists('user.json'):
with open('user.json', 'r') as fp:
json_file = json.load(fp)
json_str = json.dumps(json_file)
json_date = json.loads(json_str)
self.saved_name = json_date['username']
self.saved_password = json_date['password']
if self.saved_name != '':
self.entry_name.insert(tk.END, self.saved_name)
self.entry_password.insert(tk.END, self.saved_password)
def close_login(self):
self.frame1.destroy()
self.frame2.destroy()
self.frame3.destroy()
self.frame4.destroy()
# 密码验证成功,则加载主窗体模块界面
self.Register(UserLogin,self.Chat,self.app)
def check_login(self, *args):
global user_name
self.user_name = self.var_user_name.get()
self.user_password = self.var_user_password.get()
try:
# 如果数据库db文件不存在,会自动在当前目录创建:
conn = sqlite3.connect('chat.db')
# 创建一个Cursor:
cursor = conn.cursor()
# 执行一条SQL语句,创建user表:
cursor.execute('create table if not exists user(username varchar(20),password varchar(20))')
except Exception as e:
messagebox.showerror("出现错误", str(e))
if self.user_name == '' or self.user_password == '':
messagebox.showwarning(title='提示', message="用户名密码不能为空")
else:
# 执行查询语句:
cursor.execute('select username from user')
values = cursor.fetchall()
cursor.execute('select password from user where username="%s"' % self.user_name)
# 获得查询结果集:
values2 = cursor.fetchall()
userList = []
for i in values:
userList.append(i[0])
if self.user_name in userList:
if self.user_password == values2[0][0]:
messagebox.showinfo(title='提示', message='登录成功,欢迎进入聊天程序!')
# 解绑回车键事件
self.app.unbind('<Return>')
self.red_msg()
self.frame1.destroy()
self.frame2.destroy()
self.frame3.destroy()
self.frame4.destroy()
self.Chat(self.user_name)
else:
messagebox.showerror(title='提示', message="登录密码错误!")
else:
messagebox.showerror(title='提示', message="没有该用户名,请先注册!")
cursor.close()
conn.close()
return 'break'
# 创建注册窗口页面
class Register(object):
def __init__(self, UserLogin,Chat,master=None):
# 定义内部变量app
self.app = master
self.app.title('注册窗口')
self.Login=UserLogin
self.Chat=Chat
# 设置窗口居中
screen_width = self.app.winfo_screenwidth() # 计算水平距离
screen_height = self.app.winfo_screenheight() # 计算垂直距离
app_width = 450 # 宽
app_height = 400 # 高
x = (screen_width - app_width) / 2
y = (screen_height - app_height) / 2
self.app.geometry("%dx%d+%d+%d" % (app_width, app_height, (x + 160), y))
# 设置窗口不可放大或缩小
self.app.resizable(0, 0)
self.makeregister()
def makeregister(self):
self.frame2 = tk.Frame(self.app)
self.frame2.pack()
self.frame1 = tk.Frame(self.app)
self.frame1.pack(pady=10)
# 加载背景图片
self.benner_image = 'bg-image.jpg'
self.pic = Image.open(self.benner_image)
self.register_benner = ImageTk.PhotoImage(self.pic)
self.imageLabel = tk.Label(self.frame2, image=self.register_benner)
self.imageLabel.pack()
# 创建用户和密码标签
self.label_user = tk.Label(self.frame1, text="用户名:")
self.label_user.grid(row=0, column=0)
self.label_password = tk.Label(self.frame1, text="密 码:")
self.label_password.grid(row=1, column=0, pady=5)
self.label_repassword = tk.Label(self.frame1, text="确认密码:")
self.label_repassword.grid(row=2, column=0)
# 创建用户名输入框
self.var_user_name = tk.StringVar()
self.entry_name = tk.Entry(self.frame1, textvariable=self.var_user_name)
self.entry_name.grid(row=0, column=1)
self.entry_name.focus_set() # 获得焦点
# 自带验证功能,check_user自定义函数
self.make_check1 = self.entry_name.register(self.check_user)
self.entry_name.config(validate='all', validatecommand=(self.make_check1, '%P'))
# 创建密码输入框
self.var_user_password = tk.StringVar()
self.entry_password = tk.Entry(self.frame1, textvariable=self.var_user_password, show="*")
self.entry_password.grid(row=1, column=1)
self.make_check2 = self.entry_password.register(self.check_password)
self.entry_password.config(validate='all', validatecommand=(self.make_check2, '%d', '%S'))
# 创建确认密码输入框
self.var_user_repassword = tk.StringVar()
self.entry_repassword = tk.Entry(self.frame1, textvariable=self.var_user_repassword, show="*")
self.entry_repassword.grid(row=2, column=1)
self.frame3 = tk.Frame(self.app)
self.frame3.pack()
# 设置注册按钮绑定键盘的回车键
self.app.bind('<Return>', self.user_register)
self.register_button = tk.Button(self.frame3, text=" 注册 ", command=lambda: self.user_register())
self.register_button.grid(row=1, column=1, pady=5, padx=35)
self.quit_button = tk.Button(self.frame3, text=" 退出 ", command=sys.exit)
self.quit_button.grid(row=1, column=2)
# 底部布局
self.frame4 = tk.Frame(self.app)
self.frame4.pack(side='bottom')
self.register_button = tk.Button(self.frame4, text=" 返回登录", relief=tk.FLAT, bg='#f0f0f0', command=self.close_register)
self.register_button.pack(side='left', anchor='s')
self.la2 = tk.Label(self.frame4, width=150)
self.la2.pack()
self.tsLabel2 = tk.Label(self.frame4, text="聊天注册界面版本:V1.0", fg="red")
self.tsLabel2.pack(side='right', anchor='s', pady=5)
def close_register(self):
self.frame1.destroy()
self.frame2.destroy()
self.frame3.destroy()
self.frame4.destroy()
# 密码验证成功,则加载主窗体模块界面
self.Login(Register,self.Chat,self.app)
def check_user(self, what):
if len(what) > 8:
self.la2.config(text='用户名不能超过8个字符', fg='red')
return False
return True
def check_password(self, why, what):
if why == '1':
if what not in '0123456789':
self.la2.config(text='密码只能是数字', fg='red')
return False
return True
def user_register(self, *args):
user_name = self.var_user_name.get()
user_password = self.var_user_password.get()
user_repassword = self.var_user_repassword.get()
if user_name == '' or user_password == '' or user_repassword == '':
messagebox.showwarning(title='提示', message="用户名密码不能为空")
else:
# 如果数据库db文件不存在,会自动在当前目录创建:
conn = sqlite3.connect('chat.db')
# 创建一个Cursor:
cursor = conn.cursor()
# 执行一条SQL语句,创建user表:
cursor.execute('create table if not exists user(username varchar(20),password varchar(20))')
# 查询用户名是否存在
cursor.execute('select username from user')
values = cursor.fetchall()
userList = []
for i in values:
userList.append(i[0])
if user_name in userList:
messagebox.showwarning('提示', '该用户名已存在,请选择其它用户名!')
else:
if user_password == user_repassword:
# 插入数据
cursor.execute("insert into user (username,password) values (?,?)", (user_name, user_repassword))
if (messagebox.showinfo('提示', '注册成功!')):
self.app.unbind('<Return>') # 解绑回车键事件
self.close_register()
else:
messagebox.showerror('提示', '两次输入的密码不一致!')
# 关闭Cursor:
cursor.close()
# 提交事务:
conn.commit()
# 关闭Connection:
conn.close()
return 'break'
# 通过UDP协议创建一个socket对象
sock = socket(AF_INET, SOCK_DGRAM)
# 绑定ip地址和端口,建议不要用80、8080等其它程序默认的端口
server = ('127.0.0.1', 9999)
class ChatClient():
def __init__(self, name, scr1, scr2, online_list):
self.name = name
self.scr1 = scr1
self.scr2 = scr2
self.online_list = online_list
def to_send(self, *args):
self.msg = self.scr2.get(1.0, 'end').strip()
self.send(self.msg)
if self.msg != '':
now_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
self.scr1.configure(state=tk.NORMAL)
self.scr1.insert("end", "{} {}:\n".format(self.name, now_time), 'green')
self.scr1.insert("end", self.msg + '' + '\n')
self.scr1.see(tk.END)
self.scr2.delete('1.0', 'end')
self.scr1.config(state=tk.DISABLED)
return "break"
def private_to_send(self, *args):
self.msg = self.scr2.get(1.0, 'end').strip()
self.scr2.delete('1.0', 'end')
send_type, send_file = self.private_send(self.msg)
if self.msg != '' and self.online_list.selection() != ():
now_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
self.scr1.configure(state=tk.NORMAL)
tar_name = self.online_list.selection()[0]
self.scr1.insert("end", "{} {}:\n".format(self.name, now_time), 'green')
if send_type == 'text':
self.scr1.insert("end", f'{self.msg}')
self.scr1.insert("end", f' |私聊{tar_name}\n', 'zise')
self.scr1.see(tk.END)
self.scr2.delete('1.0', 'end')
self.scr1.config(state=tk.DISABLED)
def send_file(self, fileType, fileName, filePath):
information = {}
information["chat_type"] = "private"
information["message_type"] = "ask-file"
information["file_type"] = fileType
information["file_name"] = fileName
information["send_user"] = self.name
information["recv_user"] = self.online_list.selection()[0]
information["content"] = filePath
jsondata = json.dumps(information, ensure_ascii=False)
sock.sendto(jsondata.encode('utf-8'), server)
def cut_data(self, fhead, data):
for i in range(fhead // 1024 + 1):
# 防止数据发送太快,服务器来不及接收出错
time.sleep(0.0000000001)
# 判断是否到最后
if 1024 * (i + 1) > fhead:
# 最后一次剩下的数据传给对方
sock.sendto(data[1024 * i:], server)
else:
sock.sendto(data[1024 * i:1024 * (i + 1)], server)
def success_send(self, recv_user, filename):
self.scr1.configure(state=tk.NORMAL)
self.scr1.insert("end", f'{filename}', 'shengzise')
self.scr1.insert("end", f' |已成功发送给{recv_user}\n', 'zise')
self.scr1.see(tk.END)
self.scr1.config(state=tk.DISABLED)
def send(self, msg):
if msg != '':
information = {}
information["chat_type"] = "normal"
information["message_type"] = "text"
information["send_user"] = self.name
information["content"] = msg.strip()
jsondata = json.dumps(information, ensure_ascii=False)
sock.sendto(jsondata.encode('utf-8'), server)
def private_send(self, msg):
if self.online_list.selection() == ():
messagebox.showwarning(title='提示', message='你没有选择发送对象!')
else:
information = {}
information["chat_type"] = "private"
information["message_type"] = "text"
information["send_user"] = self.name
information["recv_user"] = self.online_list.selection()[0]
information["content"] = msg.strip()
jsondata = json.dumps(information, ensure_ascii=False)
sock.sendto(jsondata.encode('utf-8'), server)
return 'text', ''
def message_receive(self):
information = {}
information["message_type"] = "init_message"
information["content"] = self.name
json_str = json.dumps(information, ensure_ascii=False)
sock.sendto(json_str.encode('utf-8'), server)
while True:
data = sock.recv(1024)
source = data.decode('utf-8')
json_data = json.loads(data.decode('utf-8'))
now_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())
self.scr1.configure(state=tk.NORMAL)
if json_data['message_type'] == "init_message":
self.scr1.insert("end", f'欢迎{json_data["content"]}加入聊天室' + '\n', 'red')
user_list = eval(json_data["online_user"])
for user in user_list:
if str(user) not in self.online_list.get_children() and str(user) != self.name: # 判断如果不在列表中
self.online_list.insert('', 2, str(user), text=str(user).center(24), values=("1"), tags='其他用户')
elif json_data['message_type'] == "leave_message":
self.scr1.insert("end", f'{json_data["content"]}离开了聊天室...' + '\n', 'red')
if json_data["content"] in self.online_list.get_children():
self.online_list.delete(json_data["content"])
elif json_data['chat_type'] == "normal":
if json_data['message_type'] == "text":
self.scr1.insert("end", "{} {}:\n".format(json_data['send_user'], now_time), 'green')
self.scr1.insert("end", json_data['content'] + '\n')
elif json_data['chat_type'] == "private":
if json_data['message_type'] == "text":
self.scr1.insert("end", "{} {}:\n".format(json_data['send_user'], now_time), 'green')
self.scr1.insert("end", json_data['content'])
self.scr1.insert("end", f' |收到{json_data["send_user"]}的私聊消息\n', 'zise')
elif json_data['message_type'] == "Recv_msg":
if json_data['Recv_msg'] == "true":
recv_user = json_data['recv_user']
filename = json_data['file_name']
self.success_send(recv_user, filename)
class ChatWindow():
def __init__(self, app):
self.app = app
def chat_finish(self):
flag = messagebox.askokcancel(title='提示', message='你确定要退出聊天室吗?')
if flag:
information = {}
information["message_type"] = "leave_message"
information["content"] = self.name
jsondata = json.dumps(information, ensure_ascii=False)
sock.sendto(jsondata.encode('utf-8'), server)
sys.exit(0)
def chat(self, usename):
self.name = usename
self.app.title('聊天室--用户名:' + self.name)
screen_width = self.app.winfo_screenwidth() # 计算水平距离
screen_height = self.app.winfo_screenheight() # 计算垂直距离
app_width = 960 # 宽
app_height = 620 # 高
x = (screen_width - app_width) / 2
y = (screen_height - app_height) / 2
self.app.geometry("%dx%d+%d+%d" % (app_width, app_height, (x + 160), y))
# 设置窗口不可放大或缩小
self.app.resizable(0, 0)
# 告诉操作系统使用程序自身的dpi适配
ctypes.windll.shcore.SetProcessDpiAwareness(1)
# 获取屏幕的缩放因子
ScaleFactor = ctypes.windll.shcore.GetScaleFactorForDevice(0)
# 设置程序缩放
self.app.tk.call('tk', 'scaling', ScaleFactor / 75)
# 设置窗口不可放大或缩小
self.app.resizable(1, 1)
self.scr1 = scrolledtext.ScrolledText(self.app, height=18, font=('黑体', 13))
# 设置控件字体颜色
self.scr1.tag_config('green', foreground='#008C00', font=('微软雅黑', 10))
self.scr1.tag_config('red', foreground='red')
self.scr1.tag_config('zise', foreground='#aaaaff')
self.scr1.tag_config('shengzise', foreground='#9d4cff')
self.scr1.tag_config('chengse', foreground='#ff7f27')
# 创建树形列表
self.online_list = Treeview(self.app, height=30, show="tree")
self.online_list.insert('', 0, 'online_user', text='在线用户'.center(10, '-'), values=("1"), tags='在线用户')
if self.name not in self.online_list.get_children(): # 如果不在列表中
self.online_list.insert('', 1, 'me', text=self.name.center(24), values=("1"), tags='自己') # 自己在列表中颜色为红色
self.online_list.grid(row=1, column=2, rowspan=7, sticky=tk.N)
# 设置控件字体颜色
self.online_list.tag_configure('在线用户', foreground='#aa5500', font=('黑体', 13))
self.online_list.tag_configure('自己', foreground='red', font=('微软雅黑', 10))
self.online_list.tag_configure('其他用户', font=('微软雅黑', 10))
self.scr1.grid(row=1, column=1)
tk.Label(self.app, text='').grid(row=2)
tk.Label(self.app, text='请输入你要发送的聊天内容:').grid(row=3, column=1)
self.scr2 = scrolledtext.ScrolledText(self.app, height=6, font=('黑体', 13))
self.scr2.grid(row=4, column=1)
tk.Label(self.app, text='').grid(row=5)
tf = tk.Frame(self.app)
tf.grid(row=6, column=1)
chat = ChatClient(self.name, self.scr1, self.scr2, self.online_list)
tk.Button(tf, text=' 群发 ', command=chat.to_send).grid(row=1, column=0, padx=20)
tk.Button(tf, text=' 私聊 ', command=chat.private_to_send).grid(row=1, column=1, padx=20)
tk.Button(tf, text=' 退出 ', command=self.chat_finish).grid(row=1, column=2, padx=20)
thread = threading.Thread(target=chat.message_receive, args=(),
daemon=True)
thread.start()
self.app.protocol("WM_DELETE_WINDOW", self.chat_finish)
# 当前模块直接被执行
if __name__ == '__main__':
# 聊天窗口界面的实现
app = tk.Tk()
Main = ChatWindow(app)
UserLogin(Register, Main.chat, app)
# 运行主循环
app.mainloop()