├── README.md ├── LICENSE └── CloudflareSpeedTest-GUI.py /README.md: -------------------------------------------------------------------------------- 1 | CloudflareSpeedTest的图形GUI (Windows版本) 2 | 3 | gui 4 | 5 | 相关视频:https://www.youtube.com/watch?v=DkFP_DuwXtY 6 | 7 | 使用方法:将该程序和CFST程序放置在同一目录下即可使用 8 | 9 | 本程序提供cfst的图形操作界面 不包含cfst程序。 10 | 11 | 请移步 https://github.com/XIU2/CloudflareSpeedTest/releases/tag/v2.3.4 下载核心程序。 12 | 13 | 感谢XIU2大神提供的扫描工具 14 | 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 xiaolin-007 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CloudflareSpeedTest-GUI.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import json 4 | import subprocess 5 | import ctypes 6 | from functools import partial 7 | from PyQt5 import QtWidgets, QtGui, QtCore 8 | 9 | def resource_path(relative_path): 10 | """获取 PyInstaller 打包后资源文件路径""" 11 | if hasattr(sys, "_MEIPASS"): 12 | return os.path.join(sys._MEIPASS, relative_path) 13 | return os.path.join(os.path.abspath("."), relative_path) 14 | 15 | APP_ICON = resource_path("app.ico") 16 | CFST_EXE = "cfst.exe" 17 | IP_FILE_NAME = "ip.txt" 18 | SAVED_SETTINGS_FILE = "saved_settings.json" 19 | APP_USER_MODEL_ID = "com.example.cloudflarespeedtest" 20 | 21 | def _set_windows_appid(appid): 22 | try: 23 | ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(appid) 24 | except Exception: 25 | pass 26 | 27 | 28 | if sys.platform.startswith("win"): 29 | _set_windows_appid(APP_USER_MODEL_ID) 30 | 31 | class MainWin(QtWidgets.QWidget): 32 | def __init__(self): 33 | super().__init__() 34 | 35 | self.setWindowTitle("CloudflareSpeedTest_GUI -- 小琳解说") 36 | 37 | # 图标(修复:PyInstaller 下也能找到) 38 | if os.path.exists(APP_ICON): 39 | self.setWindowIcon(QtGui.QIcon(APP_ICON)) 40 | else: 41 | self.setWindowIcon(QtGui.QIcon(resource_path("app.ico"))) 42 | 43 | self.setFont(QtGui.QFont("Microsoft YaHei", 10)) 44 | self.resize(500, 520) 45 | 46 | self._build_ui() 47 | self._load_saved_settings_list() 48 | 49 | def _build_ui(self): 50 | main_layout = QtWidgets.QVBoxLayout(self) 51 | 52 | params = [ 53 | ("-n", "200", "延迟线程 1-1000"), 54 | ("-t", "4", "延迟次数"), 55 | ("-dn", "10", "下载数量"), 56 | ("-dt", "10", "下载时间(秒)"), 57 | ("-tp", "443", "端口"), 58 | ("-url", "https://cf.xiu2.xyz/url", "测速地址"), 59 | ("-httping", "", "HTTPing 模式 (勾选启用)"), 60 | ("-httping-code", "200", "HTTP 有效状态码"), 61 | ("-cfcolo", "HKG,KHH,NRT,LAX", "地区码, HTTPing 模式可用"), 62 | ("-tl", "9999", "平均延迟上限(ms)"), 63 | ("-tll", "0", "平均延迟下限(ms)"), 64 | ("-tlr", "1.00", "丢包上限 0.00-1.00"), 65 | ("-sl", "0", "下载速度下限 MB/s"), 66 | ("-p", "10", "显示结果数量"), 67 | ("-f", "ip.txt", "IP 段文件"), 68 | ("-ip", "", "指定 IP 段"), 69 | ("-o", "result.csv", "输出文件"), 70 | ("-dd", "", "禁用下载测速 (勾选启用)"), 71 | ("-allip", "", "测速全部 IP (勾选启用)") 72 | ] 73 | 74 | grid = QtWidgets.QGridLayout() 75 | grid.setColumnStretch(1, 1) 76 | 77 | self.controls = {} 78 | row = 0 79 | 80 | for key, default, hint in params: 81 | cb = QtWidgets.QCheckBox(key) 82 | cb.setChecked(False) 83 | 84 | if key == "-n": 85 | widget = QtWidgets.QSpinBox() 86 | widget.setRange(1, 1000) 87 | try: 88 | widget.setValue(int(default)) 89 | except Exception: 90 | widget.setValue(200) 91 | widget.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) 92 | widget.setEnabled(False) 93 | else: 94 | widget = QtWidgets.QLineEdit(default) 95 | if default == "": 96 | widget.setPlaceholderText(hint) 97 | widget.setEnabled(False) 98 | widget.setStyleSheet("QLineEdit:disabled { color: gray; }") 99 | 100 | lbl = QtWidgets.QLabel(hint) 101 | cb.stateChanged.connect(partial(self._on_checkbox_toggled, key)) 102 | 103 | grid.addWidget(cb, row, 0) 104 | grid.addWidget(widget, row, 1) 105 | grid.addWidget(lbl, row, 2) 106 | 107 | self.controls[key] = (cb, widget) 108 | row += 1 109 | 110 | main_layout.addLayout(grid) 111 | 112 | save_load_layout = QtWidgets.QGridLayout() 113 | save_load_layout.setColumnStretch(1, 1) 114 | 115 | save_label = QtWidgets.QLabel("保存设置名称") 116 | self.save_name_edit = QtWidgets.QLineEdit() 117 | self.save_name_edit.setPlaceholderText("填写保存设置名称") 118 | self.save_btn = QtWidgets.QPushButton("保存设置") 119 | 120 | load_label = QtWidgets.QLabel("已保存设置") 121 | self.load_combo = QtWidgets.QComboBox() 122 | self.load_combo.setEditable(False) 123 | 124 | sp = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) 125 | self.save_name_edit.setSizePolicy(sp) 126 | self.load_combo.setSizePolicy(sp) 127 | 128 | self.load_btn = QtWidgets.QPushButton("加载设置") 129 | self.delete_btn = QtWidgets.QPushButton("删除已保存") 130 | 131 | load_btns_layout = QtWidgets.QHBoxLayout() 132 | load_btns_layout.addWidget(self.load_btn) 133 | load_btns_layout.addWidget(self.delete_btn) 134 | load_btns_layout.addStretch() 135 | 136 | save_load_layout.addWidget(save_label, 0, 0) 137 | save_load_layout.addWidget(self.save_name_edit, 0, 1) 138 | save_load_layout.addWidget(self.save_btn, 0, 2) 139 | 140 | save_load_layout.addWidget(load_label, 1, 0) 141 | save_load_layout.addWidget(self.load_combo, 1, 1) 142 | save_load_layout.addLayout(load_btns_layout, 1, 2) 143 | 144 | # 运行按钮 145 | self.run_btn = QtWidgets.QPushButton("运行\n测速") 146 | btn_size = 88 147 | self.run_btn.setFixedSize(btn_size, btn_size) 148 | 149 | font = QtGui.QFont("Microsoft YaHei", 14) 150 | font.setBold(True) 151 | self.run_btn.setFont(font) 152 | self.run_btn.setStyleSheet(""" 153 | QPushButton { 154 | background-color: #0078D7; 155 | color: white; 156 | border: none; 157 | font-weight: bold; 158 | border-radius: 8px; 159 | } 160 | QPushButton:pressed { 161 | background-color: #005A9E; 162 | } 163 | """) 164 | 165 | save_load_layout.addWidget( 166 | self.run_btn, 0, 3, 2, 1, alignment=QtCore.Qt.AlignCenter 167 | ) 168 | 169 | main_layout.addLayout(save_load_layout) 170 | 171 | self.save_btn.clicked.connect(self._on_save_clicked) 172 | self.load_btn.clicked.connect(self._on_load_clicked) 173 | self.delete_btn.clicked.connect(self._on_delete_clicked) 174 | self.run_btn.clicked.connect(self._on_run_clicked) 175 | 176 | def _on_checkbox_toggled(self, key, state): 177 | cb, widget = self.controls[key] 178 | enabled = (state == 2) 179 | widget.setEnabled(enabled) 180 | if isinstance(widget, QtWidgets.QLineEdit): 181 | if enabled: 182 | widget.setStyleSheet("QLineEdit { color: black; }") 183 | else: 184 | widget.setStyleSheet("QLineEdit:disabled { color: gray; }") 185 | 186 | def _load_saved_settings_list(self): 187 | self.load_combo.clear() 188 | if not os.path.exists(SAVED_SETTINGS_FILE): 189 | return 190 | try: 191 | with open(SAVED_SETTINGS_FILE, "r", encoding="utf-8") as f: 192 | data = json.load(f) 193 | names = sorted(data.keys()) 194 | self.load_combo.addItems(names) 195 | except Exception: 196 | pass 197 | 198 | def _read_saved_settings(self): 199 | if not os.path.exists(SAVED_SETTINGS_FILE): 200 | return {} 201 | try: 202 | with open(SAVED_SETTINGS_FILE, "r", encoding="utf-8") as f: 203 | return json.load(f) 204 | except Exception: 205 | return {} 206 | 207 | def _write_saved_settings(self, data): 208 | try: 209 | with open(SAVED_SETTINGS_FILE, "w", encoding="utf-8") as f: 210 | json.dump(data, f, ensure_ascii=False, indent=2) 211 | return True 212 | except Exception: 213 | return False 214 | 215 | def _on_save_clicked(self): 216 | name = self.save_name_edit.text().strip() 217 | if not name: 218 | QtWidgets.QMessageBox.warning(self, "保存失败", "请填写保存设置的名称。") 219 | return 220 | 221 | settings = {} 222 | for k, (cb, widget) in self.controls.items(): 223 | if isinstance(widget, QtWidgets.QSpinBox): 224 | val = widget.value() 225 | else: 226 | val = widget.text() 227 | settings[k] = [cb.isChecked(), val] 228 | 229 | all_saved = self._read_saved_settings() 230 | all_saved[name] = settings 231 | 232 | ok = self._write_saved_settings(all_saved) 233 | if ok: 234 | QtWidgets.QMessageBox.information(self, "保存成功", f"设置已保存为: {name}") 235 | self._load_saved_settings_list() 236 | idx = self.load_combo.findText(name) 237 | if idx >= 0: 238 | self.load_combo.setCurrentIndex(idx) 239 | else: 240 | QtWidgets.QMessageBox.warning(self, "保存失败", "写入保存文件失败。") 241 | 242 | def _on_load_clicked(self): 243 | name = self.load_combo.currentText().strip() 244 | if not name: 245 | QtWidgets.QMessageBox.warning(self, "加载失败", "请先选择一个已保存的设置名称。") 246 | return 247 | 248 | all_saved = self._read_saved_settings() 249 | if name not in all_saved: 250 | QtWidgets.QMessageBox.warning(self, "加载失败", "所选设置不存在或已被删除。") 251 | self._load_saved_settings_list() 252 | return 253 | 254 | settings = all_saved[name] 255 | 256 | for k, (cb, widget) in self.controls.items(): 257 | if k in settings: 258 | checked, val = settings[k] 259 | cb.setChecked(bool(checked)) 260 | if isinstance(widget, QtWidgets.QSpinBox): 261 | try: 262 | widget.setValue(int(val)) 263 | except Exception: 264 | pass 265 | widget.setEnabled(bool(checked)) 266 | else: 267 | widget.setText(str(val)) 268 | widget.setEnabled(bool(checked)) 269 | widget.setStyleSheet( 270 | "QLineEdit { color: black; }" if widget.isEnabled() else "QLineEdit:disabled { color: gray; }" 271 | ) 272 | 273 | QtWidgets.QMessageBox.information(self, "加载成功", f"已加载设置: {name}") 274 | 275 | def _on_delete_clicked(self): 276 | name = self.load_combo.currentText().strip() 277 | if not name: 278 | QtWidgets.QMessageBox.warning(self, "删除失败", "请先选择一个已保存的设置名称。") 279 | return 280 | 281 | all_saved = self._read_saved_settings() 282 | if name not in all_saved: 283 | QtWidgets.QMessageBox.warning(self, "删除失败", "所选设置不存在。") 284 | self._load_saved_settings_list() 285 | return 286 | 287 | reply = QtWidgets.QMessageBox.question( 288 | self, "确认删除", 289 | f"确定要删除已保存设置: {name} ?", 290 | QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No 291 | ) 292 | 293 | if reply != QtWidgets.QMessageBox.Yes: 294 | return 295 | 296 | del all_saved[name] 297 | ok = self._write_saved_settings(all_saved) 298 | if ok: 299 | QtWidgets.QMessageBox.information(self, "删除成功", f"已删除: {name}") 300 | self._load_saved_settings_list() 301 | else: 302 | QtWidgets.QMessageBox.warning(self, "删除失败", "删除时写入文件失败。") 303 | 304 | def _find_file_case_insensitive(self, target_name): 305 | target_lower = target_name.lower() 306 | for entry in os.listdir("."): 307 | if entry.lower() == target_lower: 308 | return entry 309 | return None 310 | 311 | def _build_cmd_list(self, exe_name): 312 | cmd_list = [exe_name] 313 | 314 | for k, (cb, widget) in self.controls.items(): 315 | if not cb.isChecked(): 316 | continue 317 | 318 | if k == "-n": 319 | cmd_list.append(k) 320 | cmd_list.append(str(widget.value())) 321 | continue 322 | 323 | if k in ("-httping", "-dd", "-allip"): 324 | cmd_list.append(k) 325 | continue 326 | 327 | val = widget.text().strip() 328 | if val == "": 329 | continue 330 | 331 | cmd_list.append(k) 332 | cmd_list.append(val) 333 | 334 | return cmd_list 335 | 336 | def _on_run_clicked(self): 337 | cfst_actual = self._find_file_case_insensitive(CFST_EXE) 338 | ip_actual = self._find_file_case_insensitive(IP_FILE_NAME) 339 | 340 | missing = [] 341 | if not cfst_actual: 342 | missing.append(CFST_EXE) 343 | if not ip_actual: 344 | missing.append(IP_FILE_NAME) 345 | 346 | if missing: 347 | missing_str = ",".join(missing) 348 | QtWidgets.QMessageBox.warning( 349 | self, "文件缺失", 350 | f"未找到必要文件: {missing_str}\n请将缺失文件放在程序同目录后重试。" 351 | ) 352 | return 353 | 354 | cmd_list = self._build_cmd_list(cfst_actual) 355 | if not cmd_list: 356 | cmd_list = [cfst_actual] 357 | 358 | try: 359 | if os.name == "nt": 360 | CREATE_NEW_CONSOLE = 0x00000010 361 | subprocess.Popen(cmd_list, creationflags=CREATE_NEW_CONSOLE) 362 | else: 363 | subprocess.Popen(cmd_list) 364 | except Exception: 365 | return 366 | 367 | if __name__ == "__main__": 368 | app = QtWidgets.QApplication(sys.argv) 369 | 370 | # 全局图标 371 | app.setWindowIcon(QtGui.QIcon(APP_ICON)) 372 | 373 | w = MainWin() 374 | w.show() 375 | sys.exit(app.exec_()) --------------------------------------------------------------------------------