Flutter桌面应用实战:Windows系统代理切换工具开发

前言

在日常开发工作中,我们经常需要在不同的网络环境下切换系统代理设置。传统的做法是通过Windows系统设置手动修改,操作繁琐且容易出错。本文将详细介绍如何使用Flutter开发一个Windows桌面应用,实现快速切换系统代理的功能。

项目概述

这个代理切换工具具有以下特点:

  • 🖥️ Windows原生桌面应用
  • 🔄 一键开启/关闭系统代理
  • 💾 自动保存代理配置
  • 🔍 实时检测当前代理状态
  • ✅ 代理地址格式验证
  • 🎨 Material Design 3 UI设计

技术栈选择

核心技术

  • Flutter: 跨平台UI框架,支持桌面应用开发
  • Dart: Flutter的编程语言
  • Windows Registry: 通过注册表操作系统代理设置

主要依赖包

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.8
  win32: ^5.0.0  # Windows API调用
  ffi: ^2.1.0    # 外部函数接口
  window_size:   # 窗口大小控制
    git:
      url: https://github.com/google/flutter-desktop-embedding.git
      path: plugins/window_size
  path_provider: ^2.1.2  # 文件路径获取

完整代码实现

pubspec.yaml配置文件

yaml 复制代码
name: proxy_switch
description: "Windows系统代理切换工具"
publish_to: 'none'

version: 1.0.0+1

environment:
  sdk: ^3.5.3

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.8
  win32: ^5.0.0
  ffi: ^2.1.0
  window_size:
    git:
      url: https://github.com/google/flutter-desktop-embedding.git
      path: plugins/window_size
  path_provider: ^2.1.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^4.0.0

flutter:
  uses-material-design: true

主程序代码

dart 复制代码
import 'dart:io';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:window_size/window_size.dart';

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  if (Platform.isWindows) {
    setWindowTitle('系统代理设置');
    setWindowMinSize(const Size(640, 480));
    setWindowMaxSize(const Size(960, 640));
  }
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '系统代理设置',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        useMaterial3: true,
        fontFamily: 'Microsoft YaHei',
        textTheme: const TextTheme(
          displayLarge: TextStyle(fontFamily: 'Microsoft YaHei'),
          displayMedium: TextStyle(fontFamily: 'Microsoft YaHei'),
          displaySmall: TextStyle(fontFamily: 'Microsoft YaHei'),
          headlineLarge: TextStyle(fontFamily: 'Microsoft YaHei'),
          headlineMedium: TextStyle(fontFamily: 'Microsoft YaHei'),
          headlineSmall: TextStyle(fontFamily: 'Microsoft YaHei'),
          titleLarge: TextStyle(fontFamily: 'Microsoft YaHei'),
          titleMedium: TextStyle(fontFamily: 'Microsoft YaHei'),
          titleSmall: TextStyle(fontFamily: 'Microsoft YaHei'),
          bodyLarge: TextStyle(fontFamily: 'Microsoft YaHei'),
          bodyMedium: TextStyle(fontFamily: 'Microsoft YaHei'),
          bodySmall: TextStyle(fontFamily: 'Microsoft YaHei'),
          labelLarge: TextStyle(fontFamily: 'Microsoft YaHei'),
          labelMedium: TextStyle(fontFamily: 'Microsoft YaHei'),
          labelSmall: TextStyle(fontFamily: 'Microsoft YaHei'),
        ),
      ),
      home: const ProxySettingsPage(),
    );
  }
}

class ProxySettingsPage extends StatefulWidget {
  const ProxySettingsPage({super.key});

  @override
  State<ProxySettingsPage> createState() => _ProxySettingsPageState();
}

class _ProxySettingsPageState extends State<ProxySettingsPage> {
  final TextEditingController _proxyController = TextEditingController();
  bool _isProxyEnabled = false;
  String? _proxyError;
  late final File _configFile;

  @override
  void initState() {
    super.initState();
    _initializeConfig().then((_) {
      _checkCurrentProxyStatus();
      if (_proxyController.text.isEmpty) {
        _loadConfig();
      }
    });
  }

