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 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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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,4 @@
1
+ from PySide6.QtCore import *
2
+ from PySide6.QtWidgets import *
3
+ from PySide6.QtGui import *
4
+ import sys
@@ -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
+ 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()