uiml 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- uiml-0.1.0/LICENSE +21 -0
- uiml-0.1.0/PKG-INFO +70 -0
- uiml-0.1.0/README.md +49 -0
- uiml-0.1.0/pyproject.toml +32 -0
- uiml-0.1.0/setup.cfg +4 -0
- uiml-0.1.0/setup.py +23 -0
- uiml-0.1.0/src/uiml/__init__.py +452 -0
- uiml-0.1.0/src/uiml/uilib.py +4 -0
- uiml-0.1.0/src/uiml.egg-info/PKG-INFO +70 -0
- uiml-0.1.0/src/uiml.egg-info/SOURCES.txt +12 -0
- uiml-0.1.0/src/uiml.egg-info/dependency_links.txt +1 -0
- uiml-0.1.0/src/uiml.egg-info/requires.txt +1 -0
- uiml-0.1.0/src/uiml.egg-info/top_level.txt +1 -0
- uiml-0.1.0/tests/test_data.py +20 -0
uiml-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xystudiocode
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
uiml-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uiml
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Home-page: https://github.com/xystudio889/
|
|
5
|
+
Author: xystudio
|
|
6
|
+
Author-email: xystudio <173288240@qq.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/xystudio889/
|
|
9
|
+
Keywords: uiml,PySide6,Qt,ui,xml,design
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: PySide6>=6.10.0
|
|
17
|
+
Dynamic: author
|
|
18
|
+
Dynamic: home-page
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
Dynamic: requires-python
|
|
21
|
+
|
|
22
|
+
<div align="center">
|
|
23
|
+
<img src="./imgs/readme/logo.png" width="400" alt="logo" />
|
|
24
|
+
<h1>uiml</h1>
|
|
25
|
+
一个使用特殊的xml格式,用于绘制PySide6的ui界面
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 介绍
|
|
31
|
+
uiml是一个用于绘制PySide6的ui界面的工具,它使用特殊的xml格式来定义界面的布局,样式和信号。uiml的语法非常简单,易于学习和使用。你可以使用uiml来创建复杂的界面,而不需要编写大量的代码。
|
|
32
|
+
|
|
33
|
+
uiml的语法在xml的基础上,增加支持的python对象,所以它既有xml的轻便性,又有python对象的灵活性。
|
|
34
|
+
|
|
35
|
+
## 安装
|
|
36
|
+
要使用uiml,你需要先安装它。你可以使用pip来安装uiml:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install uiml
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 使用
|
|
43
|
+
要使用uiml,你需要创建一个uiml文件,并在其中定义界面的布局,样式和信号。以下是一个简单的示例:
|
|
44
|
+
|
|
45
|
+
```xml
|
|
46
|
+
<layout name="central_layout" direction="v">
|
|
47
|
+
<QLabel name="text" arg=["This is a label"] />
|
|
48
|
+
<layout name="bottom_layout" direction="h" stretch="true">
|
|
49
|
+
<QPushButton name="ok_button" arg=["Close the window"] style="selected" signals={"clicked": self.close} />
|
|
50
|
+
</layout>
|
|
51
|
+
</layout>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 属性
|
|
55
|
+
### layout
|
|
56
|
+
这是布局对象,可以在子项添加布局对象,或者添加控件对象。
|
|
57
|
+
参数:
|
|
58
|
+
- name:布局对象的名称,用于在代码中引用。
|
|
59
|
+
- direction:布局的方向,可以是“v”(垂直)或“h”(水平),可以自己扩展。
|
|
60
|
+
- stretch:布局对象的拉伸因子,可以是“true”或“false”。
|
|
61
|
+
|
|
62
|
+
### Widget
|
|
63
|
+
这是组件对象,对象的tag名是控件的名称,例如“QLabel”、“QPushButton”等。
|
|
64
|
+
参数:
|
|
65
|
+
- name:控件对象的名称,用于在代码中引用。
|
|
66
|
+
= arg:控件对象的参数,使用列表存储,可以是字符串、列表、字典等。
|
|
67
|
+
- kwarg:控件对象的属性,带有关键字,使用字典存储,可以是字符串、列表、字典等。
|
|
68
|
+
- style:控件对象的样式,可以是字符串、列表、字典等。
|
|
69
|
+
- signals:控件对象的信号,使用字典存储,键是信号名称,值是信号处理函数。
|
|
70
|
+
- init_steps:控件对象的初始化步骤,使用列表存储,子项使用字典。
|
uiml-0.1.0/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="./imgs/readme/logo.png" width="400" alt="logo" />
|
|
3
|
+
<h1>uiml</h1>
|
|
4
|
+
一个使用特殊的xml格式,用于绘制PySide6的ui界面
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 介绍
|
|
10
|
+
uiml是一个用于绘制PySide6的ui界面的工具,它使用特殊的xml格式来定义界面的布局,样式和信号。uiml的语法非常简单,易于学习和使用。你可以使用uiml来创建复杂的界面,而不需要编写大量的代码。
|
|
11
|
+
|
|
12
|
+
uiml的语法在xml的基础上,增加支持的python对象,所以它既有xml的轻便性,又有python对象的灵活性。
|
|
13
|
+
|
|
14
|
+
## 安装
|
|
15
|
+
要使用uiml,你需要先安装它。你可以使用pip来安装uiml:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install uiml
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 使用
|
|
22
|
+
要使用uiml,你需要创建一个uiml文件,并在其中定义界面的布局,样式和信号。以下是一个简单的示例:
|
|
23
|
+
|
|
24
|
+
```xml
|
|
25
|
+
<layout name="central_layout" direction="v">
|
|
26
|
+
<QLabel name="text" arg=["This is a label"] />
|
|
27
|
+
<layout name="bottom_layout" direction="h" stretch="true">
|
|
28
|
+
<QPushButton name="ok_button" arg=["Close the window"] style="selected" signals={"clicked": self.close} />
|
|
29
|
+
</layout>
|
|
30
|
+
</layout>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 属性
|
|
34
|
+
### layout
|
|
35
|
+
这是布局对象,可以在子项添加布局对象,或者添加控件对象。
|
|
36
|
+
参数:
|
|
37
|
+
- name:布局对象的名称,用于在代码中引用。
|
|
38
|
+
- direction:布局的方向,可以是“v”(垂直)或“h”(水平),可以自己扩展。
|
|
39
|
+
- stretch:布局对象的拉伸因子,可以是“true”或“false”。
|
|
40
|
+
|
|
41
|
+
### Widget
|
|
42
|
+
这是组件对象,对象的tag名是控件的名称,例如“QLabel”、“QPushButton”等。
|
|
43
|
+
参数:
|
|
44
|
+
- name:控件对象的名称,用于在代码中引用。
|
|
45
|
+
= arg:控件对象的参数,使用列表存储,可以是字符串、列表、字典等。
|
|
46
|
+
- kwarg:控件对象的属性,带有关键字,使用字典存储,可以是字符串、列表、字典等。
|
|
47
|
+
- style:控件对象的样式,可以是字符串、列表、字典等。
|
|
48
|
+
- signals:控件对象的信号,使用字典存储,键是信号名称,值是信号处理函数。
|
|
49
|
+
- init_steps:控件对象的初始化步骤,使用列表存储,子项使用字典。
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "uiml"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = ""
|
|
9
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "xystudio", email = "173288240@qq.com" }
|
|
12
|
+
]
|
|
13
|
+
license = { text = "MIT" }
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
keywords = ["uiml", "PySide6", "Qt", "ui", "xml", "design"]
|
|
20
|
+
requires-python = ">=3"
|
|
21
|
+
dependencies = [
|
|
22
|
+
"PySide6>=6.10.0"
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/xystudio889/"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools]
|
|
29
|
+
package-dir = { "" = "src" }
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.packages.find]
|
|
32
|
+
where = ["src"]
|
uiml-0.1.0/setup.cfg
ADDED
uiml-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name = "uiml",
|
|
5
|
+
version = "0.1.0",
|
|
6
|
+
package_dir={"": "src"},
|
|
7
|
+
packages=find_packages(where="src"),
|
|
8
|
+
install_requires = ["PySide6>=6.10.0"],
|
|
9
|
+
python_requires = ">=3.10",
|
|
10
|
+
author = "xystudio",
|
|
11
|
+
author_email = "173288240@qq.com",
|
|
12
|
+
description = "",
|
|
13
|
+
long_description = open("README.md",encoding="utf-8").read(),
|
|
14
|
+
long_description_content_type="text/markdown",
|
|
15
|
+
license = "MIT",
|
|
16
|
+
url = "https://github.com/xystudio889/",
|
|
17
|
+
classifiers=[
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
],
|
|
22
|
+
keywords = ['uiml', 'PySide6', 'Qt', 'ui', 'xml', 'design'],
|
|
23
|
+
)
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
from uiml.uilib import *
|
|
3
|
+
import inspect
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
_CURRENT_MODULE_NAME = __name__
|
|
7
|
+
|
|
8
|
+
def xml_parse(xml_str: str, namespace: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
9
|
+
'''
|
|
10
|
+
将扩展 XML 字符串转换为 Python 字典。
|
|
11
|
+
|
|
12
|
+
参数:
|
|
13
|
+
xml_str: XML 字符串,支持属性值不带引号、Python 字面量(列表、字典、集合)、
|
|
14
|
+
变量/函数引用、表达式以及 true/false/null 字面量(任意嵌套)。
|
|
15
|
+
namespace: 解析函数/变量时使用的命名空间(默认为调用方的全局+局部变量合并)。
|
|
16
|
+
|
|
17
|
+
返回:
|
|
18
|
+
解析后的字典,包含 'name' 键(标签名)、'content' 键(子元素或文本)以及所有属性。
|
|
19
|
+
'''
|
|
20
|
+
if namespace is None:
|
|
21
|
+
# 获取当前帧
|
|
22
|
+
frame = inspect.currentframe().f_back
|
|
23
|
+
# 向上查找,跳过所有属于 uiml 模块的帧
|
|
24
|
+
while frame and frame.f_globals.get('__name__') == _CURRENT_MODULE_NAME:
|
|
25
|
+
frame = frame.f_back
|
|
26
|
+
if frame is None:
|
|
27
|
+
# 如果没有找到非 uiml 的帧,则回退到直接调用者的上一级(兼容性)
|
|
28
|
+
frame = inspect.currentframe().f_back
|
|
29
|
+
# 合并全局和局部变量(局部优先)
|
|
30
|
+
namespace = {}
|
|
31
|
+
namespace.update(frame.f_globals)
|
|
32
|
+
namespace.update(frame.f_locals)
|
|
33
|
+
|
|
34
|
+
idx = 0
|
|
35
|
+
n = len(xml_str)
|
|
36
|
+
|
|
37
|
+
def skip_whitespace():
|
|
38
|
+
nonlocal idx
|
|
39
|
+
while idx < n and xml_str[idx].isspace():
|
|
40
|
+
idx += 1
|
|
41
|
+
|
|
42
|
+
def peek_char() -> str:
|
|
43
|
+
return xml_str[idx] if idx < n else ''
|
|
44
|
+
|
|
45
|
+
def parse_comment():
|
|
46
|
+
nonlocal idx
|
|
47
|
+
if xml_str.startswith('<!--', idx):
|
|
48
|
+
idx += 4
|
|
49
|
+
end = xml_str.find('-->', idx)
|
|
50
|
+
if end == -1:
|
|
51
|
+
raise ValueError("未找到注释结束标记 '-->'")
|
|
52
|
+
idx = end + 3
|
|
53
|
+
return None
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
def parse_text() -> str:
|
|
57
|
+
nonlocal idx
|
|
58
|
+
start = idx
|
|
59
|
+
while idx < n and xml_str[idx] != '<':
|
|
60
|
+
idx += 1
|
|
61
|
+
return xml_str[start:idx]
|
|
62
|
+
|
|
63
|
+
def parse_tag_name_with_lt() -> str:
|
|
64
|
+
nonlocal idx
|
|
65
|
+
if xml_str[idx] != '<':
|
|
66
|
+
raise ValueError(f"期望 '<',实际 '{xml_str[idx]}'")
|
|
67
|
+
idx += 1
|
|
68
|
+
start = idx
|
|
69
|
+
while idx < n and (xml_str[idx].isalnum() or xml_str[idx] == '_'):
|
|
70
|
+
idx += 1
|
|
71
|
+
if start == idx:
|
|
72
|
+
raise ValueError('无效的标签名')
|
|
73
|
+
return xml_str[start:idx]
|
|
74
|
+
|
|
75
|
+
def parse_tag_name_only() -> str:
|
|
76
|
+
nonlocal idx
|
|
77
|
+
start = idx
|
|
78
|
+
while idx < n and (xml_str[idx].isalnum() or xml_str[idx] == '_'):
|
|
79
|
+
idx += 1
|
|
80
|
+
if start == idx:
|
|
81
|
+
raise ValueError('无效的标签名')
|
|
82
|
+
return xml_str[start:idx]
|
|
83
|
+
|
|
84
|
+
def preprocess_expr(expr: str) -> str:
|
|
85
|
+
# 将表达式中的独立 true/false/null 替换为 Python 字面量
|
|
86
|
+
expr = re.sub(r'\btrue\b', 'True', expr)
|
|
87
|
+
expr = re.sub(r'\bfalse\b', 'False', expr)
|
|
88
|
+
expr = re.sub(r'\bnull\b', 'None', expr)
|
|
89
|
+
return expr
|
|
90
|
+
|
|
91
|
+
def parse_attribute_value() -> Any:
|
|
92
|
+
nonlocal idx
|
|
93
|
+
if idx >= n:
|
|
94
|
+
raise ValueError('属性值意外结束')
|
|
95
|
+
|
|
96
|
+
ch = xml_str[idx]
|
|
97
|
+
if ch in ('"', "'"):
|
|
98
|
+
quote = ch
|
|
99
|
+
idx += 1
|
|
100
|
+
start = idx
|
|
101
|
+
while idx < n and xml_str[idx] != quote:
|
|
102
|
+
if xml_str[idx] == '\\' and idx + 1 < n:
|
|
103
|
+
idx += 2
|
|
104
|
+
else:
|
|
105
|
+
idx += 1
|
|
106
|
+
if idx >= n or xml_str[idx] != quote:
|
|
107
|
+
raise ValueError(f'未找到匹配的引号 {quote}')
|
|
108
|
+
value_str = xml_str[start:idx]
|
|
109
|
+
idx += 1
|
|
110
|
+
return value_str
|
|
111
|
+
|
|
112
|
+
start = idx
|
|
113
|
+
depth = 0
|
|
114
|
+
while idx < n:
|
|
115
|
+
c = xml_str[idx]
|
|
116
|
+
if c in '[{':
|
|
117
|
+
depth += 1
|
|
118
|
+
elif c in ']}':
|
|
119
|
+
depth -= 1
|
|
120
|
+
elif depth == 0 and (c.isspace() or c == '>' or (c == '/' and idx + 1 < n and xml_str[idx + 1] == '>')):
|
|
121
|
+
break
|
|
122
|
+
idx += 1
|
|
123
|
+
expr = xml_str[start:idx].strip()
|
|
124
|
+
if not expr:
|
|
125
|
+
raise ValueError('空的属性值')
|
|
126
|
+
|
|
127
|
+
expr_processed = preprocess_expr(expr)
|
|
128
|
+
try:
|
|
129
|
+
return eval(expr_processed, namespace)
|
|
130
|
+
except NameError as e:
|
|
131
|
+
raise NameError(f"未定义的标识符 '{expr}',请检查命名空间或拼写") from e
|
|
132
|
+
except Exception as e:
|
|
133
|
+
raise ValueError(f"无法解析属性值 '{expr}': {e}")
|
|
134
|
+
|
|
135
|
+
def parse_attributes() -> Dict[str, Any]:
|
|
136
|
+
nonlocal idx
|
|
137
|
+
attrs = {}
|
|
138
|
+
while idx < n and xml_str[idx] != '>' and not (xml_str[idx] == '/' and idx + 1 < n and xml_str[idx + 1] == '>'):
|
|
139
|
+
skip_whitespace()
|
|
140
|
+
if xml_str[idx] == '>' or (xml_str[idx] == '/' and idx + 1 < n and xml_str[idx + 1] == '>'):
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
start = idx
|
|
144
|
+
while idx < n and (xml_str[idx].isalnum() or xml_str[idx] == '_'):
|
|
145
|
+
idx += 1
|
|
146
|
+
if start == idx:
|
|
147
|
+
raise ValueError('无效的属性名')
|
|
148
|
+
attr_name = xml_str[start:idx]
|
|
149
|
+
|
|
150
|
+
skip_whitespace()
|
|
151
|
+
if xml_str[idx] != '=':
|
|
152
|
+
raise ValueError(f"属性 '{attr_name}' 后缺少 '='")
|
|
153
|
+
idx += 1
|
|
154
|
+
|
|
155
|
+
skip_whitespace()
|
|
156
|
+
attr_value = parse_attribute_value()
|
|
157
|
+
attrs[attr_name] = attr_value
|
|
158
|
+
|
|
159
|
+
return attrs
|
|
160
|
+
|
|
161
|
+
def parse_element() -> Dict[str, Any]:
|
|
162
|
+
nonlocal idx
|
|
163
|
+
|
|
164
|
+
while True:
|
|
165
|
+
skip_whitespace()
|
|
166
|
+
if xml_str.startswith('<!--', idx):
|
|
167
|
+
if parse_comment() is None:
|
|
168
|
+
continue
|
|
169
|
+
break
|
|
170
|
+
|
|
171
|
+
if peek_char() != '<':
|
|
172
|
+
text = parse_text()
|
|
173
|
+
if text.strip():
|
|
174
|
+
return {"content": text.strip()}
|
|
175
|
+
return {}
|
|
176
|
+
|
|
177
|
+
tag_name = parse_tag_name_with_lt()
|
|
178
|
+
skip_whitespace()
|
|
179
|
+
attrs = parse_attributes()
|
|
180
|
+
|
|
181
|
+
if xml_str[idx] == '/' and idx + 1 < n and xml_str[idx + 1] == '>':
|
|
182
|
+
idx += 2
|
|
183
|
+
return {"type": tag_name, **attrs}
|
|
184
|
+
|
|
185
|
+
if xml_str[idx] != '>':
|
|
186
|
+
raise ValueError(f"期望 '>',实际 '{xml_str[idx]}'")
|
|
187
|
+
idx += 1
|
|
188
|
+
|
|
189
|
+
children = []
|
|
190
|
+
text_content = None
|
|
191
|
+
|
|
192
|
+
while idx < n:
|
|
193
|
+
skip_whitespace()
|
|
194
|
+
if idx >= n:
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
if xml_str.startswith('<!--', idx):
|
|
198
|
+
parse_comment()
|
|
199
|
+
continue
|
|
200
|
+
|
|
201
|
+
if xml_str.startswith('</', idx):
|
|
202
|
+
idx += 2
|
|
203
|
+
end_tag_name = parse_tag_name_only()
|
|
204
|
+
skip_whitespace()
|
|
205
|
+
if xml_str[idx] != '>':
|
|
206
|
+
raise ValueError(f"期望 '>',实际 '{xml_str[idx]}'")
|
|
207
|
+
idx += 1
|
|
208
|
+
if end_tag_name != tag_name:
|
|
209
|
+
raise ValueError(f"结束标签不匹配: 期望 '{tag_name}',实际 '{end_tag_name}'")
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
if xml_str[idx] == '<':
|
|
213
|
+
child = parse_element()
|
|
214
|
+
if child:
|
|
215
|
+
children.append(child)
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
text = parse_text()
|
|
219
|
+
if text:
|
|
220
|
+
if children:
|
|
221
|
+
pass
|
|
222
|
+
else:
|
|
223
|
+
text_content = text.strip()
|
|
224
|
+
|
|
225
|
+
result = {"type": tag_name, **attrs}
|
|
226
|
+
if text_content is not None:
|
|
227
|
+
result["content"] = text_content
|
|
228
|
+
elif children:
|
|
229
|
+
result["content"] = children
|
|
230
|
+
return result
|
|
231
|
+
|
|
232
|
+
skip_whitespace()
|
|
233
|
+
root = parse_element()
|
|
234
|
+
return root
|
|
235
|
+
|
|
236
|
+
def xml_parse_file(file_path: str, namespace: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
237
|
+
'''
|
|
238
|
+
从文件中读取扩展 XML 字符串并解析为 Python 字典。
|
|
239
|
+
|
|
240
|
+
参数:
|
|
241
|
+
file_path: 文件路径,支持属性值不带引号、Python 字面量(列表、字典、集合)、
|
|
242
|
+
变量/函数引用、表达式等。
|
|
243
|
+
namespace: 解析函数/变量时使用的命名空间(默认为调用方的全局命名空间)。
|
|
244
|
+
'''
|
|
245
|
+
with open(file_path, 'r', encoding='utf-8') as file:
|
|
246
|
+
xml_str = file.read() # 读取文件内容
|
|
247
|
+
return xml_parse(xml_str, namespace) # 解析 XML 字符串
|
|
248
|
+
|
|
249
|
+
def set_style(widget, class_name: str):
|
|
250
|
+
'''
|
|
251
|
+
设置按钮的class属性并刷新样式
|
|
252
|
+
'''
|
|
253
|
+
# 1. 设置class属性
|
|
254
|
+
widget.setProperty('class', class_name)
|
|
255
|
+
|
|
256
|
+
# 2. 强制样式刷新
|
|
257
|
+
widget.style().unpolish(widget)
|
|
258
|
+
widget.style().polish(widget)
|
|
259
|
+
|
|
260
|
+
# 3. 触发重绘
|
|
261
|
+
widget.update()
|
|
262
|
+
|
|
263
|
+
def set_namespace(value_replace_func=None, layout_parser_func=None, widget_parser_func=None):
|
|
264
|
+
'''
|
|
265
|
+
设置解析函数/变量时使用的操作。
|
|
266
|
+
'''
|
|
267
|
+
global replacer, layout_parser, widget_parser
|
|
268
|
+
if value_replace_func is not None:replacer = value_replace_func
|
|
269
|
+
if layout_parser_func is not None:layout_parser = layout_parser_func
|
|
270
|
+
if widget_parser_func is not None:widget_parser = widget_parser_func
|
|
271
|
+
|
|
272
|
+
def default_replacer(value: str):
|
|
273
|
+
'''默认替换函数,返回原值,可以自定义添加函数,如替换变量、函数调用等。'''
|
|
274
|
+
return value
|
|
275
|
+
|
|
276
|
+
def default_layout_parser(ui_data: Dict[str, Any]):
|
|
277
|
+
'''默认布局解析函数,进行递归解析返回'''
|
|
278
|
+
compiled_list = []
|
|
279
|
+
for item in ui_data.get('content', []):
|
|
280
|
+
compiled_list.append(compile_ui(item)) # 递归解析
|
|
281
|
+
return {'name': ui_data.get('name'), 'direction': ui_data.get('direction'), 'content': compiled_list, 'stretch': ui_data.get('stretch', False)}
|
|
282
|
+
|
|
283
|
+
def default_widget_parser(widget_data: Dict[str, Any], namespace: Dict[str, Any]):
|
|
284
|
+
'''默认控件解析函数,进行解析返回'''
|
|
285
|
+
argv = [] # 参数
|
|
286
|
+
kwargv = {} # 关键字参数
|
|
287
|
+
style = widget_data.get('style', '') # 样式
|
|
288
|
+
|
|
289
|
+
for arg in widget_data.get('arg', []):
|
|
290
|
+
if type(arg) == str:
|
|
291
|
+
argv.append(replacer(arg))
|
|
292
|
+
else:
|
|
293
|
+
argv.append(arg)
|
|
294
|
+
|
|
295
|
+
for k, arg in widget_data.get('kwarg', {}).items():
|
|
296
|
+
if type(arg) == str:
|
|
297
|
+
kwargv[k] = replacer(arg)
|
|
298
|
+
else:
|
|
299
|
+
kwargv[k] = arg
|
|
300
|
+
|
|
301
|
+
widget = eval(f'{widget_data.get('type')}(*{argv}, **{kwargv})') # 获取函数
|
|
302
|
+
set_style(widget, style) # 设置样式
|
|
303
|
+
|
|
304
|
+
for func in widget_data.get('init_steps', []):
|
|
305
|
+
name = func['name']
|
|
306
|
+
args = func.get('args', [])
|
|
307
|
+
kwargs = func.get('kwargs', {})
|
|
308
|
+
getattr(widget, name)(*args, **kwargs) # 执行初始化步骤
|
|
309
|
+
|
|
310
|
+
for sign, func in widget_data.get('signals', {}).items():
|
|
311
|
+
getattr(widget, sign).connect(func) # 连接信号
|
|
312
|
+
|
|
313
|
+
return {'name': widget_data.get('name'), 'content': widget}
|
|
314
|
+
|
|
315
|
+
def compile_ui(ui_data, namespace=None):
|
|
316
|
+
if namespace is None:
|
|
317
|
+
namespace = inspect.currentframe().f_back.f_globals
|
|
318
|
+
if ui_data.get('direction'): # 这是layout类型
|
|
319
|
+
return layout_parser(ui_data)
|
|
320
|
+
else: # 这是widget类型
|
|
321
|
+
return widget_parser(ui_data, namespace)
|
|
322
|
+
|
|
323
|
+
def compile_ui_file(file_path: str, namespace=None):
|
|
324
|
+
'''
|
|
325
|
+
从文件中读取扩展 XML 字符串并解析为 Python 字典,然后编译为布局或控件对象。
|
|
326
|
+
|
|
327
|
+
参数:
|
|
328
|
+
file_path: 文件路径,支持属性值不带引号、Python 字面量(列表、字典、集合)、变量/函数引用、表达式等。
|
|
329
|
+
namespace: 解析函数/变量时使用的命名空间(默认为调用方的全局命名空间)。
|
|
330
|
+
'''
|
|
331
|
+
with open(file_path, 'r', encoding='utf-8') as file:
|
|
332
|
+
xml_str = file.read() # 读取文件内容
|
|
333
|
+
return compile_ui(xml_parse(xml_str, namespace), namespace) # 解析 XML 字符串并编译
|
|
334
|
+
|
|
335
|
+
class UIMLLayout:
|
|
336
|
+
def __init__(self, list):
|
|
337
|
+
self.list = list
|
|
338
|
+
|
|
339
|
+
def find_widget(self, path: str, data=None):
|
|
340
|
+
'''
|
|
341
|
+
在嵌套字典结构中按点分隔路径查找元素。
|
|
342
|
+
|
|
343
|
+
规则:
|
|
344
|
+
- 路径中的每一段对应字典中的 'name' 字段。
|
|
345
|
+
- 若当前节点是布局(含有 'direction' 键),且不是最后一段,则自动进入其子元素继续查找。
|
|
346
|
+
- 布局的子元素在 'content' 列表中。
|
|
347
|
+
- 最后一段如果是普通布局,返回其 'content' 列表;如果是 'u' 布局,返回 {'texts':..., 'inputs':..., 'combos':...};如果是控件,返回其 'content'。
|
|
348
|
+
- 路径必须完整且精确,找不到时抛出 KeyError。
|
|
349
|
+
|
|
350
|
+
参数:
|
|
351
|
+
path: 点分隔的路径字符串,如 "layout.vlayout.checkbox2"
|
|
352
|
+
data: 根字典(例如 {'name': 'layout', 'direction': 'h', 'content': [...]})
|
|
353
|
+
|
|
354
|
+
返回:
|
|
355
|
+
根据路径找到的控件对象、布局的 content 列表,或 'u' 布局的 texts/inputs/combos 字典。
|
|
356
|
+
'''
|
|
357
|
+
data = self.list if data is None else data
|
|
358
|
+
parts = path.split('.')
|
|
359
|
+
if not parts:
|
|
360
|
+
raise ValueError("Empty path")
|
|
361
|
+
|
|
362
|
+
# 根节点名称必须匹配第一段
|
|
363
|
+
if data.get('name') != parts[0]:
|
|
364
|
+
raise KeyError(f"Root name mismatch: expected '{parts[0]}', got '{data.get('name')}'")
|
|
365
|
+
|
|
366
|
+
current = data
|
|
367
|
+
|
|
368
|
+
for i, part in enumerate(parts):
|
|
369
|
+
# 检查当前节点名称是否匹配
|
|
370
|
+
if current.get('name') != part:
|
|
371
|
+
raise KeyError(f"Name mismatch: expected '{part}', got '{current.get('name')}'")
|
|
372
|
+
|
|
373
|
+
# 最后一段
|
|
374
|
+
if i == len(parts) - 1:
|
|
375
|
+
if 'direction' in current:
|
|
376
|
+
# 普通布局:返回 content 列表
|
|
377
|
+
content = current.get('content')
|
|
378
|
+
if content is None:
|
|
379
|
+
raise ValueError(f"Layout '{part}' has no content")
|
|
380
|
+
if not isinstance(content, list):
|
|
381
|
+
raise TypeError(f"Layout '{part}' content is not a list")
|
|
382
|
+
return content
|
|
383
|
+
else:
|
|
384
|
+
# 控件:返回 content 属性
|
|
385
|
+
content = current.get('content')
|
|
386
|
+
if content is None:
|
|
387
|
+
raise ValueError(f"Widget '{part}' has no content")
|
|
388
|
+
return content
|
|
389
|
+
|
|
390
|
+
# 不是最后一段,当前节点必须是布局
|
|
391
|
+
if 'direction' not in current:
|
|
392
|
+
raise KeyError(f"'{part}' is not a layout, cannot traverse further")
|
|
393
|
+
|
|
394
|
+
next_name = parts[i + 1]
|
|
395
|
+
found = None
|
|
396
|
+
|
|
397
|
+
# 普通布局从 content 中查找
|
|
398
|
+
for child in current.get('content', []):
|
|
399
|
+
if child.get('name') == next_name:
|
|
400
|
+
found = child
|
|
401
|
+
break
|
|
402
|
+
|
|
403
|
+
if found is None:
|
|
404
|
+
raise KeyError(f"Child '{next_name}' not found in layout '{part}'")
|
|
405
|
+
current = found
|
|
406
|
+
|
|
407
|
+
# 正常流程不会执行到这里
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
def show(self):
|
|
411
|
+
'''显示布局,返回布局对象和类型字符串'''
|
|
412
|
+
return self.draw_layout()[0]
|
|
413
|
+
|
|
414
|
+
def draw_layout(self, list_content=None):
|
|
415
|
+
'''绘制布局,返回布局对象和类型字符串'''
|
|
416
|
+
list_content = self.list if list_content is None else list_content
|
|
417
|
+
if list_content.get('direction') is not None: # 这是layout类型
|
|
418
|
+
if list_content['direction'].lower() == 'h':
|
|
419
|
+
layout = QHBoxLayout()
|
|
420
|
+
elif list_content['direction'].lower() == 'v':
|
|
421
|
+
layout = QVBoxLayout()
|
|
422
|
+
else:
|
|
423
|
+
return self.extend_layout(list_content)
|
|
424
|
+
if list_content.get('stretch', False):
|
|
425
|
+
layout.addStretch(1)
|
|
426
|
+
for item in list_content['content']:
|
|
427
|
+
widget = self.draw_layout(item)
|
|
428
|
+
if widget[1] == 'widget':
|
|
429
|
+
layout.addWidget(widget[0])
|
|
430
|
+
elif widget[1] == 'layout':
|
|
431
|
+
layout.addLayout(widget[0])
|
|
432
|
+
else:
|
|
433
|
+
raise ValueError('Content must be a widget or a layout')
|
|
434
|
+
return layout, 'layout'
|
|
435
|
+
else: # 这是widget类型
|
|
436
|
+
return list_content['content'], 'widget'
|
|
437
|
+
|
|
438
|
+
def extend_layout(self, list_info):
|
|
439
|
+
'''扩展布局,可以在不复制draw_layout()函数的情况下,直接在原布局上添加加载时代码'''
|
|
440
|
+
WidgetError.direction_error() # 默认这里抛出错误,因为widget类型没有direction属性
|
|
441
|
+
|
|
442
|
+
def extend_widget(self, widget_info):
|
|
443
|
+
'''扩展控件,可以在不复制draw_layout()函数的情况下,直接在原控件上添加加载时代码'''
|
|
444
|
+
return widget_info['content'], 'widget'
|
|
445
|
+
|
|
446
|
+
class WidgetError:
|
|
447
|
+
@staticmethod
|
|
448
|
+
def direction_error():
|
|
449
|
+
'''错误方案:在widget类型中找不到direction属性时,抛出错误。可以自定义添加函数,如替换变量、函数调用等。'''
|
|
450
|
+
raise ValueError('Direction error: cannot find direction attribute in widget type')
|
|
451
|
+
|
|
452
|
+
set_namespace(default_replacer, default_layout_parser, default_widget_parser) # 设置默认解析函数和布局解析函数
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uiml
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Home-page: https://github.com/xystudio889/
|
|
5
|
+
Author: xystudio
|
|
6
|
+
Author-email: xystudio <173288240@qq.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/xystudio889/
|
|
9
|
+
Keywords: uiml,PySide6,Qt,ui,xml,design
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: PySide6>=6.10.0
|
|
17
|
+
Dynamic: author
|
|
18
|
+
Dynamic: home-page
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
Dynamic: requires-python
|
|
21
|
+
|
|
22
|
+
<div align="center">
|
|
23
|
+
<img src="./imgs/readme/logo.png" width="400" alt="logo" />
|
|
24
|
+
<h1>uiml</h1>
|
|
25
|
+
一个使用特殊的xml格式,用于绘制PySide6的ui界面
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 介绍
|
|
31
|
+
uiml是一个用于绘制PySide6的ui界面的工具,它使用特殊的xml格式来定义界面的布局,样式和信号。uiml的语法非常简单,易于学习和使用。你可以使用uiml来创建复杂的界面,而不需要编写大量的代码。
|
|
32
|
+
|
|
33
|
+
uiml的语法在xml的基础上,增加支持的python对象,所以它既有xml的轻便性,又有python对象的灵活性。
|
|
34
|
+
|
|
35
|
+
## 安装
|
|
36
|
+
要使用uiml,你需要先安装它。你可以使用pip来安装uiml:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install uiml
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 使用
|
|
43
|
+
要使用uiml,你需要创建一个uiml文件,并在其中定义界面的布局,样式和信号。以下是一个简单的示例:
|
|
44
|
+
|
|
45
|
+
```xml
|
|
46
|
+
<layout name="central_layout" direction="v">
|
|
47
|
+
<QLabel name="text" arg=["This is a label"] />
|
|
48
|
+
<layout name="bottom_layout" direction="h" stretch="true">
|
|
49
|
+
<QPushButton name="ok_button" arg=["Close the window"] style="selected" signals={"clicked": self.close} />
|
|
50
|
+
</layout>
|
|
51
|
+
</layout>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 属性
|
|
55
|
+
### layout
|
|
56
|
+
这是布局对象,可以在子项添加布局对象,或者添加控件对象。
|
|
57
|
+
参数:
|
|
58
|
+
- name:布局对象的名称,用于在代码中引用。
|
|
59
|
+
- direction:布局的方向,可以是“v”(垂直)或“h”(水平),可以自己扩展。
|
|
60
|
+
- stretch:布局对象的拉伸因子,可以是“true”或“false”。
|
|
61
|
+
|
|
62
|
+
### Widget
|
|
63
|
+
这是组件对象,对象的tag名是控件的名称,例如“QLabel”、“QPushButton”等。
|
|
64
|
+
参数:
|
|
65
|
+
- name:控件对象的名称,用于在代码中引用。
|
|
66
|
+
= arg:控件对象的参数,使用列表存储,可以是字符串、列表、字典等。
|
|
67
|
+
- kwarg:控件对象的属性,带有关键字,使用字典存储,可以是字符串、列表、字典等。
|
|
68
|
+
- style:控件对象的样式,可以是字符串、列表、字典等。
|
|
69
|
+
- signals:控件对象的信号,使用字典存储,键是信号名称,值是信号处理函数。
|
|
70
|
+
- init_steps:控件对象的初始化步骤,使用列表存储,子项使用字典。
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
src/uiml/__init__.py
|
|
6
|
+
src/uiml/uilib.py
|
|
7
|
+
src/uiml.egg-info/PKG-INFO
|
|
8
|
+
src/uiml.egg-info/SOURCES.txt
|
|
9
|
+
src/uiml.egg-info/dependency_links.txt
|
|
10
|
+
src/uiml.egg-info/requires.txt
|
|
11
|
+
src/uiml.egg-info/top_level.txt
|
|
12
|
+
tests/test_data.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PySide6>=6.10.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uiml
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import uiml
|
|
2
|
+
|
|
3
|
+
def function_a(a, b):
|
|
4
|
+
return a, b
|
|
5
|
+
|
|
6
|
+
def test_data():
|
|
7
|
+
with open('tests/test-data.gui', 'r') as f:
|
|
8
|
+
data = f.read()
|
|
9
|
+
data = uiml.xml_parse(data) # This will raise an exception if the XML is not valid or if there are any errors in the data.
|
|
10
|
+
|
|
11
|
+
app = uiml.QApplication()
|
|
12
|
+
ui = uiml.compile_ui(data)
|
|
13
|
+
widget = uiml.QWidget()
|
|
14
|
+
layout = uiml.UIWindow(ui).show()
|
|
15
|
+
widget.setLayout(layout)
|
|
16
|
+
widget.show()
|
|
17
|
+
app.exec() # This will start the Qt event loop and display the widget.
|
|
18
|
+
|
|
19
|
+
if __name__ == '__main__':
|
|
20
|
+
test_data()
|