  Future<void> _initializeConfig() async {
    final exePath = Platform.resolvedExecutable;
    final exeDir = Directory(File(exePath).parent.path);
    _configFile = File('${exeDir.path}\\proxy_config.json');
    
    if (!await _configFile.exists()) {
      await _configFile.create(recursive: true);
      await _saveConfig({'proxyAddress': ''});
    }
  }

  Future<void> _loadConfig() async {
    try {
      if (await _configFile.exists()) {
        final contents = await _configFile.readAsString();
        if (contents.isNotEmpty) {
          final config = json.decode(contents) as Map<String, dynamic>;
          setState(() {
            _proxyController.text = config['proxyAddress'] as String? ?? '';
          });
        }
      }
    } catch (e) {
      debugPrint('加载配置文件失败: $e');
    }
  }

  Future<void> _saveConfig(Map<String, dynamic> config) async {
    try {
      await _configFile.writeAsString(json.encode(config));
    } catch (e) {
      debugPrint('保存配置文件失败: $e');
    }
  }

  void _checkCurrentProxyStatus() {
    try {
      // 从注册表读取当前代理状态
      final result = Process.runSync('reg', [
        'query',
        'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings',
        '/v',
        'ProxyEnable'
      ]);

      if (result.exitCode == 0) {
        // 解析注册表输出
        final output = result.stdout.toString();
        final isEnabled = output.contains('0x1');
        
        // 如果代理已启用,读取代理地址
        if (isEnabled) {
          final proxyResult = Process.runSync('reg', [
            'query',
            'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings',
            '/v',
            'ProxyServer'
          ]);
          
          if (proxyResult.exitCode == 0) {
            final proxyOutput = proxyResult.stdout.toString();
            final proxyAddress = proxyOutput.split('REG_SZ')[1].trim();
            
            // 如果获取到有效的代理地址,更新状态并保存到配置文件
            if (proxyAddress.isNotEmpty) {
              setState(() {
                _proxyController.text = proxyAddress;
                _isProxyEnabled = true;
              });
              // 同时更新配置文件
              _saveConfig({'proxyAddress': proxyAddress});
              return;
            }
          }
        }
        
        setState(() {
          _isProxyEnabled = isEnabled;
        });
      }
    } catch (e) {
      debugPrint('检查代理状态时出错: $e');
    }
  }

