Files
Easy-User-Interface-Lang/easy_ui_interpreter.py

686 lines
28 KiB
Python
Raw Permalink Normal View History

2025-10-26 12:17:46 +00:00
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 <EWUI文件路径>")
print("支持组件:窗口、标签、按钮、输入框、下拉框、复选框等")
print("=" * 50)
sys.exit(0)