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(""" +

Easy Windows UI 语法参考 (v1.8 完整版)

+

核心语法:标签名=属性1=值1,属性2=值2,...; (每条语句必须以分号结尾)

+

属性规则:字符串值需用双引号包裹,数值/布尔值直接写,列表用[]包裹(元素用逗号分隔)

+ +

📌 注释格式(支持语法高亮)

+
+

# 单行注释:# 开头(绿色斜体)

+

// 单行注释:// 开头(绿色斜体)

+

/* 多行注释:/* 开头,*/ 结尾 +
支持跨越多行文本 +
全程绿色高亮 */

+

// 示例:带注释的代码 +
window=title="测试窗口",width=500,height=300;# 行尾也可加注释

+
+ +

🎯 完整组件列表(含新增功能)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
组件类型标签名必选属性可选属性实战示例
主窗口windowtitle="窗口标题", width=数值, height=数值icon="本地图标路径", tooltip="窗口提示"window=title="用户管理系统",width=800,height=600,icon="logo.ico";
文字标签labeltext="显示文本", id=唯一IDtooltip="鼠标悬浮提示"label=text="用户名:",id=user_label,tooltip="请输入账号";
输入框entryhint="占位提示", id=唯一IDreadonly=true/false, type=text/numberentry=hint="请输入手机号",id=phone_input,type=number,readonly=false;
下拉选择框combolabel="选择标题", id=唯一ID, options=["选项1","选项2"]-combo=label="所属部门",id=dept_combo,options=["技术部","财务部","市场部"];
多选框组checkboxlabel="组标题", id=唯一ID, options=["选项1","选项2"]-checkbox=label="兴趣爱好",id=hobby_check,options=["读书","编程","运动"];
单选按钮组radiogrouplabel="组标题", id=唯一ID, options=["选项1","选项2"]-radiogroup=label="性别",id=gender_radio,options=["男","女","其他"];
网络音频audiourl="音频地址", id=唯一ID-audio=url="https://xxx.mp3",id=net_audio;
本地音频audioos="本地路径", id=唯一ID-audio=os="music/background.mp3",id=local_audio;
图片显示imagepath="图片路径", id=唯一ID, width=数值, height=数值tooltip="图片说明"image=path="img/banner.png",id=banner_img,width=800,height=200,tooltip="顶部横幅";
按钮buttontext="按钮文本", id=唯一ID, click="触发动作"tooltip="按钮功能说明"button=text="播放音乐",id=play_btn,click="play_audio=net_audio",tooltip="点击播放网络音乐";
滑块控件sliderlabel="滑块标题", id=唯一ID, min=最小值, max=最大值, value=初始值-slider=label="音量调节",id=vol_slider,min=0,max=100,value=70;
文本区域textarealabel="区域标题", id=唯一ID, rows=行数readonly=true/falsetextarea=label="备注信息",id=note_area,rows=5,readonly=false;
进度条progresslabel="进度标题", id=唯一ID, min=最小值, max=最大值, value=初始值-progress=label="下载进度",id=down_progress,min=0,max=100,value=30;
日历控件calendarlabel="选择标题", id=唯一IDtooltip="选择日期"calendar=label="生日选择",id=birth_cal,tooltip="点击选择出生日期";
分隔线separatorid=唯一IDtext="分隔文本(居中显示)"separator=text="用户信息区",id=sep1;
分组框groupboxtitle="分组标题", id=唯一ID-groupbox=title="登录信息",id=login_group;
定时器timerid=唯一ID, interval=毫秒数, action="循环动作"-timer=id=progress_timer,interval=1000,action="update_progress=down_progress,value=+1";
+ +

🔧 核心动作说明(按钮/定时器可用)

+
+
1. 组件控制动作
+ + +
2. 音频控制动作
+ + +
3. 进度条控制动作(新增)
+ +
+ +

💡 语法高亮说明(编辑区视觉提示)

+
+

注释内容(#、//、/* */)→ 绿色斜体

+

标签名(window、label、audio等)→ 蓝色加粗

+

属性名(title、id、bind_volume等)→ 青色

+

字符串值(""包裹的内容)→ 橙色

+

关键字(true、false、text、number等)→ 紫色

+

标点符号(=、,、;、[]等)→ 深灰色

+
+ +

⚠️ 常见错误提醒

+ + """) + + dock.setWidget(help_content) + self.addDockWidget(Qt.RightDockWidgetArea, dock) + + def scan_interpreters(self, quick_scan=False): + self.status_bar.showMessage("正在快速扫描解释器...") + + interpreter_paths = [] + base_path = get_base_path() + target_names = {"easy_ui_interpreter.exe", "easy_ui_interpreter.py"} + + search_paths = [ + base_path, + os.path.join(base_path, "interpreters"), + os.path.expanduser("~\\Desktop"), + os.path.expanduser("~\\Documents"), + os.path.expanduser("~\\Downloads"), + "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") + ] + + for path in os.environ.get("PATH", "").split(os.pathsep): + if path and path not in search_paths: + search_paths.append(path) + + for path in search_paths: + if not os.path.exists(path) or not os.path.isdir(path): + continue + for root, dirs, files in os.walk(path): + for file in files: + if file.lower() in target_names: + full_path = os.path.join(root, file) + if full_path not in interpreter_paths: + interpreter_paths.append(full_path) + + interpreter_paths = sorted(list(set(interpreter_paths))) + + self.update_interpreter_combo(interpreter_paths) + + if interpreter_paths: + self.status_bar.showMessage(f"快速扫描找到 {len(interpreter_paths)} 个解释器") + return interpreter_paths + else: + self.status_bar.showMessage("快速扫描未找到解释器,建议进行全电脑扫描") + return [] + + def full_scan_interpreters_in_background(self): + if self.search_in_progress and self.search_thread and self.search_thread.isRunning(): + self.search_thread.stop_search() + self.search_thread.wait() + self.search_in_progress = False + self.status_bar.showMessage("全电脑搜索已取消") + return + + self.search_thread = InterpreterSearchThread() + self.search_in_progress = True + + self.search_thread.progress_updated.connect(self.update_search_progress) + self.search_thread.search_complete.connect(self.on_search_complete) + + self.search_thread.start() + self.status_bar.showMessage("全电脑搜索已在后台启动,不影响正常操作...") + + def update_search_progress(self, progress): + if self.search_in_progress: + if isinstance(progress, str): + self.status_bar.showMessage(progress) + else: + self.status_bar.showMessage(f"后台搜索中... 整体进度: {progress}%") + + def on_search_complete(self, interpreter_paths): + self.search_in_progress = False + + self.update_interpreter_combo(interpreter_paths) + + if interpreter_paths: + count = len(interpreter_paths) + self.status_bar.showMessage(f"后台搜索完成,找到 {count} 个解释器") + else: + self.status_bar.showMessage("后台搜索完成,未找到任何解释器,请手动选择") + + def choose_interpreter(self): + interpreters = [] + if self.search_thread and hasattr(self.search_thread, 'found_paths'): + interpreters = list(self.search_thread.found_paths) + + if not interpreters: + interpreters = self.scan_interpreters(quick_scan=True) + + dialog = InterpreterSelector(interpreters, self) + + if dialog.exec_(): + selected_path = dialog.get_selected_path() + if selected_path and os.path.exists(selected_path): + file_name = os.path.basename(selected_path) + if file_name.lower() in ["easy_ui_interpreter.exe", "easy_ui_interpreter.py"]: + self.interpreter_path = selected_path + for i in range(self.interpreter_combo.count()): + if self.interpreter_combo.itemData(i) == selected_path: + self.interpreter_combo.setCurrentIndex(i) + return + self.interpreter_combo.addItem(selected_path, selected_path) + self.interpreter_combo.setCurrentIndex(self.interpreter_combo.count() - 1) + self.status_bar.showMessage(f"已选择解释器: {os.path.basename(selected_path)}") + else: + QMessageBox.warning(self, "警告", "请选择easy_ui_interpreter.exe或easy_ui_interpreter.py文件") + else: + QMessageBox.warning(self, "警告", "无效的解释器路径") + + def force_full_scan(self): + reply = QMessageBox.question( + self, "强制全扫", "此操作将扫描所有驱动器的所有目录,可能耗时5-10分钟,是否继续?", + QMessageBox.Yes | QMessageBox.No, QMessageBox.No + ) + if reply != QMessageBox.Yes: + return + + if self.search_in_progress and self.search_thread: + self.search_thread.stop_search() + self.search_thread.wait() + + self.search_thread = InterpreterSearchThread() + self.search_in_progress = True + + def skip_none(dir_path): + system_blacklist = ["windows\\system32", "windows\\syswow64", "$recycle.bin"] + return any(black in dir_path.lower() for black in system_blacklist) + self.search_thread.should_skip_directory = skip_none + + self.search_thread.progress_updated.connect(self.update_search_progress) + self.search_thread.search_complete.connect(self.on_search_complete) + self.search_thread.start() + self.status_bar.showMessage("强制全扫已启动,请勿关闭程序...") + + def change_directory(self): + new_dir = QFileDialog.getExistingDirectory( + self, "选择目录", self.file_tree.current_dir + ) + if new_dir: + self.file_tree.change_directory(new_dir) + + def add_new_tab(self): + editor = CompleterTextEdit() + editor.setFont(QFont("Consolas", 12)) + editor.setAcceptRichText(False) + + self.highlighter = EasyUISyntaxHighlighter(editor.document()) + self.setup_completer(editor) + + index = self.tab_widget.addTab(editor, "未命名") + self.tab_widget.setCurrentIndex(index) + + if self.tab_widget.count() == 1: + self.load_example_code() + + def setup_completer(self, editor): + completion_texts = [word[0] for word in self.completion_words] + display_texts = [f"{word[0]} ({word[1]})" for word in self.completion_words] + + completer_model = QStringListModel() + completer_model.setStringList(display_texts) + + completer = QCompleter() + completer.setModel(completer_model) + completer.setCompletionMode(QCompleter.PopupCompletion) + completer.setCaseSensitivity(Qt.CaseInsensitive) + completer.setCompletionPrefix("") + completer.setMaxVisibleItems(10) + + editor.setCompleter(completer) + + def close_tab(self, index): + if self.tab_widget.count() > 1: + self.tab_widget.removeTab(index) + else: + self.tab_widget.widget(index).clear() + self.tab_widget.setTabText(index, "未命名") + self.current_file = None + self.setWindowTitle("Easy Windows UI Editor - [未命名]") + + def get_current_editor(self): + return self.tab_widget.currentWidget() + + def run_code(self): + if not self.interpreter_path or not os.path.exists(self.interpreter_path): + QMessageBox.warning(self, "解释器未找到", "请先选择有效的解释器") + self.choose_interpreter() + return + + file_name = os.path.basename(self.interpreter_path) + if file_name.lower() not in ["easy_ui_interpreter.exe", "easy_ui_interpreter.py"]: + QMessageBox.warning(self, "无效解释器", "请选择easy_ui_interpreter.exe或easy_ui_interpreter.py作为解释器") + self.choose_interpreter() + return + + editor = self.get_current_editor() + code = editor.toPlainText() + + if not code.strip(): + QMessageBox.warning(self, "警告", "代码不能为空!") + return + + if hasattr(self, 'interpreter_thread') and self.interpreter_thread.isRunning(): + self.show_error("已有进程在运行,请先等待其结束") + return + + self.output_panel.clear() + self.status_bar.showMessage(f"正在运行代码...(超时时间: {self.run_timeout}秒,按Ctrl+F5可停止)") + self.show_output("=== 代码运行开始 ===") + + self.interpreter_thread = InterpreterThread(code, self.temp_file, self.interpreter_path, self.run_timeout) + self.interpreter_thread.error_occurred.connect(self.show_error) + self.interpreter_thread.output_received.connect(self.show_output) + self.interpreter_thread.finished.connect(self.run_finished) + self.interpreter_thread.timeout_occurred.connect(lambda: self.status_bar.showMessage("代码运行超时已终止")) + self.interpreter_thread.start() + + def show_error(self, message): + timestamp = QDateTime.currentDateTime().toString("HH:mm:ss") + error_msg = f"[{timestamp}] {message}" + self.status_bar.showMessage(message.split(']')[-1].strip()[:50]) + self.output_panel.append(f'{error_msg}') + self.output_panel.verticalScrollBar().setValue(self.output_panel.verticalScrollBar().maximum()) + + def show_output(self, message): + timestamp = QDateTime.currentDateTime().toString("HH:mm:ss") + output_msg = f"[{timestamp}] {message}" + self.output_panel.append(f'{output_msg}') + self.output_panel.verticalScrollBar().setValue(self.output_panel.verticalScrollBar().maximum()) + + def run_finished(self): + self.show_output("=== 代码运行结束 ===") + self.status_bar.showMessage("代码运行完成(输出已更新)") + + def clear_current_tab(self): + editor = self.get_current_editor() + editor.clear() + self.status_bar.showMessage("已清空当前编辑区") + + def open_file(self): + file_path, _ = QFileDialog.getOpenFileName( + self, "打开文件", self.file_tree.current_dir, "Easy UI Files (*.eui);;Python Files (*.py);;C++ Files (*.cpp *.h);;Java Files (*.java);;All Files (*)" + ) + + if file_path: + self.open_file_from_path(file_path) + + def open_file_from_path(self, file_path): + try: + with open(file_path, 'r', encoding='utf-8') as file: + code = file.read() + + for i in range(self.tab_widget.count()): + if self.tab_widget.tabText(i) == os.path.basename(file_path): + self.tab_widget.setCurrentIndex(i) + self.tab_widget.currentWidget().setPlainText(code) + self.current_file = file_path + return + + editor = CompleterTextEdit() + editor.setFont(QFont("Consolas", 12)) + editor.setAcceptRichText(False) + editor.setPlainText(code) + + EasyUISyntaxHighlighter(editor.document()) + self.setup_completer(editor) + + index = self.tab_widget.addTab(editor, os.path.basename(file_path)) + self.tab_widget.setCurrentIndex(index) + + self.current_file = file_path + self.setWindowTitle(f"Easy Windows UI Editor - {os.path.basename(file_path)}") + self.status_bar.showMessage(f"已打开文件: {file_path}") + except Exception as e: + QMessageBox.critical(self, "错误", f"无法打开文件: {str(e)}") + self.status_bar.showMessage("打开文件失败") + + def save_file(self): + if self.current_file: + try: + editor = self.get_current_editor() + with open(self.current_file, 'w', encoding='utf-8') as file: + file.write(editor.toPlainText()) + self.status_bar.showMessage(f"已保存文件: {self.current_file}") + self.file_tree.refresh_tree() + return True + except Exception as e: + QMessageBox.critical(self, "错误", f"无法保存文件: {str(e)}") + self.status_bar.showMessage("保存文件失败") + return False + else: + return self.save_file_as() + + def save_file_as(self): + file_path, _ = QFileDialog.getSaveFileName( + self, "保存文件", self.file_tree.current_dir, "Easy UI Files (*.eui);;Python Files (*.py);;Text Files (*.txt);;All Files (*)" + ) + + if file_path: + self.current_file = file_path + current_index = self.tab_widget.currentIndex() + self.tab_widget.setTabText(current_index, os.path.basename(file_path)) + self.setWindowTitle(f"Easy Windows UI Editor - {os.path.basename(file_path)}") + result = self.save_file() + if result: + self.file_tree.refresh_tree() + return result + return False + + def load_example_code(self): + example = """/* +这是一个多媒体演示程序示例 +包含多种UI组件和注释用法 +*/ +window=title="多媒体信息窗口",width=600,height=600; // 主窗口设置 + +label=text="=== 多媒体演示程序 ===",id=title_label; # 标题标签 +separator=text="用户信息",id=sep1; // 分隔线 + +// 用户信息输入区 +label=text="请输入您的昵称:",id=nickname_label; +entry=hint="昵称",id=nickname_input; + +# 音乐偏好选择 +combo=label="喜欢的音乐类型",id=music_type,options=["流行","摇滚","古典","民谣"]; +checkbox=label="音乐功能",id=music_func,options=["播放网络音乐","播放本地音乐"]; +radiogroup=label="音质选择",id=quality_radio,options=["标准","高清","无损"]; + +/* 音量控制 + 范围0-100,默认70 */ +slider=label="音量调节",id=vol_slider,min=0,max=100,value=70; + +separator=text="音乐控制",id=sep2; // 功能分隔 + +// 音频组件(网络和本地) +audio=url="https://lrgdmc.cn/static/mp3/jbd.mp3",id=net_music; # 网络音频 + +// 控制按钮 +button=text="显示信息",id=show_info,click="显示=nickname_input"; +button=text="播放网络音乐",id=play_net,click="play_audio=net_music"; +button=text="暂停音乐",id=pause_music,click="pause_audio=net_music"; +button=text="停止音乐",id=stop_music,click="stop_audio=local_music"; +""" + + editor = self.get_current_editor() + editor.setPlainText(example) + self.status_bar.showMessage("已加载带多媒体功能的示例代码") + + def show_about(self): + QMessageBox.about(self, "关于 Easy Windows UI", + "Easy Windows UI 1.8\n\n新增功能:优化注释高亮和文件关联记忆\n一个简单易用的UI创建工具,让您用极少的代码创建Windows界面。") + + def closeEvent(self, event): + if hasattr(self, 'interpreter_thread') and self.interpreter_thread.isRunning(): + self.interpreter_thread.stop() + + if self.search_thread and self.search_thread.isRunning(): + self.search_thread.stop_search() + self.search_thread.wait() + + if os.path.exists(self.temp_file): + try: + os.remove(self.temp_file) + except Exception as e: + print(f"清理临时文件失败: {e}") + + event.accept() + + +if __name__ == "__main__": + app = QApplication(sys.argv) + font = app.font() + font.setFamily("SimHei") + app.setFont(font) + + editor = EasyUIEditor() + editor.show() + sys.exit(app.exec_()) \ No newline at end of file diff --git a/easy_ui_interpreter.py b/easy_ui_interpreter.py new file mode 100644 index 0000000..83eeb10 --- /dev/null +++ b/easy_ui_interpreter.py @@ -0,0 +1,686 @@ +import sys +import os +import re +from PyQt5.QtWidgets import (QApplication, QMainWindow, QLabel, QLineEdit, + QComboBox, QCheckBox, QPushButton, QWidget, + QVBoxLayout, QHBoxLayout, QMessageBox, QFrame, + QTextEdit, QSlider, QProgressBar, QCalendarWidget, + QGroupBox, QRadioButton) +from PyQt5.QtCore import Qt, QUrl, QTimer, pyqtSlot +from PyQt5.QtGui import QIcon, QIntValidator, QPixmap, QImage +from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent +from urllib.request import urlopen + +class EasyUIInterpreter: + def __init__(self): + self.app = None + self.window = None + self.widgets = {} # 存储所有组件 + self.variables = {} # 存储可交互组件 + self.main_layout = None + self.media_players = {} # 仅保留音频播放器 + self.timers = {} # 存储定时器 + self.groups = {} + + def parse_and_run(self, code): + if not QApplication.instance(): + self.app = QApplication(sys.argv) + else: + self.app = QApplication.instance() + + # 重置UI状态 + self.widgets = {} + self.variables = {} + self.media_players = {} + self.timers = {} + self.groups = {} + self.window = None + self.main_layout = None + + lines = [line.strip() for line in code.split('\n') if line.strip()] + for line in lines: + self.parse_line(line) + + if not self.window: + self.create_window("EUI默认窗口", 400, 300) + else: + self.main_layout.addStretch() + + self.window.show() + sys.exit(self.app.exec_()) + + # ---------------------- 解析逻辑 ---------------------- + def parse_line(self, line): + line = line.strip().rstrip(';') + if not line: + return + + # 窗口配置 + window_pattern = r'window\s*=\s*title="([^"]+)"\s*,\s*width=(\d+)\s*,\s*height=(\d+)(?:\s*,\s*icon="([^"]+)")?' + window_match = re.match(window_pattern, line) + if window_match: + title = window_match.group(1) + width = int(window_match.group(2)) + height = int(window_match.group(3)) + icon_path = window_match.group(4) if window_match.group(4) else None + self.create_window(title, width, height, icon_path) + return + + # 文字标签 + label_match = re.match(r'label\s*=\s*text="([^"]+)"\s*,\s*id=(\w+)', line) + if label_match: + self.create_label(label_match.group(1), label_match.group(2)) + return + + # 输入框 + entry_pattern = r'entry\s*=\s*hint="([^"]+)"\s*,\s*id=(\w+)(?:\s*,\s*readonly=(true|false))?(?:\s*,\s*type=(number|text))?' + entry_match = re.match(entry_pattern, line) + if entry_match: + hint = entry_match.group(1) + widget_id = entry_match.group(2) + readonly = entry_match.group(3).lower() == 'true' if entry_match.group(3) else False + input_type = entry_match.group(4) if entry_match.group(4) else 'text' + self.create_entry(hint, widget_id, readonly, input_type) + return + + # 下拉选择框 + combo_match = re.match(r'combo\s*=\s*label="([^"]+)"\s*,\s*id=(\w+)\s*,\s*options=\[(.*?)\]', line) + if combo_match: + options = [opt.strip().strip('"') for opt in combo_match.group(3).split(',') if opt.strip()] + self.create_combobox(combo_match.group(1), combo_match.group(2), options) + return + + # 多选框组 + check_match = re.match(r'checkbox\s*=\s*label="([^"]+)"\s*,\s*id=(\w+)\s*,\s*options=\[(.*?)\]', line) + if check_match: + options = [opt.strip().strip('"') for opt in check_match.group(3).split(',') if opt.strip()] + self.create_checkboxes(check_match.group(1), check_match.group(2), options) + return + + # 按钮 + button_match = re.match(r'button\s*=\s*text="([^"]+)"\s*,\s*id=(\w+)\s*,\s*click="([^"]+)"', line) + if button_match: + self.create_button(button_match.group(1), button_match.group(2), button_match.group(3)) + return + + # 音频播放器 + audio_pattern = r'audio\s*=\s*(url|os)="([^"]+)"\s*,\s*id=(\w+)' + audio_match = re.match(audio_pattern, line) + if audio_match: + self.create_audio_player(audio_match.group(1), audio_match.group(2), audio_match.group(3)) + return + + # 图片组件 + image_pattern = r'image\s*=\s*(path|url|os)="([^"]+)"\s*,\s*id=(\w+)(?:\s*,\s*width=(\d+))?(?:\s*,\s*height=(\d+))?(?:\s*,\s*tooltip="([^"]+)")?' + image_match = re.match(image_pattern, line) + if image_match: + img_type = image_match.group(1) + img_path = image_match.group(2) + img_id = image_match.group(3) + width = int(image_match.group(4)) if image_match.group(4) else None + height = int(image_match.group(5)) if image_match.group(5) else None + tooltip = image_match.group(6) if image_match.group(6) else "" + self.create_image(img_type, img_path, img_id, width, height, tooltip) + return + + # 滑块控件 + slider_pattern = r'slider\s*=\s*label="([^"]+)"\s*,\s*id=(\w+)\s*,\s*min=(\d+)\s*,\s*max=(\d+)\s*,\s*value=(\d+)' + slider_match = re.match(slider_pattern, line) + if slider_match: + self.create_slider( + slider_match.group(1), slider_match.group(2), + int(slider_match.group(3)), int(slider_match.group(4)), int(slider_match.group(5)) + ) + return + + # 文本区域 + textarea_pattern = r'textarea\s*=\s*label="([^"]+)"\s*,\s*id=(\w+)\s*,\s*rows=(\d+)(?:\s*,\s*readonly=(true|false))?' + textarea_match = re.match(textarea_pattern, line) + if textarea_match: + readonly = textarea_match.group(4).lower() == 'true' if textarea_match.group(4) else False + self.create_textarea(textarea_match.group(1), textarea_match.group(2), int(textarea_match.group(3)), readonly) + return + + # 分隔线 + separator_match = re.match(r'separator\s*=\s*text="([^"]*)"\s*,\s*id=(\w+)', line) + if separator_match: + self.create_separator(separator_match.group(1), separator_match.group(2)) + return + + # 进度条 + progress_pattern = r'progress\s*=\s*label="([^"]+)"\s*,\s*id=(\w+)\s*,\s*min=(\d+)\s*,\s*max=(\d+)\s*,\s*value=(\d+)' + progress_match = re.match(progress_pattern, line) + if progress_match: + self.create_progressbar( + progress_match.group(1), progress_match.group(2), + int(progress_match.group(3)), int(progress_match.group(4)), int(progress_match.group(5)) + ) + return + + # 日历控件 + calendar_match = re.match(r'calendar\s*=\s*label="([^"]+)"\s*,\s*id=(\w+)', line) + if calendar_match: + self.create_calendar(calendar_match.group(1), calendar_match.group(2)) + return + + # 单选按钮组 + radio_match = re.match(r'radiogroup\s*=\s*label="([^"]+)"\s*,\s*id=(\w+)\s*,\s*options=\[(.*?)\]', line) + if radio_match: + options = [opt.strip().strip('"') for opt in radio_match.group(3).split(',') if opt.strip()] + self.create_radiogroup(radio_match.group(1), radio_match.group(2), options) + return + + # 分组框 + groupbox_match = re.match(r'groupbox\s*=\s*title="([^"]+)"\s*,\s*id=(\w+)', line) + if groupbox_match: + self.create_groupbox(groupbox_match.group(1), groupbox_match.group(2)) + return + + # 定时器 + timer_pattern = r'timer\s*=\s*id=(\w+)\s*,\s*interval=(\d+)\s*,\s*action="([^"]+)"' + timer_match = re.match(timer_pattern, line) + if timer_match: + self.create_timer(timer_match.group(1), int(timer_match.group(2)), timer_match.group(3)) + return + + # ---------------------- 组件创建方法 ---------------------- + def create_window(self, title, width, height, icon_path=None): + self.window = QMainWindow() + self.window.setWindowTitle(title) + self.window.resize(width, height) + + if icon_path and os.path.exists(icon_path): + try: + self.window.setWindowIcon(QIcon(icon_path)) + except Exception as e: + QMessageBox.warning(self.window, "警告", f"图标设置失败:{str(e)}") + + central_widget = QWidget() + self.window.setCentralWidget(central_widget) + self.main_layout = QVBoxLayout(central_widget) + self.main_layout.setContentsMargins(20, 20, 20, 20) + self.main_layout.setSpacing(15) + + def create_label(self, text, widget_id): + if not self.window: + self.create_window("默认窗口", 400, 300) + label = QLabel(text) + label.setMinimumHeight(30) + self._get_current_layout().addWidget(label) + self.widgets[widget_id] = label + + def create_entry(self, hint, widget_id, readonly=False, input_type='text'): + if not self.window: + self.create_window("默认窗口", 400, 300) + + container = QWidget() + container.setMinimumHeight(30) + layout = QHBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(10) + + label = QLabel(hint) + entry = QLineEdit() + entry.setReadOnly(readonly) + if input_type == 'number': + entry.setValidator(QIntValidator()) + + layout.addWidget(label) + layout.addWidget(entry) + self._get_current_layout().addWidget(container) + self.widgets[widget_id] = entry + self.variables[widget_id] = entry + + def create_combobox(self, label_text, widget_id, options): + if not self.window: + self.create_window("默认窗口", 400, 300) + + container = QWidget() + container.setMinimumHeight(30) + layout = QHBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(10) + + label = QLabel(label_text) + combo = QComboBox() + combo.addItems(options) + + layout.addWidget(label) + layout.addWidget(combo) + self._get_current_layout().addWidget(container) + self.widgets[widget_id] = combo + self.variables[widget_id] = combo + + def create_checkboxes(self, label_text, widget_id, options): + if not self.window: + self.create_window("默认窗口", 400, 300) + + container = QWidget() + container.setMinimumHeight(60) + layout = QVBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + + title_label = QLabel(label_text) + layout.addWidget(title_label) + + check_layout = QHBoxLayout() + check_layout.setSpacing(15) + checkboxes = [] + for opt in options: + cb = QCheckBox(opt) + check_layout.addWidget(cb) + checkboxes.append(cb) + + layout.addLayout(check_layout) + self._get_current_layout().addWidget(container) + self.widgets[widget_id] = checkboxes + self.variables[widget_id] = checkboxes + + def create_button(self, text, widget_id, action): + if not self.window: + self.create_window("默认窗口", 400, 300) + + button = QPushButton(text) + button.setMinimumHeight(30) + button.setMaximumWidth(150) + button.clicked.connect(lambda checked, a=action: self.handle_button_click(a)) + self._get_current_layout().addWidget(button, alignment=Qt.AlignLeft) + self.widgets[widget_id] = button + + def create_audio_player(self, audio_type, audio_path, audio_id): + player = QMediaPlayer() + self.media_players[audio_id] = { + "player": player, + "type": "audio" + } + + try: + if audio_type == "url": + media = QMediaContent(QUrl(audio_path)) + else: + abs_path = os.path.abspath(audio_path).replace(" ", "%20") # 处理空格 + if not os.path.exists(abs_path.replace("%20", " ")): + QMessageBox.warning(self.window, "警告", f"音频文件不存在:{abs_path.replace('%20', ' ')}") + return + media = QMediaContent(QUrl.fromLocalFile(abs_path)) + + player.setMedia(media) + except Exception as e: + QMessageBox.warning(self.window, "警告", f"音频加载失败:{str(e)}") + + def create_image(self, img_type, img_path, img_id, width=None, height=None, tooltip=""): + if not self.window: + self.create_window("默认窗口", 400, 300) + + container = QWidget() + container.setMinimumHeight(height if height else 100) + container.setMinimumWidth(width if width else 100) + layout = QVBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + + img_label = QLabel() + img_label.setToolTip(tooltip) + img_label.setAlignment(Qt.AlignCenter) + + pixmap = None + try: + if img_type == "path": + if img_path.startswith(('http://', 'https://')): + with urlopen(img_path) as response: + img_data = response.read() + image = QImage.fromData(img_data) + pixmap = QPixmap.fromImage(image) + else: + abs_path = os.path.abspath(img_path) + if os.path.exists(abs_path): + pixmap = QPixmap(abs_path) + else: + img_label.setText("图片文件不存在") + QMessageBox.warning(self.window, "警告", f"本地图片路径不存在:{abs_path}") + + elif img_type == "url": + with urlopen(img_path) as response: + img_data = response.read() + image = QImage.fromData(img_data) + pixmap = QPixmap.fromImage(image) + + elif img_type == "os": + abs_path = os.path.abspath(img_path) + if os.path.exists(abs_path): + pixmap = QPixmap(abs_path) + else: + img_label.setText("图片文件不存在") + QMessageBox.warning(self.window, "警告", f"本地图片路径不存在:{abs_path}") + + except Exception as e: + img_label.setText("图片加载失败") + QMessageBox.warning(self.window, "警告", f"图片加载失败:{str(e)}") + + if pixmap and not pixmap.isNull(): + if width and height: + pixmap = pixmap.scaled(width, height, Qt.KeepAspectRatio, Qt.SmoothTransformation) + elif width: + pixmap = pixmap.scaledToWidth(width, Qt.SmoothTransformation) + elif height: + pixmap = pixmap.scaledToHeight(height, Qt.SmoothTransformation) + + img_label.setPixmap(pixmap) + + layout.addWidget(img_label) + self._get_current_layout().addWidget(container) + self.widgets[img_id] = img_label + self.variables[img_id] = img_label + + def create_slider(self, label_text, widget_id, min_val, max_val, value): + if not self.window: + self.create_window("默认窗口", 400, 300) + + container = QWidget() + container.setMinimumHeight(60) + layout = QVBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + + value_label = QLabel(f"{label_text}:{value}") + slider = QSlider(Qt.Horizontal) + slider.setRange(min_val, max_val) + slider.setValue(value) + slider.setTickInterval(1) + slider.setTickPosition(QSlider.TicksBelow) + slider.valueChanged.connect(lambda v: value_label.setText(f"{label_text}:{v}")) + + layout.addWidget(value_label) + layout.addWidget(slider) + self._get_current_layout().addWidget(container) + self.widgets[widget_id] = slider + self.variables[widget_id] = slider + + def create_textarea(self, label_text, widget_id, rows, readonly=False): + if not self.window: + self.create_window("默认窗口", 400, 300) + + container = QWidget() + layout = QVBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + + label = QLabel(label_text) + textarea = QTextEdit() + textarea.setReadOnly(readonly) + textarea.setMinimumHeight(rows * 25) + + layout.addWidget(label) + layout.addWidget(textarea) + self._get_current_layout().addWidget(container) + self.widgets[widget_id] = textarea + self.variables[widget_id] = textarea + + def create_separator(self, text, widget_id): + if not self.window: + self.create_window("默认窗口", 400, 300) + + if text: + container = QWidget() + layout = QHBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(10) + + left_line = QFrame() + left_line.setFrameShape(QFrame.HLine) + left_line.setFrameShadow(QFrame.Sunken) + + right_line = QFrame() + right_line.setFrameShape(QFrame.HLine) + right_line.setFrameShadow(QFrame.Sunken) + + label = QLabel(text) + layout.addWidget(left_line, 1) + layout.addWidget(label, 0, Qt.AlignCenter) + layout.addWidget(right_line, 1) + + self._get_current_layout().addWidget(container) + self.widgets[widget_id] = container + else: + line = QFrame() + line.setFrameShape(QFrame.HLine) + line.setFrameShadow(QFrame.Sunken) + self._get_current_layout().addWidget(line) + self.widgets[widget_id] = line + + def create_progressbar(self, label_text, widget_id, min_val, max_val, value): + if not self.window: + self.create_window("默认窗口", 400, 300) + + container = QWidget() + container.setMinimumHeight(50) + layout = QVBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + + label = QLabel(label_text) + progress = QProgressBar() + progress.setRange(min_val, max_val) + progress.setValue(value) + progress.setTextVisible(True) + + layout.addWidget(label) + layout.addWidget(progress) + self._get_current_layout().addWidget(container) + self.widgets[widget_id] = progress + self.variables[widget_id] = progress + + def create_calendar(self, label_text, widget_id): + if not self.window: + self.create_window("默认窗口", 400, 300) + + container = QWidget() + layout = QVBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(10) + + label = QLabel(label_text) + calendar = QCalendarWidget() + calendar.setSelectionMode(QCalendarWidget.SingleSelection) + + layout.addWidget(label) + layout.addWidget(calendar) + self._get_current_layout().addWidget(container) + self.widgets[widget_id] = calendar + self.variables[widget_id] = calendar + + def create_radiogroup(self, label_text, widget_id, options): + if not self.window: + self.create_window("默认窗口", 400, 300) + + container = QWidget() + layout = QVBoxLayout(container) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(5) + + title_label = QLabel(label_text) + layout.addWidget(title_label) + + radio_buttons = [] + for i, opt in enumerate(options): + radio = QRadioButton(opt) + if i == 0: + radio.setChecked(True) + layout.addWidget(radio) + radio_buttons.append(radio) + + self._get_current_layout().addWidget(container) + self.widgets[widget_id] = radio_buttons + self.variables[widget_id] = radio_buttons + + def create_groupbox(self, title, group_id): + if not self.window: + self.create_window("默认窗口", 400, 300) + + groupbox = QGroupBox(title) + group_layout = QVBoxLayout(groupbox) + group_layout.setContentsMargins(15, 15, 15, 15) + group_layout.setSpacing(10) + + self._get_current_layout().addWidget(groupbox) + self.groups[group_id] = group_layout + self.widgets[group_id] = groupbox + + def create_timer(self, timer_id, interval, action): + if timer_id in self.timers: + self.timers[timer_id]['timer'].stop() + + timer = QTimer() + timer.setInterval(interval) + timer.timeout.connect(lambda: self.handle_timer_timeout(timer_id)) + self.timers[timer_id] = { + 'timer': timer, + 'action': action + } + + # ---------------------- 事件处理 ---------------------- + def _get_current_layout(self): + return list(self.groups.values())[-1] if self.groups else self.main_layout + + @pyqtSlot() + def handle_timer_timeout(self, timer_id): + if timer_id not in self.timers: + return + + timer_info = self.timers[timer_id] + action = timer_info['action'] + + if action.startswith("update_progress="): + try: + progress_part, step_part = action.split(",") + progress_id = progress_part.split("=")[1].strip() + step = int(step_part.split("=")[1].strip()) + + progress_bar = self.widgets.get(progress_id) + if not progress_bar or not isinstance(progress_bar, QProgressBar): + return + + current_value = progress_bar.value() + new_value = current_value + step + new_value = max(progress_bar.minimum(), min(progress_bar.maximum(), new_value)) + progress_bar.setValue(new_value) + + if new_value >= progress_bar.maximum(): + timer_info['timer'].stop() + + except Exception as e: + QMessageBox.warning(self.window, "定时器错误", f"更新进度条失败:{str(e)}") + + def handle_button_click(self, action): + # 音频控制 + if action.startswith("play_audio="): + self._control_audio(action.split("=")[1], "play") + return + if action.startswith("pause_audio="): + self._control_audio(action.split("=")[1], "pause") + return + if action.startswith("stop_audio="): + self._control_audio(action.split("=")[1], "stop") + return + + # 定时器控制 + if action.startswith("start_timer="): + timer_id = action.split("=")[1].strip() + self._control_timer(timer_id, "start") + return + if action.startswith("stop_timer="): + timer_id = action.split("=")[1].strip() + self._control_timer(timer_id, "stop") + return + + # 进度条控制 + if action.startswith("set_progress="): + parts = action.split(",") + if len(parts) >= 2 and parts[1].startswith("value="): + try: + p_id = parts[0].split("=")[1].strip() + val = int(parts[1].split("=")[1].strip()) + if p_id in self.widgets and isinstance(self.widgets[p_id], QProgressBar): + self.widgets[p_id].setValue(val) + except Exception as e: + QMessageBox.warning(self.window, "错误", f"设置进度条失败:{str(e)}") + return + + # 显示组件值 + if action.startswith("显示="): + self._show_widget_value(action.split("=")[1].strip()) + return + + def _control_audio(self, audio_id, action): + if audio_id not in self.media_players or self.media_players[audio_id]["type"] != "audio": + QMessageBox.warning(self.window, "警告", f"音频组件ID不存在:{audio_id}") + return + player = self.media_players[audio_id]["player"] + if action == "play": + player.play() + elif action == "pause": + player.pause() + elif action == "stop": + player.stop() + + def _control_timer(self, timer_id, action): + if timer_id not in self.timers: + QMessageBox.warning(self.window, "警告", f"定时器ID不存在:{timer_id}") + return + timer = self.timers[timer_id]['timer'] + if action == "start": + timer.start() + elif action == "stop": + timer.stop() + + def _show_widget_value(self, widget_id): + if widget_id not in self.variables: + QMessageBox.warning(self.window, "警告", f"组件ID不存在:{widget_id}") + return + + target = self.variables[widget_id] + msg = "" + + if isinstance(target, list) and all(isinstance(x, QCheckBox) for x in target): + selected = [cb.text() for cb in target if cb.isChecked()] + msg = f"多选框选中项:{', '.join(selected) if selected else '无'}" + elif isinstance(target, list) and all(isinstance(x, QRadioButton) for x in target): + selected = [rb.text() for rb in target if rb.isChecked()] + msg = f"单选框选中项:{', '.join(selected)}" + elif isinstance(target, QComboBox): + msg = f"下拉框选中:{target.currentText()}" + elif isinstance(target, QLineEdit): + msg = f"输入框内容:{target.text()}" + elif isinstance(target, QSlider): + msg = f"滑块值:{target.value()}" + elif isinstance(target, QTextEdit): + content = target.toPlainText() + msg = f"文本区域内容:{content[:100]}..." if len(content) > 100 else f"文本区域内容:{content}" + elif isinstance(target, QCalendarWidget): + msg = f"选中日期:{target.selectedDate().toString('yyyy-MM-dd')}" + elif isinstance(target, QProgressBar): + msg = f"进度条值:{target.value()}%" + elif isinstance(target, QLabel) and hasattr(target, 'pixmap') and target.pixmap(): + msg = f"图片信息:已加载图片({target.pixmap().width()}x{target.pixmap().height()})" + + QMessageBox.information(self.window, "组件值", msg) + +# ---------------------- 运行入口 ---------------------- +if __name__ == "__main__": + if len(sys.argv) > 1: + file_path = sys.argv[1] + try: + with open(file_path, 'r', encoding='utf-8') as f: + ewui_code = f.read() + interpreter = EasyUIInterpreter() + interpreter.parse_and_run(ewui_code) + except Exception as e: + print(f"[EUI解释器错误]:{str(e)}", file=sys.stderr) + sys.exit(1) + else: + print("=" * 50) + print("Easy UI 解释器(基础版)") + print("用法:python easy_ui.py ") + print("支持组件:窗口、标签、按钮、输入框、下拉框、复选框等") + print("=" * 50) + sys.exit(0) \ No newline at end of file