在下文基础上添加了一些自动生成的脚本:
https://blog.csdn.net/xulibo5828/article/details/145801919
模板简述:
- 目录结构:
python
pyside6_project
├── DataBase/
├── Job/
├── Settings/
├── UI/
│ ├── SRC/
│ ├── UI_forms/
│ │ ├── UI_process.py
│ │ ├── __init__.py
│ │ ├── base_form.ui
│ │ ├── main_window/
│ │ │ ├── main_window.ui
│ │ │ ├── main_window_func.py
│ │ │ ├── main_window_ui.py
│ │ │ └── 本目录说明.md
│ │ ├── window_1/
│ │ │ ├── window_1.ui
│ │ │ ├── window_1_func.py
│ │ │ └── window_1_ui.py
│ │ └── 本目录说明.md
│ ├── UI_functions/
│ │ ├── __init__.py
│ │ ├── __pycache__
│ │ └── 本目录说明.md
│ ├── __init__.py
│ └── settings/
├── main.py
├── tmp/
└── UI_project.py
UI/UI_forms/目录下保存有界面文件,按照文件同名目录存放。每个目录下有三个主要文件:
- xxx.ui:使用designer组态的界面文件,这里只设计画面的的图像特征,亦即只设计前端的显示特征,不牵涉后端任何的逻辑和功能,为前后端分离打下基础。
- xxx_ui.py:使用pyuic将xxx.ui转换为xxx_ui.py,这就是界面的前端模块脚本,这个文件纯自动生成,严禁手动编辑,严格遵守前后端分离的原则。
- **xxx_func.py:**后端代码,使用自编脚本自动创建后端代码的基础框架,然后手动补充完成所有的功能函数和逻辑处理。
使用流程:
1、创建项目的文件目录。
2、使用designer组态界面文件,保存在同名目录下。
3、运行UI_process.py,一键生成前端模块和后端基本框架的脚本。
4、完成后端代码,在main.py中调用。
当界面文件发生增减、编辑、改变名称,再运行一次UI_process.py,半自动更新项目。界面的各个文件名称是由它所在的文件夹名用UI_process.py自动更新的。
主要脚本内容:
- main.py:主程序
python
# -*- coding: utf-8 -*-
import os
import sys
from PySide6.QtWidgets import QApplication
from UI import UI_functions # 导入所有功能函数
current_dir = os.path.dirname(os.path.abspath(__file__)) # 获取当前脚本所在目录
def main():
UI_functions.add_forms() # 添加所有组态好的窗口到uiproj对象中
if __name__ == "__main__":
app = QApplication(sys.argv) # 创建一个QApplication对象
main() # 执行主函数
sys.exit(app.exec()) # 结束主循环后退出程序
- UI_project.py:创建一个项目实体(QObject),用来管理和协调各个画面以及各种变量、信号、操控、反馈等。对于本项目而言,这个项目实体(QObject)的直系成员都是全局成员。这样,通过这个项目实体,就很容易在各个画面和功能作业函数之间进行数据交互。
python
# 这里定义项目级别的信号和变量、参数等
from PySide6.QtCore import Signal, QObject, Slot
# # 获取当前脚本所在目录
# current_dir = os.path.dirname(os.path.abspath(__file__))
# # 构建上一级目录的路径
# parent_dir = os.path.dirname(current_dir)
# # 构建数据库路径
# db_file_path = os.path.join(parent_dir, "DataBase", "project_db.db")
# 定义全局的ui项目类
class UiProj(QObject):
# 在这里定义全局信号
global_signal_1 = Signal() # 一个全局信号示例
def __init__(self):
super(UiProj, self).__init__()
self.forms = []
self.run()
def run(self):
self.slot_signal() # 连接全局的信号和槽函数
self.num = 0 # 一个全局变量示例
# 信号和槽函数
def slot_signal(self):
# 全局的一个槽函数示例
@Slot()
def slot_1():
self.num += 2
self.form_window_1.label.setText(f"全局信号的槽函数{self.num}") # 使用全局信号跨窗口传递
self.global_signal_1.connect(slot_1) # 连接信号和槽函数,尽可能使槽函数和信号连接成对出现,方便管理。
uiproj = UiProj() # 生成全局的ui项目对象
- UI_process.py:读取UI/UI_forms/目录下各个子文件夹下的.ui文件,并自动生成前端模块和后端基本框架的脚本。
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import sys
import xml.etree.ElementTree as ET
import re
from pathlib import Path
# Windows终端编码设置,避免中文乱码
if sys.platform == "win32":
os.system("chcp 65001 > nul")
# 定义UI根目录(脚本运行目录)
UI_ROOT = Path.cwd()
def get_conda_scripts_path():
"""自动推导当前Python环境的Scripts目录,无需硬编码"""
python_exe = Path(sys.executable)
scripts_path = python_exe.parent / "Scripts"
pyside6_uic_path = scripts_path / "pyside6-uic.exe"
if pyside6_uic_path.exists():
return str(pyside6_uic_path)
raise FileNotFoundError(
f"未找到pyside6-uic.exe,请检查PySide6是否安装!\n查找路径:{scripts_path}"
)
def get_ui_widget_class(ui_file_path: Path) -> str:
"""解析ui文件,获取第一个widget class属性值"""
try:
tree = ET.parse(ui_file_path)
root = tree.getroot()
for elem in root.iter():
if elem.tag.endswith('widget') and 'class' in elem.attrib:
return elem.attrib['class']
return 'QWidget'
except Exception as e:
print(f"解析UI文件失败: {e}")
return 'QWidget'
def get_ui_frontend_class(ui_py_path: Path) -> str:
"""从生成的_ui.py文件中获取前端类名"""
try:
with open(ui_py_path, 'r', encoding='utf-8') as f:
content = f.read()
match = re.search(r'class Ui_(\w+)\(object\):', content)
if match:
return f"Ui_{match.group(1)}"
return 'Ui_Form'
except Exception as e:
print(f"解析前端类名失败: {e}")
return 'Ui_Form'
def update_func_file_content(func_file: Path, subdir_name: str, widget_class: str, frontend_class: str):
"""
检查并更新_func.py文件的关键代码行,保留其他内容不变
:param func_file: 目标func文件路径
:param subdir_name: 子目录名
:param widget_class: UI文件解析出的widget类名(如QMainWindow/QWidget)
:param frontend_class: _ui.py中的前端类名(如Ui_Form)
"""
try:
with open(func_file, 'r', encoding='utf-8') as f:
lines = f.readlines()
updated_lines = []
# 标记是否已处理过目标行,避免重复添加
imported_widget = False
imported_ui = False
class_defined = False
for line in lines:
# 1. 检查并修正:from PySide6.QtWidgets import XXXX
if re.match(r'^\s*from PySide6.QtWidgets import ', line.strip()):
updated_line = f"from PySide6.QtWidgets import {widget_class}\n"
updated_lines.append(updated_line)
imported_widget = True
print(f" 修正widget导入行: {line.strip()} -> {updated_line.strip()}")
# 2. 检查并修正:from . import XXXX_ui
elif re.match(r'^\s*from \. import .*_ui', line.strip()):
updated_line = f"from . import {subdir_name}_ui\n"
updated_lines.append(updated_line)
imported_ui = True
print(f" 修正UI模块导入行: {line.strip()} -> {updated_line.strip()}")
# 3. 检查并修正:class form_XXX(XXX, XXX_ui.XXX):
elif re.match(r'^\s*class form_\w+\(', line.strip()):
updated_line = f"class form_{subdir_name}({widget_class}, {subdir_name}_ui.{frontend_class}):\n"
updated_lines.append(updated_line)
class_defined = True
print(f" 修正类定义行: {line.strip()} -> {updated_line.strip()}")
# 其他行保持不变
else:
updated_lines.append(line)
# 若关键行缺失,补充到文件开头(防止文件内容异常)
if not imported_widget:
updated_lines.insert(0, f"from PySide6.QtWidgets import {widget_class}\n")
if not imported_ui:
updated_lines.insert(1, f"from . import {subdir_name}_ui\n")
if not class_defined:
# 找合适位置插入类定义(简单处理:插入到导入行后)
insert_pos = 2
while insert_pos < len(updated_lines) and updated_lines[insert_pos].strip().startswith('from '):
insert_pos += 1
class_def = f"\n# 定义窗口类\nclass form_{subdir_name}({widget_class}, {subdir_name}_ui.{frontend_class}):\n"
class_def += " def __init__(self):\n super().__init__()\n self.setupUi(self)\n self.run()\n\n def run(self):\n pass\n"
updated_lines.insert(insert_pos, class_def)
# 写入更新后的内容
with open(func_file, 'w', encoding='utf-8') as f:
f.writelines(updated_lines)
print(f" 完成{func_file.name}内容检查与修正")
except Exception as e:
print(f" 警告:更新{func_file.name}内容失败 - {e}")
def process_subdirectory(subdir: Path):
"""处理单个子目录"""
# 自动获取pyside6-uic路径
try:
pyside6_uic_path = get_conda_scripts_path()
except FileNotFoundError as e:
print(f"\n处理目录 {subdir.name} 失败: {e}")
return
subdir_name = subdir.name
print(f"\n处理目录: {subdir_name}")
# 步骤1: 查找并处理.ui文件
ui_files = list(subdir.glob('*.ui'))
if not ui_files:
print(f" 无UI文件,跳过")
return
# 重命名UI文件为目录同名
target_ui_file = subdir / f"{subdir_name}.ui"
if len(ui_files) > 1:
print(f" 警告: 发现多个UI文件,仅保留{target_ui_file.name}")
for ui_file in ui_files:
if ui_file != target_ui_file:
if target_ui_file.exists():
target_ui_file.unlink()
ui_file.rename(target_ui_file)
print(f" 重命名UI文件: {ui_file.name} -> {target_ui_file.name}")
# 步骤2: 删除旧_ui.py并重新生成
for py_file in subdir.glob('*_ui.py'):
py_file.unlink()
print(f" 删除旧文件: {py_file.name}")
target_ui_py = subdir / f"{subdir_name}_ui.py"
try:
# 路径加双引号,兼容空格/特殊字符
cmd = f"{pyside6_uic_path} {target_ui_file} -o {target_ui_py}"
print(f" 执行命令: {cmd}")
exit_code = os.system(cmd)
if exit_code != 0:
raise Exception(f"命令执行失败(退出码:{exit_code})")
print(f" 生成新文件: {target_ui_py.name}")
except Exception as e:
print(f" 错误: 生成UI文件失败 - {e}")
return
# 步骤3: 处理_func.py文件
target_func_file = subdir / f"{subdir_name}_func.py"
func_files = list(subdir.glob('*_func.py'))
# 重命名非目标func文件
for func_file in func_files:
if func_file != target_func_file:
if target_func_file.exists():
target_func_file.unlink()
func_file.rename(target_func_file)
print(f" 重命名功能文件: {func_file.name} -> {target_func_file.name}")
# 获取必要的类名(用于新建/更新func文件)
widget_class = get_ui_widget_class(target_ui_file)
frontend_class = get_ui_frontend_class(target_ui_py)
# 如果目标func文件不存在则新建
if not target_func_file.exists():
print(f" 创建新文件: {target_func_file.name}")
func_content = f'''from PySide6.QtCore import Slot, Qt
from PySide6.QtWidgets import {widget_class} # 导入本界面所属的widget类,在UI文件中也可以查到它的定义
from . import {subdir_name}_ui # 导入使用pyuic生成的前端UI模块
# 定义窗口类
class form_{subdir_name}({widget_class}, {subdir_name}_ui.{frontend_class}): # 继承自widget类和前端UI类
def __init__(self):
super().__init__()
self.setupUi(self) # 加载窗口
self.run() # 实例化以后的初始化运行窗口
# 初始化运行窗口,继续完成参数的设定
def run(self):
# self.setWindowFlag(Qt.WindowStaysOnTopHint) # 可以单独设置窗口是否置顶
# self.setWindowFlag(Qt.FramelessWindowHint) # 可以单独设置窗口是否无边框
# self.setWindowFlag(Qt.WindowMinimizeButtonHint) # 可以单独设置窗口是否显示最小化按钮
# self.setWindowFlag(Qt.WindowMaximizeButtonHint) # 可以单独设置窗口是否显示最大化按钮
# self.setWindowFlag(Qt.WindowCloseButtonHint) # 可以单独设置窗口是否显示关闭按钮
# self.setWindowFlag(Qt.WindowContextHelpButtonHint) # 可以单独设置窗口是否显示帮助按钮
# self.setWindowFlag(Qt.WindowFullscreenButtonHint) # 可以单独设置窗口是否显示全屏按钮
# self.setWindowFlag(Qt.WindowSystemMenuHint) # 可以单独设置窗口是否显示系统菜单
# self.setWindowFlag(Qt.WindowTitleHint) # 可以单独设置窗口是否显示标题栏
# 以及其他窗口设置
self.show() # 执行一次显示窗口,以便完成窗口的所有初始化
self.hide() # 如果是起始主窗口,请注释掉本行代码,使其能在程序启动后显示
@Slot()
def on_Object_handled(self): # 槽函数,Object是UI文件中定义的对象名,handled是事件名,
# 由于pyuic在生成前端模块时包含了QMetaObject.connectSlotsByName()语句,
# 因此这里可以直接使用对象名和事件名来连接槽函数
pass
'''
with open(target_func_file, 'w', encoding='utf-8') as f:
f.write(func_content)
print(f" 写入预置内容到: {target_func_file.name}")
else:
# 文件存在时,检查并更新关键代码行
print(f" 检查{target_func_file.name}内容并修正...")
update_func_file_content(target_func_file, subdir_name, widget_class, frontend_class)
def generate_initFile(root_dir):
"""
自动生成目录下的__init__.py文件
:param root_dir: __init__.py所在目录
"""
# 1. 定义初始化内容的模板开头
init_content = []
# 2. 收集需要导出的变量名
all_list = []
# 3. 扫描root_dir下的目录
for dir_name in os.listdir(root_dir):
dir_path = os.path.join(root_dir, dir_name)
# 只处理目录,且符合windowN命名规则
if os.path.isdir(dir_path) and not dir_name.startswith('__'):
# 构造导入语句:from . windowN.windowN import form_windowN
import_line = f'from . {dir_name}.{dir_name}_func import form_{dir_name}'
init_content.append(import_line)
# 添加到__all__列表
all_list.append(f'form_{dir_name}')
# 4. 拼接完整的__init__.py内容
init_content.append('') # 空行分隔
init_content.append(f'__all__ = {all_list}')
final_content = '\n'.join(init_content)
# 5. 写入__init__.py文件
init_file_path = os.path.join(root_dir, '__init__.py')
with open(init_file_path, 'w', encoding='utf-8') as f:
f.write(final_content)
print(f"成功生成__init__.py:\n{final_content}")
print(f"文件路径:{init_file_path}")
def main():
"""主函数"""
print(f"开始处理UI目录: {UI_ROOT}")
print("=" * 50)
# 遍历第一层子目录(过滤隐藏目录)
for item in UI_ROOT.iterdir():
if item.is_dir() and not item.name.startswith('__'):
process_subdirectory(item)
print("\n" + "=" * 50)
print("处理完成!")
generate_initFile(UI_ROOT)
if __name__ == "__main__":
# 检查运行目录
if not UI_ROOT.name == "UI_forms":
print("警告: 建议在UI_forms目录下运行此脚本!")
main()