  bool _validateProxyAddress(String address) {
    if (address.isEmpty) {
      setState(() {
        _proxyError = '代理地址不能为空';
      });
      return false;
    }

    // 检查格式:IP:端口 或 域名:端口
    final RegExp ipRegex = RegExp(
      r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):\d{1,5}$'
    );
    final RegExp domainRegex = RegExp(
      r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}:\d{1,5}$'
    );

    if (!ipRegex.hasMatch(address) && !domainRegex.hasMatch(address)) {
      setState(() {
        _proxyError = '代理地址格式无效,应为 IP:端口 或 域名:端口';
      });
      return false;
    }

    // 检查端口范围
    final port = int.parse(address.split(':').last);
    if (port < 1 || port > 65535) {
      setState(() {
        _proxyError = '端口号必须在 1-65535 之间';
      });
      return false;
    }

    setState(() {
      _proxyError = null;
    });
    return true;
  }

  void _toggleProxy(bool value) {
    if (value && !_validateProxyAddress(_proxyController.text)) {
      return;
    }

    setState(() {
      _isProxyEnabled = value;
    });

    if (value) {
      _setWindowsProxy(_proxyController.text);
      // 保存新的代理地址到配置文件
      _saveConfig({'proxyAddress': _proxyController.text});
    } else {
      _clearWindowsProxy();
    }
  }

  void _setWindowsProxy(String proxyAddress) {
    // 设置系统代理
    final cmdProcess = Process.runSync('reg', [
      'add',
      'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings',
      '/v',
      'ProxyEnable',
      '/t',
      'REG_DWORD',
      '/d',
      '1',
      '/f'
    ]);

    if (cmdProcess.exitCode == 0) {
      Process.runSync('reg', [
        'add',
        'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings',
        '/v',
        'ProxyServer',
        '/t',
        'REG_SZ',
        '/d',
        proxyAddress,
        '/f'
      ]);
    }
  }

  void _clearWindowsProxy() {
    Process.runSync('reg', [
      'add',
      'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings',
      '/v',
      'ProxyEnable',
      '/t',
      'REG_DWORD',
      '/d',
      '0',
      '/f'
    ]);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('系统代理设置'),
        centerTitle: true,
        actions: [
          IconButton(
            icon: const Icon(Icons.refresh),
            onPressed: () {
              _checkCurrentProxyStatus();
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('代理状态已刷新')),
              );
            },
            tooltip: '刷新代理状态',
          ),
        ],
      ),
      body: Container(
        padding: const EdgeInsets.all(20.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            Card(
              elevation: 2,
              child: Padding(
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  children: [
                    Row(
                      children: [
                        Expanded(
                          child: TextField(
                            controller: _proxyController,
                            decoration: InputDecoration(
                              labelText: '代理地址',
                              hintText: '例如: 127.0.0.1:7890',
                              errorText: _proxyError,
                              border: OutlineInputBorder(
                                borderRadius: BorderRadius.circular(8),
                              ),
                              prefixIcon: const Icon(Icons.computer),
                            ),
                            onChanged: (value) {
                              if (_proxyError != null) {
                                setState(() {
                                  _proxyError = null;
                                });
                              }
                            },
                          ),
                        ),
                        const SizedBox(width: 8),
                        IconButton(
                          icon: const Icon(Icons.save),
                          onPressed: () {
                            if (_validateProxyAddress(_proxyController.text)) {
                              _saveConfig({'proxyAddress': _proxyController.text});
                              // 如果当前代理是启用状态,则更新系统代理地址
                              if (_isProxyEnabled) {
                                _setWindowsProxy(_proxyController.text);
                              }
                              ScaffoldMessenger.of(context).showSnackBar(
                                const SnackBar(content: Text('代理地址已保存')),
                              );
                            }
                          },
                          tooltip: '保存代理地址',
                        ),
                      ],
                    ),
                    const SizedBox(height: 20),
                    Container(
                      padding: const EdgeInsets.symmetric(
                        horizontal: 16,
                        vertical: 8,
                      ),
                      decoration: BoxDecoration(
                        color: Theme.of(context).colorScheme.surfaceContainerHighest,
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: [
                          Row(
                            children: [
                              Icon(
                                _isProxyEnabled
                                    ? Icons.cloud_done
                                    : Icons.cloud_off,
                                color: _isProxyEnabled
                                    ? Colors.green
                                    : Colors.grey,
                              ),
                              const SizedBox(width: 8),
                              const Text(
                                '启用系统代理',
                                style: TextStyle(
                                  fontSize: 16,
                                  fontWeight: FontWeight.w500,
                                ),
                              ),
                            ],
                          ),
                          Switch(
                            value: _isProxyEnabled,
                            onChanged: _toggleProxy,
                          ),
                        ],
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _proxyController.dispose();
    super.dispose();
  }
}

核心功能解析

1. 窗口配置与初始化

应用启动时,我们首先配置了窗口的基本属性:

dart 复制代码
void main() {
  WidgetsFlutterBinding.ensureInitialized();
  if (Platform.isWindows) {
    setWindowTitle('系统代理设置');
    setWindowMinSize(const Size(640, 480));
    setWindowMaxSize(const Size(960, 640));
  }
  runApp(const MyApp());
}

这里使用了window_size插件来控制窗口大小,确保应用在Windows平台上有合适的显示尺寸。

2. 配置文件管理

应用需要持久化保存用户的代理配置,我们将配置文件保存在可执行文件同目录下:

dart 复制代码
Future<void> _initializeConfig() async {
  final exePath = Platform.resolvedExecutable;
  final exeDir = Directory(File(exePath).parent.path);
  _configFile = File('${exeDir.path}\\proxy_config.json');
  
  if (!await _configFile.exists()) {
    await _configFile.create(recursive: true);
    await _saveConfig({'proxyAddress': ''});
  }
}

3. Windows注册表操作

这是整个应用的核心功能,通过操作Windows注册表来控制系统代理:

检查当前代理状态:

dart 复制代码
void _checkCurrentProxyStatus() {
  try {
    // 查询ProxyEnable注册表项
    final result = Process.runSync('reg', [
      'query',
      'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings',
      '/v',
      'ProxyEnable'
    ]);
    
    if (result.exitCode == 0) {
      final output = result.stdout.toString();
      final isEnabled = output.contains('0x1');
      // ... 后续处理
    }
  } catch (e) {
    debugPrint('检查代理状态时出错: $e');
  }
}

设置系统代理:

dart 复制代码
void _setWindowsProxy(String proxyAddress) {
  // 启用代理
  Process.runSync('reg', [
    'add',
    'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings',
    '/v', 'ProxyEnable',
    '/t', 'REG_DWORD',
    '/d', '1',
    '/f'
  ]);
  
  // 设置代理服务器地址
  Process.runSync('reg', [
    'add',
    'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings',
    '/v', 'ProxyServer',
    '/t', 'REG_SZ',
    '/d', proxyAddress,
    '/f'
  ]);
}

4. 输入验证

为了确保用户输入的代理地址格式正确,我们实现了完善的验证机制:

dart 复制代码
bool _validateProxyAddress(String address) {
  if (address.isEmpty) {
    setState(() {
      _proxyError = '代理地址不能为空';
    });
    return false;
  }

  // IP地址格式验证
  final RegExp ipRegex = RegExp(
    r'^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?):\d{1,5}$'
  );
  
  // 域名格式验证
  final RegExp domainRegex = RegExp(
    r'^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}:\d{1,5}$'
  );

  if (!ipRegex.hasMatch(address) && !domainRegex.hasMatch(address)) {
    setState(() {
      _proxyError = '代理地址格式无效,应为 IP:端口 或 域名:端口';
    });
    return false;
  }

  return true;
}

运行与部署

开发环境要求

  • Flutter 3.5.3+
  • Windows 10/11
  • Visual Studio Code 或 Android Studio

编译打包

bash 复制代码
# 构建Windows桌面应用
flutter build windows

# 生成的可执行文件位于
# build/windows/x64/runner/Release/proxy_switch.exe

使用说明

  1. 在代理地址输入框中填入代理服务器地址(如:127.0.0.1:7890)
  2. 点击保存按钮保存配置
  3. 使用开关控制代理的启用/禁用
  4. 点击刷新按钮检查当前系统代理状态

实用小工具

App Store 截图生成器应用图标生成器在线图片压缩Chrome插件-强制开启复制-护眼模式-网页乱码设置编码
乖猫记账,AI智能分类最好用的聊天记账App。

相关推荐
笔沫拾光2 小时前
hooks_riverpod框架解析
flutter·hooks·hooks_riverpod
程序员老刘7 小时前
Cursor vs Claude Code vs AS+AI助手:谁才是客户端的编程神器?
flutter·ai编程·客户端
love530love7 小时前
怎么更新 cargo.exe ?(Rust 工具链)
人工智能·windows·python·rust·r语言
耳東陈10 小时前
【重磅发布】flutter_chen_updater - 版本升级更新
flutter
wordbaby13 小时前
Flutter列表渲染的"诡异"问题:为什么我的数据总是第一个?
前端·flutter
恋猫de小郭14 小时前
谷歌开启 Android 开发者身份验证,明年可能开始禁止“未经验证”应用的侧载,要求所有开发者向谷歌表明身份
android·前端·flutter
Univin1 天前
8.25作业
数据结构·windows
徐子元竟然被占了!!1 天前
Windows Server 2019 DateCenter搭建 FTP 服务器
运维·服务器·windows
winkel_wang1 天前
身份管理与安全 (Protect identities)
windows·安全