python tkinter运用-实现内容传输工具

目的

博文目的:分享这个不成熟的小案例,希望能给还在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()



程序本身是为了满足某种情境而编写,实际上有很多问题需要去解决和完善,但是不打算精力就暂不解决了,有兴趣的朋友可以补充:

  1. 一些异常的捕获处理
  2. 更友好的界面设计和提示信息
  3. 联系人记忆功能,避免每次添加IP
  4. 需要协商才能发送

对于一些没提及的实现过程,由于不知道你的问题,就没有完全罗列了。


版权声明:本文为yeshankuangrenaaaaa原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。