mac os 修改网卡小工具

版本 3.0 更新内容 import sys import re import subprocess import json import os from PyQt6.QtWidgets import ( QApplication, QWidget, QLabel, QLineEdit, QPush...
mac os 修改网卡小工具
mac os 修改网卡小工具

image

版本 3.0 更新内容

import sys
import re
import subprocess
import json
import os
from PyQt6.QtWidgets import (
    QApplication, QWidget, QLabel, QLineEdit, QPushButton, QVBoxLayout,
    QMessageBox, QComboBox, QGridLayout, QGroupBox, QStyleFactory, QListWidget, QHBoxLayout, QSizePolicy
)
from PyQt6.QtGui import QFont, QPalette, QColor
from PyQt6.QtCore import Qt
 
class IPChanger(QWidget):
    def __init__(self):
        super().__init__()
        self.config_file = os.path.join(os.path.expanduser("~"), "Library", "Application Support", "IPChanger", "ip_configs.json")
        os.makedirs(os.path.dirname(self.config_file), exist_ok=True)
        self.configs = self.load_configs()
        self.initUI()
 
    def initUI(self):
        print("Starting initUI...")  # 调试日志
        self.setWindowTitle("macOS IP 修改工具")
        self.setGeometry(200, 200, 900, 505)
        self.setMinimumSize(600, 400)
 
        # 设置全局字体
        print("Setting global font...")  # 调试日志
        try:
            app_font = QFont("SF Pro", 13)  # 使用 macOS 原生字体 SF Pro,13pt
            app_font.setWeight(QFont.Weight.Normal)
            QApplication.setFont(app_font)
        except Exception as e:
            print(f"Failed to set font: {e}")  # 调试日志
            QMessageBox.warning(self, "警告", f"无法设置字体: {str(e)}")
 
        # 主题美化
        print("Setting style to macos...")  # 调试日志
        try:
            QApplication.setStyle(QStyleFactory.create("macos"))
        except Exception as e:
            print(f"Failed to set style: {e}")  # 调试日志
            QMessageBox.warning(self, "警告", f"无法设置界面主题: {str(e)}")
 
        main_layout = QHBoxLayout()
        main_layout.setSpacing(10)
        main_layout.setContentsMargins(10, 10, 10, 10)
 
        # 左侧布局:网卡信息 + 已保存配置 + 路由信息表
        left_layout = QVBoxLayout()
 
        # 网卡信息分组
        print("Initializing network group...")  # 调试日志
        net_group = QGroupBox("网卡信息")
        net_group.setFont(QFont("SF Pro", 14, QFont.Weight.Bold))
        net_layout = QGridLayout()
        net_group.setLayout(net_layout)
 
        self.interface_label = QLabel("选择网卡:")
        self.interface_label.setFont(QFont("SF Pro", 14, QFont.Weight.Bold))
        self.interface_dropdown = QComboBox()
        self.interface_dropdown.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
        self.interface_dropdown.currentIndexChanged.connect(self.update_ui)
 
        self.status_label = QLabel("网卡状态:")
        self.status_label.setFont(QFont("SF Pro", 14, QFont.Weight.Bold))
        self.status_value = QLabel("N/A")
        self.current_ip_label = QLabel("当前 IP:")
        self.current_ip_label.setFont(QFont("SF Pro", 14, QFont.Weight.Bold))
        self.current_ip_value = QLabel("N/A")
        self.subnet_label = QLabel("子网掩码:")
        self.subnet_label.setFont(QFont("SF Pro", 14, QFont.Weight.Bold))
        self.subnet_value = QLabel("N/A")
        self.gateway_label = QLabel("网关:")
        self.gateway_label.setFont(QFont("SF Pro", 14, QFont.Weight.Bold))
        self.gateway_value = QLabel("N/A")
 
        net_layout.addWidget(self.interface_label, 0, 0)
        net_layout.addWidget(self.interface_dropdown, 0, 1)
        net_layout.addWidget(self.status_label, 1, 0)
        net_layout.addWidget(self.status_value, 1, 1)
        net_layout.addWidget(self.current_ip_label, 2, 0)
        net_layout.addWidget(self.current_ip_value, 2, 1)
        net_layout.addWidget(self.subnet_label, 3, 0)
        net_layout.addWidget(self.subnet_value, 3, 1)
        net_layout.addWidget(self.gateway_label, 4, 0)
        net_layout.addWidget(self.gateway_value, 4, 1)
 
        # 保存的配置分组
        print("Initializing saved config group...")  # 调试日志
        saved_group = QGroupBox("已保存的配置")
        saved_group.setFont(QFont("SF Pro", 14, QFont.Weight.Bold))
        saved_layout = QVBoxLayout()
        saved_group.setLayout(saved_layout)
 
        self.saved_configs_list = QListWidget()
        self.saved_configs_list.setFont(QFont("SF Pro", 12))
        self.saved_configs_list.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
        self.load_saved_configs_to_list()
        self.saved_configs_list.itemClicked.connect(self.load_selected_config)
 
        self.delete_config_btn = QPushButton("删除选中配置")
        self.delete_config_btn.clicked.connect(self.delete_selected_config)
        self.delete_config_btn.setStyleSheet("""
            QPushButton {
                background-color: #F44336;
                color: white;
                font-family: 'SF Pro';
                font-size: 14px;
                font-weight: bold;
                padding: 8px;
                border-radius: 5px;
            }
            QPushButton:hover {
                background-color: #D32F2F;
            }
        """)
 
        saved_layout.addWidget(self.saved_configs_list)
        saved_layout.addWidget(self.delete_config_btn)
 
        # 路由信息表分组
        print("Initializing route list group...")  # 调试日志
        route_list_group = QGroupBox("路由信息表")
        route_list_group.setFont(QFont("SF Pro", 14, QFont.Weight.Bold))
        route_list_layout = QVBoxLayout()
        route_list_group.setLayout(route_list_layout)
 
        print("Creating route_list...")  # 调试日志
        self.route_list = QListWidget()
        self.route_list.setFont(QFont("SF Pro", 12))
        self.route_list.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
        print("route_list created")  # 调试日志
 
        self.delete_route_btn = QPushButton("删除选中路由")
        self.delete_route_btn.clicked.connect(self.delete_route)
        self.delete_route_btn.setStyleSheet("""
            QPushButton {
                background-color: #F44336;
                color: white;
                font-family: 'SF Pro';
                font-size: 14px;
                font-weight: bold;
                padding: 8px;
                border-radius: 5px;
            }
            QPushButton:hover {
                background-color: #D32F2F;
            }
        """)
 
        route_list_layout.addWidget(self.route_list)
        route_list_layout.addWidget(self.delete_route_btn)
 
        left_layout.addWidget(net_group)
        left_layout.addWidget(saved_group)
        left_layout.addWidget(route_list_group)
        left_layout.addStretch(1)
 
        # 右侧布局:IP 配置 + 路由管理
        right_layout = QVBoxLayout()
 
        # IP 配置分组
        print("Initializing IP config group...")  # 调试日志
        ip_group = QGroupBox("IP 配置")
        ip_group.setFont(QFont("SF Pro", 14, QFont.Weight.Bold))
        ip_layout = QGridLayout()
        ip_group.setLayout(ip_layout)
 
        self.profile_name_label = QLabel("配置名称:")
        self.profile_name_label.setFont(QFont("SF Pro", 14, QFont.Weight.Bold))
        self.profile_name_input = QLineEdit()
        self.profile_name_input.setFont(QFont("SF Pro", 12))
        self.ip_label = QLabel("新 IP:")
        self.ip_label.setFont(QFont("SF Pro", 14, QFont.Weight.Bold))
        self.ip_input = QLineEdit()
        self.ip_input.setFont(QFont("SF Pro", 12))
        self.ip_input.textChanged.connect(lambda: self.validate_input(self.ip_input))
        self.ip_status = QLabel("")
        self.subnet_label = QLabel("子网掩码:")
        self.subnet_label.setFont(QFont("SF Pro", 14, QFont.Weight.Bold))
        self.subnet_input = QLineEdit()
        self.subnet_input.setFont(QFont("SF Pro", 12))
        self.subnet_input.textChanged.connect(lambda: self.validate_input(self.subnet_input))
        self.subnet_status = QLabel("")
        self.gateway_label = QLabel("网关:")
        self.gateway_label.setFont(QFont("SF Pro", 14, QFont.Weight.Bold))
        self.gateway_input = QLineEdit()
        self.gateway_input.setFont(QFont("SF Pro", 12))
        self.gateway_input.textChanged.connect(lambda: self.validate_input(self.gateway_input))
        self.gateway_status = QLabel("")
 
        ip_layout.addWidget(self.profile_name_label, 0, 0)
        ip_layout.addWidget(self.profile_name_input, 0, 1)
        ip_layout.addWidget(self.ip_label, 1, 0)
        ip_layout.addWidget(self.ip_input, 1, 1)
        ip_layout.addWidget(self.ip_status, 1, 2)
        ip_layout.addWidget(self.subnet_label, 2, 0)
        ip_layout.addWidget(self.subnet_input, 2, 1)
        ip_layout.addWidget(self.subnet_status, 2, 2)
        ip_layout.addWidget(self.gateway_label, 3, 0)
        ip_layout.addWidget(self.gateway_input, 3, 1)
        ip_layout.addWidget(self.gateway_status, 3, 2)
 
        self.apply_btn = QPushButton("修改 IP")
        self.apply_btn.clicked.connect(self.change_ip)
        self.apply_btn.setStyleSheet("""
            QPushButton {
                background-color: #4CAF50;
                color: white;
                font-family: 'SF Pro';
                font-size: 14px;
                font-weight: bold;
                padding: 8px;
                border-radius: 5px;
            }
            QPushButton:hover {
                background-color: #388E3C;
            }
        """)
 
        self.dhcp_btn = QPushButton("恢复 DHCP")
        self.dhcp_btn.clicked.connect(self.set_dhcp)
        self.dhcp_btn.setStyleSheet("""
            QPushButton {
                background-color: #2196F3;
                color: white;
                font-family: 'SF Pro';
                font-size: 14px;
                font-weight: bold;
                padding: 8px;
                border-radius: 5px;
            }
            QPushButton:hover {
                background-color: #1976D2;
            }
        """)
 
        self.save_config_btn = QPushButton("保存配置")
        self.save_config_btn.clicked.connect(self.save_config)
        self.save_config_btn.setStyleSheet("""
            QPushButton {
                background-color: #FFC107;
                color: white;
                font-family: 'SF Pro';
                font-size: 14px;
                font-weight: bold;
                padding: 8px;
                border-radius: 5px;
            }
            QPushButton:hover {
                background-color: #FFA000;
            }
        """)
 
        # 路由管理分组
        print("Initializing route group...")  # 调试日志
        route_group = QGroupBox("路由管理")
        route_group.setFont(QFont("SF Pro", 14, QFont.Weight.Bold))
        route_layout = QGridLayout()
        route_group.setLayout(route_layout)
 
        self.route_dest_label = QLabel("目标网络:")
        self.route_dest_label.setFont(QFont("SF Pro", 14, QFont.Weight.Bold))
        self.route_dest_input = QLineEdit()
        self.route_dest_input.setFont(QFont("SF Pro", 12))
        self.route_dest_input.textChanged.connect(lambda: self.validate_input(self.route_dest_input))
        self.route_dest_status = QLabel("")
        self.route_mask_label = QLabel("子网掩码:")
        self.route_mask_label.setFont(QFont("SF Pro", 14, QFont.Weight.Bold))
        self.route_mask_input = QLineEdit()
        self.route_mask_input.setFont(QFont("SF Pro", 12))
        self.route_mask_input.textChanged.connect(lambda: self.validate_input(self.route_mask_input))
        self.route_mask_status = QLabel("")
        self.route_gateway_label = QLabel("网关:")
        self.route_gateway_label.setFont(QFont("SF Pro", 14, QFont.Weight.Bold))
        self.route_gateway_input = QLineEdit()
        self.route_gateway_input.setFont(QFont("SF Pro", 12))
        self.route_gateway_input.textChanged.connect(lambda: self.validate_input(self.route_gateway_input))
        self.route_gateway_status = QLabel("")
 
        route_layout.addWidget(self.route_dest_label, 0, 0)
        route_layout.addWidget(self.route_dest_input, 0, 1)
        route_layout.addWidget(self.route_dest_status, 0, 2)
        route_layout.addWidget(self.route_mask_label, 1, 0)
        route_layout.addWidget(self.route_mask_input, 1, 1)
        route_layout.addWidget(self.route_mask_status, 1, 2)
        route_layout.addWidget(self.route_gateway_label, 2, 0)
        route_layout.addWidget(self.route_gateway_input, 2, 1)
        route_layout.addWidget(self.route_gateway_status, 2, 2)
 
        self.add_route_btn = QPushButton("添加临时路由")
        self.add_route_btn.clicked.connect(self.add_route)
        self.add_route_btn.setStyleSheet("""
            QPushButton {
                background-color: #FF5722;
                color: white;
                font-family: 'SF Pro';
                font-size: 14px;
                font-weight: bold;
                padding: 8px;
                border-radius: 5px;
            }
            QPushButton:hover {
                background-color: #E64A19;
            }
        """)
 
        route_layout.addWidget(self.add_route_btn, 3, 0, 1, 2)
 
        right_layout.addWidget(ip_group)
        right_layout.addWidget(self.apply_btn)
        right_layout.addWidget(self.dhcp_btn)
        right_layout.addWidget(self.save_config_btn)
        right_layout.addWidget(route_group)
        right_layout.addStretch(1)
 
        # 主界面布局
        main_layout.addLayout(left_layout, 1)
        main_layout.addLayout(right_layout, 1)
        self.setLayout(main_layout)
 
        # 延迟初始化网卡和路由列表
        print("Initializing network interfaces...")  # 调试日志
        try:
            self.get_network_interfaces()
        except Exception as e:
            print(f"Failed to get network interfaces: {e}")  # 调试日志
            QMessageBox.critical(self, "错误", f"无法获取网卡列表: {str(e)}")
 
        print("initUI completed, calling update_ui...")  # 调试日志
        try:
            self.update_ui()
        except Exception as e:
            print(f"Failed to update UI: {e}")  # 调试日志
            QMessageBox.warning(self, "警告", f"无法更新界面: {str(e)}")
 
    def validate_input(self, input_field):
        """实时校验输入框中的 IP 格式"""
        text = input_field.text().strip()
        status_label = None
        if input_field == self.ip_input:
            status_label = self.ip_status
        elif input_field == self.subnet_input:
            status_label = self.subnet_status
        elif input_field == self.gateway_input:
            status_label = self.gateway_status
        elif input_field == self.route_dest_input:
            status_label = self.route_dest_status
        elif input_field == self.route_mask_input:
            status_label = self.route_mask_status
        elif input_field == self.route_gateway_input:
            status_label = self.route_gateway_status
 
        if not text:
            status_label.setText("")
            input_field.setStyleSheet("")
            return
 
        if self.validate_ip(text):
            status_label.setText("✓")
            status_label.setStyleSheet("color: green; font-family: 'SF Pro'; font-size: 14px; font-weight: bold;")
            input_field.setStyleSheet("border: 1px solid green; font-family: 'SF Pro'; font-size: 12px;")
        else:
            status_label.setText("✗")
            status_label.setStyleSheet("color: red; font-family: 'SF Pro'; font-size: 14px; font-weight: bold;")
            input_field.setStyleSheet("border: 1px solid red; font-family: 'SF Pro'; font-size: 12px;")
 
    def get_network_interfaces(self):
        """获取 macOS 可用的网络接口列表"""
        result = subprocess.run("networksetup -listallnetworkservices", shell=True, capture_output=True, text=True)
        interfaces = result.stdout.strip().split("\n")[1:]  # 去掉第一行(标题)
        if not interfaces:
            raise Exception("No network interfaces found")
        self.interface_dropdown.addItems(interfaces)
 
    def get_network_info(self, interface):
        """获取网卡详细状态"""
        try:
            result = subprocess.run(f"networksetup -getinfo \"{interface}\"", shell=True, capture_output=True, text=True)
            output = result.stdout.strip()
 
            status_match = re.search(r"IP address: (.+)", output)
            subnet_match = re.search(r"Subnet mask: (.+)", output)
            gateway_match = re.search(r"Router: (.+)", output)
 
            ip_address = status_match.group(1) if status_match else "未连接"
            subnet = subnet_match.group(1) if subnet_match else "N/A"
            gateway = gateway_match.group(1) if gateway_match else "N/A"
 
            if "DHCP Configuration" in output:
                return "动态 IP (DHCP)", ip_address, subnet, gateway
            else:
                return "静态 IP", ip_address, subnet, gateway
        except Exception:
            return "未知", "N/A", "N/A", "N/A"
 
    def update_ui(self):
        """更新 UI(显示当前网卡状态和详细信息)"""
        print("Updating UI...")  # 调试日志
        interface = self.interface_dropdown.currentText()
        if not interface:
            print("No interface selected, skipping UI update")  # 调试日志
            return
        try:
            status, ip, subnet, gateway = self.get_network_info(interface)
            if hasattr(self, 'status_value') and hasattr(self, 'current_ip_value') and \
               hasattr(self, 'subnet_value') and hasattr(self, 'gateway_value'):
                self.status_value.setText(status)
                self.current_ip_value.setText(ip)
                self.subnet_value.setText(subnet)
                self.gateway_value.setText(gateway)
            else:
                print("Network info attributes not initialized")  # 调试日志
                QMessageBox.warning(self, "警告", "网络信息组件未初始化")
        except Exception as e:
            print(f"Failed to get network info: {e}")  # 调试日志
            QMessageBox.warning(self, "警告", f"无法获取网卡信息: {str(e)}")
 
        if hasattr(self, 'route_list'):
            print("Loading routes to list...")  # 调试日志
            try:
                self.load_routes_to_list()
            except Exception as e:
                print(f"Failed to load routes: {e}")  # 调试日志
                QMessageBox.warning(self, "警告", f"无法加载路由表: {str(e)}")
        else:
            print("route_list not initialized")  # 调试日志
 
    def validate_ip(self, ip):
        """校验 IP 格式"""
        pattern = r"^(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9][0-9]?)$"
        return re.match(pattern, ip) is not None
 
    def change_ip(self):
        """修改 IP 地址"""
        interface = self.interface_dropdown.currentText()
        ip = self.ip_input.text().strip()
        subnet = self.subnet_input.text().strip()
        gateway = self.gateway_input.text().strip()
 
        if not interface:
            QMessageBox.warning(self, "错误", "请先选择网卡!")
            return
 
        if not (self.validate_ip(ip) and self.validate_ip(subnet) and self.validate_ip(gateway)):
            QMessageBox.warning(self, "错误", "IP、子网掩码或网关格式不正确!")
            return
 
        cmd = f"osascript -e 'do shell script \"networksetup -setmanual \\\"{interface}\\\" {ip} {subnet} {gateway}\" with administrator privileges'"
        self.run_command(cmd)
 
    def set_dhcp(self):
        """切换到 DHCP 自动获取 IP"""
        interface = self.interface_dropdown.currentText()
        if not interface:
            QMessageBox.warning(self, "错误", "请先选择网卡!")
            return
 
        cmd = f"osascript -e 'do shell script \"networksetup -setdhcp \\\"{interface}\\\"\" with administrator privileges'"
        self.run_command(cmd)
 
    def add_route(self):
        """添加临时路由"""
        destination = self.route_dest_input.text().strip()
        mask = self.route_mask_input.text().strip()
        gateway = self.route_gateway_input.text().strip()
 
        if not (self.validate_ip(destination) and self.validate_ip(mask) and self.validate_ip(gateway)):
            QMessageBox.warning(self, "错误", "目标网络、子网掩码或网关格式不正确!")
            return
 
        cmd = f"osascript -e 'do shell script \"route -n add -net {destination} -netmask {mask} {gateway}\" with administrator privileges'"
        self.run_command(cmd)
        self.load_routes_to_list()
        self.route_dest_input.clear()
        self.route_mask_input.clear()
        self.route_gateway_input.clear()
 
    def delete_route(self):
        """删除选中的临时路由"""
        if not hasattr(self, 'route_list'):
            QMessageBox.warning(self, "错误", "路由列表未初始化!")
            return
        current_item = self.route_list.currentItem()
        if not current_item:
            QMessageBox.warning(self, "错误", "请先选择一个路由!")
            return
 
        destination = current_item.text().split(" ")[0].strip()
        cmd = f"osascript -e 'do shell script \"route -n delete -net {destination}\" with administrator privileges'"
        self.run_command(cmd)
        self.load_routes_to_list()
 
    def load_routes_to_list(self):
        """加载当前路由表到列表"""
        print("Clearing route_list...")  # 调试日志
        if not hasattr(self, 'route_list'):
            print("route_list not initialized in load_routes_to_list")  # 调试日志
            return
        self.route_list.clear()
        try:
            result = subprocess.run("netstat -rn | grep -E '^[0-9]'", shell=True, capture_output=True, text=True)
            routes = result.stdout.strip().split("\n")
            for route in routes:
                parts = route.split()
                if len(parts) >= 2:
                    self.route_list.addItem(f"{parts[0]} via {parts[1]}")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"无法加载路由表: {str(e)}")
 
    def run_command(self, cmd):
        """运行 shell 命令"""
        try:
            result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
            if result.returncode == 0:
                QMessageBox.information(self, "成功", "操作成功")
                self.update_ui()
            else:
                QMessageBox.critical(self, "错误", f"执行失败:\n{result.stderr}")
        except Exception as e:
            QMessageBox.critical(self, "异常", f"发生错误:\n{str(e)}")
 
    def load_configs(self):
        """加载保存的配置"""
        if os.path.exists(self.config_file):
            try:
                with open(self.config_file, 'r') as f:
                    return json.load(f)
            except Exception as e:
                QMessageBox.critical(self, "错误", f"无法加载配置文件: {str(e)}")
                return {}
        return {}
 
    def save_configs(self):
        """保存配置到文件"""
        try:
            with open(self.config_file, 'w') as f:
                json.dump(self.configs, f, indent=2)
        except Exception as e:
            QMessageBox.critical(self, "错误", f"无法保存配置文件: {str(e)}")
 
    def save_config(self):
        """保存当前 IP 配置"""
        profile_name = self.profile_name_input.text().strip()
        ip = self.ip_input.text().strip()
        subnet = self.subnet_input.text().strip()
        gateway = self.gateway_input.text().strip()
 
        if not profile_name:
            QMessageBox.warning(self, "错误", "请输入配置名称!")
            return
 
        if not (self.validate_ip(ip) and self.validate_ip(subnet) and self.validate_ip(gateway)):
            QMessageBox.warning(self, "错误", "IP、子网掩码或网关格式不正确!")
            return
 
        if profile_name in self.configs:
            reply = QMessageBox.question(self, "确认", "配置已存在,是否覆盖?",
                                         QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
            if reply == QMessageBox.StandardButton.No:
                return
 
        self.configs[profile_name] = {
            "ip": ip,
            "subnet": subnet,
            "gateway": gateway
        }
        self.save_configs()
        self.load_saved_configs_to_list()
        QMessageBox.information(self, "成功", "配置已保存")
        self.profile_name_input.clear()
        self.ip_input.clear()
        self.subnet_input.clear()
        self.gateway_input.clear()
 
    def load_saved_configs_to_list(self):
        """加载保存的配置到列表显示"""
        if not hasattr(self, 'saved_configs_list'):
            print("saved_configs_list not initialized")  # 调试日志
            return
        self.saved_configs_list.clear()
        for profile_name, config in self.configs.items():
            self.saved_configs_list.addItem(
                f"{profile_name}: {config['ip']} / {config['subnet']} / {config['gateway']}")
 
    def load_selected_config(self, item):
        """加载选中的配置到输入框"""
        if not item:
            return
        profile_name = item.text().split(":")[0].strip()
        if profile_name in self.configs:
            config = self.configs[profile_name]
            self.ip_input.setText(config["ip"])
            self.subnet_input.setText(config["subnet"])
            self.gateway_input.setText(config["gateway"])
            self.profile_name_input.setText(profile_name)
 
    def delete_selected_config(self):
        """删除选中的配置"""
        if not hasattr(self, 'saved_configs_list'):
            QMessageBox.warning(self, "错误", "配置列表未初始化!")
            return
        current_item = self.saved_configs_list.currentItem()
        if not current_item:
            QMessageBox.warning(self, "错误", "请先选择一个配置!")
            return
 
        profile_name = current_item.text().split(":")[0].strip()
        if profile_name in self.configs:
            reply = QMessageBox.question(self, "确认", f"确定要删除配置 '{profile_name}' 吗?",
                                         QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
            if reply == QMessageBox.StandardButton.Yes:
                del self.configs[profile_name]
                self.save_configs()
                self.load_saved_configs_to_list()
                QMessageBox.information(self, "成功", "配置已删除")
                self.profile_name_input.clear()
                self.ip_input.clear()
                self.subnet_input.clear()
                self.gateway_input.clear()
 
if __name__ == "__main__":
    try:
        app = QApplication(sys.argv)
        window = IPChanger()
        window.show()
        sys.exit(app.exec())
    except Exception as e:
        print(f"Application failed to start: {e}")
        sys.exit(1)
新增功能
  • 路由管理功能
    • 新增“路由信息表”分组,显示当前系统路由表(通过 netstat -rn 获取)。
    • 支持添加临时路由,包含目标网络、子网掩码和网关输入框。
    • 支持删除选中的临时路由。
  • 实时输入校验
    • 为 IP、子网掩码、网关、路由目标网络、路由子网掩码和路由网关输入框添加实时格式校验。
    • 校验通过时显示绿色“✓”,失败时显示红色“✗”,并为输入框添加相应的边框颜色。
  • 配置覆盖确认
    • 保存配置时,若配置名称已存在,会提示用户是否覆盖现有配置。
  • 删除配置确认
    • 删除配置时,增加确认提示框,防止误操作。
界面改进
  • 字体优化
    • 全局使用 macOS 原生字体 SF Pro,标题加粗,增强视觉层次。
    • 设置统一的字体大小(标签 14pt,输入框和列表 12pt)。
  • 主题优化
    • 使用 macos 主题(QStyleFactory.create("macos")),替换之前的 Macintosh 主题,提供更现代化的 macOS 原生风格。
    • 按钮样式优化,添加 hover 效果,统一圆角(5px)和填充(8px)。
  • 布局改进
    • 主窗口最小尺寸设为 600x400,确保内容完整显示。
    • 增加布局间距(main_layout.setSpacing(10))和边距(setContentsMargins(10, 10, 10, 10)),提升界面美观度。
    • 为网卡下拉框和列表控件设置 QSizePolicy.Expanding,确保自适应窗口大小。
    • 左侧和右侧布局通过 addStretch(1) 优化空间分配。
  • 路由管理分组
    • 新增“路由管理”分组,包含目标网络、子网掩码和网关输入框,以及“添加临时路由”按钮。
功能优化
  • 配置文件路径
    • 配置文件存储路径从 __file__ 目录更改为用户目录下的 ~/Library/Application Support/IPChanger/ip_configs.json,符合 macOS 应用标准。
    • 自动创建配置文件目录(os.makedirs)。
  • 错误处理
    • 增强异常捕获,初始化界面时捕获字体、主题、网卡列表和 UI 更新的异常,并显示友好提示。
    • 启动应用程序时添加全局异常捕获,记录错误并退出。
  • 调试日志
    • initUI 方法中添加详细的调试日志,记录界面初始化步骤,便于排查问题。
  • 网卡选择校验
    • 在修改 IP 或切换 DHCP 时,增加网卡选择是否有效的校验,防止空网卡操作。
  • 路由表加载
    • 使用 netstat -rn | grep -E '^[0-9]' 加载路由表,仅显示以数字开头的路由条目,确保数据清晰。
问题修复
  • 网卡信息初始化
    • 修复 status_value 等属性可能未初始化的问题,增加属性存在性检查。
  • 路由列表初始化
    • 修复路由列表可能未初始化的问题,增加 hasattr 检查。
  • 配置列表初始化
    • 修复保存配置列表可能未初始化的问题,增加 hasattr 检查。
代码优化
  • 代码结构
    • 优化布局代码,分组更清晰,逻辑分块更合理。
    • 统一按钮样式定义,使用多行字符串减少代码冗余。
  • 输入校验逻辑
    • 将输入校验逻辑提取为独立方法 validate_input,支持多个输入框的动态校验。
  • 路由操作
    • 使用 route -n addroute -n delete 命令实现临时路由的添加和删除,带管理员权限。
已知问题
  • 路由管理仅支持临时路由,系统重启后路由会丢失(后续版本可考虑持久化路由)。
  • 部分 macOS 系统版本可能对 networksetup 命令响应较慢,可能导致 UI 延迟。
  • 字体 SF Pro 在某些 macOS 版本可能不可用,需进一步测试兼容性。

1 个帖子 - 1 位参与者

阅读完整话题

来源: LinuxDo 最新话题查看原文