From 4e67c1a040e8375b550a5f272a531d5ed80aab7d Mon Sep 17 00:00:00 2001 From: xiaolin-007 <205569736+xiaolin-007@users.noreply.github.com> Date: Tue, 9 Dec 2025 11:32:40 +0800 Subject: [PATCH] Add files via upload --- CloudflareSpeedTest-GUI.py | 375 +++++++++++++++++++++++++++++++++++++ 1 file changed, 375 insertions(+) create mode 100644 CloudflareSpeedTest-GUI.py diff --git a/CloudflareSpeedTest-GUI.py b/CloudflareSpeedTest-GUI.py new file mode 100644 index 0000000..a8577dc --- /dev/null +++ b/CloudflareSpeedTest-GUI.py @@ -0,0 +1,375 @@ +import sys +import os +import json +import subprocess +import ctypes +from functools import partial +from PyQt5 import QtWidgets, QtGui, QtCore + +def resource_path(relative_path): + """获取 PyInstaller 打包后资源文件路径""" + if hasattr(sys, "_MEIPASS"): + return os.path.join(sys._MEIPASS, relative_path) + return os.path.join(os.path.abspath("."), relative_path) + +APP_ICON = resource_path("app.ico") +CFST_EXE = "cfst.exe" +IP_FILE_NAME = "ip.txt" +SAVED_SETTINGS_FILE = "saved_settings.json" +APP_USER_MODEL_ID = "com.example.cloudflarespeedtest" + +def _set_windows_appid(appid): + try: + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid) + except Exception: + pass + + +if sys.platform.startswith("win"): + _set_windows_appid(APP_USER_MODEL_ID) + +class MainWin(QtWidgets.QWidget): + def __init__(self): + super().__init__() + + self.setWindowTitle("CloudflareSpeedTest_GUI -- 小琳解说") + + # 图标(修复:PyInstaller 下也能找到) + if os.path.exists(APP_ICON): + self.setWindowIcon(QtGui.QIcon(APP_ICON)) + else: + self.setWindowIcon(QtGui.QIcon(resource_path("app.ico"))) + + self.setFont(QtGui.QFont("Microsoft YaHei", 10)) + self.resize(500, 520) + + self._build_ui() + self._load_saved_settings_list() + + def _build_ui(self): + main_layout = QtWidgets.QVBoxLayout(self) + + params = [ + ("-n", "200", "延迟线程 1-1000"), + ("-t", "4", "延迟次数"), + ("-dn", "10", "下载数量"), + ("-dt", "10", "下载时间(秒)"), + ("-tp", "443", "端口"), + ("-url", "https://cf.xiu2.xyz/url", "测速地址"), + ("-httping", "", "HTTPing 模式 (勾选启用)"), + ("-httping-code", "200", "HTTP 有效状态码"), + ("-cfcolo", "HKG,KHH,NRT,LAX", "地区码, HTTPing 模式可用"), + ("-tl", "9999", "平均延迟上限(ms)"), + ("-tll", "0", "平均延迟下限(ms)"), + ("-tlr", "1.00", "丢包上限 0.00-1.00"), + ("-sl", "0", "下载速度下限 MB/s"), + ("-p", "10", "显示结果数量"), + ("-f", "ip.txt", "IP 段文件"), + ("-ip", "", "指定 IP 段"), + ("-o", "result.csv", "输出文件"), + ("-dd", "", "禁用下载测速 (勾选启用)"), + ("-allip", "", "测速全部 IP (勾选启用)") + ] + + grid = QtWidgets.QGridLayout() + grid.setColumnStretch(1, 1) + + self.controls = {} + row = 0 + + for key, default, hint in params: + cb = QtWidgets.QCheckBox(key) + cb.setChecked(False) + + if key == "-n": + widget = QtWidgets.QSpinBox() + widget.setRange(1, 1000) + try: + widget.setValue(int(default)) + except Exception: + widget.setValue(200) + widget.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) + widget.setEnabled(False) + else: + widget = QtWidgets.QLineEdit(default) + if default == "": + widget.setPlaceholderText(hint) + widget.setEnabled(False) + widget.setStyleSheet("QLineEdit:disabled { color: gray; }") + + lbl = QtWidgets.QLabel(hint) + cb.stateChanged.connect(partial(self._on_checkbox_toggled, key)) + + grid.addWidget(cb, row, 0) + grid.addWidget(widget, row, 1) + grid.addWidget(lbl, row, 2) + + self.controls[key] = (cb, widget) + row += 1 + + main_layout.addLayout(grid) + + save_load_layout = QtWidgets.QGridLayout() + save_load_layout.setColumnStretch(1, 1) + + save_label = QtWidgets.QLabel("保存设置名称") + self.save_name_edit = QtWidgets.QLineEdit() + self.save_name_edit.setPlaceholderText("填写保存设置名称") + self.save_btn = QtWidgets.QPushButton("保存设置") + + load_label = QtWidgets.QLabel("已保存设置") + self.load_combo = QtWidgets.QComboBox() + self.load_combo.setEditable(False) + + sp = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) + self.save_name_edit.setSizePolicy(sp) + self.load_combo.setSizePolicy(sp) + + self.load_btn = QtWidgets.QPushButton("加载设置") + self.delete_btn = QtWidgets.QPushButton("删除已保存") + + load_btns_layout = QtWidgets.QHBoxLayout() + load_btns_layout.addWidget(self.load_btn) + load_btns_layout.addWidget(self.delete_btn) + load_btns_layout.addStretch() + + save_load_layout.addWidget(save_label, 0, 0) + save_load_layout.addWidget(self.save_name_edit, 0, 1) + save_load_layout.addWidget(self.save_btn, 0, 2) + + save_load_layout.addWidget(load_label, 1, 0) + save_load_layout.addWidget(self.load_combo, 1, 1) + save_load_layout.addLayout(load_btns_layout, 1, 2) + + # 运行按钮 + self.run_btn = QtWidgets.QPushButton("运行\n测速") + btn_size = 88 + self.run_btn.setFixedSize(btn_size, btn_size) + + font = QtGui.QFont("Microsoft YaHei", 14) + font.setBold(True) + self.run_btn.setFont(font) + self.run_btn.setStyleSheet(""" + QPushButton { + background-color: #0078D7; + color: white; + border: none; + font-weight: bold; + border-radius: 8px; + } + QPushButton:pressed { + background-color: #005A9E; + } + """) + + save_load_layout.addWidget( + self.run_btn, 0, 3, 2, 1, alignment=QtCore.Qt.AlignCenter + ) + + main_layout.addLayout(save_load_layout) + + self.save_btn.clicked.connect(self._on_save_clicked) + self.load_btn.clicked.connect(self._on_load_clicked) + self.delete_btn.clicked.connect(self._on_delete_clicked) + self.run_btn.clicked.connect(self._on_run_clicked) + + def _on_checkbox_toggled(self, key, state): + cb, widget = self.controls[key] + enabled = (state == 2) + widget.setEnabled(enabled) + if isinstance(widget, QtWidgets.QLineEdit): + if enabled: + widget.setStyleSheet("QLineEdit { color: black; }") + else: + widget.setStyleSheet("QLineEdit:disabled { color: gray; }") + + def _load_saved_settings_list(self): + self.load_combo.clear() + if not os.path.exists(SAVED_SETTINGS_FILE): + return + try: + with open(SAVED_SETTINGS_FILE, "r", encoding="utf-8") as f: + data = json.load(f) + names = sorted(data.keys()) + self.load_combo.addItems(names) + except Exception: + pass + + def _read_saved_settings(self): + if not os.path.exists(SAVED_SETTINGS_FILE): + return {} + try: + with open(SAVED_SETTINGS_FILE, "r", encoding="utf-8") as f: + return json.load(f) + except Exception: + return {} + + def _write_saved_settings(self, data): + try: + with open(SAVED_SETTINGS_FILE, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False, indent=2) + return True + except Exception: + return False + + def _on_save_clicked(self): + name = self.save_name_edit.text().strip() + if not name: + QtWidgets.QMessageBox.warning(self, "保存失败", "请填写保存设置的名称。") + return + + settings = {} + for k, (cb, widget) in self.controls.items(): + if isinstance(widget, QtWidgets.QSpinBox): + val = widget.value() + else: + val = widget.text() + settings[k] = [cb.isChecked(), val] + + all_saved = self._read_saved_settings() + all_saved[name] = settings + + ok = self._write_saved_settings(all_saved) + if ok: + QtWidgets.QMessageBox.information(self, "保存成功", f"设置已保存为: {name}") + self._load_saved_settings_list() + idx = self.load_combo.findText(name) + if idx >= 0: + self.load_combo.setCurrentIndex(idx) + else: + QtWidgets.QMessageBox.warning(self, "保存失败", "写入保存文件失败。") + + def _on_load_clicked(self): + name = self.load_combo.currentText().strip() + if not name: + QtWidgets.QMessageBox.warning(self, "加载失败", "请先选择一个已保存的设置名称。") + return + + all_saved = self._read_saved_settings() + if name not in all_saved: + QtWidgets.QMessageBox.warning(self, "加载失败", "所选设置不存在或已被删除。") + self._load_saved_settings_list() + return + + settings = all_saved[name] + + for k, (cb, widget) in self.controls.items(): + if k in settings: + checked, val = settings[k] + cb.setChecked(bool(checked)) + if isinstance(widget, QtWidgets.QSpinBox): + try: + widget.setValue(int(val)) + except Exception: + pass + widget.setEnabled(bool(checked)) + else: + widget.setText(str(val)) + widget.setEnabled(bool(checked)) + widget.setStyleSheet( + "QLineEdit { color: black; }" if widget.isEnabled() else "QLineEdit:disabled { color: gray; }" + ) + + QtWidgets.QMessageBox.information(self, "加载成功", f"已加载设置: {name}") + + def _on_delete_clicked(self): + name = self.load_combo.currentText().strip() + if not name: + QtWidgets.QMessageBox.warning(self, "删除失败", "请先选择一个已保存的设置名称。") + return + + all_saved = self._read_saved_settings() + if name not in all_saved: + QtWidgets.QMessageBox.warning(self, "删除失败", "所选设置不存在。") + self._load_saved_settings_list() + return + + reply = QtWidgets.QMessageBox.question( + self, "确认删除", + f"确定要删除已保存设置: {name} ?", + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No + ) + + if reply != QtWidgets.QMessageBox.Yes: + return + + del all_saved[name] + ok = self._write_saved_settings(all_saved) + if ok: + QtWidgets.QMessageBox.information(self, "删除成功", f"已删除: {name}") + self._load_saved_settings_list() + else: + QtWidgets.QMessageBox.warning(self, "删除失败", "删除时写入文件失败。") + + def _find_file_case_insensitive(self, target_name): + target_lower = target_name.lower() + for entry in os.listdir("."): + if entry.lower() == target_lower: + return entry + return None + + def _build_cmd_list(self, exe_name): + cmd_list = [exe_name] + + for k, (cb, widget) in self.controls.items(): + if not cb.isChecked(): + continue + + if k == "-n": + cmd_list.append(k) + cmd_list.append(str(widget.value())) + continue + + if k in ("-httping", "-dd", "-allip"): + cmd_list.append(k) + continue + + val = widget.text().strip() + if val == "": + continue + + cmd_list.append(k) + cmd_list.append(val) + + return cmd_list + + def _on_run_clicked(self): + cfst_actual = self._find_file_case_insensitive(CFST_EXE) + ip_actual = self._find_file_case_insensitive(IP_FILE_NAME) + + missing = [] + if not cfst_actual: + missing.append(CFST_EXE) + if not ip_actual: + missing.append(IP_FILE_NAME) + + if missing: + missing_str = ",".join(missing) + QtWidgets.QMessageBox.warning( + self, "文件缺失", + f"未找到必要文件: {missing_str}\n请将缺失文件放在程序同目录后重试。" + ) + return + + cmd_list = self._build_cmd_list(cfst_actual) + if not cmd_list: + cmd_list = [cfst_actual] + + try: + if os.name == "nt": + CREATE_NEW_CONSOLE = 0x00000010 + subprocess.Popen(cmd_list, creationflags=CREATE_NEW_CONSOLE) + else: + subprocess.Popen(cmd_list) + except Exception: + return + +if __name__ == "__main__": + app = QtWidgets.QApplication(sys.argv) + + # 全局图标 + app.setWindowIcon(QtGui.QIcon(APP_ICON)) + + w = MainWin() + w.show() + sys.exit(app.exec_()) \ No newline at end of file