import sys import os import re import glob import tempfile import shutil import winreg from PyQt5.QtWidgets import (QApplication, QMainWindow, QTextEdit, QPushButton, QVBoxLayout, QHBoxLayout, QWidget, QMenuBar, QMenu, QAction, QFileDialog, QMessageBox, QStatusBar, QSplitter, QListWidget, QTabWidget, QLabel, QDockWidget, QToolBar, QDialog, QRadioButton, QGroupBox, QDialogButtonBox, QCompleter, QTreeWidget, QTreeWidgetItem, QInputDialog, QMenu as QContextMenu, QComboBox) from PyQt5.QtGui import (QFont, QSyntaxHighlighter, QTextCharFormat, QColor, QTextDocument, QTextCursor, QIcon, QPixmap) from PyQt5.QtCore import Qt, QThread, pyqtSignal, QProcess, QDateTime, QTimer, QStringListModel # 文件关联相关功能 class FileAssociation: @staticmethod def is_associated(): try: with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, ".eui", 0, winreg.KEY_READ) as key: prog_id, _ = winreg.QueryValueEx(key, "") if prog_id != "EasyUIEditor.eui": return False with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, "EasyUIEditor.eui\\shell\\open\\command", 0, winreg.KEY_READ) as key: command, _ = winreg.QueryValueEx(key, "") current_exe = sys.executable if current_exe.endswith("python.exe"): script_path = os.path.abspath(sys.argv[0]) return script_path in command else: return current_exe in command return True except WindowsError: return False @staticmethod def set_association(): try: if getattr(sys, 'frozen', False): current_path = sys.executable else: current_path = os.path.abspath(sys.argv[0]) icon_path = os.path.join(get_base_path(), "icon", "eui.ico") if not os.path.exists(icon_path): reply = QMessageBox.question( None, "图标文件未找到", f"未找到图标文件: {icon_path}\n仍要继续设置文件关联吗?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply != QMessageBox.Yes: return False with winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, ".eui") as key: winreg.SetValueEx(key, "", 0, winreg.REG_SZ, "EasyUIEditor.eui") winreg.SetValueEx(key, "Content Type", 0, winreg.REG_SZ, "text/plain") with winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, "EasyUIEditor.eui") as key: winreg.SetValueEx(key, "", 0, winreg.REG_SZ, "Easy UI 文件") if os.path.exists(icon_path): with winreg.CreateKey(key, "DefaultIcon") as icon_key: winreg.SetValueEx(icon_key, "", 0, winreg.REG_SZ, f"{icon_path},0") cmd_path = f'"{current_path}" "%1"' with winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, "EasyUIEditor.eui\\shell\\open\\command") as key: winreg.SetValueEx(key, "", 0, winreg.REG_SZ, cmd_path) with winreg.CreateKey(winreg.HKEY_CLASSES_ROOT, ".eui\\OpenWithProgids") as key: winreg.SetValueEx(key, "EasyUIEditor.eui", 0, winreg.REG_NONE, b"") return True except WindowsError as e: QMessageBox.critical(None, "文件关联失败", f"设置文件关联时出错:\n{str(e)}\n请尝试以管理员身份运行程序。") return False @staticmethod def remove_association(): try: winreg.DeleteKey(winreg.HKEY_CLASSES_ROOT, "EasyUIEditor.eui\\shell\\open\\command") winreg.DeleteKey(winreg.HKEY_CLASSES_ROOT, "EasyUIEditor.eui\\shell\\open") winreg.DeleteKey(winreg.HKEY_CLASSES_ROOT, "EasyUIEditor.eui\\shell") winreg.DeleteKey(winreg.HKEY_CLASSES_ROOT, "EasyUIEditor.eui\\DefaultIcon") winreg.DeleteKey(winreg.HKEY_CLASSES_ROOT, "EasyUIEditor.eui") with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, ".eui", 0, winreg.KEY_SET_VALUE) as key: winreg.DeleteValue(key, "") with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, ".eui\\OpenWithProgids", 0, winreg.KEY_SET_VALUE) as key: winreg.DeleteValue(key, "EasyUIEditor.eui") return True except WindowsError as e: QMessageBox.critical(None, "移除关联失败", f"移除文件关联时出错:\n{str(e)}\n请尝试以管理员身份运行程序。") return False class CompleterTextEdit(QTextEdit): def __init__(self, parent=None): super().__init__(parent) self.completer = None def setCompleter(self, completer): if self.completer: self.completer.activated.disconnect() self.completer = completer if not self.completer: return self.completer.setWidget(self) self.completer.activated.connect(self.insertCompletion) def insertCompletion(self, completion): if not self.completer: return completion_text = completion.split(" ")[0] cursor = self.textCursor() prefix_length = len(self.completer.completionPrefix()) cursor.movePosition(QTextCursor.Left, QTextCursor.KeepAnchor, prefix_length) cursor.insertText(completion_text) self.setTextCursor(cursor) def textUnderCursor(self): cursor = self.textCursor() cursor.select(QTextCursor.WordUnderCursor) return cursor.selectedText() def focusInEvent(self, event): if self.completer: self.completer.setWidget(self) super().focusInEvent(event) def keyPressEvent(self, event): if self.completer and self.completer.popup().isVisible(): if event.key() in (Qt.Key_Enter, Qt.Key_Return, Qt.Key_Escape, Qt.Key_Tab, Qt.Key_Backtab): event.ignore() return super().keyPressEvent(event) prefix = self.textUnderCursor() if not prefix: self.completer.popup().hide() return if prefix != self.completer.completionPrefix(): self.completer.setCompletionPrefix(prefix) self.completer.popup().setCurrentIndex(self.completer.completionModel().index(0, 0)) cr = self.cursorRect() cr.setWidth(self.completer.popup().sizeHintForColumn(0) + self.completer.popup().verticalScrollBar().sizeHint().width()) self.completer.complete(cr) def get_base_path(): if getattr(sys, 'frozen', False): return os.path.dirname(sys.executable) else: return os.path.dirname(os.path.abspath(__file__)) class EasyUISyntaxHighlighter(QSyntaxHighlighter): def __init__(self, parent=None): super().__init__(parent) # 优化后的颜色方案 self.highlight_formats = { 'comment': self._create_format(QColor(106, 153, 85), italic=True), # 注释-绿色斜体 'tag': self._create_format(QColor(86, 156, 214), bold=True), # 标签-亮蓝色加粗 'attribute': self._create_format(QColor(152, 221, 255)), # 属性-青色 'string': self._create_format(QColor(242, 178, 66)), # 字符串-橙色 'keyword': self._create_format(QColor(197, 134, 250)), # 关键字-紫色 'punctuation': self._create_format(QColor(150, 150, 150)) # 标点-中灰色 } # 高亮规则 self.highlight_rules = [ (r'#.*$', self.highlight_formats['comment']), # #单行注释 (r'//.*$', self.highlight_formats['comment']), # //单行注释 (r'^\w+(?==)', self.highlight_formats['tag']), # 标签名 (r'(?<=[,=])\s*(id|options|type|readonly|min|max|value|rows|interval)(?==)', self.highlight_formats['keyword']), # 关键字 (r'(?<==)\s*\w+(?=[=,;])', self.highlight_formats['attribute']), # 属性值 (r'"[^"]*"', self.highlight_formats['string']), # 字符串 (r'[=,;[\]]', self.highlight_formats['punctuation']) # 标点符号 ] def _create_format(self, color, bold=False, italic=False): text_format = QTextCharFormat() text_format.setForeground(color) if bold: text_format.setFontWeight(QFont.Bold) if italic: text_format.setFontItalic(True) return text_format def highlightBlock(self, text): # 处理多行注释(/* */) self.setCurrentBlockState(0) start_index = 0 # 检查上一行是否处于多行注释中 if self.previousBlockState() != 1: # 从文本起始位置查找 /* start_index = self._match_multiline(text, r'/\*', 1) # 循环处理所有多行注释 while start_index >= 0: # 查找 */ 结束符 end_index = self._match_multiline(text, r'\*/', 0, start_index) if end_index == -1: # 没有找到结束符,标记当前行为多行注释中 self.setCurrentBlockState(1) comment_length = len(text) - start_index self.setFormat(start_index, comment_length, self.highlight_formats['comment']) break else: # 找到结束符,高亮整个注释块 comment_length = end_index - start_index + 2 # +2 包含 */ self.setFormat(start_index, comment_length, self.highlight_formats['comment']) # 继续查找下一个 /* start_index = self._match_multiline(text, r'/\*', 1, end_index + 2) # 处理其他高亮规则 for pattern, text_format in self.highlight_rules: for match in re.finditer(pattern, text): start = match.start() length = match.end() - start self.setFormat(start, length, text_format) def _match_multiline(self, text, pattern, state, start=0): # 修复:使用字符串切片实现起始位置偏移 sliced_text = text[start:] match = re.search(pattern, sliced_text, re.DOTALL) if match: return start + match.start() # 加上偏移量 return -1 class InterpreterSelector(QDialog): def __init__(self, interpreter_paths, parent=None): super().__init__(parent) self.setWindowTitle("选择解释器") self.setGeometry(300, 300, 800, 200) layout = QVBoxLayout(self) group_box = QGroupBox("找到以下解释器,请选择一个:") group_layout = QVBoxLayout() group_box.setLayout(group_layout) self.interpreter_combo = QComboBox() self.interpreter_combo.setMinimumWidth(700) self.interpreter_combo.setToolTip("选择要使用的解释器") for path in interpreter_paths: self.interpreter_combo.addItem(path, path) group_layout.addWidget(self.interpreter_combo) manual_layout = QHBoxLayout() self.manual_check = QRadioButton("手动选择解释器...") manual_layout.addWidget(self.manual_check) group_layout.addLayout(manual_layout) layout.addWidget(group_box) buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) buttons.accepted.connect(self.accept) buttons.rejected.connect(self.reject) layout.addWidget(buttons) if interpreter_paths: self.interpreter_combo.setCurrentIndex(0) else: self.manual_check.setChecked(True) def get_selected_path(self): if not self.manual_check.isChecked() and self.interpreter_combo.count() > 0: return self.interpreter_combo.currentData() file_path, _ = QFileDialog.getOpenFileName( self, "选择解释器", "", "可执行文件 (*.exe);;Python文件 (*.py);;所有文件 (*)" ) return file_path if file_path else None class InterpreterThread(QThread): error_occurred = pyqtSignal(str) output_received = pyqtSignal(str) finished = pyqtSignal() timeout_occurred = pyqtSignal() def __init__(self, code, file_path, interpreter_path, timeout=30): super().__init__() self.code = code self.file_path = file_path self.interpreter_path = interpreter_path self.process = None self.timeout = timeout * 1000 self.timeout_timer = None def run(self): try: with open(self.file_path, 'w', encoding='utf-8-sig') as f: f.write(self.code) if not os.path.exists(self.interpreter_path): self.error_occurred.emit(f"解释器文件不存在: {self.interpreter_path}") self.finished.emit() return self.process = QProcess() self.process.setProcessChannelMode(QProcess.SeparateChannels) self.process.setReadChannel(QProcess.StandardOutput) self.process.readyReadStandardOutput.connect(self.handle_output) self.process.readyReadStandardError.connect(self.handle_error) self.process.finished.connect(self.on_process_finished) self.process.errorOccurred.connect(self.on_process_error) self.timeout_timer = QTimer() self.timeout_timer.setSingleShot(True) self.timeout_timer.timeout.connect(self.on_timeout) self.timeout_timer.start(self.timeout) if self.interpreter_path.endswith('.exe'): self.process.start(self.interpreter_path, [self.file_path]) else: self.process.start(sys.executable, [self.interpreter_path, self.file_path]) if not self.process.waitForStarted(5000): self.error_occurred.emit(f"进程启动失败,可能是解释器路径错误或权限不足") self.cleanup() self.finished.emit() return except Exception as e: self.error_occurred.emit(f"线程初始化错误: {str(e)}") self.cleanup() self.finished.emit() def handle_output(self): if self.timeout_timer and self.timeout_timer.isActive(): self.timeout_timer.start(self.timeout) while self.process and self.process.canReadLine(): try: output = self.process.readLine().data().decode('utf-8').rstrip('\n') except UnicodeDecodeError: output = self.process.readLine().data().decode('gbk', errors='replace').rstrip('\n') if output: self.output_received.emit(f"[输出] {output}") def handle_error(self): while self.process and self.process.canReadLine(QProcess.StandardError): try: error = self.process.readLine(QProcess.StandardError).data().decode('utf-8').rstrip('\n') except UnicodeDecodeError: error = self.process.readLine(QProcess.StandardError).data().decode('gbk', errors='replace').rstrip('\n') if error: self.error_occurred.emit(f"[错误] {error}") def on_process_finished(self, exit_code, exit_status): self.cleanup() if exit_status == QProcess.CrashExit: self.error_occurred.emit(f"进程崩溃,可能是代码语法错误或解释器异常") elif exit_code != 0: self.error_occurred.emit(f"进程异常退出,退出代码: {exit_code}") else: self.output_received.emit(f"[提示] 进程正常结束,退出代码: {exit_code}") self.finished.emit() def on_process_error(self, error): error_messages = { QProcess.FailedToStart: "进程启动失败 - 可能是解释器不存在或权限不足", QProcess.Crashed: "进程已崩溃", QProcess.Timedout: "进程超时", QProcess.ReadError: "读取错误", QProcess.WriteError: "写入错误", QProcess.UnknownError: "未知错误" } self.error_occurred.emit(f"[进程错误] {error_messages.get(error, f'发生错误: {error}')}") self.cleanup() self.finished.emit() def on_timeout(self): self.error_occurred.emit(f"[超时] 代码运行时间超过 {self.timeout/1000} 秒,已自动终止") self.stop() self.timeout_occurred.emit() self.finished.emit() def stop(self): if self.process and self.process.state() == QProcess.Running: self.process.terminate() if not self.process.waitForFinished(2000): self.process.kill() self.error_occurred.emit(f"[提示] 进程已手动终止") self.cleanup() def cleanup(self): if self.timeout_timer and self.timeout_timer.isActive(): self.timeout_timer.stop() self.timeout_timer = None class InterpreterSearchThread(QThread): progress_updated = pyqtSignal(object) search_complete = pyqtSignal(list) def __init__(self): super().__init__() self.searching = True self.found_paths = set() self.search_names = [ "easy_ui_interpreter.exe", "easy_ui_interpreter.py" ] def run(self): try: drives = self.get_available_drives() total_drives = len(drives) drive_count = 0 self.search_quick_paths() for drive in drives: if not self.searching: break self.progress_updated.emit(f"正在扫描驱动器: {drive}({drive_count+1}/{total_drives})") self.search_directory(drive) drive_count += 1 progress = int((drive_count / total_drives) * 100) if total_drives > 0 else 0 self.progress_updated.emit(progress) sorted_paths = sorted(list(self.found_paths)) self.search_complete.emit(sorted_paths) except Exception as e: print(f"搜索解释器时出错: {str(e)}") self.search_complete.emit(list(self.found_paths)) def get_available_drives(self): drives = [] seen = set() if sys.platform.startswith('win'): try: with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders") as key: for i in range(winreg.QueryInfoKey(key)[1]): try: value_name, value_data, _ = winreg.EnumValue(key, i) if value_data and len(value_data) >= 3 and value_data[1] == ':' and value_data[2] == '\\': drive = value_data[:3].upper() if drive not in seen and os.path.exists(drive) and os.access(drive, os.R_OK): seen.add(drive) drives.append(drive) except WindowsError: continue except Exception: for drive_letter in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ': drive = f"{drive_letter}:\\" if os.path.exists(drive) and os.access(drive, os.R_OK): drive_upper = drive.upper() if drive_upper not in seen: seen.add(drive_upper) drives.append(drive_upper) else: return ["/", os.path.expanduser("~")] drives.sort() return drives def search_quick_paths(self): base_path = get_base_path() quick_paths = [ base_path, os.path.join(base_path, "interpreters"), "D:\\Easy-Windows-UI-Lang", os.path.join(os.environ.get("ProgramFiles", ""), "Easy-Windows-UI-Lang"), os.path.join(os.environ.get("ProgramFiles(x86)", ""), "Easy-Windows-UI-Lang"), os.path.expanduser("~\\Desktop"), os.path.expanduser("~\\Documents"), os.path.expanduser("~\\Downloads") ] for path in os.environ.get("PATH", "").split(os.pathsep): if path and path not in quick_paths: quick_paths.append(path) for path in quick_paths: if os.path.exists(path) and os.path.isdir(path): self.search_directory(path, depth_limit=None) def search_directory(self, root_dir, depth_limit=None, current_depth=0): if depth_limit is not None and current_depth > depth_limit: return try: if not os.access(root_dir, os.R_OK): return for name in self.search_names: file_path = os.path.join(root_dir, name) if os.path.exists(file_path) and os.path.isfile(file_path): self.found_paths.add(file_path) for item in os.listdir(root_dir): if not self.searching: return item_path = os.path.join(root_dir, item) if os.path.isdir(item_path): if self.should_skip_directory(item_path): continue self.search_directory(item_path, depth_limit, current_depth + 1) except Exception as e: pass def should_skip_directory(self, dir_path): dir_name = os.path.basename(dir_path).lower() full_path = dir_path.lower() system_blacklist = [ "windows\\system32", "windows\\syswow64", "windows\\system", "$recycle.bin", "system volume information", "windows\\recovery" ] if any(black_dir in full_path for black_dir in system_blacklist): return True if dir_name in ["node_modules", "venv", "env"]: return True return False def stop_search(self): self.searching = False class FileTreeWidget(QTreeWidget): def __init__(self, parent=None): super().__init__(parent) self.parent = parent self.setHeaderLabel("文件目录") self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.show_context_menu) self.itemDoubleClicked.connect(self.on_item_double_clicked) self.init_icons() self.current_dir = get_base_path() self.refresh_tree() def init_icons(self): self.dir_icon = QIcon() self.dir_icon.addPixmap(QPixmap(":/icons/folder.png"), QIcon.Normal, QIcon.Off) self.python_icon = QIcon() self.python_icon.addPixmap(QPixmap(":/icons/python.png"), QIcon.Normal, QIcon.Off) self.cpp_icon = QIcon() self.cpp_icon.addPixmap(QPixmap(":/icons/cpp.png"), QIcon.Normal, QIcon.Off) self.java_icon = QIcon() self.java_icon.addPixmap(QPixmap(":/icons/java.png"), QIcon.Normal, QIcon.Off) self.eui_icon = QIcon() eui_icon_path = os.path.join(get_base_path(), "icon", "eui.ico") if os.path.exists(eui_icon_path): self.eui_icon.addPixmap(QPixmap(eui_icon_path), QIcon.Normal, QIcon.Off) else: self.eui_icon = None self.eui_text = "📝" self.file_icon = QIcon() self.file_icon.addPixmap(QPixmap(":/icons/file.png"), QIcon.Normal, QIcon.Off) self.dir_text = "📁" self.python_text = "🐍" self.cpp_text = "++" self.java_text = "☕" self.file_text = "📄" def get_file_icon(self, file_name): lower_name = file_name.lower() if lower_name.endswith('.eui'): if self.eui_icon: return self.eui_icon return self.eui_text elif lower_name.endswith('.py'): if not self.python_icon.isNull(): return self.python_icon return self.python_text elif lower_name.endswith(('.cpp', '.h', '.c', '.hpp')): if not self.cpp_icon.isNull(): return self.cpp_icon return self.cpp_text elif lower_name.endswith('.java'): if not self.java_icon.isNull(): return self.java_icon return self.java_text else: if not self.file_icon.isNull(): return self.file_icon return self.file_text def refresh_tree(self): self.clear() if not self.current_dir or not os.path.isdir(self.current_dir): return root = QTreeWidgetItem([os.path.basename(self.current_dir)]) root.setData(0, Qt.UserRole, self.current_dir) if not self.dir_icon.isNull(): root.setIcon(0, self.dir_icon) root.setExpanded(True) self.addTopLevelItem(root) self.add_directory_items(root, self.current_dir) self.parent.status_bar.showMessage(f"显示目录: {self.current_dir}") def add_directory_items(self, parent_item, directory): try: items = os.listdir(directory) dirs = [] files = [] for item in items: item_path = os.path.join(directory, item) if os.path.isdir(item_path) and not item.startswith('.'): dirs.append(item) elif os.path.isfile(item_path): files.append(item) for dir_name in sorted(dirs): dir_path = os.path.join(directory, dir_name) dir_item = QTreeWidgetItem([dir_name]) dir_item.setData(0, Qt.UserRole, dir_path) if not self.dir_icon.isNull(): dir_item.setIcon(0, self.dir_icon) parent_item.addChild(dir_item) self.add_directory_items(dir_item, dir_path) dir_item.setExpanded(False) for file_name in sorted(files): file_path = os.path.join(directory, file_name) file_item = QTreeWidgetItem([file_name]) file_item.setData(0, Qt.UserRole, file_path) icon = self.get_file_icon(file_name) if isinstance(icon, QIcon) and not icon.isNull(): file_item.setIcon(0, icon) else: file_item.setText(0, f"{icon} {file_name}") parent_item.addChild(file_item) except Exception as e: pass def on_item_double_clicked(self, item, column): item_path = item.data(0, Qt.UserRole) if not item_path: return if os.path.isdir(item_path): if item.childCount() == 0: self.add_directory_items(item, item_path) item.setExpanded(not item.isExpanded()) elif os.path.isfile(item_path): if item_path.lower().endswith(('.eui', '.txt', '.py', '.cpp', '.h', '.java')): self.parent.open_file_from_path(item_path) else: self.parent.status_bar.showMessage(f"不支持的文件类型: {os.path.basename(item_path)}") def show_context_menu(self, position): item = self.itemAt(position) if not item: self.show_empty_context_menu(position) return item_path = item.data(0, Qt.UserRole) if not item_path: return menu = QContextMenu() open_action = menu.addAction("打开") open_action.triggered.connect(lambda: self.open_item(item)) if os.path.isdir(item_path): menu.addSeparator() new_file_action = menu.addAction("新建文件") new_file_action.triggered.connect(lambda: self.new_file(item)) new_folder_action = menu.addAction("新建文件夹") new_folder_action.triggered.connect(lambda: self.new_folder(item)) set_as_root_action = menu.addAction("设为根目录") set_as_root_action.triggered.connect(lambda: self.set_as_root(item)) add_file_action = menu.addAction("添加文件到此处") add_file_action.triggered.connect(lambda: self.add_file_to_directory(item)) else: menu.addSeparator() rename_action = menu.addAction("重命名") rename_action.triggered.connect(lambda: self.rename_item(item)) delete_action = menu.addAction("删除") delete_action.triggered.connect(lambda: self.delete_item(item)) copy_action = menu.addAction("复制") copy_action.triggered.connect(lambda: self.copy_item(item)) cut_action = menu.addAction("剪切") cut_action.triggered.connect(lambda: self.cut_item(item)) menu.exec_(self.viewport().mapToGlobal(position)) def show_empty_context_menu(self, position): menu = QContextMenu() new_file_action = menu.addAction("新建文件") new_file_action.triggered.connect(lambda: self.new_file_in_current_dir()) new_folder_action = menu.addAction("新建文件夹") new_folder_action.triggered.connect(lambda: self.new_folder_in_current_dir()) menu.addSeparator() refresh_action = menu.addAction("刷新") refresh_action.triggered.connect(self.refresh_tree) menu.exec_(self.viewport().mapToGlobal(position)) def new_file_in_current_dir(self): self.new_file(None) def new_folder_in_current_dir(self): self.new_folder(None) def open_item(self, item): item_path = item.data(0, Qt.UserRole) if os.path.isdir(item_path): if item.childCount() == 0: self.add_directory_items(item, item_path) item.setExpanded(True) else: self.parent.open_file_from_path(item_path) def set_as_root(self, item): item_path = item.data(0, Qt.UserRole) if os.path.isdir(item_path): self.current_dir = item_path self.refresh_tree() def new_folder(self, item): if item: item_path = item.data(0, Qt.UserRole) else: item_path = self.current_dir if not os.path.isdir(item_path): return folder_name, ok = QInputDialog.getText(self, "新建文件夹", "文件夹名称:") if ok and folder_name: new_folder_path = os.path.join(item_path, folder_name) try: os.makedirs(new_folder_path) self.refresh_tree() self.parent.status_bar.showMessage(f"已创建文件夹: {folder_name}") except Exception as e: QMessageBox.critical(self, "错误", f"无法创建文件夹: {str(e)}") def new_file(self, item): if item: dir_path = item.data(0, Qt.UserRole) else: dir_path = self.current_dir if not os.path.isdir(dir_path): return file_name, ok = QInputDialog.getText(self, "新建文件", "文件名称(例如: myfile.eui):") if ok and file_name: file_path = os.path.join(dir_path, file_name) try: with open(file_path, 'w', encoding='utf-8') as f: pass self.refresh_tree() self.parent.status_bar.showMessage(f"已创建文件: {file_name}") self.parent.open_file_from_path(file_path) except Exception as e: QMessageBox.critical(self, "错误", f"无法创建文件: {str(e)}") def add_file_to_directory(self, item): if not item: return dir_path = item.data(0, Qt.UserRole) if not os.path.isdir(dir_path): return file_paths, _ = QFileDialog.getOpenFileNames( self, "选择要添加的文件", "", "所有文件 (*)" ) if file_paths: success_count = 0 fail_count = 0 for file_path in file_paths: try: dest_path = os.path.join(dir_path, os.path.basename(file_path)) if os.path.exists(dest_path): reply = QMessageBox.question( self, "文件已存在", f"{os.path.basename(file_path)} 已存在,是否覆盖?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply != QMessageBox.Yes: fail_count += 1 continue shutil.copy2(file_path, dest_path) success_count += 1 except Exception as e: print(f"复制文件失败: {str(e)}") fail_count += 1 self.refresh_tree() self.parent.status_bar.showMessage( f"添加完成: 成功 {success_count} 个,失败 {fail_count} 个" ) def rename_item(self, item): item_path = item.data(0, Qt.UserRole) old_name = os.path.basename(item_path) new_name, ok = QInputDialog.getText(self, "重命名", "新名称:", text=old_name) if ok and new_name and new_name != old_name: parent_dir = os.path.dirname(item_path) new_path = os.path.join(parent_dir, new_name) try: os.rename(item_path, new_path) self.refresh_tree() self.parent.status_bar.showMessage(f"已重命名为: {new_name}") except Exception as e: QMessageBox.critical(self, "错误", f"无法重命名: {str(e)}") def delete_item(self, item): item_path = item.data(0, Qt.UserRole) if not item_path: return reply = QMessageBox.question( self, "确认删除", f"确定要删除 {os.path.basename(item_path)} 吗?\n此操作不可恢复。", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: try: if os.path.isfile(item_path): os.remove(item_path) elif os.path.isdir(item_path): shutil.rmtree(item_path) self.refresh_tree() self.parent.status_bar.showMessage(f"已删除: {os.path.basename(item_path)}") except Exception as e: QMessageBox.critical(self, "错误", f"无法删除: {str(e)}") def copy_item(self, item): item_path = item.data(0, Qt.UserRole) if not item_path: return self.parent.copied_path = item_path self.parent.is_cut = False self.parent.status_bar.showMessage(f"已复制: {os.path.basename(item_path)}") def cut_item(self, item): item_path = item.data(0, Qt.UserRole) if not item_path: return self.parent.copied_path = item_path self.parent.is_cut = True self.parent.status_bar.showMessage(f"已剪切: {os.path.basename(item_path)}") def change_directory(self, new_dir): if os.path.isdir(new_dir): self.current_dir = new_dir self.refresh_tree() return True return False class EasyUIEditor(QMainWindow): def __init__(self): super().__init__() self.current_file = None self.temp_file = os.path.join(tempfile.gettempdir(), "temp_ewui_code.eui") self.status_bar = None self.interpreter_path = None self.run_timeout = 30 self.search_thread = None self.search_in_progress = False self.copied_path = None self.is_cut = False self.cmd_line_file = sys.argv[1] if len(sys.argv) > 1 and sys.argv[1].lower().endswith('.eui') else None self.init_completion_words() self.init_status_bar() self.init_ui() self.check_file_association_prompt() # 检查文件关联(带记忆功能) self.scan_interpreters(quick_scan=True) self.full_scan_interpreters_in_background() if self.cmd_line_file: self.open_file_from_path(self.cmd_line_file) def init_completion_words(self): self.completion_words = [ ("window", "标签 - 窗口"), ("label", "标签 - 文字显示"), ("entry", "标签 - 输入框"), ("combo", "标签 - 选择框"), ("checkbox", "标签 - 多选框"), ("button", "标签 - 按钮"), ("audio", "标签 - 音频组件"), ("slider", "标签 - 滑块控件"), ("textarea", "标签 - 文本区域"), ("separator", "标签 - 分隔线"), ("progress", "标签 - 进度条"), ("calendar", "标签 - 日历控件"), ("radiogroup", "标签 - 单选按钮组"), ("groupbox", "标签 - 分组框"), ("timer", "标签 - 定时器"), ("title", "属性 - 窗口标题"), ("width", "属性 - 宽度"), ("height", "属性 - 高度"), ("icon", "属性 - 窗口图标路径"), ("text", "属性 - 显示文本"), ("id", "属性 - 组件ID(必选)"), ("hint", "属性 - 输入框提示文本"), ("readonly", "属性 - 输入框只读(true/false)"), ("label", "属性 - 选择框/多选框标题"), ("options", "属性 - 选项列表(如[\"选项1\",\"选项2\"])"), ("click", "属性 - 按钮触发动作"), ("url", "属性 - 网络音频地址"), ("os", "属性 - 本地音频文件路径"), ("min", "属性 - 最小值"), ("max", "属性 - 最大值"), ("value", "属性 - 当前值"), ("rows", "属性 - 文本区域行数"), ("interval", "属性 - 定时器间隔(毫秒)"), ("action", "属性 - 定时器动作"), ("true", "值 - 布尔值(只读/启用)"), ("false", "值 - 布尔值(可写/禁用)"), ("显示=", "动作 - 显示组件内容(如显示=组件ID)"), ("play_audio=", "动作 - 播放音频(如play_audio=音频ID)"), ("pause_audio=", "动作 - 暂停音频(如pause_audio=音频ID)"), ("stop_audio=", "动作 - 停止音频(如stop_audio=音频ID)"), ("start_timer=", "动作 - 启动定时器(如start_timer=定时器ID)"), ("stop_timer=", "动作 - 停止定时器(如stop_timer=定时器ID)"), ("set_progress=", "动作 - 设置进度条(如set_progress=进度条ID,value=50)"), (";", "符号 - 语句结束符"), (",", "符号 - 属性分隔符"), ("=[", "符号 - 选项列表开始(如options=[)"), ("]", "符号 - 选项列表结束") ] def init_status_bar(self): self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) self.status_bar.showMessage("初始化中...") # 带记忆功能的文件关联检查 def check_file_association_prompt(self): # 检查是否已经设置过关联 if FileAssociation.is_associated(): self.status_bar.showMessage(".eui文件已关联到此程序") return # 检查是否已经提示过(使用注册表记录) if self.has_prompted_association(): self.status_bar.showMessage(".eui文件未关联,可在工具菜单设置") return # 首次未关联状态,显示提示 reply = QMessageBox.question( self, "文件关联", "尚未设置.eui文件关联,是否将.eui文件默认用此程序打开并设置图标?\n(需要管理员权限)", QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes ) # 记录已提示状态 self.set_prompted_association(True) if reply == QMessageBox.Yes: if FileAssociation.set_association(): QMessageBox.information(self, "成功", "文件关联设置成功!\n可能需要重启资源管理器才能看到图标变化。") self.status_bar.showMessage("已成功设置.eui文件关联") else: self.status_bar.showMessage("已取消文件关联设置,可在工具菜单重新设置") # 检查是否已提示过关联 def has_prompted_association(self): try: with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\EasyUIEditor", 0, winreg.KEY_READ) as key: prompted, _ = winreg.QueryValueEx(key, "AssociationPrompted") return bool(prompted) except WindowsError: return False # 设置已提示关联的标记 def set_prompted_association(self, value): try: key = winreg.CreateKey(winreg.HKEY_CURRENT_USER, r"Software\EasyUIEditor") winreg.SetValueEx(key, "AssociationPrompted", 0, winreg.REG_DWORD, 1 if value else 0) winreg.CloseKey(key) except WindowsError: pass # 忽略注册表操作错误 def init_ui(self): self.setWindowTitle("Easy Windows UI Editor - [未命名]") self.setGeometry(100, 100, 1400, 800) self.setStyleSheet(""" QMainWindow { background-color: #1e1e1e; color: #d4d4d4; } QTextEdit, CompleterTextEdit { background-color: #1e1e1e; color: #d4d4d4; border: 1px solid #3c3c3c; } QLabel { color: #d4d4d4; } QPushButton { background-color: #3c3c3c; color: #d4d4d4; border: 1px solid #5e5e5e; padding: 5px 10px; border-radius: 3px; } QPushButton:hover { background-color: #4a4a4a; } QPushButton:pressed { background-color: #2d2d2d; } QTabBar::tab { background-color: #2d2d2d; color: #d4d4d4; padding: 8px 16px; border: 1px solid #5e5e5e; border-bottom: none; } QTabBar::tab:selected { background-color: #1e1e1e; border-top: 2px solid #007acc; } QListWidget, QTreeWidget { background-color: #2d2d2d; color: #d4d4d4; border: 1px solid #3c3c3c; } QTreeWidget::item { border-bottom: 1px solid #3c3c3c; } QTreeWidget::item:selected { background-color: #3c3c3c; } QStatusBar { background-color: #1e1e1e; color: #858585; border-top: 1px solid #3c3c3c; } QMenuBar { background-color: #1e1e1e; color: #d4d4d4; border-bottom: 1px solid #3c3c3c; } QToolBar { background-color: #1e1e1e; border-bottom: 1px solid #3c3c3c; spacing: 5px; } QDockWidget { color: #d4d4d4; titlebar-close-icon: url(); titlebar-normal-icon: url(); } QDockWidget::title { background-color: #2d2d2d; padding: 5px; border-bottom: 1px solid #3c3c3c; } QCompleter QListView { background-color: #2d2d2d; color: #d4d4d4; border: 1px solid #5e5e5e; padding: 2px; } QCompleter QListView::item:selected { background-color: #007acc; color: white; } QMenu { background-color: #2d2d2d; color: #d4d4d4; border: 1px solid #5e5e5e; } QMenu::item:selected { background-color: #3c3c3c; } QComboBox { background-color: #2d2d2d; color: #d4d4d4; border: 1px solid #5e5e5e; padding: 3px; min-width: 500px; } QComboBox::drop-down { border-left: 1px solid #5e5e5e; } QComboBox::down-arrow { image: url(:/icons/arrow-down.png); width: 12px; height: 12px; } """) self.create_menu_bar() self.create_tool_bar() main_splitter = QSplitter(Qt.Horizontal) self.file_tree = FileTreeWidget(self) self.file_tree.setMaximumWidth(300) main_splitter.addWidget(self.file_tree) right_container = QWidget() right_layout = QVBoxLayout(right_container) interpreter_layout = QHBoxLayout() interpreter_layout.addWidget(QLabel("当前解释器:")) self.interpreter_combo = QComboBox() self.interpreter_combo.setToolTip("选择要使用的解释器") self.interpreter_combo.currentIndexChanged.connect(self.on_interpreter_changed) interpreter_layout.addWidget(self.interpreter_combo) interpreter_layout.addStretch() refresh_interpreter_btn = QPushButton("刷新解释器列表") refresh_interpreter_btn.setToolTip("重新扫描可用的解释器") refresh_interpreter_btn.clicked.connect(lambda: self.scan_interpreters(quick_scan=True)) interpreter_layout.addWidget(refresh_interpreter_btn) right_layout.addLayout(interpreter_layout) self.tab_widget = QTabWidget() self.tab_widget.setTabsClosable(True) self.tab_widget.tabCloseRequested.connect(self.close_tab) self.add_new_tab() right_layout.addWidget(self.tab_widget) self.output_panel = QTextEdit() self.output_panel.setReadOnly(True) self.output_panel.setMaximumHeight(150) self.output_panel.setAcceptRichText(True) self.output_panel.setHtml('[提示] 输出面板将显示代码运行日志和报错信息(按F5运行代码)') right_layout.addWidget(self.output_panel) main_splitter.addWidget(right_container) main_splitter.setSizes([250, 1150]) self.setCentralWidget(main_splitter) self.add_help_dock() def on_interpreter_changed(self, index): if index >= 0 and self.interpreter_combo.count() > 0: self.interpreter_path = self.interpreter_combo.currentData() self.status_bar.showMessage(f"已选择解释器: {os.path.basename(self.interpreter_path)}") def create_tool_bar(self): toolbar = QToolBar("主工具栏") self.addToolBar(toolbar) new_btn = QPushButton("新建") new_btn.setToolTip("新建文件 (Ctrl+N)") new_btn.clicked.connect(self.add_new_tab) toolbar.addWidget(new_btn) open_btn = QPushButton("打开") open_btn.setToolTip("打开文件 (Ctrl+O)") open_btn.clicked.connect(self.open_file) toolbar.addWidget(open_btn) change_dir_btn = QPushButton("更改目录") change_dir_btn.setToolTip("更改文件树显示的目录") change_dir_btn.clicked.connect(self.change_directory) toolbar.addWidget(change_dir_btn) save_btn = QPushButton("保存") save_btn.setToolTip("保存文件 (Ctrl+S)") save_btn.clicked.connect(self.save_file) toolbar.addWidget(save_btn) toolbar.addSeparator() run_btn = QPushButton("运行") run_btn.setToolTip("运行代码 (F5)") run_btn.clicked.connect(self.run_code) run_btn.setStyleSheet("color: green; font-weight: bold;") toolbar.addWidget(run_btn) stop_btn = QPushButton("停止") stop_btn.setToolTip("停止运行 (Ctrl+F5)") stop_btn.clicked.connect(self.stop_running) stop_btn.setStyleSheet("color: red; font-weight: bold;") toolbar.addWidget(stop_btn) interpreter_btn = QPushButton("解释器") interpreter_btn.setToolTip("选择解释器") interpreter_btn.clicked.connect(self.choose_interpreter) toolbar.addWidget(interpreter_btn) full_scan_btn = QPushButton("全扫描") full_scan_btn.setToolTip("全电脑后台搜索解释器") full_scan_btn.clicked.connect(self.full_scan_interpreters_in_background) full_scan_btn.setStyleSheet("color: #00ccff; font-weight: bold;") toolbar.addWidget(full_scan_btn) force_scan_btn = QPushButton("强制全扫") force_scan_btn.setToolTip("无限制扫描所有驱动器(确保找到全部解释器)") force_scan_btn.setStyleSheet("color: orange; font-weight: bold;") force_scan_btn.clicked.connect(self.force_full_scan) toolbar.addWidget(force_scan_btn) clear_btn = QPushButton("清空") clear_btn.setToolTip("清空编辑区") clear_btn.clicked.connect(self.clear_current_tab) toolbar.addWidget(clear_btn) def update_interpreter_combo(self, interpreter_paths): current_path = self.interpreter_path self.interpreter_combo.clear() for path in interpreter_paths: self.interpreter_combo.addItem(path, path) if current_path and os.path.exists(current_path): for i in range(self.interpreter_combo.count()): if self.interpreter_combo.itemData(i) == current_path: self.interpreter_combo.setCurrentIndex(i) return if self.interpreter_combo.count() > 0: self.interpreter_combo.setCurrentIndex(0) self.interpreter_path = self.interpreter_combo.currentData() def create_menu_bar(self): menubar = self.menuBar() file_menu = menubar.addMenu("文件(&F)") new_action = QAction("新建(&N)", self) new_action.setShortcut("Ctrl+N") new_action.triggered.connect(self.add_new_tab) file_menu.addAction(new_action) open_action = QAction("打开(&O)", self) open_action.setShortcut("Ctrl+O") open_action.triggered.connect(self.open_file) file_menu.addAction(open_action) change_dir_action = QAction("更改目录(&C)", self) change_dir_action.triggered.connect(self.change_directory) file_menu.addAction(change_dir_action) save_action = QAction("保存(&S)", self) save_action.setShortcut("Ctrl+S") save_action.triggered.connect(self.save_file) file_menu.addAction(save_action) save_as_action = QAction("另存为(&A)", self) save_as_action.setShortcut("Ctrl+Shift+S") save_as_action.triggered.connect(self.save_file_as) file_menu.addAction(save_as_action) paste_action = QAction("粘贴(&P)", self) paste_action.setShortcut("Ctrl+V") paste_action.triggered.connect(self.paste_file) file_menu.addAction(paste_action) file_menu.addSeparator() exit_action = QAction("退出(&X)", self) exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) run_menu = menubar.addMenu("运行(&R)") run_action = QAction("运行代码(&R)", self) run_action.setShortcut("F5") run_action.triggered.connect(self.run_code) run_menu.addAction(run_action) stop_action = QAction("停止运行(&S)", self) stop_action.setShortcut("Ctrl+F5") stop_action.triggered.connect(self.stop_running) run_menu.addAction(stop_action) timeout_menu = run_menu.addMenu("运行超时设置") self.timeout_actions = {} for timeout in [10, 30, 60, 120]: act = QAction(f"{timeout}秒", self, checkable=True) act.setData(timeout) if timeout == self.run_timeout: act.setChecked(True) act.triggered.connect(self.set_timeout) self.timeout_actions[timeout] = act timeout_menu.addAction(act) tool_menu = menubar.addMenu("工具(&T)") assoc_action = QAction("设置.eui文件关联", self) assoc_action.triggered.connect(self.set_file_association) tool_menu.addAction(assoc_action) unassoc_action = QAction("取消.eui文件关联", self) unassoc_action.triggered.connect(self.remove_file_association) tool_menu.addAction(unassoc_action) tool_menu.addSeparator() interpreter_action = QAction("选择解释器(&I)", self) interpreter_action.triggered.connect(self.choose_interpreter) tool_menu.addAction(interpreter_action) scan_action = QAction("快速扫描解释器(&S)", self) scan_action.triggered.connect(lambda: self.scan_interpreters(quick_scan=True)) tool_menu.addAction(scan_action) full_scan_action = QAction("全电脑扫描解释器(&F)", self) full_scan_action.triggered.connect(self.full_scan_interpreters_in_background) tool_menu.addAction(full_scan_action) help_menu = menubar.addMenu("帮助(&H)") example_action = QAction("示例代码(&E)", self) example_action.triggered.connect(self.load_example_code) help_menu.addAction(example_action) about_action = QAction("关于(&A)", self) about_action.triggered.connect(self.show_about) help_menu.addAction(about_action) def set_file_association(self): if FileAssociation.is_associated(): reply = QMessageBox.question( self, "已关联", ".eui文件已关联到此程序,是否重新设置?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply != QMessageBox.Yes: return if FileAssociation.set_association(): QMessageBox.information(self, "成功", "文件关联设置成功!\n可能需要重启资源管理器才能看到图标变化。") def remove_file_association(self): if not FileAssociation.is_associated(): QMessageBox.information(self, "未关联", ".eui文件尚未关联到此程序") return reply = QMessageBox.question( self, "确认取消", "确定要取消.eui文件与本程序的关联吗?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.Yes: if FileAssociation.remove_association(): QMessageBox.information(self, "成功", "已取消.eui文件关联") def paste_file(self): if not self.copied_path or not os.path.exists(self.copied_path): self.status_bar.showMessage("没有可粘贴的内容") return target_dir = self.file_tree.current_dir try: item_name = os.path.basename(self.copied_path) target_path = os.path.join(target_dir, item_name) if os.path.exists(target_path): reply = QMessageBox.question( self, "文件已存在", f"{item_name} 已存在,是否覆盖?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply != QMessageBox.Yes: self.status_bar.showMessage("粘贴已取消") return if self.is_cut: if os.path.isdir(self.copied_path): shutil.move(self.copied_path, target_path) else: os.rename(self.copied_path, target_path) self.status_bar.showMessage(f"已移动: {item_name}") self.copied_path = None self.is_cut = False else: if os.path.isdir(self.copied_path): shutil.copytree(self.copied_path, target_path) else: shutil.copy2(self.copied_path, target_path) self.status_bar.showMessage(f"已复制: {item_name}") self.file_tree.refresh_tree() except Exception as e: QMessageBox.critical(self, "错误", f"粘贴失败: {str(e)}") def set_timeout(self): sender = self.sender() if sender: self.run_timeout = sender.data() for act in self.timeout_actions.values(): act.setChecked(act.data() == self.run_timeout) self.status_bar.showMessage(f"已设置运行超时时间为 {self.run_timeout} 秒") def stop_running(self): if hasattr(self, 'interpreter_thread') and self.interpreter_thread.isRunning(): self.interpreter_thread.stop() self.run_finished() else: self.status_bar.showMessage("没有正在运行的进程") def add_help_dock(self): dock = QDockWidget("语法帮助", self) dock.setAllowedAreas(Qt.RightDockWidgetArea) help_content = QTextEdit() help_content.setReadOnly(True) help_content.setHtml("""
核心语法:标签名=属性1=值1,属性2=值2,...; (每条语句必须以分号结尾)
属性规则:字符串值需用双引号包裹,数值/布尔值直接写,列表用[]包裹(元素用逗号分隔)
# 单行注释:# 开头(绿色斜体)
// 单行注释:// 开头(绿色斜体)
/* 多行注释:/* 开头,*/ 结尾
支持跨越多行文本
全程绿色高亮 */
// 示例:带注释的代码
window=title="测试窗口",width=500,height=300;# 行尾也可加注释
| 组件类型 | 标签名 | 必选属性 | 可选属性 | 实战示例 |
|---|---|---|---|---|
| 主窗口 | window | title="窗口标题", width=数值, height=数值 | icon="本地图标路径", tooltip="窗口提示" | window=title="用户管理系统",width=800,height=600,icon="logo.ico"; |
| 文字标签 | label | text="显示文本", id=唯一ID | tooltip="鼠标悬浮提示" | label=text="用户名:",id=user_label,tooltip="请输入账号"; |
| 输入框 | entry | hint="占位提示", id=唯一ID | readonly=true/false, type=text/number | entry=hint="请输入手机号",id=phone_input,type=number,readonly=false; |
| 下拉选择框 | combo | label="选择标题", id=唯一ID, options=["选项1","选项2"] | - | combo=label="所属部门",id=dept_combo,options=["技术部","财务部","市场部"]; |
| 多选框组 | checkbox | label="组标题", id=唯一ID, options=["选项1","选项2"] | - | checkbox=label="兴趣爱好",id=hobby_check,options=["读书","编程","运动"]; |
| 单选按钮组 | radiogroup | label="组标题", id=唯一ID, options=["选项1","选项2"] | - | radiogroup=label="性别",id=gender_radio,options=["男","女","其他"]; |
| 网络音频 | audio | url="音频地址", id=唯一ID | - | audio=url="https://xxx.mp3",id=net_audio; |
| 本地音频 | audio | os="本地路径", id=唯一ID | - | audio=os="music/background.mp3",id=local_audio; |
| 图片显示 | image | path="图片路径", id=唯一ID, width=数值, height=数值 | tooltip="图片说明" | image=path="img/banner.png",id=banner_img,width=800,height=200,tooltip="顶部横幅"; |
| 按钮 | button | text="按钮文本", id=唯一ID, click="触发动作" | tooltip="按钮功能说明" | button=text="播放音乐",id=play_btn,click="play_audio=net_audio",tooltip="点击播放网络音乐"; |
| 滑块控件 | slider | label="滑块标题", id=唯一ID, min=最小值, max=最大值, value=初始值 | - | slider=label="音量调节",id=vol_slider,min=0,max=100,value=70; |
| 文本区域 | textarea | label="区域标题", id=唯一ID, rows=行数 | readonly=true/false | textarea=label="备注信息",id=note_area,rows=5,readonly=false; |
| 进度条 | progress | label="进度标题", id=唯一ID, min=最小值, max=最大值, value=初始值 | - | progress=label="下载进度",id=down_progress,min=0,max=100,value=30; |
| 日历控件 | calendar | label="选择标题", id=唯一ID | tooltip="选择日期" | calendar=label="生日选择",id=birth_cal,tooltip="点击选择出生日期"; |
| 分隔线 | separator | id=唯一ID | text="分隔文本(居中显示)" | separator=text="用户信息区",id=sep1; |
| 分组框 | groupbox | title="分组标题", id=唯一ID | - | groupbox=title="登录信息",id=login_group; |
| 定时器 | timer | id=唯一ID, interval=毫秒数, action="循环动作" | - | timer=id=progress_timer,interval=1000,action="update_progress=down_progress,value=+1"; |
显示=组件ID → 弹窗显示输入框/选择框的当前值start_timer=定时器ID → 开始定时器循环stop_timer=定时器ID → 停止定时器循环play_audio=音频ID → 播放指定音频(支持暂停后继续)pause_audio=音频ID → 暂停指定音频stop_audio=音频ID → 停止指定音频(需重新播放)set_progress=进度条ID,value=数值 → 直接设置进度值(如:set_progress=down_progress,value=50)update_progress=进度条ID,value=±数值 → 增减进度值(如:update_progress=down_progress,value=+1)• 注释内容(#、//、/* */)→ 绿色斜体
• 标签名(window、label、audio等)→ 蓝色加粗
• 属性名(title、id、bind_volume等)→ 青色
• 字符串值(""包裹的内容)→ 橙色
• 关键字(true、false、text、number等)→ 紫色
• 标点符号(=、,、;、[]等)→ 深灰色
; 结尾,否则解析失败" 包裹,数值/布尔值无需包裹options=["选项1","选项2"]