目的
博文目的:分享这个不成熟的小案例,希望能给还在tkinter控件整合上没有思路的同志一些参考。
案例目的:在无法访问互联网的情况下,实现一个局域网内机器传输文件和文本的工具
效果图
实现
环境:Python 3.7 windows
上面是我在本机运行两个同样的脚本,进行文本或文件互发的效果,这里运用到下面几个点:
- 信息对话框和文件选择框
- 控件值的获取和值的更新
- 界面的大小变化(比较原始界面和点击setup之后延展出来的界面)
- 基本socket服务端与客户端
- 线程规避tkinter卡顿问题
内容发送与接收
关于内容的发送与接收,脚本主要运用socket套接字实现客户端或服务器端,点击按钮[启动监听]的过程实际上是程序创建一个监听指定端口的服务端,而发送内容的过程则是向目标程序的服务端发起交互的过程
另一个问题是对于文件和文本的接收处理方式是不同的,如果接收到的是文本,我们要将文本内容插入文本框显示,如果是文件则要做保存文件的操作。因此我们需要让服务端socket判断接收到的内容属于哪种类型,需要在客户端socket发送时带上类型标志位,服务端socket接收到时就可以根据标志位做不同的行为。
下面是客户端在发送文本和文件时的区别
# 发送文本时:
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect((host,port))
client.send(bytes('#msg#',encoding='utf-8'))
client.send(bytes(msg,encoding='utf-8'))
client.close()
# 发送文件时:
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect((host,port))
client.send(bytes(f'#file#{filename}#',encoding='utf-8'))
client.send(content)
可以看到在发送内容之前,我附带上了类型字段,这样我们的服务端socket接收到内容时就可以做下面的解析
recv_data = conn.recv(1024)
if recv_data[1] == 102:
endindex = recv_data.find(b'#',6)
filename = recv_data[6:endindex].decode('utf-8')
answer = messagebox.askyesno("Recv File",f"收到来自{addr[0]}主机的文件\n{filename}\n是否接收?")
if not answer:
continue
savepath = os.path.join(self.path,filename)
with open(savepath,"wb") as f:
f.write(recv_data[endindex+1:])
while recv_data:
recv_data = conn.recv(1024)
f.write(recv_data)
messagebox.showinfo("Recv File","文件接收成功!")
elif recv_data[1] == 109:
msg = recv_data[5:].decode('utf-8').strip()
self.recvbox.insert(f'{self.index}.0',f'From:{addr[0]}\n')
self.recvbox.insert(f'{self.index+1}.0',f"{msg}\n")
self.index +=2
conn.close()
注:脚本中的102和109对应的是f和m的ascill码.
界面实现
避免点击按钮后等待时间过长,使界面无反应的卡顿感,可以使用线程解决
def thread_it(self,func):
t = threading.Thread(target=func)
t.daemon = True
t.start()
tk.Button(self.f_panel,text='Send',bg=self.btn_color,command=lambda:self.thread_it(self.send)).pack(fill=tk.X)
打开文件存储文件夹,实际上有两种方法,这里列举一种,笔者发现其中一种方法在打包成exe时不能生效
def opendir(self):
path = os.path.abspath(self.path)
os.popen(f'start {path}')
完整代码
# -*- coding: utf-8 -*-
"""
Created on Sun Apr 28 14:46:58 2019
@author: HJY
"""
import tkinter as tk
from tkinter import (
ttk,
filedialog,
messagebox,
colorchooser,
)
import socket
import os.path
import threading
import subprocess
import os
class page:
def __init__(self,root):
self.btn_color = '#D1EEEE'
self.bg_color = '#EEE0E5'
self.index = 1
self.path = './'
self.flag_listen = False
self.showsetup = False
self.root = root
self.root.title('文本分享')
self.root.config(bg=self.bg_color)
self.root.resizable(width=False,height=False)
self.root.geometry('400x300')
self.makepage()
def makepage(self):
menubar = tk.Menu(self.root)
menubar.add_command(label="setup",command=self.setup)
menubar.add_command(label="openDir",command=self.opendir)
self.root.config(menu=menubar)
self.ip = tk.StringVar()
self.ip.set('127.0.0.1')
self.port = tk.StringVar()
self.port.set('19999')
self.listen_port = tk.StringVar()
self.listen_port.set('19999')
f_profile = tk.Frame(self.root,bg=self.bg_color)
f_profile.pack()
tk.Label(f_profile,text="Send&Recv Friend",bg=self.bg_color).pack()
tk.Label(f_profile,text="IP:",bg=self.bg_color).pack(side=tk.LEFT)
ttk.Entry(f_profile,textvariable=self.ip,width=20).pack(side=tk.LEFT)
tk.Label(f_profile,text="Port:",bg=self.bg_color).pack(side=tk.LEFT)
ttk.Entry(f_profile,textvariable=self.port,width=5).pack()
btn_choice = tk.Frame(self.root,pady=5,bg=self.bg_color)
btn_choice.pack()
tk.Button(btn_choice,text='发送文本',
bg=self.btn_color,
padx=15,
width=10,
command=self.choicetxt).grid(row=0,column=1,padx=2,)
tk.Button(btn_choice,text='发送文件',
bg=self.btn_color,
padx=15,
width=10,
command=self.choicefile).grid(row=0,column=2)
f_listen = tk.Frame(self.root,bg=self.bg_color)
f_listen.pack()
tk.Label(f_listen,text="监听本机信道:",bg=self.bg_color).pack(side=tk.LEFT)
ttk.Entry(f_listen,textvariable=self.listen_port,width=5,).pack(side=tk.LEFT)
self.btn_listen = tk.Button(f_listen,
text="启动监听",
bg=self.btn_color,
padx=15,
width=10,
command=lambda:self.thread_it(self.listen))
self.btn_listen.pack()
self.scrollbar = tk.Scrollbar(self.root)
self.scrollbar.pack(side=tk.RIGHT,fill=tk.Y)
self.recvbox = tk.Text(self.root,width=40,height=10,yscrollcommand=self.scrollbar.set)
self.recvbox.pack()
btn_clear = tk.Button(self.root,width=40,bg=self.btn_color,text="清空信息框",command=self.clear)
btn_clear.pack()
# 设置界面
self.f_setup = tk.Frame(self.root,bg=self.bg_color,pady=5)
ttk.Separator(self.root,orient='horizontal').pack(fill=tk.X)
self.path_label = tk.StringVar()
tk.Label(self.f_setup,textvariable=self.path_label,bg=self.bg_color).pack()
self.path_label.set(os.path.abspath(self.path))
btn_changepath = tk.Button(self.f_setup,
text="修改存储路径",
padx=15,
width=10,
bg = self.btn_color,
command=self.changepath)
btn_changepath.pack()
btn_changecolor = tk.Button(self.f_setup,
text="修改背景颜色",
padx=15,
width=10,
bg = self.btn_color,
command=self.changecolor)
btn_changecolor.pack()
def changecolor(self):
color = colorchooser.askcolor()
self.bg_color = color[1]
print(self.bg_color)
self.f_setup.config(bg=self.bg_color)
self.root.update()
def changepath(self):
dirname = filedialog.askdirectory()
self.path = dirname if len(dirname) else os.path.abspath(self.path)
self.path_label.set(self.path)
def choicefile(self):
self.filename = filedialog.askopenfilename()
self.sendfile()
def choicetxt(self):
self.f_panel = tk.Toplevel(self.root,)
self.textbox = tk.Text(self.f_panel,width=40,height=10)
self.textbox.pack()
tk.Button(self.f_panel,text='Send',bg=self.btn_color,command=lambda:self.thread_it(self.send)).pack(fill=tk.X)
def opendir(self):
path = os.path.abspath(self.path)
os.popen(f'start {path}')
def setup(self,):
self.showsetup = not self.showsetup
if self.showsetup:
self.f_setup.pack()
size = '400x500'
else:
self.f_setup.pack_forget()
size = '400x300'
self.root.geometry(size)
self.root.update()
def clear(self):
self.recvbox.delete("0.0","end")
def send(self):
host = self.ip.get().strip()
port = int(self.port.get().strip())
msg = self.textbox.get("0.0","end")
try:
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect((host,port))
client.send(bytes('#msg#',encoding='utf-8'))
client.send(bytes(msg,encoding='utf-8'))
client.close()
# messagebox.showinfo("Send msg","信息发送成功")
# add to textbox
self.recvbox.insert(f'{self.index}.0',f"By Me:")
self.recvbox.insert(f'{self.index+1}.0',f"{msg}")
self.index += 2
print(self.scrollbar.get())
self.scrollbar.set(0,0)
except:
answer = messagebox.askyesno("Send msg","对方没用启用监听,是否重发?")
if answer:
self.send()
finally:
pass
self.textbox.delete("0.0","end")
#self.f_panel.destroy()
def sendfile(self):
host = self.ip.get().strip()
port = int(self.port.get().strip())
filename = os.path.basename(self.filename)
with open(self.filename,"rb") as f:
content = f.read()
try:
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect((host,port))
client.send(bytes(f'#file#{filename}#',encoding='utf-8'))
client.send(content)
except ConnectionRefusedError:
answer = messagebox.askyesno("Send Msg","对方没用启用监听,是否重发?")
if answer:
self.sendfile()
else:
messagebox.showinfo("Send File","文件发送成功")
finally:
client.close()
def listen(self):
def openserver():
port = int(self.listen_port.get().strip())
self.server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
self.server.bind(("0.0.0.0",port))
self.server.listen(3)
while True:
try:
conn, addr = self.server.accept()
except:
break
recv_data = conn.recv(1024)
if recv_data[1] == 102:
endindex = recv_data.find(b'#',6)
filename = recv_data[6:endindex].decode('utf-8')
answer = messagebox.askyesno("Recv File",f"收到来自{addr[0]}主机的文件\n{filename}\n是否接收?")
if not answer:
continue
savepath = os.path.join(self.path,filename)
with open(savepath,"wb") as f:
f.write(recv_data[endindex+1:])
while recv_data:
recv_data = conn.recv(1024)
f.write(recv_data)
messagebox.showinfo("Recv File","文件接收成功!")
elif recv_data[1] == 109:
msg = recv_data[5:].decode('utf-8').strip()
self.recvbox.insert(f'{self.index}.0',f'From:{addr[0]}\n')
self.recvbox.insert(f'{self.index+1}.0',f"{msg}\n")
self.index +=2
conn.close()
if not self.flag_listen:
break
self.server.close()
self.flag_listen = not self.flag_listen
if not self.flag_listen:
self.btn_listen['text'] = '启动监听'
self.btn_listen.config(bg='#C4C4C4')
try:
self.server.close()
except:
pass
return
self.btn_listen['text'] = '监听中...'
self.btn_listen.config(bg='#9ACD32')
t_server = threading.Thread(target=openserver)
t_server.daemon = True
t_server.start()
def thread_it(self,func):
t = threading.Thread(target=func)
t.daemon = True
t.start()
if __name__ == '__main__':
root = tk.Tk()
page(root)
root.mainloop()
程序本身是为了满足某种情境而编写,实际上有很多问题需要去解决和完善,但是不打算精力就暂不解决了,有兴趣的朋友可以补充:
- 一些异常的捕获处理
- 更友好的界面设计和提示信息
- 联系人记忆功能,避免每次添加IP
- 需要协商才能发送
对于一些没提及的实现过程,由于不知道你的问题,就没有完全罗列了。
版权声明:本文为yeshankuangrenaaaaa原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。