diff --git a/Setup.py b/Setup.py new file mode 100644 index 0000000..667fa8f --- /dev/null +++ b/Setup.py @@ -0,0 +1,957 @@ +import os +import sys +import time +import requests +import shutil +from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QLineEdit, + QPushButton, QVBoxLayout, QHBoxLayout, QWidget, + QFileDialog, QProgressBar, QMessageBox, QFrame, + QCheckBox, QStackedWidget, QTextEdit, QScrollArea) +from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer, QUrl +from PyQt5.QtGui import (QFont, QIcon, QPalette, QColor, QPixmap, + QImageReader, QImage, QIcon as QtGuiQIcon) + +# --------------------------- 全局样式配置 --------------------------- +GLOBAL_STYLE = """ +/* 主按钮样式:科技蓝底色+圆角 */ +QPushButton#PrimaryBtn { + background-color: #1E88E5; + color: white; + border: none; + border-radius: 4px; + padding: 8px 16px; + font-family: "微软雅黑", "Segoe UI"; + font-size: 10pt; +} +QPushButton#PrimaryBtn:hover { + background-color: #1976D2; +} +QPushButton#PrimaryBtn:disabled { + background-color: #BBDEFB; +} + +/* 次要按钮样式:灰色边框+透明底色 */ +QPushButton#SecondaryBtn { + background-color: transparent; + color: #333333; + border: 1px solid #CCCCCC; + border-radius: 4px; + padding: 8px 16px; + font-family: "微软雅黑", "Segoe UI"; + font-size: 10pt; +} +QPushButton#SecondaryBtn:hover { + background-color: #F5F5F5; +} + +/* 进度条样式:蓝色进度+圆角 */ +QProgressBar { + border: none; + border-radius: 4px; + background-color: #F5F5F5; + height: 8px; + font-family: "微软雅黑"; + font-size: 8pt; +} +QProgressBar::chunk { + background-color: #1E88E5; + border-radius: 4px; +} + +/* 输入框样式:细边框+聚焦高亮 */ +QLineEdit { + border: 1px solid #CCCCCC; + border-radius: 4px; + padding: 6px 10px; + font-family: "微软雅黑"; + font-size: 9pt; +} +QLineEdit:focus { + border-color: #1E88E5; +} + +/* 面板样式:圆角+浅灰底色 */ +QFrame#CardFrame { + background-color: white; + border-radius: 6px; + border: 1px solid #EEEEEE; +} + +/* 标题样式 */ +QLabel#TitleLabel { + font-family: "微软雅黑", "Segoe UI"; + font-size: 14pt; + font-weight: bold; + color: #212121; +} + +/* 副标题样式 */ +QLabel#SubtitleLabel, QTextEdit { + font-family: "微软雅黑", "Segoe UI"; + font-size: 10pt; + color: #666666; +} + +/* 滚动区域样式 */ +QScrollArea { + border: none; +} +""" + +# --------------------------- 下载线程 --------------------------- +class DownloadThread(QThread): + progress_updated = pyqtSignal(int, str, float) # (进度, 文件名, 下载速度MB/s) + download_finished = pyqtSignal(bool, str, str) # (成功, 消息, 文件名) + + def __init__(self, url, save_path, file_name, max_retries=2): + super().__init__() + self.url = url + self.save_path = save_path + self.file_name = file_name + self.max_retries = max_retries # 最大重试次数 + self.start_time = None + + def run(self): + retry_count = 0 + while retry_count <= self.max_retries: + try: + # 确保保存目录存在(固定目录) + save_dir = os.path.dirname(self.save_path) + if not os.path.exists(save_dir): + os.makedirs(save_dir) + + # 发送请求(增加超时设置) + self.start_time = time.time() + response = requests.get( + self.url, + stream=True, + timeout=15, + headers={"User-Agent": "EasyUI-Installer/1.0"} + ) + response.raise_for_status() # 触发HTTP错误 + total_size = int(response.headers.get('content-length', 0)) + + # 下载文件 + with open(self.save_path, 'wb') as file: + downloaded_size = 0 + for data in response.iter_content(chunk_size=8192): # 增大缓冲区,提升速度 + if data: + file.write(data) + downloaded_size += len(data) + # 计算进度和下载速度 + if total_size > 0: + progress = int((downloaded_size / total_size) * 100) + elapsed = time.time() - self.start_time + speed = downloaded_size / (1024 * 1024 * elapsed) if elapsed > 0 else 0 + self.progress_updated.emit(progress, self.file_name, round(speed, 2)) + + # 验证文件完整性 + if os.path.exists(self.save_path) and os.path.getsize(self.save_path) > 0: + self.download_finished.emit(True, self.save_path, self.file_name) + else: + raise Exception("文件下载不完整或为空") + break # 成功则退出重试循环 + + except Exception as e: + retry_count += 1 + if retry_count > self.max_retries: + self.download_finished.emit(False, f"重试{self.max_retries}次后失败:{str(e)}", self.file_name) + else: + time.sleep(2) # 重试间隔2秒 + + +# --------------------------- 图标下载线程 --------------------------- +class IconDownloadThread(QThread): + download_finished = pyqtSignal(bool, QIcon, bytes) # (成功, 图标对象, 原始数据) + + def __init__(self, icon_url): + super().__init__() + self.icon_url = icon_url + + def run(self): + try: + # 下载图标 + response = requests.get( + self.icon_url, + timeout=10, + headers={"User-Agent": "EasyUI-Installer/1.0"} + ) + response.raise_for_status() + + # 将下载的内容转换为QIcon + image = QImage() + if image.loadFromData(response.content): + icon = QIcon(QPixmap.fromImage(image)) + self.download_finished.emit(True, icon, response.content) + else: + self.download_finished.emit(False, QIcon(), b'') + + except Exception as e: + print(f"图标下载失败: {str(e)}") + self.download_finished.emit(False, QIcon(), b'') + + +# --------------------------- 多文件下载管理器 --------------------------- +class MultiFileDownloader(QThread): + overall_progress = pyqtSignal(int) # 总体进度 + file_progress = pyqtSignal(int, str, float) # (单个进度, 文件名, 速度) + file_finished = pyqtSignal(bool, str, str) # 单个文件完成 + all_finished = pyqtSignal() # 所有文件完成 + time_remaining = pyqtSignal(str) # 剩余时间(格式:mm:ss) + + def __init__(self, downloads): + super().__init__() + self.downloads = downloads # [(url, save_path, file_name), ...] + self.total_files = len(downloads) + self.completed_files = 0 + self.current_file_index = 0 + self.current_thread = None + self.stopped = False + self.current_speed = 0 # 当前下载速度(MB/s) + self.remaining_size = 0 # 剩余文件总大小(MB) + + def run(self): + # 预计算总大小(用于剩余时间估算) + self.calc_total_remaining_size() + + for i, (url, save_path, file_name) in enumerate(self.downloads): + if self.stopped: + break + + self.current_file_index = i + self.file_progress.emit(0, file_name, 0.0) + + # 创建当前文件下载线程 + self.current_thread = DownloadThread(url, save_path, file_name) + self.current_thread.progress_updated.connect(self.on_file_progress) + self.current_thread.download_finished.connect(self.on_file_complete) + self.current_thread.start() + self.current_thread.wait() + + self.all_finished.emit() + + def calc_total_remaining_size(self): + """计算剩余文件总大小(MB)""" + self.remaining_size = 0 + for url, save_path, file_name in self.downloads: + try: + response = requests.head(url, timeout=10) + file_size = int(response.headers.get('content-length', 0)) / (1024 * 1024) + self.remaining_size += file_size + except: + self.remaining_size += 10 # 估算未知大小文件为10MB + + def on_file_progress(self, progress, file_name, speed): + self.current_speed = speed + self.file_progress.emit(progress, file_name, speed) + + # 计算总体进度 + completed_size_ratio = (self.completed_files + progress/100) / self.total_files + overall = int(completed_size_ratio * 100) + self.overall_progress.emit(overall) + + # 估算剩余时间(仅当速度>0时) + if speed > 0.01: + # 计算当前文件剩余大小 + 未开始文件大小 + current_file_remaining = (1 - progress/100) * (self.remaining_size / self.total_files) + unstarted_files_size = (self.total_files - self.current_file_index - 1) * (self.remaining_size / self.total_files) + total_remaining = current_file_remaining + unstarted_files_size + + remaining_seconds = int(total_remaining / speed) + minutes = remaining_seconds // 60 + seconds = remaining_seconds % 60 + self.time_remaining.emit(f"{minutes:02d}:{seconds:02d}") + else: + self.time_remaining.emit("--:--") + + def on_file_complete(self, success, message, file_name): + self.file_finished.emit(success, message, file_name) + if success: + self.completed_files += 1 + # 更新剩余大小 + self.remaining_size -= (self.remaining_size / self.total_files) + + def stop(self): + self.stopped = True + if self.current_thread and self.current_thread.isRunning(): + self.current_thread.terminate() + + +# --------------------------- 主安装窗口 --------------------------- +class EasyUIInstaller(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Easy UI 安装程序") + self.setGeometry(300, 200, 700, 500) + self.setMinimumSize(700, 500) + self.setStyleSheet(GLOBAL_STYLE) + + # 图标配置 - 使用网络图标 + self.icon_url = "https://sunshinetown.oss-cn-shenzhen.aliyuncs.com/eui.ico" # 网络图标路径 + self.app_icon = QIcon() # 初始化空图标 + self.icon_data = b'' # 存储图标原始数据,用于创建快捷方式 + self.download_icon() # 启动图标下载 + + # 核心数据初始化 - 固定下载路径:C:\用户\自动识别用户名\JGZ_YES\EasyUILang\ + # 获取当前用户名 + self.user_name = os.path.expanduser("~").split(os.sep)[-1] + # 构建固定路径 + self.fixed_install_dir = os.path.join("C:", "用户", self.user_name, "JGZ_YES", "EasyUILang") + + self.resources = { + "interpreter": { + "name": "Easy UI 解释器", + "url": "https://sunshinetown.oss-cn-shenzhen.aliyuncs.com/easy_ui_interpreter.exe", + "default_path": os.path.join(self.fixed_install_dir, "easy_ui_interpreter.exe") + }, + "editor": { + "name": "Easy UI 编辑器", + "url": "https://sunshinetown.oss-cn-shenzhen.aliyuncs.com/EasyUI_Editor.exe", + "default_path": os.path.join(self.fixed_install_dir, "EasyUI_Editor.exe") + } + } + self.download_queue = [] + self.download_manager = None + + # 初始化分步容器 + self.init_stacked_widget() + + def download_icon(self): + """下载网络图标""" + print(f"正在下载图标: {self.icon_url}") + self.icon_thread = IconDownloadThread(self.icon_url) + self.icon_thread.download_finished.connect(self.on_icon_downloaded) + self.icon_thread.start() + + def on_icon_downloaded(self, success, icon, data): + """图标下载完成回调""" + if success and not icon.isNull(): + print("图标下载成功") + self.app_icon = icon + self.icon_data = data # 保存图标原始数据 + self.setWindowIcon(self.app_icon) + + # 更新界面上的图标 + if hasattr(self, 'logo_label'): + pixmap = self.app_icon.pixmap( + self.logo_label.width(), + self.logo_label.height(), + mode=QtGuiQIcon.Normal, + state=QtGuiQIcon.On + ) + self.logo_label.setPixmap(pixmap) + + if hasattr(self, 'complete_icon'): + pixmap = self.app_icon.pixmap( + self.complete_icon.width(), + self.complete_icon.height(), + mode=QtGuiQIcon.Normal, + state=QtGuiQIcon.On + ) + self.complete_icon.setPixmap(pixmap) + else: + print("图标下载失败,使用默认图标") + self.app_icon = QIcon.fromTheme("application-x-executable") + self.setWindowIcon(self.app_icon) + + def init_stacked_widget(self): + """创建分步容器""" + self.central_widget = QWidget() + self.setCentralWidget(self.central_widget) + self.main_layout = QVBoxLayout(self.central_widget) + self.main_layout.setContentsMargins(20, 20, 20, 20) + self.main_layout.setSpacing(20) + + # 1. 顶部Logo区域 + self.logo_layout = QHBoxLayout() + self.logo_label = QLabel() + self.logo_label.setFixedSize(40, 40) # 固定Logo大小 + + # 初始显示默认图标,下载完成后会更新 + if self.app_icon.isNull(): + palette = QPalette() + palette.setColor(QPalette.Background, QColor("#1E88E5")) + self.logo_label.setAutoFillBackground(True) + self.logo_label.setPalette(palette) + else: + pixmap = self.app_icon.pixmap( + self.logo_label.width(), + self.logo_label.height() + ) + self.logo_label.setPixmap(pixmap) + + self.app_name_label = QLabel("Easy UI") + self.app_name_label.setStyleSheet("font-size: 16pt; font-weight: bold; color: #1E88E5;") + self.logo_layout.addWidget(self.logo_label) + self.logo_layout.addWidget(self.app_name_label) + self.logo_layout.addStretch() + self.main_layout.addLayout(self.logo_layout) + + # 2. 分步内容容器 + self.stacked_widget = QStackedWidget() + self.main_layout.addWidget(self.stacked_widget, 1) # 占主要空间 + + # 3. 底部按钮区域 + self.btn_layout = QHBoxLayout() + self.btn_layout.setSpacing(10) + self.main_layout.addLayout(self.btn_layout) + + # 初始化各页面 + self.init_welcome_page() + self.init_license_page() + self.init_component_page() + self.init_progress_page() + self.init_complete_page() + + # 默认显示欢迎页 + self.stacked_widget.setCurrentIndex(0) + self.update_buttons_by_page(0) + + # --------------------------- 页面1:欢迎页 --------------------------- + def init_welcome_page(self): + self.welcome_page = QWidget() + self.welcome_layout = QVBoxLayout(self.welcome_page) + self.welcome_layout.setAlignment(Qt.AlignCenter) + self.welcome_layout.setSpacing(30) + + # 标题和副标题 + self.welcome_title = QLabel("欢迎使用 Easy UI 安装程序") + self.welcome_title.setObjectName("TitleLabel") + self.welcome_title.setAlignment(Qt.AlignCenter) + + self.welcome_subtitle = QLabel("本程序将引导您完成 Easy UI 的安装,预计耗时2-5分钟") + self.welcome_subtitle.setObjectName("SubtitleLabel") + self.welcome_subtitle.setAlignment(Qt.AlignCenter) + + # 系统要求提示 + self.system_require_label = QLabel(f""" +系统要求: +• Windows 10 及以上版本(64位) +• 至少 100MB 可用磁盘空间 +• 稳定的网络连接(用于下载组件) + +安装路径: +C:\\用户\\{self.user_name}\\JGZ_YES\\EasyUILang\\ + """) + self.system_require_label.setObjectName("SubtitleLabel") + self.system_require_label.setAlignment(Qt.AlignLeft) + + self.welcome_layout.addWidget(self.welcome_title) + self.welcome_layout.addWidget(self.welcome_subtitle) + self.welcome_layout.addWidget(self.system_require_label) + self.stacked_widget.addWidget(self.welcome_page) + + # --------------------------- 页面2:许可协议(可滚动版本) --------------------------- + def init_license_page(self): + self.license_page = QWidget() + self.license_layout = QVBoxLayout(self.license_page) + self.license_layout.setSpacing(20) + + # 标题 + self.license_title = QLabel("软件许可协议") + self.license_title.setObjectName("TitleLabel") + self.license_layout.addWidget(self.license_title) + + # 协议内容面板(带滚动功能) + self.license_frame = QFrame() + self.license_frame.setObjectName("CardFrame") + self.license_frame.setFixedHeight(250) + self.license_inner_layout = QVBoxLayout(self.license_frame) + + # 添加滚动区域 + self.license_scroll = QScrollArea() + self.license_scroll.setWidgetResizable(True) + self.license_scroll_content = QWidget() + self.license_scroll_layout = QVBoxLayout(self.license_scroll_content) + + # 协议文本 + self.license_text = QTextEdit() + self.license_text.setReadOnly(True) + self.license_text.setText(""" +Easy UI 软件许可协议 + +1. 许可范围 +本软件仅供个人非商业使用,禁止未经授权用于商业用途、盈利活动或非法行为。 + +2. 用户权利 +• 免费使用软件的全部功能及更新服务 +• 在许可范围内修改个人使用的软件副本 +• 获得软件相关的技术支持(限非商业用途) + +3. 用户义务 +• 不得传播经过篡改、破解或植入恶意代码的软件版本 +• 不得利用软件侵犯他人知识产权、隐私或其他合法权益 +• 不得违反国家法律法规及相关政策使用软件 + +4. 免责声明 +• 软件按“现状”提供,开发者不保证无故障运行或完全满足用户需求 +• 开发者不对软件使用过程中产生的任何直接或间接损失承担责任 +• 因用户违反本协议导致的法律风险,由用户自行承担 + +5. 协议生效与终止 +• 用户点击“同意”即表示完全接受本协议所有条款 +• 若用户违反本协议,开发者有权终止其使用许可 + """) + self.license_scroll_layout.addWidget(self.license_text) + self.license_scroll.setWidget(self.license_scroll_content) + + self.license_inner_layout.addWidget(self.license_scroll) + self.license_layout.addWidget(self.license_frame) + + # 同意勾选框 + self.agree_check = QCheckBox("我已阅读并同意上述许可协议") + self.agree_check.setObjectName("SubtitleLabel") + self.agree_check.setChecked(False) + self.license_layout.addWidget(self.agree_check) + + self.license_layout.addStretch() + self.stacked_widget.addWidget(self.license_page) + + # --------------------------- 页面3:组件选择 --------------------------- + def init_component_page(self): + self.component_page = QWidget() + self.component_layout = QVBoxLayout(self.component_page) + self.component_layout.setSpacing(20) + + # 标题和副标题 + self.component_title = QLabel("选择安装组件") + self.component_title.setObjectName("TitleLabel") + self.component_layout.addWidget(self.component_title) + + self.component_subtitle = QLabel("默认安装所有组件,您可根据需求取消不需要的选项") + self.component_subtitle.setObjectName("SubtitleLabel") + self.component_layout.addWidget(self.component_subtitle) + + # 组件选择面板 + self.component_frame = QFrame() + self.component_frame.setObjectName("CardFrame") + self.component_inner_layout = QVBoxLayout(self.component_frame) + self.component_inner_layout.setContentsMargins(20, 20, 20, 20) + self.component_inner_layout.setSpacing(25) + + # 解释器组件 + self.interpreter_group = QWidget() + self.interpreter_group_layout = QVBoxLayout(self.interpreter_group) + self.interpreter_group_layout.setSpacing(10) + + self.interpreter_check = QCheckBox(f"{self.resources['interpreter']['name']} (必需组件)") + self.interpreter_check.setObjectName("SubtitleLabel") + self.interpreter_check.setChecked(True) + self.interpreter_check.setEnabled(False) + self.interpreter_group_layout.addWidget(self.interpreter_check) + + # 解释器路径选择(固定路径,不可修改) + self.interpreter_path_layout = QHBoxLayout() + self.interpreter_path_label = QLabel("安装位置:") + self.interpreter_path_label.setObjectName("SubtitleLabel") + self.interpreter_path = QLineEdit(self.resources['interpreter']['default_path']) + self.interpreter_path.setReadOnly(True) # 设置为只读,防止修改 + self.interpreter_browse_btn = QPushButton("浏览...") + self.interpreter_browse_btn.setObjectName("SecondaryBtn") + self.interpreter_browse_btn.clicked.connect(lambda: self.show_path_info()) + + self.interpreter_path_layout.addWidget(self.interpreter_path_label) + self.interpreter_path_layout.addWidget(self.interpreter_path, 1) + self.interpreter_path_layout.addWidget(self.interpreter_browse_btn) + self.interpreter_group_layout.addLayout(self.interpreter_path_layout) + self.component_inner_layout.addWidget(self.interpreter_group) + + # 编辑器组件 + self.editor_group = QWidget() + self.editor_group_layout = QVBoxLayout(self.editor_group) + self.editor_group_layout.setSpacing(10) + + self.editor_check = QCheckBox(f"{self.resources['editor']['name']} (推荐组件)") + self.editor_check.setObjectName("SubtitleLabel") + self.editor_check.setChecked(True) + self.editor_group_layout.addWidget(self.editor_check) + + # 编辑器路径选择(固定路径,不可修改) + self.editor_path_layout = QHBoxLayout() + self.editor_path_label = QLabel("安装位置:") + self.editor_path_label.setObjectName("SubtitleLabel") + self.editor_path = QLineEdit(self.resources['editor']['default_path']) + self.editor_path.setReadOnly(True) # 设置为只读,防止修改 + self.editor_browse_btn = QPushButton("浏览...") + self.editor_browse_btn.setObjectName("SecondaryBtn") + self.editor_browse_btn.clicked.connect(lambda: self.show_path_info()) + + self.editor_path_layout.addWidget(self.editor_path_label) + self.editor_path_layout.addWidget(self.editor_path, 1) + self.editor_path_layout.addWidget(self.editor_browse_btn) + self.editor_group_layout.addLayout(self.editor_path_layout) + self.component_inner_layout.addWidget(self.editor_group) + + self.component_layout.addWidget(self.component_frame) + self.stacked_widget.addWidget(self.component_page) + + def show_path_info(self): + """显示路径信息提示,告知用户路径不可修改""" + QMessageBox.information( + self, "安装路径说明", + f"软件将固定安装到以下路径:\nC:\\用户\\{self.user_name}\\JGZ_YES\\EasyUILang\\" + ) + + # --------------------------- 页面4:安装进度 --------------------------- + def init_progress_page(self): + self.progress_page = QWidget() + self.progress_layout = QVBoxLayout(self.progress_page) + self.progress_layout.setSpacing(20) + + # 标题 + self.progress_title = QLabel("正在安装") + self.progress_title.setObjectName("TitleLabel") + self.progress_layout.addWidget(self.progress_title) + + # 进度面板 + self.progress_frame = QFrame() + self.progress_frame.setObjectName("CardFrame") + self.progress_inner_layout = QVBoxLayout(self.progress_frame) + self.progress_inner_layout.setContentsMargins(20, 20, 20, 20) + self.progress_inner_layout.setSpacing(15) + + # 当前任务提示 + self.current_task_label = QLabel("准备下载安装组件...") + self.current_task_label.setObjectName("SubtitleLabel") + self.progress_inner_layout.addWidget(self.current_task_label) + + # 总体进度条 + self.overall_progress_bar = QProgressBar() + self.overall_progress_bar.setValue(0) + self.progress_inner_layout.addWidget(self.overall_progress_bar) + + # 进度详情 + self.progress_detail_layout = QHBoxLayout() + self.speed_label = QLabel("速度:0.00 MB/s") + self.speed_label.setObjectName("SubtitleLabel") + self.time_remaining_label = QLabel("剩余时间:--:--") + self.time_remaining_label.setObjectName("SubtitleLabel") + self.progress_detail_layout.addWidget(self.speed_label) + self.progress_detail_layout.addStretch() + self.progress_detail_layout.addWidget(self.time_remaining_label) + self.progress_inner_layout.addLayout(self.progress_detail_layout) + + self.progress_layout.addWidget(self.progress_frame) + self.stacked_widget.addWidget(self.progress_page) + + # --------------------------- 页面5:安装完成 --------------------------- + def init_complete_page(self): + self.complete_page = QWidget() + self.complete_layout = QVBoxLayout(self.complete_page) + self.complete_layout.setSpacing(25) + self.complete_layout.setAlignment(Qt.AlignCenter) + + # 完成图标 + self.complete_icon = QLabel() + self.complete_icon.setFixedSize(80, 80) + + # 初始显示默认图标 + if self.app_icon.isNull(): + palette = QPalette() + palette.setColor(QPalette.Background, QColor("#4CAF50")) + self.complete_icon.setAutoFillBackground(True) + self.complete_icon.setPalette(palette) + self.complete_icon.setStyleSheet("border-radius: 40px;") + else: + pixmap = self.app_icon.pixmap( + self.complete_icon.width(), + self.complete_icon.height() + ) + self.complete_icon.setPixmap(pixmap) + + self.complete_layout.addWidget(self.complete_icon, alignment=Qt.AlignCenter) + + # 完成标题 + self.complete_title = QLabel("安装完成!") + self.complete_title.setObjectName("TitleLabel") + self.complete_layout.addWidget(self.complete_title, alignment=Qt.AlignCenter) + + # 完成提示 + self.complete_subtitle = QLabel(f"Easy UI 已成功安装到:\nC:\\用户\\{self.user_name}\\JGZ_YES\\EasyUILang\\") + self.complete_subtitle.setObjectName("SubtitleLabel") + self.complete_subtitle.setAlignment(Qt.AlignCenter) + self.complete_layout.addWidget(self.complete_subtitle, alignment=Qt.AlignCenter) + + # 后续操作选项 + self.complete_options_layout = QVBoxLayout() + self.run_check = QCheckBox("立即运行 Easy UI 编辑器") + self.run_check.setObjectName("SubtitleLabel") + self.run_check.setChecked(True) + + self.desktop_shortcut_check = QCheckBox("创建桌面快捷方式") + self.desktop_shortcut_check.setObjectName("SubtitleLabel") + self.desktop_shortcut_check.setChecked(True) + + self.complete_options_layout.addWidget(self.run_check) + self.complete_options_layout.addWidget(self.desktop_shortcut_check) + self.complete_layout.addLayout(self.complete_options_layout) + + self.stacked_widget.addWidget(self.complete_page) + + # --------------------------- 核心交互逻辑 --------------------------- + def update_buttons_by_page(self, page_index): + """根据当前页面更新底部按钮""" + # 清空现有按钮 + for i in range(self.btn_layout.count()): + widget = self.btn_layout.itemAt(i).widget() + if widget: + widget.deleteLater() + + # 根据页面索引添加按钮 + if page_index == 0: # 欢迎页:仅“下一步” + self.next_btn = QPushButton("下一步") + self.next_btn.setObjectName("PrimaryBtn") + self.next_btn.clicked.connect(lambda: self.switch_page(1)) + self.btn_layout.addStretch() + self.btn_layout.addWidget(self.next_btn) + + elif page_index == 1: # 许可协议:“上一步”+“下一步” + self.prev_btn = QPushButton("上一步") + self.prev_btn.setObjectName("SecondaryBtn") + self.prev_btn.clicked.connect(lambda: self.switch_page(0)) + + self.next_btn = QPushButton("下一步") + self.next_btn.setObjectName("PrimaryBtn") + self.next_btn.setEnabled(False) + self.next_btn.clicked.connect(lambda: self.switch_page(2)) + + # 勾选框联动按钮状态 + self.agree_check.stateChanged.connect( + lambda state: self.next_btn.setEnabled(state == Qt.Checked) + ) + + self.btn_layout.addWidget(self.prev_btn) + self.btn_layout.addStretch() + self.btn_layout.addWidget(self.next_btn) + + elif page_index == 2: # 组件选择:“上一步”+“安装” + self.prev_btn = QPushButton("上一步") + self.prev_btn.setObjectName("SecondaryBtn") + self.prev_btn.clicked.connect(lambda: self.switch_page(1)) + + self.install_btn = QPushButton("开始安装") + self.install_btn.setObjectName("PrimaryBtn") + self.install_btn.clicked.connect(self.start_install) + + self.btn_layout.addWidget(self.prev_btn) + self.btn_layout.addStretch() + self.btn_layout.addWidget(self.install_btn) + + elif page_index == 3: # 安装进度:“取消” + self.cancel_btn = QPushButton("取消安装") + self.cancel_btn.setObjectName("SecondaryBtn") + self.cancel_btn.clicked.connect(self.cancel_install) + self.btn_layout.addStretch() + self.btn_layout.addWidget(self.cancel_btn) + + elif page_index == 4: # 安装完成:“完成” + self.finish_btn = QPushButton("完成") + self.finish_btn.setObjectName("PrimaryBtn") + self.finish_btn.clicked.connect(self.finish_install) + self.btn_layout.addStretch() + self.btn_layout.addWidget(self.finish_btn) + + def switch_page(self, target_index): + """切换页面并更新按钮""" + self.stacked_widget.setCurrentIndex(target_index) + self.update_buttons_by_page(target_index) + + def start_install(self): + """验证组件并开始安装""" + # 构建下载队列(固定路径) + self.download_queue = [] + if self.interpreter_check.isChecked(): + inter_path = self.resources['interpreter']['default_path'] + self.download_queue.append(( + self.resources['interpreter']['url'], + inter_path, + self.resources['interpreter']['name'] + )) + + if self.editor_check.isChecked(): + edit_path = self.resources['editor']['default_path'] + self.download_queue.append(( + self.resources['editor']['url'], + edit_path, + self.resources['editor']['name'] + )) + + # 验证路径权限 + if not self.verify_path_permission(): + return + + # 跳转到进度页并启动下载 + self.switch_page(3) + self.download_manager = MultiFileDownloader(self.download_queue) + self.download_manager.overall_progress.connect(self.overall_progress_bar.setValue) + self.download_manager.file_progress.connect(self.update_progress_detail) + self.download_manager.time_remaining.connect(self.time_remaining_label.setText) + self.download_manager.file_finished.connect(self.on_file_finished) + self.download_manager.all_finished.connect(self.on_all_finished) + self.download_manager.start() + + def verify_path_permission(self): + """验证安装路径是否有写入权限""" + for url, path, name in self.download_queue: + try: + test_dir = os.path.dirname(path) + if not os.path.exists(test_dir): + os.makedirs(test_dir) + + # 测试写入 + test_file = os.path.join(test_dir, "test_permission.tmp") + with open(test_file, 'w') as f: + f.write("test") + os.remove(test_file) + except Exception as e: + QMessageBox.warning( + self, "路径权限错误", + f"无法写入{name}的安装路径:\n{str(e)}\n请检查路径权限或以管理员身份运行安装程序。" + ) + return False + return True + + def update_progress_detail(self, progress, file_name, speed): + """更新进度详情""" + self.current_task_label.setText(f"正在安装 {file_name}... ({progress}%)") + self.speed_label.setText(f"速度:{speed:.2f} MB/s") + + def on_file_finished(self, success, message, file_name): + """单个文件安装完成回调""" + if not success: + QMessageBox.warning(self, f"{file_name}安装失败", message) + + def on_all_finished(self): + """所有文件安装完成""" + # 创建桌面快捷方式(如果勾选) + if self.desktop_shortcut_check.isChecked(): + self.create_desktop_shortcut() + self.switch_page(4) + + def create_desktop_shortcut(self): + """创建桌面快捷方式""" + editor_path = self.resources['editor']['default_path'] + if not os.path.exists(editor_path): + QMessageBox.warning(self, "创建失败", "未找到Easy UI编辑器可执行文件,无法创建快捷方式") + return + + # 尝试创建快捷方式 + try: + import pythoncom + import win32com.client + from winshell import desktop + + # 初始化COM + pythoncom.CoInitialize() + + # 获取桌面路径 + desktop_path = desktop() + shortcut_path = os.path.join(desktop_path, "Easy UI 编辑器.lnk") + + # 保存图标到本地临时文件 + icon_temp_path = os.path.join(self.fixed_install_dir, "eui_icon.ico") + if self.icon_data: + with open(icon_temp_path, 'wb') as f: + f.write(self.icon_data) + + # 创建快捷方式 + shell = win32com.client.Dispatch("WScript.Shell") + shortcut = shell.CreateShortCut(shortcut_path) + shortcut.TargetPath = editor_path + shortcut.WorkingDirectory = self.fixed_install_dir + shortcut.Description = "Easy UI 编辑器" + shortcut.Hotkey = "Ctrl+Alt+E" + + # 设置图标 + if os.path.exists(icon_temp_path): + shortcut.IconLocation = icon_temp_path + else: + shortcut.IconLocation = editor_path + + shortcut.save() + pythoncom.CoUninitialize() + return + + except ImportError: + # 备用方案:使用PowerShell命令 + try: + import subprocess + + # 获取桌面路径 + desktop_path = os.path.join(os.path.expanduser("~"), "Desktop") + shortcut_path = os.path.join(desktop_path, "Easy UI 编辑器.lnk") + + # 保存图标 + icon_temp_path = os.path.join(self.fixed_install_dir, "eui_icon.ico") + if self.icon_data: + with open(icon_temp_path, 'wb') as f: + f.write(self.icon_data) + + # PowerShell命令 + ps_command = f''' + $WshShell = New-Object -ComObject WScript.Shell + $shortcut = $WshShell.CreateShortcut("{shortcut_path}") + $shortcut.TargetPath = "{editor_path}" + $shortcut.WorkingDirectory = "{self.fixed_install_dir}" + $shortcut.Description = "Easy UI 编辑器" + {f'$shortcut.IconLocation = "{icon_temp_path}"' if os.path.exists(icon_temp_path) else ''} + $shortcut.Save() + ''' + + subprocess.run( + ["powershell", "-Command", ps_command], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + return + + except Exception as e: + print(f"创建快捷方式失败:{str(e)}") + QMessageBox.warning( + self, "创建失败", + "无法自动创建桌面快捷方式,您可以手动创建:\n" + f"1. 找到文件:{editor_path}\n" + "2. 右键点击文件,选择'发送到'->'桌面快捷方式'" + ) + + def cancel_install(self): + """取消安装""" + if self.download_manager and self.download_manager.isRunning(): + confirm = QMessageBox.question( + self, "确认取消", + "取消安装将终止当前下载,已下载的文件可能不完整,是否继续?", + QMessageBox.Yes | QMessageBox.No + ) + if confirm == QMessageBox.Yes: + self.download_manager.stop() + self.switch_page(2) # 回到组件选择页 + + def finish_install(self): + """完成安装""" + # 运行程序(如果勾选) + if self.run_check.isChecked(): + editor_path = self.resources['editor']['default_path'] + if os.path.exists(editor_path): + os.startfile(editor_path) + # 关闭安装程序 + self.close() + + +# 程序入口 +if __name__ == "__main__": + # 先设置高DPI属性 + if hasattr(Qt, 'AA_EnableHighDpiScaling'): + QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) + if hasattr(Qt, 'AA_UseHighDpiPixmaps'): + QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) + + # 确保中文显示正常 + app = QApplication(sys.argv) + app.setFont(QFont("微软雅黑", 9)) + + window = EasyUIInstaller() + window.show() + + sys.exit(app.exec_()) \ No newline at end of file diff --git a/easy_ui_editor.py b/easy_ui_editor.py new file mode 100644 index 0000000..06297ca --- /dev/null +++ b/easy_ui_editor.py @@ -0,0 +1,2056 @@ +import sys +import os +import re +import glob +import tempfile +import shutil +import winreg +from PyQt5.QtWidgets import (QApplication, QMainWindow, QTextEdit, QPushButton, + QVBoxLayout, QHBoxLayout, QWidget, QMenuBar, QMenu, + QAction, QFileDialog, QMessageBox, QStatusBar, + QSplitter, QListWidget, QTabWidget, QLabel, QDockWidget, + QToolBar, QDialog, QRadioButton, QGroupBox, + QDialogButtonBox, QCompleter, QTreeWidget, QTreeWidgetItem, + QInputDialog, QMenu as QContextMenu, QComboBox) +from PyQt5.QtGui import (QFont, QSyntaxHighlighter, QTextCharFormat, QColor, + QTextDocument, QTextCursor, QIcon, QPixmap) +from PyQt5.QtCore import Qt, QThread, pyqtSignal, QProcess, QDateTime, QTimer, QStringListModel + +# 文件关联相关功能 +class FileAssociation: + @staticmethod + def is_associated(): + try: + with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, ".eui", 0, winreg.KEY_READ) as key: + prog_id, _ = winreg.QueryValueEx(key, "") + if prog_id != "EasyUIEditor.eui": + return False + + with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, "EasyUIEditor.eui\\shell\\open\\command", 0, winreg.KEY_READ) as key: + command, _ = winreg.QueryValueEx(key, "") + current_exe = sys.executable + if current_exe.endswith("python.exe"): + script_path = os.path.abspath(sys.argv[0]) + return script_path in command + else: + return current_exe in command + return True + except WindowsError: + return False + + @staticmethod + def set_association(): + try: + if getattr(sys, 'frozen', False): + current_path = sys.executable + else: + current_path = os.path.abspath(sys.argv[0]) + + icon_path = os.path.join(get_base_path(), "icon", "eui.ico") + if not os.path.exists(icon_path): + reply = QMessageBox.question( + None, "图标文件未找到", + f"未找到图标文件: {icon_path}\n仍要继续设置文件关联吗?", + QMessageBox.Yes | QMessageBox.No, QMessageBox.No + ) + if reply != QMessageBox.Yes: + return False + + with winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, ".eui") as key: + winreg.SetValueEx(key, "", 0, winreg.REG_SZ, "EasyUIEditor.eui") + winreg.SetValueEx(key, "Content Type", 0, winreg.REG_SZ, "text/plain") + + with winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, "EasyUIEditor.eui") as key: + winreg.SetValueEx(key, "", 0, winreg.REG_SZ, "Easy UI 文件") + + if os.path.exists(icon_path): + with winreg.CreateKey(key, "DefaultIcon") as icon_key: + winreg.SetValueEx(icon_key, "", 0, winreg.REG_SZ, f"{icon_path},0") + + cmd_path = f'"{current_path}" "%1"' + with winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, "EasyUIEditor.eui\\shell\\open\\command") as key: + winreg.SetValueEx(key, "", 0, winreg.REG_SZ, cmd_path) + + with winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, ".eui\\OpenWithProgids") as key: + winreg.SetValueEx(key, "EasyUIEditor.eui", 0, winreg.REG_NONE, b"") + + return True + except WindowsError as e: + QMessageBox.critical(None, "文件关联失败", f"设置文件关联时出错:\n{str(e)}\n请尝试以管理员身份运行程序。") + return False + + @staticmethod + def remove_association(): + try: + winreg.DeleteKey(winreg.HKEY_CLASSES_ROOT, "EasyUIEditor.eui\\shell\\open\\command") + winreg.DeleteKey(winreg.HKEY_CLASSES_ROOT, "EasyUIEditor.eui\\shell\\open") + winreg.DeleteKey(winreg.HKEY_CLASSES_ROOT, "EasyUIEditor.eui\\shell") + winreg.DeleteKey(winreg.HKEY_CLASSES_ROOT, "EasyUIEditor.eui\\DefaultIcon") + winreg.DeleteKey(winreg.HKEY_CLASSES_ROOT, "EasyUIEditor.eui") + + with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, ".eui", 0, winreg.KEY_SET_VALUE) as key: + winreg.DeleteValue(key, "") + + with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, ".eui\\OpenWithProgids", 0, winreg.KEY_SET_VALUE) as key: + winreg.DeleteValue(key, "EasyUIEditor.eui") + + return True + except WindowsError as e: + QMessageBox.critical(None, "移除关联失败", f"移除文件关联时出错:\n{str(e)}\n请尝试以管理员身份运行程序。") + return False + + +class CompleterTextEdit(QTextEdit): + def __init__(self, parent=None): + super().__init__(parent) + self.completer = None + + def setCompleter(self, completer): + if self.completer: + self.completer.activated.disconnect() + + self.completer = completer + if not self.completer: + return + + self.completer.setWidget(self) + self.completer.activated.connect(self.insertCompletion) + + def insertCompletion(self, completion): + if not self.completer: + return + + completion_text = completion.split(" ")[0] + + cursor = self.textCursor() + prefix_length = len(self.completer.completionPrefix()) + cursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, prefix_length) + cursor.insertText(completion_text) + self.setTextCursor(cursor) + + def textUnderCursor(self): + cursor = self.textCursor() + cursor.select(QTextCursor.WordUnderCursor) + return cursor.selectedText() + + def focusInEvent(self, event): + if self.completer: + self.completer.setWidget(self) + super().focusInEvent(event) + + def keyPressEvent(self, event): + if self.completer and self.completer.popup().isVisible(): + if event.key() in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape, Qt.Key_Tab, Qt.Key_Backtab): + event.ignore() + return + + super().keyPressEvent(event) + + prefix = self.textUnderCursor() + if not prefix: + self.completer.popup().hide() + return + + if prefix != self.completer.completionPrefix(): + self.completer.setCompletionPrefix(prefix) + self.completer.popup().setCurrentIndex(self.completer.completionModel().index(0, 0)) + + cr = self.cursorRect() + cr.setWidth(self.completer.popup().sizeHintForColumn(0) + self.completer.popup().verticalScrollBar().sizeHint().width()) + self.completer.complete(cr) + + +def get_base_path(): + if getattr(sys, 'frozen', False): + return os.path.dirname(sys.executable) + else: + return os.path.dirname(os.path.abspath(__file__)) + + +class EasyUISyntaxHighlighter(QSyntaxHighlighter): + def __init__(self, parent=None): + super().__init__(parent) + + # 优化后的颜色方案 + self.highlight_formats = { + 'comment': self._create_format(QColor(106, 153, 85), italic=True), # 注释-绿色斜体 + 'tag': self._create_format(QColor(86, 156, 214), bold=True), # 标签-亮蓝色加粗 + 'attribute': self._create_format(QColor(152, 221, 255)), # 属性-青色 + 'string': self._create_format(QColor(242, 178, 66)), # 字符串-橙色 + 'keyword': self._create_format(QColor(197, 134, 250)), # 关键字-紫色 + 'punctuation': self._create_format(QColor(150, 150, 150)) # 标点-中灰色 + } + + # 高亮规则 + self.highlight_rules = [ + (r'#.*$', self.highlight_formats['comment']), # #单行注释 + (r'//.*$', self.highlight_formats['comment']), # //单行注释 + (r'^\w+(?==)', self.highlight_formats['tag']), # 标签名 + (r'(?<=[,=])\s*(id|options|type|readonly|min|max|value|rows|interval)(?==)', self.highlight_formats['keyword']), # 关键字 + (r'(?<==)\s*\w+(?=[=,;])', self.highlight_formats['attribute']), # 属性值 + (r'"[^"]*"', self.highlight_formats['string']), # 字符串 + (r'[=,;[\]]', self.highlight_formats['punctuation']) # 标点符号 + ] + + def _create_format(self, color, bold=False, italic=False): + text_format = QTextCharFormat() + text_format.setForeground(color) + if bold: + text_format.setFontWeight(QFont.Bold) + if italic: + text_format.setFontItalic(True) + return text_format + + def highlightBlock(self, text): + # 处理多行注释(/* */) + self.setCurrentBlockState(0) + start_index = 0 + + # 检查上一行是否处于多行注释中 + if self.previousBlockState() != 1: + # 从文本起始位置查找 /* + start_index = self._match_multiline(text, r'/\*', 1) + + # 循环处理所有多行注释 + while start_index >= 0: + # 查找 */ 结束符 + end_index = self._match_multiline(text, r'\*/', 0, start_index) + if end_index == -1: + # 没有找到结束符,标记当前行为多行注释中 + self.setCurrentBlockState(1) + comment_length = len(text) - start_index + self.setFormat(start_index, comment_length, self.highlight_formats['comment']) + break + else: + # 找到结束符,高亮整个注释块 + comment_length = end_index - start_index + 2 # +2 包含 */ + self.setFormat(start_index, comment_length, self.highlight_formats['comment']) + # 继续查找下一个 /* + start_index = self._match_multiline(text, r'/\*', 1, end_index + 2) + + # 处理其他高亮规则 + for pattern, text_format in self.highlight_rules: + for match in re.finditer(pattern, text): + start = match.start() + length = match.end() - start + self.setFormat(start, length, text_format) + + def _match_multiline(self, text, pattern, state, start=0): + # 修复:使用字符串切片实现起始位置偏移 + sliced_text = text[start:] + match = re.search(pattern, sliced_text, re.DOTALL) + if match: + return start + match.start() # 加上偏移量 + return -1 + + +class InterpreterSelector(QDialog): + def __init__(self, interpreter_paths, parent=None): + super().__init__(parent) + self.setWindowTitle("选择解释器") + self.setGeometry(300, 300, 800, 200) + + layout = QVBoxLayout(self) + + group_box = QGroupBox("找到以下解释器,请选择一个:") + group_layout = QVBoxLayout() + group_box.setLayout(group_layout) + + self.interpreter_combo = QComboBox() + self.interpreter_combo.setMinimumWidth(700) + self.interpreter_combo.setToolTip("选择要使用的解释器") + + for path in interpreter_paths: + self.interpreter_combo.addItem(path, path) + + group_layout.addWidget(self.interpreter_combo) + + manual_layout = QHBoxLayout() + self.manual_check = QRadioButton("手动选择解释器...") + manual_layout.addWidget(self.manual_check) + group_layout.addLayout(manual_layout) + + layout.addWidget(group_box) + + buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + layout.addWidget(buttons) + + if interpreter_paths: + self.interpreter_combo.setCurrentIndex(0) + else: + self.manual_check.setChecked(True) + + def get_selected_path(self): + if not self.manual_check.isChecked() and self.interpreter_combo.count() > 0: + return self.interpreter_combo.currentData() + + file_path, _ = QFileDialog.getOpenFileName( + self, "选择解释器", "", "可执行文件 (*.exe);;Python文件 (*.py);;所有文件 (*)" + ) + return file_path if file_path else None + + +class InterpreterThread(QThread): + error_occurred = pyqtSignal(str) + output_received = pyqtSignal(str) + finished = pyqtSignal() + timeout_occurred = pyqtSignal() + + def __init__(self, code, file_path, interpreter_path, timeout=30): + super().__init__() + self.code = code + self.file_path = file_path + self.interpreter_path = interpreter_path + self.process = None + self.timeout = timeout * 1000 + self.timeout_timer = None + + def run(self): + try: + with open(self.file_path, 'w', encoding='utf-8-sig') as f: + f.write(self.code) + + if not os.path.exists(self.interpreter_path): + self.error_occurred.emit(f"解释器文件不存在: {self.interpreter_path}") + self.finished.emit() + return + + self.process = QProcess() + self.process.setProcessChannelMode(QProcess.SeparateChannels) + self.process.setReadChannel(QProcess.StandardOutput) + + self.process.readyReadStandardOutput.connect(self.handle_output) + self.process.readyReadStandardError.connect(self.handle_error) + self.process.finished.connect(self.on_process_finished) + self.process.errorOccurred.connect(self.on_process_error) + + self.timeout_timer = QTimer() + self.timeout_timer.setSingleShot(True) + self.timeout_timer.timeout.connect(self.on_timeout) + self.timeout_timer.start(self.timeout) + + if self.interpreter_path.endswith('.exe'): + self.process.start(self.interpreter_path, [self.file_path]) + else: + self.process.start(sys.executable, [self.interpreter_path, self.file_path]) + + if not self.process.waitForStarted(5000): + self.error_occurred.emit(f"进程启动失败,可能是解释器路径错误或权限不足") + self.cleanup() + self.finished.emit() + return + + except Exception as e: + self.error_occurred.emit(f"线程初始化错误: {str(e)}") + self.cleanup() + self.finished.emit() + + def handle_output(self): + if self.timeout_timer and self.timeout_timer.isActive(): + self.timeout_timer.start(self.timeout) + + while self.process and self.process.canReadLine(): + try: + output = self.process.readLine().data().decode('utf-8').rstrip('\n') + except UnicodeDecodeError: + output = self.process.readLine().data().decode('gbk', errors='replace').rstrip('\n') + + if output: + self.output_received.emit(f"[输出] {output}") + + def handle_error(self): + while self.process and self.process.canReadLine(QProcess.StandardError): + try: + error = self.process.readLine(QProcess.StandardError).data().decode('utf-8').rstrip('\n') + except UnicodeDecodeError: + error = self.process.readLine(QProcess.StandardError).data().decode('gbk', errors='replace').rstrip('\n') + + if error: + self.error_occurred.emit(f"[错误] {error}") + + def on_process_finished(self, exit_code, exit_status): + self.cleanup() + + if exit_status == QProcess.CrashExit: + self.error_occurred.emit(f"进程崩溃,可能是代码语法错误或解释器异常") + elif exit_code != 0: + self.error_occurred.emit(f"进程异常退出,退出代码: {exit_code}") + else: + self.output_received.emit(f"[提示] 进程正常结束,退出代码: {exit_code}") + + self.finished.emit() + + def on_process_error(self, error): + error_messages = { + QProcess.FailedToStart: "进程启动失败 - 可能是解释器不存在或权限不足", + QProcess.Crashed: "进程已崩溃", + QProcess.Timedout: "进程超时", + QProcess.ReadError: "读取错误", + QProcess.WriteError: "写入错误", + QProcess.UnknownError: "未知错误" + } + + self.error_occurred.emit(f"[进程错误] {error_messages.get(error, f'发生错误: {error}')}") + self.cleanup() + self.finished.emit() + + def on_timeout(self): + self.error_occurred.emit(f"[超时] 代码运行时间超过 {self.timeout/1000} 秒,已自动终止") + self.stop() + self.timeout_occurred.emit() + self.finished.emit() + + def stop(self): + if self.process and self.process.state() == QProcess.Running: + self.process.terminate() + if not self.process.waitForFinished(2000): + self.process.kill() + self.error_occurred.emit(f"[提示] 进程已手动终止") + self.cleanup() + + def cleanup(self): + if self.timeout_timer and self.timeout_timer.isActive(): + self.timeout_timer.stop() + self.timeout_timer = None + + +class InterpreterSearchThread(QThread): + progress_updated = pyqtSignal(object) + search_complete = pyqtSignal(list) + + def __init__(self): + super().__init__() + self.searching = True + self.found_paths = set() + self.search_names = [ + "easy_ui_interpreter.exe", + "easy_ui_interpreter.py" + ] + + def run(self): + try: + drives = self.get_available_drives() + total_drives = len(drives) + drive_count = 0 + + self.search_quick_paths() + + for drive in drives: + if not self.searching: + break + + self.progress_updated.emit(f"正在扫描驱动器: {drive}({drive_count+1}/{total_drives})") + self.search_directory(drive) + + drive_count += 1 + progress = int((drive_count / total_drives) * 100) if total_drives > 0 else 0 + self.progress_updated.emit(progress) + + sorted_paths = sorted(list(self.found_paths)) + self.search_complete.emit(sorted_paths) + + except Exception as e: + print(f"搜索解释器时出错: {str(e)}") + self.search_complete.emit(list(self.found_paths)) + + def get_available_drives(self): + drives = [] + seen = set() + + if sys.platform.startswith('win'): + try: + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders") as key: + for i in range(winreg.QueryInfoKey(key)[1]): + try: + value_name, value_data, _ = winreg.EnumValue(key, i) + if value_data and len(value_data) >= 3 and value_data[1] == ':' and value_data[2] == '\\': + drive = value_data[:3].upper() + if drive not in seen and os.path.exists(drive) and os.access(drive, os.R_OK): + seen.add(drive) + drives.append(drive) + except WindowsError: + continue + except Exception: + for drive_letter in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ': + drive = f"{drive_letter}:\\" + if os.path.exists(drive) and os.access(drive, os.R_OK): + drive_upper = drive.upper() + if drive_upper not in seen: + seen.add(drive_upper) + drives.append(drive_upper) + else: + return ["/", os.path.expanduser("~")] + + drives.sort() + return drives + + def search_quick_paths(self): + base_path = get_base_path() + + quick_paths = [ + base_path, + os.path.join(base_path, "interpreters"), + "D:\\Easy-Windows-UI-Lang", + os.path.join(os.environ.get("ProgramFiles", ""), "Easy-Windows-UI-Lang"), + os.path.join(os.environ.get("ProgramFiles(x86)", ""), "Easy-Windows-UI-Lang"), + os.path.expanduser("~\\Desktop"), + os.path.expanduser("~\\Documents"), + os.path.expanduser("~\\Downloads") + ] + + for path in os.environ.get("PATH", "").split(os.pathsep): + if path and path not in quick_paths: + quick_paths.append(path) + + for path in quick_paths: + if os.path.exists(path) and os.path.isdir(path): + self.search_directory(path, depth_limit=None) + + def search_directory(self, root_dir, depth_limit=None, current_depth=0): + if depth_limit is not None and current_depth > depth_limit: + return + + try: + if not os.access(root_dir, os.R_OK): + return + + for name in self.search_names: + file_path = os.path.join(root_dir, name) + if os.path.exists(file_path) and os.path.isfile(file_path): + self.found_paths.add(file_path) + + for item in os.listdir(root_dir): + if not self.searching: + return + + item_path = os.path.join(root_dir, item) + if os.path.isdir(item_path): + if self.should_skip_directory(item_path): + continue + self.search_directory(item_path, depth_limit, current_depth + 1) + + except Exception as e: + pass + + def should_skip_directory(self, dir_path): + dir_name = os.path.basename(dir_path).lower() + full_path = dir_path.lower() + + system_blacklist = [ + "windows\\system32", "windows\\syswow64", "windows\\system", + "$recycle.bin", "system volume information", "windows\\recovery" + ] + + if any(black_dir in full_path for black_dir in system_blacklist): + return True + if dir_name in ["node_modules", "venv", "env"]: + return True + return False + + def stop_search(self): + self.searching = False + + +class FileTreeWidget(QTreeWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.parent = parent + self.setHeaderLabel("文件目录") + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self.show_context_menu) + self.itemDoubleClicked.connect(self.on_item_double_clicked) + + self.init_icons() + self.current_dir = get_base_path() + self.refresh_tree() + + def init_icons(self): + self.dir_icon = QIcon() + self.dir_icon.addPixmap(QPixmap(":/icons/folder.png"), QIcon.Normal, QIcon.Off) + + self.python_icon = QIcon() + self.python_icon.addPixmap(QPixmap(":/icons/python.png"), QIcon.Normal, QIcon.Off) + + self.cpp_icon = QIcon() + self.cpp_icon.addPixmap(QPixmap(":/icons/cpp.png"), QIcon.Normal, QIcon.Off) + + self.java_icon = QIcon() + self.java_icon.addPixmap(QPixmap(":/icons/java.png"), QIcon.Normal, QIcon.Off) + + self.eui_icon = QIcon() + eui_icon_path = os.path.join(get_base_path(), "icon", "eui.ico") + + if os.path.exists(eui_icon_path): + self.eui_icon.addPixmap(QPixmap(eui_icon_path), QIcon.Normal, QIcon.Off) + else: + self.eui_icon = None + self.eui_text = "📝" + + self.file_icon = QIcon() + self.file_icon.addPixmap(QPixmap(":/icons/file.png"), QIcon.Normal, QIcon.Off) + + self.dir_text = "📁" + self.python_text = "🐍" + self.cpp_text = "++" + self.java_text = "☕" + self.file_text = "📄" + + def get_file_icon(self, file_name): + lower_name = file_name.lower() + + if lower_name.endswith('.eui'): + if self.eui_icon: + return self.eui_icon + return self.eui_text + elif lower_name.endswith('.py'): + if not self.python_icon.isNull(): + return self.python_icon + return self.python_text + elif lower_name.endswith(('.cpp', '.h', '.c', '.hpp')): + if not self.cpp_icon.isNull(): + return self.cpp_icon + return self.cpp_text + elif lower_name.endswith('.java'): + if not self.java_icon.isNull(): + return self.java_icon + return self.java_text + else: + if not self.file_icon.isNull(): + return self.file_icon + return self.file_text + + def refresh_tree(self): + self.clear() + if not self.current_dir or not os.path.isdir(self.current_dir): + return + + root = QTreeWidgetItem([os.path.basename(self.current_dir)]) + root.setData(0, Qt.UserRole, self.current_dir) + if not self.dir_icon.isNull(): + root.setIcon(0, self.dir_icon) + root.setExpanded(True) + self.addTopLevelItem(root) + + self.add_directory_items(root, self.current_dir) + + self.parent.status_bar.showMessage(f"显示目录: {self.current_dir}") + + def add_directory_items(self, parent_item, directory): + try: + items = os.listdir(directory) + dirs = [] + files = [] + + for item in items: + item_path = os.path.join(directory, item) + if os.path.isdir(item_path) and not item.startswith('.'): + dirs.append(item) + elif os.path.isfile(item_path): + files.append(item) + + for dir_name in sorted(dirs): + dir_path = os.path.join(directory, dir_name) + dir_item = QTreeWidgetItem([dir_name]) + dir_item.setData(0, Qt.UserRole, dir_path) + if not self.dir_icon.isNull(): + dir_item.setIcon(0, self.dir_icon) + parent_item.addChild(dir_item) + + self.add_directory_items(dir_item, dir_path) + dir_item.setExpanded(False) + + for file_name in sorted(files): + file_path = os.path.join(directory, file_name) + file_item = QTreeWidgetItem([file_name]) + file_item.setData(0, Qt.UserRole, file_path) + + icon = self.get_file_icon(file_name) + if isinstance(icon, QIcon) and not icon.isNull(): + file_item.setIcon(0, icon) + else: + file_item.setText(0, f"{icon} {file_name}") + + parent_item.addChild(file_item) + + except Exception as e: + pass + + def on_item_double_clicked(self, item, column): + item_path = item.data(0, Qt.UserRole) + if not item_path: + return + + if os.path.isdir(item_path): + if item.childCount() == 0: + self.add_directory_items(item, item_path) + item.setExpanded(not item.isExpanded()) + elif os.path.isfile(item_path): + if item_path.lower().endswith(('.eui', '.txt', '.py', '.cpp', '.h', '.java')): + self.parent.open_file_from_path(item_path) + else: + self.parent.status_bar.showMessage(f"不支持的文件类型: {os.path.basename(item_path)}") + + def show_context_menu(self, position): + item = self.itemAt(position) + if not item: + self.show_empty_context_menu(position) + return + + item_path = item.data(0, Qt.UserRole) + if not item_path: + return + + menu = QContextMenu() + + open_action = menu.addAction("打开") + open_action.triggered.connect(lambda: self.open_item(item)) + + if os.path.isdir(item_path): + menu.addSeparator() + + new_file_action = menu.addAction("新建文件") + new_file_action.triggered.connect(lambda: self.new_file(item)) + + new_folder_action = menu.addAction("新建文件夹") + new_folder_action.triggered.connect(lambda: self.new_folder(item)) + + set_as_root_action = menu.addAction("设为根目录") + set_as_root_action.triggered.connect(lambda: self.set_as_root(item)) + + add_file_action = menu.addAction("添加文件到此处") + add_file_action.triggered.connect(lambda: self.add_file_to_directory(item)) + else: + menu.addSeparator() + + rename_action = menu.addAction("重命名") + rename_action.triggered.connect(lambda: self.rename_item(item)) + + delete_action = menu.addAction("删除") + delete_action.triggered.connect(lambda: self.delete_item(item)) + + copy_action = menu.addAction("复制") + copy_action.triggered.connect(lambda: self.copy_item(item)) + + cut_action = menu.addAction("剪切") + cut_action.triggered.connect(lambda: self.cut_item(item)) + + menu.exec_(self.viewport().mapToGlobal(position)) + + def show_empty_context_menu(self, position): + menu = QContextMenu() + + new_file_action = menu.addAction("新建文件") + new_file_action.triggered.connect(lambda: self.new_file_in_current_dir()) + + new_folder_action = menu.addAction("新建文件夹") + new_folder_action.triggered.connect(lambda: self.new_folder_in_current_dir()) + + menu.addSeparator() + + refresh_action = menu.addAction("刷新") + refresh_action.triggered.connect(self.refresh_tree) + + menu.exec_(self.viewport().mapToGlobal(position)) + + def new_file_in_current_dir(self): + self.new_file(None) + + def new_folder_in_current_dir(self): + self.new_folder(None) + + def open_item(self, item): + item_path = item.data(0, Qt.UserRole) + if os.path.isdir(item_path): + if item.childCount() == 0: + self.add_directory_items(item, item_path) + item.setExpanded(True) + else: + self.parent.open_file_from_path(item_path) + + def set_as_root(self, item): + item_path = item.data(0, Qt.UserRole) + if os.path.isdir(item_path): + self.current_dir = item_path + self.refresh_tree() + + def new_folder(self, item): + if item: + item_path = item.data(0, Qt.UserRole) + else: + item_path = self.current_dir + + if not os.path.isdir(item_path): + return + + folder_name, ok = QInputDialog.getText(self, "新建文件夹", "文件夹名称:") + if ok and folder_name: + new_folder_path = os.path.join(item_path, folder_name) + try: + os.makedirs(new_folder_path) + self.refresh_tree() + self.parent.status_bar.showMessage(f"已创建文件夹: {folder_name}") + except Exception as e: + QMessageBox.critical(self, "错误", f"无法创建文件夹: {str(e)}") + + def new_file(self, item): + if item: + dir_path = item.data(0, Qt.UserRole) + else: + dir_path = self.current_dir + + if not os.path.isdir(dir_path): + return + + file_name, ok = QInputDialog.getText(self, "新建文件", "文件名称(例如: myfile.eui):") + if ok and file_name: + file_path = os.path.join(dir_path, file_name) + try: + with open(file_path, 'w', encoding='utf-8') as f: + pass + + self.refresh_tree() + self.parent.status_bar.showMessage(f"已创建文件: {file_name}") + self.parent.open_file_from_path(file_path) + except Exception as e: + QMessageBox.critical(self, "错误", f"无法创建文件: {str(e)}") + + def add_file_to_directory(self, item): + if not item: + return + + dir_path = item.data(0, Qt.UserRole) + if not os.path.isdir(dir_path): + return + + file_paths, _ = QFileDialog.getOpenFileNames( + self, "选择要添加的文件", "", "所有文件 (*)" + ) + + if file_paths: + success_count = 0 + fail_count = 0 + + for file_path in file_paths: + try: + dest_path = os.path.join(dir_path, os.path.basename(file_path)) + + if os.path.exists(dest_path): + reply = QMessageBox.question( + self, "文件已存在", + f"{os.path.basename(file_path)} 已存在,是否覆盖?", + QMessageBox.Yes | QMessageBox.No, QMessageBox.No + ) + if reply != QMessageBox.Yes: + fail_count += 1 + continue + + shutil.copy2(file_path, dest_path) + success_count += 1 + except Exception as e: + print(f"复制文件失败: {str(e)}") + fail_count += 1 + + self.refresh_tree() + self.parent.status_bar.showMessage( + f"添加完成: 成功 {success_count} 个,失败 {fail_count} 个" + ) + + def rename_item(self, item): + item_path = item.data(0, Qt.UserRole) + old_name = os.path.basename(item_path) + new_name, ok = QInputDialog.getText(self, "重命名", "新名称:", text=old_name) + + if ok and new_name and new_name != old_name: + parent_dir = os.path.dirname(item_path) + new_path = os.path.join(parent_dir, new_name) + + try: + os.rename(item_path, new_path) + self.refresh_tree() + self.parent.status_bar.showMessage(f"已重命名为: {new_name}") + except Exception as e: + QMessageBox.critical(self, "错误", f"无法重命名: {str(e)}") + + def delete_item(self, item): + item_path = item.data(0, Qt.UserRole) + if not item_path: + return + + reply = QMessageBox.question( + self, "确认删除", + f"确定要删除 {os.path.basename(item_path)} 吗?\n此操作不可恢复。", + QMessageBox.Yes | QMessageBox.No, QMessageBox.No + ) + + if reply == QMessageBox.Yes: + try: + if os.path.isfile(item_path): + os.remove(item_path) + elif os.path.isdir(item_path): + shutil.rmtree(item_path) + + self.refresh_tree() + self.parent.status_bar.showMessage(f"已删除: {os.path.basename(item_path)}") + except Exception as e: + QMessageBox.critical(self, "错误", f"无法删除: {str(e)}") + + def copy_item(self, item): + item_path = item.data(0, Qt.UserRole) + if not item_path: + return + + self.parent.copied_path = item_path + self.parent.is_cut = False + self.parent.status_bar.showMessage(f"已复制: {os.path.basename(item_path)}") + + def cut_item(self, item): + item_path = item.data(0, Qt.UserRole) + if not item_path: + return + + self.parent.copied_path = item_path + self.parent.is_cut = True + self.parent.status_bar.showMessage(f"已剪切: {os.path.basename(item_path)}") + + def change_directory(self, new_dir): + if os.path.isdir(new_dir): + self.current_dir = new_dir + self.refresh_tree() + return True + return False + + +class EasyUIEditor(QMainWindow): + def __init__(self): + super().__init__() + self.current_file = None + self.temp_file = os.path.join(tempfile.gettempdir(), "temp_ewui_code.eui") + self.status_bar = None + self.interpreter_path = None + self.run_timeout = 30 + self.search_thread = None + self.search_in_progress = False + self.copied_path = None + self.is_cut = False + + self.cmd_line_file = sys.argv[1] if len(sys.argv) > 1 and sys.argv[1].lower().endswith('.eui') else None + + self.init_completion_words() + self.init_status_bar() + self.init_ui() + self.check_file_association_prompt() # 检查文件关联(带记忆功能) + + self.scan_interpreters(quick_scan=True) + self.full_scan_interpreters_in_background() + + if self.cmd_line_file: + self.open_file_from_path(self.cmd_line_file) + + def init_completion_words(self): + self.completion_words = [ + ("window", "标签 - 窗口"), + ("label", "标签 - 文字显示"), + ("entry", "标签 - 输入框"), + ("combo", "标签 - 选择框"), + ("checkbox", "标签 - 多选框"), + ("button", "标签 - 按钮"), + ("audio", "标签 - 音频组件"), + ("slider", "标签 - 滑块控件"), + ("textarea", "标签 - 文本区域"), + ("separator", "标签 - 分隔线"), + ("progress", "标签 - 进度条"), + ("calendar", "标签 - 日历控件"), + ("radiogroup", "标签 - 单选按钮组"), + ("groupbox", "标签 - 分组框"), + ("timer", "标签 - 定时器"), + ("title", "属性 - 窗口标题"), + ("width", "属性 - 宽度"), + ("height", "属性 - 高度"), + ("icon", "属性 - 窗口图标路径"), + ("text", "属性 - 显示文本"), + ("id", "属性 - 组件ID(必选)"), + ("hint", "属性 - 输入框提示文本"), + ("readonly", "属性 - 输入框只读(true/false)"), + ("label", "属性 - 选择框/多选框标题"), + ("options", "属性 - 选项列表(如[\"选项1\",\"选项2\"])"), + ("click", "属性 - 按钮触发动作"), + ("url", "属性 - 网络音频地址"), + ("os", "属性 - 本地音频文件路径"), + ("min", "属性 - 最小值"), + ("max", "属性 - 最大值"), + ("value", "属性 - 当前值"), + ("rows", "属性 - 文本区域行数"), + ("interval", "属性 - 定时器间隔(毫秒)"), + ("action", "属性 - 定时器动作"), + ("true", "值 - 布尔值(只读/启用)"), + ("false", "值 - 布尔值(可写/禁用)"), + ("显示=", "动作 - 显示组件内容(如显示=组件ID)"), + ("play_audio=", "动作 - 播放音频(如play_audio=音频ID)"), + ("pause_audio=", "动作 - 暂停音频(如pause_audio=音频ID)"), + ("stop_audio=", "动作 - 停止音频(如stop_audio=音频ID)"), + ("start_timer=", "动作 - 启动定时器(如start_timer=定时器ID)"), + ("stop_timer=", "动作 - 停止定时器(如stop_timer=定时器ID)"), + ("set_progress=", "动作 - 设置进度条(如set_progress=进度条ID,value=50)"), + (";", "符号 - 语句结束符"), + (",", "符号 - 属性分隔符"), + ("=[", "符号 - 选项列表开始(如options=[)"), + ("]", "符号 - 选项列表结束") + ] + + def init_status_bar(self): + self.status_bar = QStatusBar() + self.setStatusBar(self.status_bar) + self.status_bar.showMessage("初始化中...") + + # 带记忆功能的文件关联检查 + def check_file_association_prompt(self): + # 检查是否已经设置过关联 + if FileAssociation.is_associated(): + self.status_bar.showMessage(".eui文件已关联到此程序") + return + + # 检查是否已经提示过(使用注册表记录) + if self.has_prompted_association(): + self.status_bar.showMessage(".eui文件未关联,可在工具菜单设置") + return + + # 首次未关联状态,显示提示 + reply = QMessageBox.question( + self, "文件关联", + "尚未设置.eui文件关联,是否将.eui文件默认用此程序打开并设置图标?\n(需要管理员权限)", + QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes + ) + + # 记录已提示状态 + self.set_prompted_association(True) + + if reply == QMessageBox.Yes: + if FileAssociation.set_association(): + QMessageBox.information(self, "成功", "文件关联设置成功!\n可能需要重启资源管理器才能看到图标变化。") + self.status_bar.showMessage("已成功设置.eui文件关联") + else: + self.status_bar.showMessage("已取消文件关联设置,可在工具菜单重新设置") + + # 检查是否已提示过关联 + def has_prompted_association(self): + try: + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\EasyUIEditor", 0, winreg.KEY_READ) as key: + prompted, _ = winreg.QueryValueEx(key, "AssociationPrompted") + return bool(prompted) + except WindowsError: + return False + + # 设置已提示关联的标记 + def set_prompted_association(self, value): + try: + key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, r"Software\EasyUIEditor") + winreg.SetValueEx(key, "AssociationPrompted", 0, winreg.REG_DWORD, 1 if value else 0) + winreg.CloseKey(key) + except WindowsError: + pass # 忽略注册表操作错误 + + def init_ui(self): + self.setWindowTitle("Easy Windows UI Editor - [未命名]") + self.setGeometry(100, 100, 1400, 800) + + self.setStyleSheet(""" + QMainWindow { + background-color: #1e1e1e; + color: #d4d4d4; + } + QTextEdit, CompleterTextEdit { + background-color: #1e1e1e; + color: #d4d4d4; + border: 1px solid #3c3c3c; + } + QLabel { + color: #d4d4d4; + } + QPushButton { + background-color: #3c3c3c; + color: #d4d4d4; + border: 1px solid #5e5e5e; + padding: 5px 10px; + border-radius: 3px; + } + QPushButton:hover { + background-color: #4a4a4a; + } + QPushButton:pressed { + background-color: #2d2d2d; + } + QTabBar::tab { + background-color: #2d2d2d; + color: #d4d4d4; + padding: 8px 16px; + border: 1px solid #5e5e5e; + border-bottom: none; + } + QTabBar::tab:selected { + background-color: #1e1e1e; + border-top: 2px solid #007acc; + } + QListWidget, QTreeWidget { + background-color: #2d2d2d; + color: #d4d4d4; + border: 1px solid #3c3c3c; + } + QTreeWidget::item { + border-bottom: 1px solid #3c3c3c; + } + QTreeWidget::item:selected { + background-color: #3c3c3c; + } + QStatusBar { + background-color: #1e1e1e; + color: #858585; + border-top: 1px solid #3c3c3c; + } + QMenuBar { + background-color: #1e1e1e; + color: #d4d4d4; + border-bottom: 1px solid #3c3c3c; + } + QToolBar { + background-color: #1e1e1e; + border-bottom: 1px solid #3c3c3c; + spacing: 5px; + } + QDockWidget { + color: #d4d4d4; + titlebar-close-icon: url(); + titlebar-normal-icon: url(); + } + QDockWidget::title { + background-color: #2d2d2d; + padding: 5px; + border-bottom: 1px solid #3c3c3c; + } + QCompleter QListView { + background-color: #2d2d2d; + color: #d4d4d4; + border: 1px solid #5e5e5e; + padding: 2px; + } + QCompleter QListView::item:selected { + background-color: #007acc; + color: white; + } + QMenu { + background-color: #2d2d2d; + color: #d4d4d4; + border: 1px solid #5e5e5e; + } + QMenu::item:selected { + background-color: #3c3c3c; + } + QComboBox { + background-color: #2d2d2d; + color: #d4d4d4; + border: 1px solid #5e5e5e; + padding: 3px; + min-width: 500px; + } + QComboBox::drop-down { + border-left: 1px solid #5e5e5e; + } + QComboBox::down-arrow { + image: url(:/icons/arrow-down.png); + width: 12px; + height: 12px; + } + """) + + self.create_menu_bar() + self.create_tool_bar() + + main_splitter = QSplitter(Qt.Horizontal) + + self.file_tree = FileTreeWidget(self) + self.file_tree.setMaximumWidth(300) + main_splitter.addWidget(self.file_tree) + + right_container = QWidget() + right_layout = QVBoxLayout(right_container) + + interpreter_layout = QHBoxLayout() + interpreter_layout.addWidget(QLabel("当前解释器:")) + + self.interpreter_combo = QComboBox() + self.interpreter_combo.setToolTip("选择要使用的解释器") + self.interpreter_combo.currentIndexChanged.connect(self.on_interpreter_changed) + + interpreter_layout.addWidget(self.interpreter_combo) + interpreter_layout.addStretch() + + refresh_interpreter_btn = QPushButton("刷新解释器列表") + refresh_interpreter_btn.setToolTip("重新扫描可用的解释器") + refresh_interpreter_btn.clicked.connect(lambda: self.scan_interpreters(quick_scan=True)) + interpreter_layout.addWidget(refresh_interpreter_btn) + + right_layout.addLayout(interpreter_layout) + + self.tab_widget = QTabWidget() + self.tab_widget.setTabsClosable(True) + self.tab_widget.tabCloseRequested.connect(self.close_tab) + self.add_new_tab() + + right_layout.addWidget(self.tab_widget) + + self.output_panel = QTextEdit() + self.output_panel.setReadOnly(True) + self.output_panel.setMaximumHeight(150) + self.output_panel.setAcceptRichText(True) + self.output_panel.setHtml('[提示] 输出面板将显示代码运行日志和报错信息(按F5运行代码)') + right_layout.addWidget(self.output_panel) + + main_splitter.addWidget(right_container) + main_splitter.setSizes([250, 1150]) + + self.setCentralWidget(main_splitter) + + self.add_help_dock() + + def on_interpreter_changed(self, index): + if index >= 0 and self.interpreter_combo.count() > 0: + self.interpreter_path = self.interpreter_combo.currentData() + self.status_bar.showMessage(f"已选择解释器: {os.path.basename(self.interpreter_path)}") + + def create_tool_bar(self): + toolbar = QToolBar("主工具栏") + self.addToolBar(toolbar) + + new_btn = QPushButton("新建") + new_btn.setToolTip("新建文件 (Ctrl+N)") + new_btn.clicked.connect(self.add_new_tab) + toolbar.addWidget(new_btn) + + open_btn = QPushButton("打开") + open_btn.setToolTip("打开文件 (Ctrl+O)") + open_btn.clicked.connect(self.open_file) + toolbar.addWidget(open_btn) + + change_dir_btn = QPushButton("更改目录") + change_dir_btn.setToolTip("更改文件树显示的目录") + change_dir_btn.clicked.connect(self.change_directory) + toolbar.addWidget(change_dir_btn) + + save_btn = QPushButton("保存") + save_btn.setToolTip("保存文件 (Ctrl+S)") + save_btn.clicked.connect(self.save_file) + toolbar.addWidget(save_btn) + + toolbar.addSeparator() + + run_btn = QPushButton("运行") + run_btn.setToolTip("运行代码 (F5)") + run_btn.clicked.connect(self.run_code) + run_btn.setStyleSheet("color: green; font-weight: bold;") + toolbar.addWidget(run_btn) + + stop_btn = QPushButton("停止") + stop_btn.setToolTip("停止运行 (Ctrl+F5)") + stop_btn.clicked.connect(self.stop_running) + stop_btn.setStyleSheet("color: red; font-weight: bold;") + toolbar.addWidget(stop_btn) + + interpreter_btn = QPushButton("解释器") + interpreter_btn.setToolTip("选择解释器") + interpreter_btn.clicked.connect(self.choose_interpreter) + toolbar.addWidget(interpreter_btn) + + full_scan_btn = QPushButton("全扫描") + full_scan_btn.setToolTip("全电脑后台搜索解释器") + full_scan_btn.clicked.connect(self.full_scan_interpreters_in_background) + full_scan_btn.setStyleSheet("color: #00ccff; font-weight: bold;") + toolbar.addWidget(full_scan_btn) + + force_scan_btn = QPushButton("强制全扫") + force_scan_btn.setToolTip("无限制扫描所有驱动器(确保找到全部解释器)") + force_scan_btn.setStyleSheet("color: orange; font-weight: bold;") + force_scan_btn.clicked.connect(self.force_full_scan) + toolbar.addWidget(force_scan_btn) + + clear_btn = QPushButton("清空") + clear_btn.setToolTip("清空编辑区") + clear_btn.clicked.connect(self.clear_current_tab) + toolbar.addWidget(clear_btn) + + def update_interpreter_combo(self, interpreter_paths): + current_path = self.interpreter_path + + self.interpreter_combo.clear() + + for path in interpreter_paths: + self.interpreter_combo.addItem(path, path) + + if current_path and os.path.exists(current_path): + for i in range(self.interpreter_combo.count()): + if self.interpreter_combo.itemData(i) == current_path: + self.interpreter_combo.setCurrentIndex(i) + return + + if self.interpreter_combo.count() > 0: + self.interpreter_combo.setCurrentIndex(0) + self.interpreter_path = self.interpreter_combo.currentData() + + def create_menu_bar(self): + menubar = self.menuBar() + + file_menu = menubar.addMenu("文件(&F)") + + new_action = QAction("新建(&N)", self) + new_action.setShortcut("Ctrl+N") + new_action.triggered.connect(self.add_new_tab) + file_menu.addAction(new_action) + + open_action = QAction("打开(&O)", self) + open_action.setShortcut("Ctrl+O") + open_action.triggered.connect(self.open_file) + file_menu.addAction(open_action) + + change_dir_action = QAction("更改目录(&C)", self) + change_dir_action.triggered.connect(self.change_directory) + file_menu.addAction(change_dir_action) + + save_action = QAction("保存(&S)", self) + save_action.setShortcut("Ctrl+S") + save_action.triggered.connect(self.save_file) + file_menu.addAction(save_action) + + save_as_action = QAction("另存为(&A)", self) + save_as_action.setShortcut("Ctrl+Shift+S") + save_as_action.triggered.connect(self.save_file_as) + file_menu.addAction(save_as_action) + + paste_action = QAction("粘贴(&P)", self) + paste_action.setShortcut("Ctrl+V") + paste_action.triggered.connect(self.paste_file) + file_menu.addAction(paste_action) + + file_menu.addSeparator() + + exit_action = QAction("退出(&X)", self) + exit_action.triggered.connect(self.close) + file_menu.addAction(exit_action) + + run_menu = menubar.addMenu("运行(&R)") + + run_action = QAction("运行代码(&R)", self) + run_action.setShortcut("F5") + run_action.triggered.connect(self.run_code) + run_menu.addAction(run_action) + + stop_action = QAction("停止运行(&S)", self) + stop_action.setShortcut("Ctrl+F5") + stop_action.triggered.connect(self.stop_running) + run_menu.addAction(stop_action) + + timeout_menu = run_menu.addMenu("运行超时设置") + self.timeout_actions = {} + for timeout in [10, 30, 60, 120]: + act = QAction(f"{timeout}秒", self, checkable=True) + act.setData(timeout) + if timeout == self.run_timeout: + act.setChecked(True) + act.triggered.connect(self.set_timeout) + self.timeout_actions[timeout] = act + timeout_menu.addAction(act) + + tool_menu = menubar.addMenu("工具(&T)") + + assoc_action = QAction("设置.eui文件关联", self) + assoc_action.triggered.connect(self.set_file_association) + tool_menu.addAction(assoc_action) + + unassoc_action = QAction("取消.eui文件关联", self) + unassoc_action.triggered.connect(self.remove_file_association) + tool_menu.addAction(unassoc_action) + + tool_menu.addSeparator() + + interpreter_action = QAction("选择解释器(&I)", self) + interpreter_action.triggered.connect(self.choose_interpreter) + tool_menu.addAction(interpreter_action) + + scan_action = QAction("快速扫描解释器(&S)", self) + scan_action.triggered.connect(lambda: self.scan_interpreters(quick_scan=True)) + tool_menu.addAction(scan_action) + + full_scan_action = QAction("全电脑扫描解释器(&F)", self) + full_scan_action.triggered.connect(self.full_scan_interpreters_in_background) + tool_menu.addAction(full_scan_action) + + help_menu = menubar.addMenu("帮助(&H)") + + example_action = QAction("示例代码(&E)", self) + example_action.triggered.connect(self.load_example_code) + help_menu.addAction(example_action) + + about_action = QAction("关于(&A)", self) + about_action.triggered.connect(self.show_about) + help_menu.addAction(about_action) + + def set_file_association(self): + if FileAssociation.is_associated(): + reply = QMessageBox.question( + self, "已关联", + ".eui文件已关联到此程序,是否重新设置?", + QMessageBox.Yes | QMessageBox.No, QMessageBox.No + ) + if reply != QMessageBox.Yes: + return + + if FileAssociation.set_association(): + QMessageBox.information(self, "成功", "文件关联设置成功!\n可能需要重启资源管理器才能看到图标变化。") + + def remove_file_association(self): + if not FileAssociation.is_associated(): + QMessageBox.information(self, "未关联", ".eui文件尚未关联到此程序") + return + + reply = QMessageBox.question( + self, "确认取消", + "确定要取消.eui文件与本程序的关联吗?", + QMessageBox.Yes | QMessageBox.No, QMessageBox.No + ) + if reply == QMessageBox.Yes: + if FileAssociation.remove_association(): + QMessageBox.information(self, "成功", "已取消.eui文件关联") + + def paste_file(self): + if not self.copied_path or not os.path.exists(self.copied_path): + self.status_bar.showMessage("没有可粘贴的内容") + return + + target_dir = self.file_tree.current_dir + + try: + item_name = os.path.basename(self.copied_path) + target_path = os.path.join(target_dir, item_name) + + if os.path.exists(target_path): + reply = QMessageBox.question( + self, "文件已存在", + f"{item_name} 已存在,是否覆盖?", + QMessageBox.Yes | QMessageBox.No, QMessageBox.No + ) + if reply != QMessageBox.Yes: + self.status_bar.showMessage("粘贴已取消") + return + + if self.is_cut: + if os.path.isdir(self.copied_path): + shutil.move(self.copied_path, target_path) + else: + os.rename(self.copied_path, target_path) + self.status_bar.showMessage(f"已移动: {item_name}") + self.copied_path = None + self.is_cut = False + else: + if os.path.isdir(self.copied_path): + shutil.copytree(self.copied_path, target_path) + else: + shutil.copy2(self.copied_path, target_path) + self.status_bar.showMessage(f"已复制: {item_name}") + + self.file_tree.refresh_tree() + + except Exception as e: + QMessageBox.critical(self, "错误", f"粘贴失败: {str(e)}") + + def set_timeout(self): + sender = self.sender() + if sender: + self.run_timeout = sender.data() + for act in self.timeout_actions.values(): + act.setChecked(act.data() == self.run_timeout) + self.status_bar.showMessage(f"已设置运行超时时间为 {self.run_timeout} 秒") + + def stop_running(self): + if hasattr(self, 'interpreter_thread') and self.interpreter_thread.isRunning(): + self.interpreter_thread.stop() + self.run_finished() + else: + self.status_bar.showMessage("没有正在运行的进程") + + def add_help_dock(self): + dock = QDockWidget("语法帮助", self) + dock.setAllowedAreas(Qt.RightDockWidgetArea) + + help_content = QTextEdit() + help_content.setReadOnly(True) + help_content.setHtml(""" +
核心语法:标签名=属性1=值1,属性2=值2,...; (每条语句必须以分号结尾)
+属性规则:字符串值需用双引号包裹,数值/布尔值直接写,列表用[]包裹(元素用逗号分隔)
+ +# 单行注释:# 开头(绿色斜体)
+// 单行注释:// 开头(绿色斜体)
+/* 多行注释:/* 开头,*/ 结尾
+
支持跨越多行文本
+
全程绿色高亮 */
// 示例:带注释的代码
+
window=title="测试窗口",width=500,height=300;# 行尾也可加注释
| 组件类型 | +标签名 | +必选属性 | +可选属性 | +实战示例 | +
|---|---|---|---|---|
| 主窗口 | +window | +title="窗口标题", width=数值, height=数值 | +icon="本地图标路径", tooltip="窗口提示" | +window=title="用户管理系统",width=800,height=600,icon="logo.ico"; |
+
| 文字标签 | +label | +text="显示文本", id=唯一ID | +tooltip="鼠标悬浮提示" | +label=text="用户名:",id=user_label,tooltip="请输入账号"; |
+
| 输入框 | +entry | +hint="占位提示", id=唯一ID | +readonly=true/false, type=text/number | +entry=hint="请输入手机号",id=phone_input,type=number,readonly=false; |
+
| 下拉选择框 | +combo | +label="选择标题", id=唯一ID, options=["选项1","选项2"] | +- | +combo=label="所属部门",id=dept_combo,options=["技术部","财务部","市场部"]; |
+
| 多选框组 | +checkbox | +label="组标题", id=唯一ID, options=["选项1","选项2"] | +- | +checkbox=label="兴趣爱好",id=hobby_check,options=["读书","编程","运动"]; |
+
| 单选按钮组 | +radiogroup | +label="组标题", id=唯一ID, options=["选项1","选项2"] | +- | +radiogroup=label="性别",id=gender_radio,options=["男","女","其他"]; |
+
| 网络音频 | +audio | +url="音频地址", id=唯一ID | +- | +audio=url="https://xxx.mp3",id=net_audio; |
+
| 本地音频 | +audio | +os="本地路径", id=唯一ID | +- | +audio=os="music/background.mp3",id=local_audio; |
+
| 图片显示 | +image | +path="图片路径", id=唯一ID, width=数值, height=数值 | +tooltip="图片说明" | +image=path="img/banner.png",id=banner_img,width=800,height=200,tooltip="顶部横幅"; |
+
| 按钮 | +button | +text="按钮文本", id=唯一ID, click="触发动作" | +tooltip="按钮功能说明" | +button=text="播放音乐",id=play_btn,click="play_audio=net_audio",tooltip="点击播放网络音乐"; |
+
| 滑块控件 | +slider | +label="滑块标题", id=唯一ID, min=最小值, max=最大值, value=初始值 | +- | +slider=label="音量调节",id=vol_slider,min=0,max=100,value=70; |
+
| 文本区域 | +textarea | +label="区域标题", id=唯一ID, rows=行数 | +readonly=true/false | +textarea=label="备注信息",id=note_area,rows=5,readonly=false; |
+
| 进度条 | +progress | +label="进度标题", id=唯一ID, min=最小值, max=最大值, value=初始值 | +- | +progress=label="下载进度",id=down_progress,min=0,max=100,value=30; |
+
| 日历控件 | +calendar | +label="选择标题", id=唯一ID | +tooltip="选择日期" | +calendar=label="生日选择",id=birth_cal,tooltip="点击选择出生日期"; |
+
| 分隔线 | +separator | +id=唯一ID | +text="分隔文本(居中显示)" | +separator=text="用户信息区",id=sep1; |
+
| 分组框 | +groupbox | +title="分组标题", id=唯一ID | +- | +groupbox=title="登录信息",id=login_group; |
+
| 定时器 | +timer | +id=唯一ID, interval=毫秒数, action="循环动作" | +- | +timer=id=progress_timer,interval=1000,action="update_progress=down_progress,value=+1"; |
+
显示=组件ID → 弹窗显示输入框/选择框的当前值start_timer=定时器ID → 开始定时器循环stop_timer=定时器ID → 停止定时器循环play_audio=音频ID → 播放指定音频(支持暂停后继续)pause_audio=音频ID → 暂停指定音频stop_audio=音频ID → 停止指定音频(需重新播放)set_progress=进度条ID,value=数值 → 直接设置进度值(如:set_progress=down_progress,value=50)update_progress=进度条ID,value=±数值 → 增减进度值(如:update_progress=down_progress,value=+1)• 注释内容(#、//、/* */)→ 绿色斜体
+• 标签名(window、label、audio等)→ 蓝色加粗
+• 属性名(title、id、bind_volume等)→ 青色
+• 字符串值(""包裹的内容)→ 橙色
+• 关键字(true、false、text、number等)→ 紫色
+• 标点符号(=、,、;、[]等)→ 深灰色
+; 结尾,否则解析失败" 包裹,数值/布尔值无需包裹options=["选项1","选项2"]