局域网文件传输器安卓版本+win版本

如果一个文件要从手机发送给电脑,没有数据线,但这个文件又有点保密,该怎么办。只有将电脑和手机连接到同一个WiFi路由器,用下面我的程序就能解决。

1.安卓程序,用beeware生成

python 复制代码
import socket
import os
import threading
import time
import toga
from toga.style import Pack
from toga.style.pack import COLUMN, ROW
import re


class FileTransferApp(toga.App):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # 只初始化必要的属性
        self.file_path = ""
        self.file_name = ""
        self.file_uri = None
        self.usetime = 0
        self.sudu = 0
        self.server_running = False
        self.temp_files = []
    
    def startup(self):
        # 主窗口 - 必须在 startup 中创建
        self.main_window = toga.MainWindow(title="局域网文件传输工具", size=(380, 600))
        
        # 创建UI组件
        self.create_ui()
        
        # 设置窗口内容
        self.main_window.content = self.main_box
        self.main_window.show()
        
        # 获取本机IP
        self.get_local_ip()
        
        # 检查是否有启动intent(从其他应用分享的文件)
        self.check_launch_intent()
    
    def create_ui(self):
        # 本机IP显示(作为服务器)
        local_ip_box = toga.Box(style=Pack(direction=COLUMN, padding=3))
        local_ip_box.add(toga.Label("本机IP:", style=Pack(padding=(0, 3))))
        self.local_ip_label = toga.Label(
            "正在获取...",
            style=Pack(padding=3, font_weight="bold")
        )
        local_ip_box.add(self.local_ip_label)
        
        # 启动服务器按钮
        self.start_server_btn = toga.Button(
            "启动服务器",
            on_press=self.start_fwq,
            style=Pack(padding=8, background_color="#4CAF50")
        )
        local_ip_box.add(self.start_server_btn)
        
        # 分隔线
        separator = toga.Box(style=Pack(direction=ROW, background_color="#E0E0E0", padding=1))
        
        # 对方IP输入
        target_ip_box = toga.Box(style=Pack(direction=COLUMN, padding=3))
        target_ip_box.add(toga.Label("对方IP:", style=Pack(padding=(0, 3))))
        
        ip_input_box = toga.Box(style=Pack(direction=ROW, padding=3))
        self.ip_prefix_input = toga.TextInput(
            readonly=True,
            style=Pack(flex=1, padding=3, width=100)
        )
        self.ip_suffix_input = toga.TextInput(
            placeholder="最后一段",
            style=Pack(flex=1, padding=3, width=50)
        )
        ip_input_box.add(self.ip_prefix_input)
        ip_input_box.add(toga.Label(".", style=Pack(padding=3)))
        ip_input_box.add(self.ip_suffix_input)
        target_ip_box.add(ip_input_box)
        
        # 文件信息显示
        file_box = toga.Box(style=Pack(direction=COLUMN, padding=5))
        file_box.add(toga.Label("共享的文件:", style=Pack(padding=(0, 3), font_weight="bold")))
        
        self.file_info_label = toga.Label(
            "等待其他App分享文件...",
            style=Pack(padding=5, text_align="center")
        )
        file_box.add(self.file_info_label)
        
        # 发送按钮
        self.send_btn = toga.Button(
            "发送文件",
            on_press=self.start_send,
            style=Pack(padding=8, background_color="#2196F3"),
            enabled=False
        )
        file_box.add(self.send_btn)
        
        # 进度显示
        progress_box = toga.Box(style=Pack(direction=COLUMN, padding=5))
        progress_box.add(toga.Label("发送进度:", style=Pack(padding=(0, 3))))
        self.send_progress = toga.ProgressBar(max=100, value=0, style=Pack(padding=3))
        progress_box.add(self.send_progress)
        
        progress_box.add(toga.Label("接收进度:", style=Pack(padding=(0, 3))))
        self.receive_progress = toga.ProgressBar(max=100, value=0, style=Pack(padding=3))
        progress_box.add(self.receive_progress)
        
        # 日志区域
        log_box = toga.Box(style=Pack(direction=COLUMN, padding=5))
        log_box.add(toga.Label("传输日志:", style=Pack(padding=(0, 3))))
        self.log_area = toga.MultilineTextInput(
            readonly=True,
            style=Pack(flex=1, height=150, padding=3)
        )
        log_box.add(self.log_area)
        
        # 主布局
        self.main_box = toga.Box(
            children=[local_ip_box, separator, target_ip_box, file_box, progress_box, log_box],
            style=Pack(direction=COLUMN, padding=8)
        )
    
    def get_local_ip(self):
        """获取本机局域网IP地址"""
        def _get_ip():
            try:
                # 尝试通过连接外部地址来获取本机IP
                s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                s.connect(("8.8.8.8", 80))
                ip = s.getsockname()[0]
                s.close()
                
                # 在主线程中更新UI
                self._safe_ui_update(lambda: self.update_local_ip(ip))
                
            except Exception as e:
                # 如果上述方法失败,尝试其他方法
                try:
                    hostname = socket.gethostname()
                    ip = socket.gethostbyname(hostname)
                    self._safe_ui_update(lambda: self.update_local_ip(ip))
                except:
                    self._safe_ui_update(lambda: self.update_local_ip("无法获取IP"))
        
        threading.Thread(target=_get_ip, daemon=True).start()
    
    def _safe_ui_update(self, update_func):
        """安全地在UI线程中执行更新"""
        try:
            if hasattr(self, 'main_window') and self.main_window and hasattr(self.main_window, 'app'):
                self.main_window.app._impl.loop.call_soon_threadsafe(update_func)
            else:
                # 如果主窗口尚未初始化,稍后重试
                threading.Timer(0.1, lambda: self._safe_ui_update(update_func)).start()
        except Exception as e:
            print(f"UI更新错误: {e}")
    
    def update_local_ip(self, ip):
        """更新本地IP显示"""
        self.local_ip_label.text = ip
        
        # 设置对方IP前缀(前3段)
        if ip != "无法获取IP" and ip.count('.') == 3:
            ip_parts = ip.split('.')
            ip_prefix = '.'.join(ip_parts[0:3])
            self.ip_prefix_input.value = ip_prefix
    
    def request_all_files_permission(self):
        """请求所有文件访问权限"""
        try:
            from rubicon.java import JavaClass
            
            Python = JavaClass('com.chaquo.python.Python')
            Context = JavaClass('android.content.Context')
            Intent = JavaClass('android.content.Intent')
            Settings = JavaClass('android.provider.Settings')
            Uri = JavaClass('android.net.Uri')
            
            # 获取应用上下文
            context = Python.getPlatform().getApplication()
            
            # 检查是否已有权限
            if hasattr(Settings, 'ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION'):
                # 跳转到所有文件访问权限设置页面
                intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
                uri = Uri.fromParts("package", context.getPackageName(), None)
                intent.setData(uri)
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                
                context.startActivity(intent)
                self.log("请在设置中授予所有文件访问权限")
            else:
                # 对于较旧的Android版本,请求标准存储权限
                self.log("请求存储权限...")
                
        except Exception as e:
            self.log(f"请求所有文件权限失败: {e}")
    
    def check_launch_intent(self):
        """检查启动intent,处理从其他应用分享的文件"""
        try:
            # 使用rubicon-java获取Android Intent
            from rubicon.java import JavaClass
            
            # 获取Intent类
            Intent = JavaClass("android.content.Intent")
            Uri = JavaClass("android.net.Uri")
            
            # 获取当前Activity
            activity = self._impl.native
            
            # 获取启动intent
            intent = activity.getIntent()
            if intent is None:
                return
                
            action = intent.getAction()
            mime_type = intent.getType()
            
            # 检查是否是分享文件的intent
            if action == Intent.ACTION_SEND and mime_type is not None:
                self.log("检测到文件分享intent")
                
                # 获取文件URI
                stream_extra = intent.getParcelableExtra(Intent.EXTRA_STREAM)
                if stream_extra is not None:
                    file_uri = stream_extra.toString()
                    self.handle_shared_file(file_uri)
                    
        except Exception as e:
            self.log(f"检查启动intent时出错: {e}")
    
    def handle_intent(self, intent):
        """处理从其他App分享的intent(当应用已在运行时)"""
        try:
            from rubicon.java import JavaClass
            
            Intent = JavaClass("android.content.Intent")
            
            action = intent.getAction()
            mime_type = intent.getType()
            
            if action == Intent.ACTION_SEND and mime_type is not None:
                self.log("接收到文件分享intent")
                
                # 获取文件URI
                stream_extra = intent.getParcelableExtra(Intent.EXTRA_STREAM)
                if stream_extra is not None:
                    file_uri = stream_extra.toString()
                    self.handle_shared_file(file_uri)
                    
        except Exception as e:
            self.log(f"处理intent时出错: {e}")
    
    def handle_shared_file(self, file_uri_str):
        """处理分享的文件URI"""
        try:
            self.log(f"处理分享文件: {file_uri_str}")
            
            # 使用rubicon-java解析URI和获取文件信息
            from rubicon.java import JavaClass
            
            Uri = JavaClass("android.net.Uri")
            
            # 解析URI
            file_uri = Uri.parse(file_uri_str)
            
            # 获取ContentResolver
            activity = self._impl.native
            content_resolver = activity.getContentResolver()
            
            # 查询文件信息
            cursor = content_resolver.query(
                file_uri, 
                None,  # projection - 所有列
                None,  # selection
                None,  # selectionArgs
                None   # sortOrder
            )
            
            if cursor is not None and cursor.moveToFirst():
                # 获取文件名
                display_name_index = cursor.getColumnIndex("_display_name")
                if display_name_index >= 0:
                    self.file_name = cursor.getString(display_name_index)
                else:
                    # 如果无法获取文件名,使用默认名称
                    self.file_name = f"shared_file_{int(time.time())}"
                
                cursor.close()
            else:
                # 如果无法查询到文件信息,使用默认名称
                self.file_name = f"shared_file_{int(time.time())}"
            
            self.file_uri = file_uri_str
            
            # 更新UI
            self.file_info_label.text = f"文件: {self.file_name}\n准备发送"
            self.send_btn.enabled = True
            
            self.log(f"已准备发送文件: {self.file_name}")
            
        except Exception as e:
            self.log(f"处理分享文件时出错: {e}")
    
    def read_shared_file(self, file_uri_str):
        """使用ContentResolver读取分享的文件内容到临时文件"""
        try:
            self.log("开始读取分享的文件...")
            
            from rubicon.java import JavaClass
            
            Uri = JavaClass("android.net.Uri")
            File = JavaClass("java.io.File")
            FileOutputStream = JavaClass("java.io.FileOutputStream")
            
            # 获取Activity和ContentResolver
            activity = self._impl.native
            content_resolver = activity.getContentResolver()
            
            # 解析URI
            file_uri = Uri.parse(file_uri_str)
            
            # 打开输入流
            input_stream = content_resolver.openInputStream(file_uri)
            if input_stream is None:
                self.log("无法打开文件输入流")
                return None
            
            # 创建临时文件
            cache_dir = activity.getCacheDir()
            temp_file = File(cache_dir, self.file_name)
            
            # 创建输出流
            output_stream = FileOutputStream(temp_file)
            
            # 复制数据 - 使用较小的缓冲区避免cython错误
            buffer_size = 8192  # 8KB缓冲区
            
            # 正确创建Java字节数组
            # 使用反射API创建byte数组
            Array = JavaClass("java.lang.reflect.Array")
            Byte = JavaClass("java.lang.Byte")
            buffer = Array.newInstance(Byte.TYPE, buffer_size)
            
            total_read = 0
            
            bytes_read = input_stream.read(buffer)
            while bytes_read != -1:
                output_stream.write(buffer, 0, bytes_read)
                total_read += bytes_read
                bytes_read = input_stream.read(buffer)
            
            # 关闭流
            input_stream.close()
            output_stream.close()
            
            temp_file_path = temp_file.getAbsolutePath()
            self.temp_files.append(temp_file_path)  # 记录临时文件,用于后续清理
            
            self.log(f"文件读取完成,大小: {total_read} 字节")
            return temp_file_path
            
        except Exception as e:
            self.log(f"读取分享文件时出错: {e}")
            return None
    
    def read_shared_file_simple(self, file_uri_str):
        """简化版文件读取方法,使用Python原生方式"""
        try:
            self.log("开始读取分享的文件(简化版)...")
            
            from rubicon.java import JavaClass
            
            Uri = JavaClass("android.net.Uri")
            File = JavaClass("java.io.File")
            FileOutputStream = JavaClass("java.io.FileOutputStream")
            
            # 获取Activity和ContentResolver
            activity = self._impl.native
            content_resolver = activity.getContentResolver()
            
            # 解析URI
            file_uri = Uri.parse(file_uri_str)
            
            # 打开输入流
            input_stream = content_resolver.openInputStream(file_uri)
            if input_stream is None:
                self.log("无法打开文件输入流")
                return None
            
            # 创建临时文件
            cache_dir = activity.getCacheDir()
            temp_file = File(cache_dir, self.file_name)
            
            # 创建输出流
            output_stream = FileOutputStream(temp_file)
            
            # 使用Python字节数组进行读取
            buffer_size = 8192  # 8KB缓冲区
            
            total_read = 0
            
            # 使用更简单的方法读取数据
            while True:
                # 创建一个新的字节数组
                Array = JavaClass("java.lang.reflect.Array")
                Byte = JavaClass("java.lang.Byte")
                buffer = Array.newInstance(Byte.TYPE, buffer_size)
                
                bytes_read = input_stream.read(buffer)
                if bytes_read == -1:
                    break
                
                output_stream.write(buffer, 0, bytes_read)
                total_read += bytes_read
            
            # 关闭流
            input_stream.close()
            output_stream.close()
            
            temp_file_path = temp_file.getAbsolutePath()
            self.temp_files.append(temp_file_path)  # 记录临时文件,用于后续清理
            
            self.log(f"文件读取完成,大小: {total_read} 字节")
            return temp_file_path
            
        except Exception as e:
            self.log(f"读取分享文件时出错: {e}")
            return None
    
    def read_shared_file_fallback(self, file_uri_str):
        """备用文件读取方法,使用更简单的方式"""
        try:
            self.log("使用备用方法读取分享的文件...")
            
            # 如果上述方法都失败,尝试使用更直接的方式
            # 这里我们直接使用文件URI作为路径(仅适用于file:// URI)
            if file_uri_str.startswith("file://"):
                file_path = file_uri_str[7:]  # 移除"file://"前缀
                if os.path.exists(file_path):
                    return file_path
            
            self.log("无法读取分享的文件URI")
            return None
            
        except Exception as e:
            self.log(f"备用方法读取文件时出错: {e}")
            return None
    
    def cleanup_temp_files(self):
        """清理临时文件"""
        for temp_file in self.temp_files:
            try:
                if os.path.exists(temp_file):
                    os.remove(temp_file)
                    self.log(f"已清理临时文件: {temp_file}")
            except Exception as e:
                self.log(f"清理临时文件时出错: {e}")
        
        self.temp_files = []
    
    def detect_encoding(self, data):
        """检测字节数据的编码"""
        # 常见编码列表
        encodings = ['utf-8', 'gbk', 'gb2312', 'latin-1', 'ascii']
        
        for encoding in encodings:
            try:
                decoded = data.decode(encoding)
                return encoding, decoded
            except UnicodeDecodeError:
                continue
        
        # 如果所有编码都失败,使用 'utf-8' 并忽略错误
        return 'utf-8', data.decode('utf-8', errors='ignore')
    
    def start_server(self, host='0.0.0.0', port=8888):
        """启动文件接收服务器"""
        server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        
        try:
            server_socket.bind((host, port))
            server_socket.listen(1)
            self.log(f"服务器启动,监听端口 {port}")
            
            while self.server_running:
                try:
                    server_socket.settimeout(1.0)
                    client_socket, addr = server_socket.accept()
                    self.log(f"连接来自 {addr}")

                    # 接收文件名和大小
                    raw_data = client_socket.recv(1024)
                    
                    # 检测编码并解码
                    encoding, file_info = self.detect_encoding(raw_data)
                    
                    # 修复: 处理文件信息格式错误
                    parts = file_info.strip().split('|')
                    if len(parts) < 2:
                        self.log("文件信息格式错误")
                        client_socket.close()
                        continue
                    
                    file_name = parts[0]
                    try:
                        file_size = int(parts[1])
                    except ValueError:
                        self.log("文件大小格式错误")
                        client_socket.close()
                        continue

                    # 创建接收目录 - 保存到Download文件夹
                    from rubicon.java import JavaClass
                    File = JavaClass("java.io.File")
                    FileOutputStream = JavaClass("java.io.FileOutputStream")
                    Environment = JavaClass("android.os.Environment")
                    
                    # 获取Download目录
                    download_dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
                    if download_dir is None:
                        # 备用方案:使用应用的files目录
                        activity = self._impl.native
                        download_dir = activity.getFilesDir()
                    
                    receive_file = File(download_dir, file_name)
                    file_output = FileOutputStream(receive_file)

                    # 接收文件数据
                    received = 0
                    while received < file_size:
                        data = client_socket.recv(4096)
                        if not data:
                            break
                        file_output.write(data)
                        received += len(data)
                        progress = int(received / file_size * 100)
                        self._safe_ui_update(lambda p=progress: self.update_receive_progress(p))

                    file_output.close()
                    client_socket.close()
                    
                    self.log(f"文件 {file_name} 接收完成")
                    self.log(f"保存路径: {receive_file.getAbsolutePath()}")
                    
                except socket.timeout:
                    continue
                except Exception as e:
                    if self.server_running:
                        self.log(f"接收文件时出错: {e}")
                    
        except Exception as e:
            self.log(f"服务器启动失败: {e}")
        finally:
            server_socket.close()
            self.server_running = False
            self._safe_ui_update(lambda: self.update_server_button(False))
    
    def update_receive_progress(self, value):
        """更新接收进度"""
        self.receive_progress.value = value
    
    def update_server_button(self, running):
        """更新服务器按钮状态"""
        self.start_server_btn.enabled = not running
        if running:
            self.start_server_btn.text = "服务器运行中"
            self.start_server_btn.style.background_color = "#FF9800"
        else:
            self.start_server_btn.text = "启动服务器"
            self.start_server_btn.style.background_color = "#4CAF50"
    
    def start_fwq(self, widget):
        """启动服务器并请求权限"""
        # 首先请求文件权限
        self.request_all_files_permission()
        
        # 然后启动服务器
        if not self.server_running:
            self.server_running = True
            self.update_server_button(True)
            threading.Thread(
                target=self.start_server, 
                args=('0.0.0.0', 8888), 
                daemon=True
            ).start()
    
    def send_file(self, server_ip, file_path, port=8888):
        """发送文件到目标服务器"""
        client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            client_socket.connect((server_ip, port))
            file_size = os.path.getsize(file_path)
    
            # 发送文件信息(使用UTF-8编码)
            # 按照接收端的协议,发送固定1024字节的文件信息
            file_info = f"{self.file_name}|{file_size}"
            file_info_bytes = file_info.encode('utf-8')
            
            # 如果文件信息不足1024字节,用空格填充
            if len(file_info_bytes) < 1024:
                file_info_bytes += b' ' * (1024 - len(file_info_bytes))
            else:
                # 如果超过1024字节,截断
                file_info_bytes = file_info_bytes[:1024]
            
            # 发送固定1024字节的文件信息
            client_socket.send(file_info_bytes)
    
            # 分块发送文件数据
            with open(file_path, 'rb') as file:
                sent = 0
                start = time.time()
                while sent < file_size:
                    data = file.read(4096)
                    client_socket.send(data)
                    sent += len(data)
                    progress = int(sent / file_size * 100)
                    self._safe_ui_update(lambda p=progress: self.update_send_progress(p))
            
            self.usetime = time.time() - start
            self.sudu = file_size / 1024 / 1024 / self.usetime if self.usetime > 0 else 0
            
        except Exception as e:
            self.log(f"发送文件时出错: {e}")
        finally:
            client_socket.close()
    
    def update_send_progress(self, value):
        """更新发送进度"""
        self.send_progress.value = value
    
    def start_send(self, widget):
        # 构建完整IP地址
        ip_prefix = self.ip_prefix_input.value.strip()
        ip_suffix = self.ip_suffix_input.value.strip()
        
        if not ip_prefix or not ip_suffix:
            self.main_window.error_dialog("错误", "请输入完整的对方IP地址")
            return
        
        server_ip = f"{ip_prefix}.{ip_suffix}"
        
        # 验证IP格式
        ip_pattern = re.compile(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
        if not ip_pattern.match(server_ip):
            self.main_window.error_dialog("错误", "请输入有效的IP地址")
            return
        
        if not self.file_uri:
            self.main_window.error_dialog("错误", "没有可发送的文件")
            return

        # 重置进度条
        self.send_progress.value = 0
        
        # 在新线程中发送文件
        threading.Thread(
            target=self.send_file_thread, 
            args=(server_ip,),
            daemon=True
        ).start()
    
    def send_file_thread(self, server_ip):
        try:
            self.log("开始发送文件...")
            
            # 尝试多种方法读取分享的文件
            temp_file_path = None
            
            # 方法1: 使用标准方法
            temp_file_path = self.read_shared_file(self.file_uri)
            
            # 方法2: 如果方法1失败,使用简化方法
            if not temp_file_path or not os.path.exists(temp_file_path):
                self.log("标准方法失败,尝试简化方法")
                temp_file_path = self.read_shared_file_simple(self.file_uri)
            
            # 方法3: 如果方法2失败,使用备用方法
            if not temp_file_path or not os.path.exists(temp_file_path):
                self.log("简化方法失败,尝试备用方法")
                temp_file_path = self.read_shared_file_fallback(self.file_uri)
            
            if temp_file_path and os.path.exists(temp_file_path):
                self.send_file(server_ip, temp_file_path)
                self._safe_ui_update(lambda: self.send_complete())
                
                # 清理临时文件
                self.cleanup_temp_files()
            else:
                self.log("无法读取分享的文件,请检查文件权限")
                
        except Exception as e:
            self.log(f"发送文件时出错: {e}")
    
    def send_complete(self):
        """发送完成处理"""
        self.send_progress.value = 100
        current_text = self.log_area.value
        new_text = f"{current_text}文件发送完成!耗时{self.usetime:.4f}秒, 速度:{self.sudu:.2f} MB/s\n"
        self.log_area.value = new_text
    
    def log(self, message):
        """记录日志"""
        def _log():
            current_text = self.log_area.value
            new_text = f"{current_text}{message}\n" if current_text else f"{message}\n"
            self.log_area.value = new_text
        
        self._safe_ui_update(_log)


def main():
    return FileTransferApp("文件传输工具", "com.example.filetransfer")


if __name__ == "__main__":
    app = main()
    app.main_loop()

记得修改AndroidManifest.xml,下面是参考

XML 复制代码
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" >
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/formal_name"
        android:networkSecurityConfig="@xml/network_security_config"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:requestLegacyExternalStorage="true"
        android:theme="@style/AppTheme.Launcher" >
        <!-- https://developer.android.com/guide/topics/resources/runtime-changes#HandlingTheChange -->
        <activity
            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
            android:name="org.beeware.android.MainActivity"
            android:exported="true" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.SEND" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="*/*" />
            </intent-filter>
        </activity>
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.example.sendfile.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths">
            </meta-data>
        </provider>
        
    </application>
    
</manifest>

运行界面:

2.win版,实际是原代码,linux也可以运行

python 复制代码
# -*- coding: utf-8 -*-
"""
Created on Mon Sep  8 14:31:28 2025
@author: YBK
"""
import socket
import os
import tkinter as tk
from tkinter import filedialog, messagebox, scrolledtext
import threading
from tkinter import ttk
import time
from tkinter import LabelFrame
import struct
import sys
import subprocess
import pyperclip
from datetime import datetime

# 尝试导入tkinterdnd2,如果失败则使用标准tkinter
try:
    from tkinterdnd2 import DND_FILES, TkinterDnD
    HAS_DND = True
except ImportError:
    # 回退到标准 tkinter
    print("警告: tkinterdnd2未安装,拖拽功能不可用")
    print("如需拖拽功能,请运行: pip install tkinterdnd2")
    TkinterDnD = tk.Tk
    DND_FILES = None
    HAS_DND = False

class FileTransferGUI:
    def __init__(self, master):
        self.master = master
        master.title("局域网文件传输工具")
        master.geometry("700x700")  # 增加尺寸以适应更多功能
        
        # 服务器运行状态
        self.server_running = False
        
        # 存储接收的文件信息
        self.received_files = []  # 存储(文件名, 路径, 大小, 接收时间)的列表
        
        # 默认保存路径 - 使用正确的路径分隔符
        if sys.platform == "win32":
            # Windows下使用反斜杠
            self.save_path = os.path.join(os.path.expanduser('~'), 'Downloads')
        else:
            # Linux/macOS使用斜杠
            self.save_path = '/root/sd/Download'
        
        # ========== 主布局 ==========
        main_frame = tk.Frame(master)
        main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        # 左侧控制面板
        left_frame = tk.Frame(main_frame)
        left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(0, 10))
        
        # 右侧文件列表和日志
        right_frame = tk.Frame(main_frame)
        right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
        
        # ========== 左侧控制面板 ==========
        # IP设置
        tk.Label(left_frame, text="对方IP:").pack(pady=5, anchor=tk.W)
        self.ip_entry = tk.Entry(left_frame, width=25)
        self.ip_entry.pack(pady=5, fill=tk.X)
        ip_parts = self.get_local_ip().split('.')
        ip_prefix = '.'.join(ip_parts[0:3])
        self.ip_entry.insert(0, f"{ip_prefix}.")
        
        tk.Label(left_frame, text="本机IP:").pack(pady=5, anchor=tk.W)
        self.bip_entry = tk.Entry(left_frame, width=25)
        self.bip_entry.pack(pady=5, fill=tk.X)
        self.ipfwq = self.get_local_ip()
        self.bip_entry.insert(0, self.ipfwq)
        
        # 保存路径设置
        path_frame = tk.LabelFrame(left_frame, text="文件保存路径")
        path_frame.pack(fill=tk.X, pady=10)
        
        self.save_path_var = tk.StringVar(value=self.save_path)
        tk.Entry(path_frame, textvariable=self.save_path_var, width=25).pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5, pady=5)
        tk.Button(path_frame, text="浏览", command=self.browse_save_path, width=8).pack(side=tk.RIGHT, padx=5, pady=5)
        
        # 服务器控制
        server_frame = tk.Frame(left_frame)
        server_frame.pack(pady=10, fill=tk.X)
        
        self.bt_fwq = tk.Button(server_frame, text="启动本机服务器", command=self.start_fwq, width=15)
        self.bt_fwq.pack(side=tk.LEFT, padx=(0, 5))
        
        self.stop_fwq_btn = tk.Button(server_frame, text="停止服务器", command=self.stop_fwq, 
                                     state=tk.DISABLED, width=12)
        self.stop_fwq_btn.pack(side=tk.LEFT)
        
        # 文件选择
        tk.Button(left_frame, text="选择文件", command=self.select_file).pack(pady=5, fill=tk.X)
        
        self.file_path = ""
        self.file_name = ""
        
        # 文件拖放区域(兼容模式)
        if HAS_DND:
            # 使用tkinterdnd2的拖拽
            drop_frame = tk.LabelFrame(left_frame, text="拖放文件到此区域")
            drop_frame.pack(fill=tk.X, pady=5)
            drop_label = tk.Label(drop_frame, text="将文件拖放到此区域", height=2)
            drop_label.pack(pady=5, padx=5)
            drop_label.drop_target_register(DND_FILES)
            drop_label.dnd_bind('<<Drop>>', self.on_file_drop)
        else:
            # 使用标准按钮
            drop_frame = tk.LabelFrame(left_frame, text="文件操作")
            drop_frame.pack(fill=tk.X, pady=5)
            tk.Button(drop_frame, text="点击选择文件", command=self.select_file).pack(pady=5)
        
        # 发送按钮
        tk.Button(left_frame, text="发送文件", command=self.start_send).pack(pady=10, fill=tk.X)
        
        # 进度条
        progress_frame = tk.LabelFrame(left_frame, text="传输进度")
        progress_frame.pack(fill=tk.X, pady=10)
        
        tk.Label(progress_frame, text="发送进度:").pack(anchor=tk.W, pady=(5,0))
        self.progress_var = tk.DoubleVar()
        self.progress_bar = ttk.Progressbar(progress_frame, variable=self.progress_var, maximum=100)
        self.progress_bar.pack(fill=tk.X, padx=5, pady=(0,10))
        
        tk.Label(progress_frame, text="接收进度:").pack(anchor=tk.W)
        self.progress_var1 = tk.DoubleVar()
        self.progress_bar1 = ttk.Progressbar(progress_frame, variable=self.progress_var1, maximum=100)
        self.progress_bar1.pack(fill=tk.X, padx=5, pady=(0,5))
        
        # 状态栏
        self.status_var = tk.StringVar()
        self.status_var.set("就绪")
        status_bar = tk.Label(left_frame, textvariable=self.status_var, bd=1, relief=tk.SUNKEN, 
                             anchor=tk.W, height=2)
        status_bar.pack(side=tk.BOTTOM, fill=tk.X, pady=(10, 0))
        
        # ========== 右侧文件列表和日志 ==========
        # 接收文件列表区域
        files_frame = tk.LabelFrame(right_frame, text="接收的文件列表")
        files_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 10))
        
        # 创建Treeview
        columns = ("文件名", "大小", "接收时间", "保存路径", "状态")
        self.tree = ttk.Treeview(files_frame, columns=columns, show="headings", height=8)
        
        # 设置列标题
        self.tree.heading("文件名", text="文件名")
        self.tree.heading("大小", text="大小")
        self.tree.heading("接收时间", text="接收时间")
        self.tree.heading("保存路径", text="保存路径")
        self.tree.heading("状态", text="状态")
        
        # 设置列宽度
        self.tree.column("文件名", width=150)
        self.tree.column("大小", width=80)
        self.tree.column("接收时间", width=120)
        self.tree.column("保存路径", width=150)
        self.tree.column("状态", width=80)
        
        # 添加滚动条
        tree_scroll = ttk.Scrollbar(files_frame, orient="vertical", command=self.tree.yview)
        self.tree.configure(yscrollcommand=tree_scroll.set)
        
        # 布局Treeview和滚动条
        self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        tree_scroll.pack(side=tk.RIGHT, fill=tk.Y)
        
        # 文件操作按钮框架
        buttons_frame = tk.Frame(right_frame)
        buttons_frame.pack(fill=tk.X, pady=(0, 10))
        
        tk.Button(buttons_frame, text="打开选中文件", command=self.open_selected_file,
                 width=12).pack(side=tk.LEFT, padx=2)
        tk.Button(buttons_frame, text="复制文件路径", command=self.copy_selected_path,
                 width=12).pack(side=tk.LEFT, padx=2)
        tk.Button(buttons_frame, text="打开文件夹", command=self.open_selected_folder,
                 width=12).pack(side=tk.LEFT, padx=2)
        tk.Button(buttons_frame, text="删除记录", command=self.delete_selected_record,
                 width=12).pack(side=tk.LEFT, padx=2)
        
        # 批量操作按钮
        batch_frame = tk.Frame(right_frame)
        batch_frame.pack(fill=tk.X, pady=(0, 10))
        
        tk.Button(batch_frame, text="全部打开", command=self.open_all_files,
                 width=10).pack(side=tk.LEFT, padx=2)
        tk.Button(batch_frame, text="清除列表", command=self.clear_file_list,
                 width=10).pack(side=tk.LEFT, padx=2)
        
        # 日志区域
        log_frame = tk.LabelFrame(right_frame, text="传输日志")
        log_frame.pack(fill=tk.BOTH, expand=True)
        
        self.log_area = scrolledtext.ScrolledText(log_frame, height=10)
        self.log_area.pack(fill=tk.BOTH, expand=True)
        self.log_area.config(state=tk.DISABLED)
        
        # 如果支持拖拽,注册日志区域的拖拽目标
        if HAS_DND:
            self.log_area.drop_target_register(DND_FILES)
            self.log_area.dnd_bind('<<Drop>>', self.on_file_drop)
        
        # 绑定Treeview双击事件
        self.tree.bind("<Double-1>", self.on_tree_double_click)
        
        # 绑定键盘事件
        self.master.bind("<Control-o>", lambda e: self.open_selected_file())
        self.master.bind("<Control-c>", lambda e: self.copy_selected_path())
        self.master.bind("<Delete>", lambda e: self.delete_selected_record())
        
        # 创建保存目录
        self.ensure_save_path()
    
    def ensure_save_path(self):
        """确保保存目录存在"""
        self.save_path = self.save_path_var.get()
        # 确保路径使用正确的分隔符
        self.save_path = os.path.normpath(self.save_path)
        
        if not os.path.exists(self.save_path):
            try:
                os.makedirs(self.save_path, exist_ok=True)
                self.log(f"创建保存目录: {self.save_path}")
            except Exception as e:
                self.log(f"创建目录失败: {e}")
                # 回退到临时目录
                self.save_path = os.path.join(os.path.expanduser('~'), 'Desktop')
                if sys.platform != "win32":
                    self.save_path = '/tmp'
                self.save_path_var.set(self.save_path)
                os.makedirs(self.save_path, exist_ok=True)
    
    def browse_save_path(self):
        """浏览并选择保存路径"""
        directory = filedialog.askdirectory(title="选择文件保存目录")
        if directory:
            # 使用正确的路径格式
            directory = os.path.normpath(directory)
            self.save_path_var.set(directory)
            self.save_path = directory
            self.log(f"保存路径已更改为: {directory}")
    
    def parse_dropped_files(self, data):
        """解析拖拽的文件路径,正确处理包含空格的情况"""
        import re
        files = []

        if HAS_DND:
            # tkinterdnd2 用花括号包裹包含空格的文件路径
            brace_pattern = r'\{[^}]+\}'
            brace_matches = re.findall(brace_pattern, data)

            for match in brace_matches:
                # 移除花括号
                file_path = match[1:-1]
                files.append(file_path)
                # 从原始数据中移除已匹配的部分
                data = data.replace(match, '', 1)

        # 处理剩余的不带花括号的文件路径
        remaining_files = [f.strip() for f in data.split() if f.strip()]
        files.extend(remaining_files)

        return files

    def on_file_drop(self, event):
        """处理文件拖放事件"""
        files = self.parse_dropped_files(event.data)
        if files:
            self.file_path = files[0]
            self.file_name = os.path.basename(self.file_path)
            self.log(f"已选择文件: {self.file_path}")
            self.status_var.set(f"已选择: {self.file_name}")

    def get_local_ip(self):
        """获取本机局域网IP地址"""
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("8.8.8.8", 80))
            ip = s.getsockname()[0]
            s.close()
            return ip
        except Exception as e:
            return f"127.0.0.1"
    
    def format_file_size(self, size_in_bytes):
        """格式化文件大小"""
        for unit in ['B', 'KB', 'MB', 'GB']:
            if size_in_bytes < 1024.0:
                return f"{size_in_bytes:.2f} {unit}"
            size_in_bytes /= 1024.0
        return f"{size_in_bytes:.2f} TB"
    
    def add_file_to_list(self, file_name, file_path, file_size, status="已完成"):
        """添加文件到Treeview列表"""
        # 格式化文件大小
        size_str = self.format_file_size(file_size)
        
        # 获取当前时间
        current_time = datetime.now().strftime("%H:%M:%S")
        
        # 显示缩短的路径
        short_path = file_path
        if len(file_path) > 30:
            short_path = "..." + file_path[-27:]
        
        # 添加到Treeview
        item_id = self.tree.insert("", tk.END, values=(file_name, size_str, current_time, short_path, status))
        
        # 存储详细信息
        self.received_files.append({
            'id': item_id,
            'name': file_name,
            'path': file_path,
            'size': file_size,
            'time': current_time,
            'status': status
        })
        
        # 更新状态
        self.status_var.set(f"已添加: {file_name}")
        
        # 如果列表太长,自动滚动到底部
        self.tree.see(item_id)
    
    def get_selected_file_info(self):
        """获取选中文件的信息"""
        selection = self.tree.selection()
        if not selection:
            messagebox.showwarning("提示", "请先选择一个文件")
            return None
        
        item_id = selection[0]
        for file_info in self.received_files:
            if file_info['id'] == item_id:
                return file_info
        
        return None
    
    def open_selected_file(self):
        """打开选中的文件"""
        file_info = self.get_selected_file_info()
        if not file_info:
            return
        
        file_path = file_info['path']
        # 确保路径格式正确
        file_path = os.path.normpath(file_path)
        
        if not os.path.exists(file_path):
            messagebox.showerror("错误", f"文件不存在:\n{file_path}")
            # 更新状态
            self.tree.set(file_info['id'], column="状态", value="文件丢失")
            return
        
        try:
            if sys.platform == "win32":
                # Windows使用startfile
                os.startfile(file_path)
            elif sys.platform == "darwin":
                subprocess.call(["open", file_path])
            else:
                subprocess.call(["xdg-open", file_path])
            
            self.log(f"已打开文件: {file_info['name']}")
            self.status_var.set(f"正在打开: {file_info['name']}")
        except Exception as e:
            messagebox.showerror("错误", f"无法打开文件: {e}")
            self.log(f"打开文件失败: {e}")
    
    def copy_selected_path(self):
        """复制选中文件的路径"""
        file_info = self.get_selected_file_info()
        if not file_info:
            return
        
        file_path = file_info['path']
        # 确保路径格式正确
        file_path = os.path.normpath(file_path)
        
        try:
            # 尝试使用pyperclip
            try:
                import pyperclip
                pyperclip.copy(file_path)
            except ImportError:
                # 备用方案:使用tkinter剪贴板
                self.master.clipboard_clear()
                self.master.clipboard_append(file_path)
                self.master.update()
            
            self.log(f"已复制路径: {file_path}")
            self.status_var.set("路径已复制到剪贴板")
            
            # 显示临时提示
            self.show_temp_message("路径已复制!")
        except Exception as e:
            messagebox.showerror("错误", f"无法复制到剪贴板: {e}")
    
    def open_selected_folder(self):
        """打开选中文件所在的文件夹"""
        file_info = self.get_selected_file_info()
        if not file_info:
            return
        
        file_path = file_info['path']
        # 确保路径格式正确
        file_path = os.path.normpath(file_path)
        
        if not os.path.exists(file_path):
            messagebox.showerror("错误", f"文件不存在:\n{file_path}")
            return
        
        try:
            folder_path = os.path.dirname(file_path)
            
            if sys.platform == "win32":
                # Windows: 在资源管理器中打开并选中文件
                # 注意:这里需要使用双引号包裹路径,以防路径中有空格
                subprocess.run(f'explorer /select,"{file_path}"', shell=True)
            elif sys.platform == "darwin":
                subprocess.call(["open", "-R", file_path])
            else:
                subprocess.call(["xdg-open", folder_path])
            
            self.log(f"已打开文件夹: {folder_path}")
            self.status_var.set("正在打开文件夹")
        except Exception as e:
            messagebox.showerror("错误", f"无法打开文件夹: {e}")
            self.log(f"打开文件夹失败: {e}")
    
    def delete_selected_record(self):
        """删除选中的记录"""
        selection = self.tree.selection()
        if not selection:
            messagebox.showwarning("提示", "请先选择一个文件记录")
            return
        
        for item_id in selection:
            # 从Treeview删除
            self.tree.delete(item_id)
            
            # 从存储列表中删除
            self.received_files = [f for f in self.received_files if f['id'] != item_id]
        
        self.status_var.set("已删除选中记录")
        self.log(f"删除了 {len(selection)} 条记录")
    
    def open_all_files(self):
        """打开所有文件"""
        if not self.received_files:
            messagebox.showinfo("提示", "没有可打开的文件")
            return
        
        success_count = 0
        for file_info in self.received_files:
            file_path = file_info['path']
            # 确保路径格式正确
            file_path = os.path.normpath(file_path)
            
            if os.path.exists(file_path):
                try:
                    if sys.platform == "win32":
                        os.startfile(file_path)
                    elif sys.platform == "darwin":
                        subprocess.call(["open", file_path])
                    else:
                        subprocess.call(["xdg-open", file_path])
                    success_count += 1
                except:
                    pass
        
        self.log(f"尝试打开了 {success_count}/{len(self.received_files)} 个文件")
        self.status_var.set(f"已打开 {success_count} 个文件")
    
    def clear_file_list(self):
        """清空文件列表"""
        if not self.received_files:
            return
        
        if messagebox.askyesno("确认", f"确定要清除所有 {len(self.received_files)} 条文件记录吗?"):
            # 清空Treeview
            for item in self.tree.get_children():
                self.tree.delete(item)
            
            # 清空存储列表
            self.received_files.clear()
            
            self.log("已清空文件列表")
            self.status_var.set("文件列表已清空")
    
    def on_tree_double_click(self, event):
        """Treeview双击事件"""
        region = self.tree.identify("region", event.x, event.y)
        if region == "cell":
            # 双击文件打开
            self.open_selected_file()
    
    def show_temp_message(self, message, duration=1500):
        """显示临时消息"""
        temp_label = tk.Label(
            self.master, 
            text=message, 
            bg="lightgreen", 
            fg="black",
            font=("Arial", 10, "bold")
        )
        
        # 计算位置
        temp_label.place(relx=0.75, rely=0.25, anchor=tk.CENTER)
        
        # 定时移除消息
        def remove_label():
            temp_label.destroy()
        
        self.master.after(duration, remove_label)
    
    def start_server(self, host, port=8888):
        """启动文件接收服务器(使用改进的协议)"""
        self.server_running = True
        server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        
        try:
            server_socket.bind((host, port))
            server_socket.listen(1)
            self.log(f"服务器启动,监听 {host}:{port}")
            
            # 确保保存目录存在
            self.ensure_save_path()
            
            while self.server_running:
                try:
                    server_socket.settimeout(1.0)
                    client_socket, addr = server_socket.accept()
                    self.log(f"连接来自 {addr}")
 
                    # 接收固定1024字节的文件信息
                    file_info_bytes = client_socket.recv(1024)
                    
                    if not file_info_bytes:
                        client_socket.close()
                        continue
                    
                    # 解码文件信息(移除填充的空格)
                    try:
                        file_info = file_info_bytes.decode('utf-8').strip()
                    except UnicodeDecodeError:
                        # 如果utf-8失败,尝试其他编码
                        try:
                            file_info = file_info_bytes.decode('gbk').strip()
                        except:
                            # 最后尝试latin-1
                            file_info = file_info_bytes.decode('latin-1', errors='ignore').strip()
                    
                    # 解析文件信息
                    parts = file_info.split('|')
                    if len(parts) < 2:
                        self.log(f"文件信息格式错误: {file_info}")
                        client_socket.close()
                        continue
                    
                    file_name = parts[0]
                    try:
                        file_size = int(parts[1])
                    except ValueError:
                        self.log(f"文件大小格式错误: {parts[1]}")
                        client_socket.close()
                        continue
 
                    # 创建保存路径 - 使用os.path.join确保正确的路径分隔符
                    save_path = os.path.join(self.save_path, file_name)
                    
                    # 如果文件已存在,添加时间戳
                    if os.path.exists(save_path):
                        name, ext = os.path.splitext(file_name)
                        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
                        file_name = f"{name}_{timestamp}{ext}"
                        save_path = os.path.join(self.save_path, file_name)
                    
                    self.log(f"开始接收文件: {file_name} ({file_size} 字节)")
 
                    # 接收文件数据
                    with open(save_path, 'wb') as file:
                        received = 0
                        while received < file_size:
                            # 计算剩余字节数
                            remaining = file_size - received
                            # 每次最多接收4096字节
                            chunk_size = min(4096, remaining)
                            
                            data = client_socket.recv(chunk_size)
                            if not data:
                                break
                            
                            file.write(data)
                            received += len(data)
                            
                            # 更新进度
                            progress = int(received / file_size * 100)
                            self.progress_var1.set(progress)
 
                    client_socket.close()
                    
                    if received == file_size:
                        # 添加到文件列表
                        self.master.after(0, lambda fn=file_name, fp=save_path, fs=file_size: 
                                         self.add_file_to_list(fn, fp, fs, "已完成"))
                        
                        self.log(f"文件 {file_name} 接收完成")
                        self.log(f"保存路径: {save_path}")
                        
                        # 显示完成对话框
                        self.master.after(0, lambda fn=file_name, sp=save_path: 
                                         messagebox.showinfo("接收完成", 
                                                           f"文件 {fn} 接收完成\n\n保存到:\n{sp}"))
                    else:
                        self.log(f"文件接收不完整: {received}/{file_size} 字节")
                        self.master.after(0, lambda fn=file_name, rec=received, total=file_size: 
                                         self.add_file_to_list(fn, save_path, total, f"不完整 {rec}/{total}"))
                    
                except socket.timeout:
                    continue
                except Exception as e:
                    if self.server_running:
                        self.log(f"接收文件时出错: {str(e)}")
                    
        except Exception as e:
            self.log(f"服务器启动失败: {str(e)}")
        finally:
            server_socket.close()
            self.server_running = False
            self.master.after(0, self.update_server_button)
    
    def update_server_button(self):
        """更新服务器按钮状态"""
        self.bt_fwq.config(state=tk.NORMAL, bg="SystemButtonFace")
        self.stop_fwq_btn.config(state=tk.DISABLED)
        self.log("服务器已停止")
        self.status_var.set("服务器已停止")
    
    def start_fwq(self):
        """启动服务器"""
        fwq_ip = self.bip_entry.get()
        if not fwq_ip:
            messagebox.showerror("错误", "请输入本机IP地址")
            return
        
        # 在新线程中启动服务器
        server_thread = threading.Thread(target=self.start_server, args=(fwq_ip, 8888))
        server_thread.daemon = True
        server_thread.start()
        
        self.bt_fwq.config(state=tk.DISABLED, bg="lightgray")
        self.stop_fwq_btn.config(state=tk.NORMAL)
        self.log(f"服务器已在 {fwq_ip}:8888 启动")
        self.status_var.set("服务器运行中...")
    
    def stop_fwq(self):
        """停止服务器"""
        self.server_running = False
        self.stop_fwq_btn.config(state=tk.DISABLED)
        self.log("正在停止服务器...")
        self.status_var.set("正在停止服务器...")
    
    def send_file(self, server_ip, file_path, port=8888):
        """发送文件到目标服务器(使用改进的协议)"""
        if not os.path.exists(file_path):
            self.log(f"文件不存在: {file_path}")
            return
        
        # 确保文件路径格式正确
        file_path = os.path.normpath(file_path)
            
        self.file_name = os.path.basename(file_path)
        client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            client_socket.connect((server_ip, port))
            file_size = os.path.getsize(file_path)
    
            # 发送文件信息(使用UTF-8编码)
            # 按照接收端的协议,发送固定1024字节的文件信息
            file_info = f"{self.file_name}|{file_size}"
            file_info_bytes = file_info.encode('utf-8')
            
            # 如果文件信息不足1024字节,用空格填充
            if len(file_info_bytes) < 1024:
                file_info_bytes += b' ' * (1024 - len(file_info_bytes))
            else:
                # 如果超过1024字节,截断
                file_info_bytes = file_info_bytes[:1024]
            
            # 发送固定1024字节的文件信息
            client_socket.send(file_info_bytes)
            self.log(f"发送文件信息: {self.file_name} ({file_size} 字节)")
    
            # 分块发送文件数据
            with open(file_path, 'rb') as file:
                sent = 0
                start = time.time()
                while sent < file_size:
                    data = file.read(4096)
                    client_socket.send(data)
                    sent += len(data)
                    progress = int(sent / file_size * 100)
                    self.progress_var.set(progress)
            
            elapsed_time = time.time() - start
            speed = file_size / 1024 / 1024 / elapsed_time if elapsed_time > 0 else 0
            
            self.log(f"文件发送成功, 耗时{elapsed_time:.2f}秒, 速度:{speed:.2f} MB/s")
            
        except ConnectionRefusedError:
            self.log(f"连接被拒绝,请检查服务器IP和端口")
            self.status_var.set("连接被拒绝")
        except Exception as e:
            self.log(f"发送文件时出错: {str(e)}")
        finally:
            client_socket.close()
    
    def select_file(self):
        """选择文件"""
        self.file_path = filedialog.askopenfilename()
        if self.file_path:
            # 确保文件路径格式正确
            self.file_path = os.path.normpath(self.file_path)
            self.file_name = os.path.basename(self.file_path)
            self.log(f"已选择文件: {self.file_path}")
            self.status_var.set(f"已选择: {self.file_name}")
    
    def start_send(self):
        """开始发送文件"""
        server_ip = self.ip_entry.get()
        if not server_ip or not self.file_path:
            messagebox.showerror("错误", "请输入对方IP并选择文件")
            return
        
        # 在新线程中发送文件
        send_thread = threading.Thread(target=self.send_file_thread, args=(server_ip, self.file_path))
        send_thread.daemon = True
        send_thread.start()
    
    def send_file_thread(self, server_ip, file_path):
        """发送文件的线程"""
        try:
            self.log("开始发送文件...")
            self.send_file(server_ip, file_path)
            self.progress_var.set(100)
            
            # 发送完成后显示消息
            self.master.after(0, lambda: messagebox.showinfo("发送完成", "文件发送完成"))
        except Exception as e:
            self.log(f"发送文件时出错: {str(e)}")
    
    def log(self, message):
        """添加日志"""
        self.log_area.config(state=tk.NORMAL)
        self.log_area.insert(tk.END, message + "\n")
        self.log_area.config(state=tk.DISABLED)
        self.log_area.yview(tk.END)

if __name__ == "__main__":
    # 检查必要的库
    try:
        import pyperclip
    except ImportError:
        print("注意: pyperclip未安装,复制功能可能受限")
        print("如需完整功能,请运行: pip install pyperclip")
    
    # 创建窗口
    if HAS_DND:
        root = TkinterDnD.Tk()
    else:
        root = tk.Tk()
    
    app = FileTransferGUI(root)
    
    # 设置窗口最小尺寸
    root.minsize(600, 700)
    
    root.mainloop()

运行界面:

相关推荐
未来猫咪花1 小时前
LiveData "数据倒灌":一个流行的错误概念
android·android jetpack
旦莫1 小时前
Pytest教程: Pytest ini配置文件深度剖析
python·单元测试·自动化·pytest
天才测试猿1 小时前
Jmeter压测实战:Jmeter二次开发之自定义函数
自动化测试·软件测试·python·测试工具·jmeter·职场和发展·压力测试
2501_937154931 小时前
神马影视 8.8 源码:1.5 秒加载 + 双系统部署
android·源码·源代码管理·机顶盒
haiyu_y2 小时前
Day 30 函数专题 1
python
培根芝士2 小时前
使用Scripting API获取CS2游戏数据
python·游戏
吳所畏惧2 小时前
少走弯路:uniapp里将h5链接打包为apk,并设置顶/底部安全区域自动填充显示,阻止webview默认全屏化
android·安全·uni-app·json·html5·webview·js
CesareCheung2 小时前
用python写一个websocket接口,并用jmeter压测websocket接口
python·websocket·jmeter
金士顿2 小时前
Ethercat耦合器添加的IO导出xml 初始化IO参数
android·xml·java