Add files via upload

This commit is contained in:
xiaolin-007
2025-12-09 11:32:40 +08:00
committed by GitHub
parent 7545801d12
commit 4e67c1a040

375
CloudflareSpeedTest-GUI.py Normal file
View File

@@ -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_())