关于QComboBox下拉框列表显示错乱问题

探讨QComboBox组件在用户操作过程中,如何避免因实时更新列表导致的显示错乱问题。通过在增加新项前调用hidePopup()方法,有效解决了列表显示混乱的现象。

QComboBox下拉框列表有个小问题:当用户鼠标点击右侧倒三角箭头打开显示列表时,如果此时某个动作增加了新的项到下拉框列表中去,会导致列表出现显示错乱的问题,例如用户打开下拉框列表时,如果鼠标移动到第二项会触发槽函数自动往列表中新增一项,此时列表就会看起来特别的不舒服,哪怕是重新打开列表也是错乱显示,如下图所示:

其实要解决这个问题也很简单的,不需要重写QComboBox,也不需要去限制用户的点击,只需要在新增列表项前调用hidePopup()关闭列表的弹出即可,这个方法会在列表显示时隐藏显示并且重置列表内部的状态(我理解为自动调整列表的显示),如下图所示:

from PySide2 import QtGui from PySide2 import QtWidgets, QtCore from shiboken2 import wrapInstance from maya.app.general.mayaMixin import MayaQWidgetDockableMixin from maya import OpenMayaUI as omui import maya.OpenMaya as om import maya.cmds as cmds import maya.mel as mel import os, platform, subprocess, sys import xml.dom.minidom as xml # 用于XML设置 DEBUG = False PRNT_STRING = "<RizomUV桥接工具>" # 日志前缀汉化 try: # Python 3 from . import scriptlist except ImportError: import scriptlist class Settings(): def __init__(self): config_path = os.path.join('%s/RizomBridge' % os.getenv('APPDATA')) config_file = 'uisettings.xml' # 脚本路径设置 rizom_script_path = os.path.join(config_path, "MayaRizomBridgeScript.lua") self.rizom_script_path = rizom_script_path.replace('\\', '/') self.apppath = None self.config_file_path = os.path.join(config_path, config_file) self.check_config_exists(config_path) try: self.read_settings() except: print("<RizomUV桥接工具> 读取设置失败,正在创建新的xml文件。") # 日志汉化 self.set_defaults() self.save_xml() self.read_settings() self.exportFile = os.path.join(config_path, self.objname) self.exportFile = self.exportFile.replace('\\', '/') def read_settings(self): doc = xml.parse(self.config_file_path) ui_app = doc.getElementsByTagName("Application")[0] self.apppath = ui_app.getAttribute("apppath") self.objname = ui_app.getAttribute("objname") self.upaxis = ui_app.getAttribute("upaxis") self.loaduvs = self.str_to_bool(ui_app.getAttribute("loaduvs")) self.useuvlink = self.str_to_bool(ui_app.getAttribute("useuvlink")) self.fixuvnames = self.str_to_bool(ui_app.getAttribute("fixuvnames")) ui_packer = doc.getElementsByTagName("Packer")[0] self.quality = int(ui_packer.getAttribute("quality")) self.mutations = int(ui_packer.getAttribute("mutations")) self.margin = int(ui_packer.getAttribute("margin")) self.spacing = int(ui_packer.getAttribute("spacing")) self.resolution = int(ui_packer.getAttribute("resolution")) self.initscaleavg = self.str_to_bool(ui_packer.getAttribute("initscaleavg")) self.autofit = self.str_to_bool(ui_packer.getAttribute("autofit")) def set_defaults(self): self.objname = "MayaRizomExport.fbx" self.upaxis = "Y" self.loaduvs = True self.useuvlink = True self.fixuvnames = False self.quality = 2 self.mutations = 256 self.margin = 2 self.spacing = 2 self.resolution = 1024 self.initscaleavg = True self.autofit = True self.apppath = self.findall_rizom_installs()[-1] def save_xml(self): print(PRNT_STRING, f"将设置保存到磁盘: {self.config_file_path}") # 日志汉化 doc = xml.Document() root_element = doc.createElement("RizomBridge") element_application = doc.createElement("Application") element_application.setAttribute("apppath", str(self.apppath)) element_application.setAttribute("objname", str(self.objname)) element_application.setAttribute("upaxis", str(self.upaxis)) element_application.setAttribute("loaduvs", str(self.loaduvs)) element_application.setAttribute("useuvlink", str(self.useuvlink)) element_application.setAttribute("fixuvnames", str(self.fixuvnames)) element_packer = doc.createElement("Packer") element_packer.setAttribute("quality", str(self.quality)) element_packer.setAttribute("mutations", str(self.mutations)) element_packer.setAttribute("margin", str(self.margin)) element_packer.setAttribute("spacing", str(self.spacing)) element_packer.setAttribute("resolution", str(self.resolution)) element_packer.setAttribute("initscaleavg", str(self.initscaleavg)) element_packer.setAttribute("autofit", str(self.autofit)) doc.appendChild(root_element) root_element.appendChild(element_application) root_element.appendChild(element_packer) doc.writexml(open(self.config_file_path, 'w'), indent=" ", addindent=" ", newl='\n') def check_config_exists(self, config_path): print(PRNT_STRING, f"检查配置文件: {self.config_file_path}") # 日志汉化 if not os.path.exists(config_path): os.makedirs(config_path) if not os.path.exists(self.config_file_path): print(PRNT_STRING, "未找到配置文件。") # 日志汉化 self.set_defaults() self.save_xml() else: print(PRNT_STRING, "找到配置文件。") # 日志汉化 def findall_rizom_installs(self): """ 返回注册表中所有存在的rizom.exe路径 """ try: import winreg except: self.manual_locate_rizom() return [self.apppath] key_path = "SOFTWARE\\Rizom Lab\\" parent_key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path) installs = [] i = 0 while i < 1000: try: key = winreg.EnumKey(parent_key, i) try: exe_path = winreg.QueryValue(winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path + key), "rizomuv.exe") + ".exe" except FileNotFoundError: exe_path = "" if os.path.exists(exe_path): installs.append(exe_path.replace("\\", "/")) except WindowsError: break i += 1 print(PRNT_STRING, "在Windows注册表中找到RizomUV安装路径:") # 日志汉化 for inst in sorted(installs): print(" ..", inst) return sorted(installs) def manual_locate_rizom(self): om.MGlobal.displayWarning(f"无法定位rizomuv.exe: {self.apppath}") # 提示汉化 fname = QtWidgets.QFileDialog.getOpenFileName(None, '定位rizomuv.exe', 'C:/Program Files/Rizom Lab/', "可执行文件 (*.exe)") # 对话框标题及过滤器汉化 self.apppath = fname[0] self.appver = self.get_version() self.save_xml() def get_version(self): """ 从文件夹名称读取版本号,返回列表形式,例如 [2022, 2] """ # 注释汉化 return os.path.dirname(self.apppath).split()[-1].split('.') def check_lua_path(self): if not os.path.exists(self.rizom_script_path): with open(self.rizom_script_path, 'w') as f: if DEBUG: print(PRNT_STRING, f"在{self.rizom_script_path}创建空白lua文件") # 日志汉化 f.write('') def str_to_bool(self, s): if s == 'True': return True else: # 其他情况均返回False,否则可能返回None导致崩溃 return False class RizomUVBridgeWindow(MayaQWidgetDockableMixin, QtWidgets.QDialog): def __init__(self, rootWidget=None, *args, **kwargs): super(RizomUVBridgeWindow, self).__init__() self.conf = Settings() self.conf.check_lua_path() print("Rizom路径:", self.conf.apppath) # 日志汉化 self.setWindowTitle("桥接工具") # 窗口标题汉化 self.sj_num_selchange = cmds.scriptJob(parent=self.objectName(), event=['SelectionChanged', self.ui_update_uvchannels]) self.resize(250, 300) self.create_widgets() self.create_layouts() self.create_connections() self.exported_objects = [] self.ui_update_uvchannels() self.cleanse_namespaces() self.config_updated = False if not os.path.exists(self.conf.apppath): self.conf.manual_locate_rizom() v = self.conf.get_version() self.btn_run.setText(f'运行RizomUV {v[0]}.{v[1]}') # 按钮文本汉化 rizom_link_path = os.path.dirname(self.conf.apppath) + '/RizomUVLink' self.port = None if rizom_link_path not in sys.path: sys.path.append(os.path.dirname(self.conf.apppath) + '/RizomUVLink') try: from RizomUVLink import CRizomUVLink self.link = CRizomUVLink() except: self.link = None def create_widgets(self): v = ".".join(self.conf.get_version()) self.btn_run = QtWidgets.QPushButton(f'运行RizomUV {v}', self) # 按钮文本汉化 self.btn_export = QtWidgets.QPushButton('导出', self) # 按钮文本汉化 self.btn_import = QtWidgets.QPushButton('导入', self) # 按钮文本汉化 self.cbx_use_link = QtWidgets.QCheckBox('尝试使用CRizomUVLink连接', self) # 复选框文本汉化 self.cbx_use_link.setChecked(self.conf.useuvlink) self.cbx_fix_set_names = QtWidgets.QCheckBox('导入时修复UV集名称', self) # 复选框文本汉化 self.cbx_fix_set_names.setChecked(self.conf.fixuvnames) if not os.path.exists(self.conf.exportFile): self.btn_import.setEnabled(False) # 导出设置组件 self.radioAxisY = QtWidgets.QRadioButton('Y轴') # 单选按钮文本汉化 self.radioAxisZ = QtWidgets.QRadioButton('Z轴') # 单选按钮文本汉化 if self.conf.upaxis == 'Y': self.radioAxisY.setChecked(True) else: self.radioAxisZ.setChecked(True) self.cbx_keepuv = QtWidgets.QCheckBox('加载现有UV', self) # 复选框文本汉化 self.cbx_keepuv.setChecked(self.conf.loaduvs) self.combo_scripts = QtWidgets.QComboBox(self) self.combo_scripts.addItem("无脚本") # 下拉框文本汉化 for s in scriptlist.scripts: self.combo_scripts.addItem(s[1]) # 工具组件 self.btn_fix_shell_normals = QtWidgets.QPushButton('将UV边界边设为硬边') # 按钮文本汉化 self.btn_edit_settings = QtWidgets.QPushButton('打开设置文件夹') # 按钮文本汉化 self.combo_pack_uvset = QtWidgets.QComboBox(self) self.btn_pack = QtWidgets.QPushButton('导出并打包UV', self) # 按钮文本汉化 self.combo_pack_quality = QtWidgets.QComboBox(self) self.combo_pack_quality.addItems(['低', '普通', '高', '较高', '极高']) # 下拉框文本汉化 self.combo_pack_quality.setCurrentIndex(self.conf.quality) self.slider_pack_uvmap = QtWidgets.QSlider(QtCore.Qt.Orientation(1)) self.slider_pack_uvmap.setMinimum(0) self.slider_pack_uvmap.setMaximum(5) self.slider_pack_uvmap.setValue(1) self.label_uvmap = QtWidgets.QLabel("1") self.dspin_pack_mutations = QtWidgets.QSpinBox() self.dspin_pack_mutations.setSingleStep(1) self.dspin_pack_mutations.setRange(1, 1000) self.dspin_pack_mutations.setWrapping(False) self.dspin_pack_mutations.setValue(self.conf.mutations) self.dspin_pack_resolution = QtWidgets.QSpinBox() self.dspin_pack_resolution.setSingleStep(8) self.dspin_pack_resolution.setRange(8, 8192) self.dspin_pack_resolution.setWrapping(False) self.dspin_pack_resolution.setValue(self.conf.resolution) self.dspin_pack_margin = QtWidgets.QSpinBox() self.dspin_pack_margin.setSingleStep(1) self.dspin_pack_margin.setWrapping(False) self.dspin_pack_margin.setValue(self.conf.margin) self.dspin_pack_spacing = QtWidgets.QSpinBox() self.dspin_pack_spacing.setSingleStep(1) self.dspin_pack_spacing.setWrapping(False) self.dspin_pack_spacing.setValue(self.conf.spacing) self.cbx_initial_scale_avg = QtWidgets.QCheckBox("使用'平均'初始缩放", self) # 复选框文本汉化 self.cbx_initial_scale_avg.setChecked(self.conf.initscaleavg) self.cbx_layout_scaling = QtWidgets.QCheckBox("布局:自动适配", self) # 复选框文本汉化 self.cbx_layout_scaling.setChecked(self.conf.autofit) def create_layouts(self): main_layout = QtWidgets.QVBoxLayout(self) grp_layout = QtWidgets.QVBoxLayout() grp_layout.addWidget(self.btn_run) grp_layout.addWidget(self.btn_export) grp_layout.addWidget(self.btn_import) grp_layout.addWidget(self.cbx_use_link) grp_layout.addWidget(self.cbx_fix_set_names) grp = QtWidgets.QGroupBox("UV操作") # 组框标题汉化 grp.setLayout(grp_layout) main_layout.addWidget(grp) # 导出设置 grp_layout = QtWidgets.QVBoxLayout() grp_layout.addWidget(self.cbx_keepuv) hor_layout = QtWidgets.QHBoxLayout() hor_layout.addWidget(QtWidgets.QLabel("脚本")) # 标签文本汉化 hor_layout.addWidget(self.combo_scripts) grp_layout.addLayout(hor_layout) hor_layout = QtWidgets.QHBoxLayout() hor_layout.addWidget(QtWidgets.QLabel("上方向轴")) # 标签文本汉化 hor_layout.addWidget(self.radioAxisY) hor_layout.addWidget(self.radioAxisZ) grp_layout.addLayout(hor_layout) grp = QtWidgets.QGroupBox("导出设置") # 组框标题汉化 grp.setLayout(grp_layout) main_layout.addWidget(grp) # 工具 grp_layout = QtWidgets.QVBoxLayout() grp_layout.addWidget(self.btn_fix_shell_normals) grp_layout.addWidget(self.btn_edit_settings) grp = QtWidgets.QGroupBox("工具") # 组框标题汉化 grp.setLayout(grp_layout) main_layout.addWidget(grp) # UV打包 grp_layout = QtWidgets.QVBoxLayout() grp = QtWidgets.QGroupBox("UV打包") # 组框标题汉化 hor_layout = QtWidgets.QHBoxLayout() hor_layout.addWidget(self.btn_pack) grp_layout.addLayout(hor_layout) hor_layout = QtWidgets.QHBoxLayout() hor_layout.addWidget(QtWidgets.QLabel("要打包的UV集")) # 标签文本汉化 hor_layout.addWidget(self.combo_pack_uvset) grp_layout.addLayout(hor_layout) hor_layout = QtWidgets.QHBoxLayout() hor_layout.addWidget(QtWidgets.QLabel("打包质量")) # 标签文本汉化 hor_layout.addWidget(self.combo_pack_quality) grp_layout.addLayout(hor_layout) hor_layout = QtWidgets.QHBoxLayout() hor_layout.addWidget(QtWidgets.QLabel("迭代次数")) # 标签文本汉化 hor_layout.addWidget(self.dspin_pack_mutations) hor_layout.addWidget(QtWidgets.QLabel("分辨率")) # 标签文本汉化 hor_layout.addWidget(self.dspin_pack_resolution) grp_layout.addLayout(hor_layout) hor_layout = QtWidgets.QHBoxLayout() hor_layout.addWidget(QtWidgets.QLabel("边距")) # 标签文本汉化 hor_layout.addWidget(self.dspin_pack_margin) hor_layout.addWidget(QtWidgets.QLabel("间距")) # 标签文本汉化 hor_layout.addWidget(self.dspin_pack_spacing) grp_layout.addLayout(hor_layout) hor_layout = QtWidgets.QHBoxLayout() hor_layout.addWidget(self.cbx_initial_scale_avg) grp_layout.addLayout(hor_layout) hor_layout = QtWidgets.QHBoxLayout() hor_layout.addWidget(self.cbx_layout_scaling) grp_layout.addLayout(hor_layout) grp_layout.addStretch() grp.setLayout(grp_layout) main_layout.addWidget(grp) def create_connections(self): self.btn_run.clicked.connect(self.riz_run) self.btn_export.clicked.connect(self.riz_export) self.btn_import.clicked.connect(self.riz_import) self.btn_fix_shell_normals.clicked.connect(fix_shell_border_normals) self.btn_edit_settings.clicked.connect(self.browse_settings_location) self.cbx_keepuv.stateChanged.connect(self.set_config) self.slider_pack_uvmap.valueChanged.connect(self.ui_pack_update_labels) self.radioAxisY.clicked.connect(self.set_config) self.radioAxisZ.clicked.connect(self.set_config) self.dspin_pack_mutations.valueChanged.connect(self.set_config) self.dspin_pack_resolution.valueChanged.connect(self.set_config) self.dspin_pack_margin.valueChanged.connect(self.set_config) self.dspin_pack_spacing.valueChanged.connect(self.set_config) self.cbx_layout_scaling.stateChanged.connect(self.set_config) self.cbx_use_link.stateChanged.connect(self.set_config) self.cbx_fix_set_names.stateChanged.connect(self.set_config) self.btn_pack.clicked.connect(self.riz_pack) return def riz_run(self): # 确认应用程序路径 if not os.path.exists(self.conf.apppath): self.manual_locate_rizom() if self.link and self.cbx_use_link.isChecked(): self.port = self.link.RunRizomUV() print(PRNT_STRING, f"RizomUV {self.link.RizomUVVersion()}链接已建立。正在TCP端口{self.port}监听命令") # 日志汉化 else: # 链接不可用时使用原始方法 print(PRNT_STRING, "RizomUV链接不可用。将使用LUA脚本文件通信。") # 日志汉化 cmd = '"' + self.conf.apppath + '" -cf "' + self.conf.rizom_script_path + '"' print(cmd) if platform.system() == "Windows": self.sp = subprocess.Popen(cmd, shell=True) else: self.sp = subprocess.Popen(["open", "-a", self.conf.apppath, "-cf", self.conf.rizom_script_path]) self.btn_export.setEnabled(True) return def riz_pack(self): # 导出模型不加载脚本 current_uv = self.combo_pack_uvset.currentText() exported = self.riz_export(False, False) if not exported: return # 根据GUI选项构建LUA代码 cmd = '' # 重复使用的文本块 cmd_properties_prefix = 'ZomIslandGroups({Mode="SetGroupsProperties", WorkingSet="Visible", MergingPolicyString="A_ADD|AIB_ADD_A_VALUE_B|B_CLONE", GroupPaths={ "RootGroup" }, ' # 加载模型 cmd += 'ZomLoad({File={Path="'+self.conf.exportFile+'", ImportGroups=true, XYZUVW=true, UVWProps=true}})\n' cmd += 'ZomUvset({Mode="SetCurrent", Name="%s"})\n' % current_uv cmd += cmd_properties_prefix + 'Properties={Pack={Rotate={Step=90}}}})\n' cmd += cmd_properties_prefix + 'Properties={Pack={Rotate={Mode=0}}}})\n' margin = float(self.dspin_pack_margin.value()) / self.dspin_pack_resolution.value() spacing = float(self.dspin_pack_spacing.value()) / self.dspin_pack_resolution.value() cmd += cmd_properties_prefix + 'Properties={Pack={SpacingSize=%f}}})\n' % spacing cmd += cmd_properties_prefix + 'Properties={Pack={MarginSize=%f}}})\n' % margin quality = [128, 256, 512, 1024, 2048] cmd += cmd_properties_prefix + 'Properties={Pack={Resolution=%i}}})\n' % quality[self.combo_pack_quality.currentIndex()] cmd += cmd_properties_prefix + 'Properties={Pack={MaxMutations=%i}}})\n' % self.dspin_pack_mutations.value() init_scale = 0 if self.cbx_initial_scale_avg.isChecked(): init_scale = 2 layout_scale = 0 if self.cbx_layout_scaling.isChecked(): layout_scale = 2 cmd += 'ZomSet({Path="Prefs.PackOptions.__ScalingMode", Value=%i})\n' % init_scale cmd += 'ZomSave({File={Path="c:/users/root/appdata/local/temp/MayaRizomExport.fbx", UVWProps=true}, __UpdateUIObjFileName=true})\n' cmd += 'ZomIslandGroups({Mode="DistributeInTilesEvenly", WorkingSet="Visible&Flat", MergingPolicyString="A_ADD|AIB_ADD_A_VALUE_B|B_CLONE", UseTileLocks=true, UseIslandLocks=true})\n' cmd += 'ZomPack({RootGroup="RootGroup", WorkingSet="Visible&Flat", ProcessTileSelection=false, RecursionDepth=1, Translate=true, LayoutScalingMode=%i, Scaling={Mode=%i}})\n' % (layout_scale, init_scale) if DEBUG: print(cmd) self.write_to_lua_file(cmd) return def fbx_export(self): # FBX导出 if not cmds.pluginInfo("fbxmaya", loaded=True, query=True): cmds.loadPlugin("fbxmaya") cmds.undoInfo(openChunk=True) mel.eval('ConvertInstanceToObject;') # 这是唯一需要撤销的命令 mel.eval('FBXExportSmoothingGroups -v true;') mel.eval('FBXExportTriangulate -v false;') mel.eval('FBXExportSmoothMesh -v false;') mel.eval('FBXExportUpAxis {};'.format(self.conf.upaxis)) print(f'FBXExport -s -f "{self.conf.exportFile}";') mel.eval(f'FBXExport -s -f "{self.conf.exportFile}";') try: cmds.undo() except RuntimeError as RE: # 没有更多可撤销的命令 pass # 结束FBX导出 return def riz_export(self, use_script=True, load_model=True): if self.link and self.cbx_use_link.isChecked(): print("Rizom链接版本", self.link.Version()) self.exported_objects = cmds.ls(selection=True, tr=True) # FBX导出 self.fbx_export() if not self.port: self.riz_run() params = { "File.Path": self.conf.exportFile, "File.XYZUVW": True, # 加载3D+UV数据(使用File.XYZ仅加载3D数据) "File.UVWProps": True, # 加载UV属性(如固定、纹理密度设置等) "File.ImportGroups": True, # 加载岛组层级 "__Focus": True, # 在视口中聚焦加载的网格 } self.link.Load(params) if DEBUG: print("<RIZOM> 导出至:", self.conf.exportFile) # 日志汉化 cmds.select(self.exported_objects) # 启用导入按钮(如果之前禁用) self.btn_import.setEnabled(True) else: # 传统方法 # print("<RIZOM> 正在导出模型") # 日志汉化 self.cleanse_namespaces() # 强制清理 self.exported_objects = cmds.ls(selection=True, tr=True) if not self.exported_objects: return False # 删除历史记录(防止存在空组被此操作移除) # 否则导入时会被清除,导致对象计数不匹配 cmds.bakePartialHistory() cmd = '' # 导出选项 if load_model: if self.cbx_keepuv.isChecked(): cmd += 'ZomLoad({File={Path="'+self.conf.exportFile+'", ImportGroups=true, XYZUVW=true, UVWProps=true}})\n' else: cmd += 'ZomLoad({File={Path="'+self.conf.exportFile+'", ImportGroups=true, XYZ=true}, NormalizeUVW=true})\n' # 添加脚本 if self.combo_scripts.currentIndex() and use_script is True: script_name = scriptlist.scripts[self.combo_scripts.currentIndex()-1][0] script_path = os.path.join(os.path.dirname(__file__), 'lua_scripts/%s' % script_name) with open(script_path, 'r') as lua_script: cmd += lua_script.read() if DEBUG: print("<RizomUV桥接工具>", cmd) # 日志汉化 # FBX导出 self.fbx_export() if DEBUG: print("<RIZOM> 导出至:", self.conf.exportFile) # 日志汉化 cmds.select(self.exported_objects) self.write_to_lua_file(cmd) self.btn_import.setEnabled(True) # 如果有显著更改,将配置写入磁盘 if self.config_updated == True: self.conf.save_xml() return True def riz_import(self): if not os.path.exists(self.conf.exportFile): om.MGlobal.displayError(f"无法找到导出文件: {self.conf.exportFile}") # 错误提示汉化 self.btn_import.setEnabled(False) if self.cbx_use_link.isChecked(): if self.link: try: self.link.Save({"File.Path": self.conf.exportFile}) except RuntimeError as RE: cmds.error('无法通过Python链接向Rizom发送保存命令。导出时是否启用了链接?') # 错误提示汉化 # FBX导入 namespace = ':RIZOMUV' if not cmds.namespace(ex=namespace): cmds.namespace(add=namespace) cmds.namespace(set=namespace) mel.eval('FBXImportMode -v add;') mel.eval(f'FBXImport -f "{self.conf.exportFile}";') # 结束FBX导入 imported_objects = cmds.ls('RIZOMUV:*', long=True, type="transform") original_matches = [] for riz_obj in imported_objects: print("导入的对象名称", riz_obj) # 日志汉化 original = riz_obj.replace('RIZOMUV:', '') original_matches.append(original) # 列出每个项目的UV集,无UV集则跳过(排除组节点等对象) original_uvsets = cmds.polyUVSet(original, allUVSets=True, q=True) imported_uvsets = cmds.polyUVSet(riz_obj, allUVSets=True, q=True) if original_uvsets: # 检查名称 # if self.cbx_fix_set_names.isChecked(): for i in range(len(original_uvsets)): try: if not original_uvsets[i] == imported_uvsets[i]: cmds.polyUVSet(riz_obj, rename=True, uvSet=imported_uvsets[i], newUVSet=original_uvsets[i]) except IndexError: # 可能因Maya奇怪的bug导致(polyUVSet命令列出的UV集数量与UV集编辑器中不同) print("UV集索引错误。对象的UV集数量似乎不匹配") # 日志汉化 print("原始UV集:", original_uvsets, original) print("导入的UV集:", imported_uvsets, riz_obj) pass try: cmds.polyTransfer(original, ao=riz_obj, ch=False, uv=True) except RuntimeError as rt: print(f"<RizomUV桥接工具> 无法从{riz_obj}向{original}传输UV") # 日志汉化 if DEBUG: print(f"<RizomUV桥接工具> 从{riz_obj}向{original}传输UV") # 日志汉化 cmds.select(original_matches) cmds.bakePartialHistory() cmds.delete(':RIZOMUV:*') cmds.namespace(rm=':RIZOMUV') return def browse_settings_location(self): os.startfile(os.path.dirname(self.conf.config_file_path)) def ui_pack_update_labels(self): print("值已更改") # 日志汉化 self.label_uvmap.setText(str(self.slider_pack_uvmap.value())) def ui_update_uvchannels(self): sel_obj = cmds.ls(sl=True, tr=True) if not sel_obj: self.combo_pack_uvset.clear() return uvsets = cmds.polyUVSet(sel_obj[0], allUVSets=True, q=True) current_set = cmds.polyUVSet(sel_obj[0], cuv=True, q=True) if uvsets: print(uvsets) self.combo_pack_uvset.clear() self.combo_pack_uvset.addItems(uvsets) self.combo_pack_uvset.setCurrentIndex(uvsets.index(current_set[0])) def cleanse_namespaces(self): """ 第一步 -- 尝试删除所有现有RIZOMUV命名空间直到失败,这应该能清除像 RIZOMUV:RIZOMUV:RIZOMUV这样的嵌套实例 """ fail = False while(not fail): try: cmds.namespace(rm=':RIZOMUV', mnr=True) except: fail = True """ 第二步 -- 在某些场景中,命名空间可能被重命名为RIZOMUV1、RIZOMUV2、RIZOMUV3等 因此需要查找并删除这些残留 """ """ 第二步(更新)-- 尝试删除除RIZOM之外的其他命名空间,因为存在其他命名空间可能导致导入失败 """ c = cmds.listRelatives(ad=True) if not c: return for obj in c: ns_split = obj.split(':') if len(ns_split)>1: result = cmds.confirmDialog( m="选中的对象已分配命名空间。\n" # 对话框文本汉化 "使用工具前必须移除这些命名空间\n" "是否为您删除?(包括未选中的对象)\n\n" "对象上的命名空间:\n%s" % ns_split[0], button=['移除', '取消'] # 按钮文本汉化 ) if result == '移除': cmds.namespace(rm=ns_split[0], mnr=True) else: return return def write_to_lua_file(self, command): with open(self.conf.rizom_script_path, 'w') as f: f.write(command) print("<RIZOM> 已将命令写入lua文件:", self.conf.rizom_script_path) # 日志汉化 for line in command.split("\n"): print("\t", line) return def set_config(self): self.conf.loaduvs = self.cbx_keepuv.isChecked() if self.radioAxisY.isChecked(): self.conf.upaxis = "Y" else: self.conf.upaxis = "Z" self.conf.mutations = self.dspin_pack_mutations.value() self.conf.resolution = self.dspin_pack_resolution.value() self.conf.margin = self.dspin_pack_margin.value() self.conf.spacing = self.dspin_pack_spacing.value() self.conf.autofit = self.cbx_layout_scaling.isChecked() self.conf.fixuvnames = self.cbx_fix_set_names.isChecked() self.conf.useuvlink = self.cbx_use_link.isChecked() self.config_updated = True self.conf.save_xml() def closeEvent(self, event): self.conf.save_xml() def GetMayaWidget(): """ 返回Maya主窗口部件的Python对象 """ # 注释汉化 main_window_ptr = omui.MQtUtil.mainWindow() if sys.version_info.major >= 3: return wrapInstance(int(main_window_ptr), QtWidgets.QWidget) else: return wrapInstance(long(main_window_ptr), QtWidgets.QWidget) def fix_shell_border_normals(): obj_list = cmds.ls(sl=True, o=True) all_final_borders = [] for sub_obj in obj_list: cmds.select(sub_obj, r=True) cmds.polyNormalPerVertex(ufn=True) cmds.polySoftEdge(sub_obj, a=180, ch=1) print("全部软化") # 日志汉化 # 选择对象UV cmds.select(sub_obj + '.map[*]') mel.eval('polySelectBorderShell 1;') uv_border = cmds.polyListComponentConversion(te=True, internal=True) uv_border = cmds.ls(uv_border, fl=True) final_border = [] # 特殊过滤 for curEdge in uv_border: edge_uvs = cmds.polyListComponentConversion(curEdge, tuv=True) edge_uvs = cmds.ls(edge_uvs, fl=True) if len(edge_uvs) > 2: final_border.append(curEdge) cmds.polySoftEdge(final_border, a=0, ch=1) all_final_borders.append(final_border) cmds.select(cl=True) for sel_l in all_final_borders: cmds.select(sel_l, add=True) cmds.hilite(obj_list) def run(): scriptJobs = cmds.scriptJob(listJobs=True) for sj in scriptJobs: if "RizomBridge" in sj: print(PRNT_STRING, f"终止已存在的scriptJob: {sj}") # 日志汉化 cmds.scriptJob(kill=int(sj.split(':')[0])) d = RizomUVBridgeWindow() d.show(dockable=True) if __name__ == "__main__": run()
最新发布
11-02
以下是main_window.py的完整代码: # 主窗口 - PyQt6 版本 import os import sys import datetime import logging import faulthandler # 启用详细日志和调试模式 import tracemalloc # 内存监控 import ctypes # 启用故障处理程序 faulthandler.enable() # 添加虚拟环境检查 if not hasattr(sys, 'real_prefix') and not hasattr(sys, 'base_prefix'): logging.warning("警告: 未在虚拟环境中运行,建议使用虚拟环境") # 配置详细日志 logging.basicConfig( level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler("app_debug.log"), logging.StreamHandler() ] ) logger = logging.getLogger(__name__) logger.info("应用程序启动") logger.info(f"Python 路径: {sys.executable}") logger.info(f"参数: {sys.argv}") # 添加更多诊断信息 logger.info(f"系统路径: {sys.path}") logger.info(f"工作目录: {os.getcwd()}") # 启用详细错误报告 faulthandler.enable() os.environ['QT_DEBUG_PLUGINS'] = '1' # 启用 Qt 插件调试 os.environ['QTWEBENGINE_CHROMIUM_FLAGS'] = '--disable-gpu --no-sandbox' # 禁用 GPU 加速 # 设置 Windows 错误报告 if sys.platform == 'win32': ctypes.windll.kernel32.SetErrorMode(0x0001 | 0x0002) # SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX # 设置堆栈保护 os.environ['QT_FATAL_CRITICALS'] = '1' os.environ['QT_FATAL_WARNINGS'] = '1' # 确保先导入PyQt6核心模块 try: from PyQt6.QtCore import QObject, pyqtSignal, PYQT_VERSION_STR, Qt, QRectF, QTimer from PyQt6.QtGui import QIcon, QPixmap, QColor, QFont, QAction from PyQt6.QtWidgets import ( QApplication, QMainWindow, QFileDialog, QSplitter, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QComboBox, QLabel, QStatusBar, QToolBar, QDockWidget, QTabWidget, QMessageBox, QInputDialog, QDialog, QMenu ) logger.info(f"PyQt6 版本: {PYQT_VERSION_STR}") except ImportError as e: logger.error(f"导入 PyQt6 失败: {str(e)}") # 添加更详细的错误信息 import traceback logger.error(traceback.format_exc()) # 无法继续运行,退出程序 sys.exit(1) # 然后导入其他模块 from src.auto_print_system.ui.template_editor import TemplateCanvas, TextRegionItem, QRRegionItem, LogoRegionItem from src.auto_print_system.ui.data_mapper import DataMapperWidget from src.auto_print_system.ui.preview_dialog import PreviewDialog from src.auto_print_system.ui.history_dialog import HistoryDialog from src.auto_print_system.core.project_manager import ProjectManager from src.auto_print_system.core.file_compositor import FileCompositor from src.auto_print_system.core.font_manager import FontManager logger = logging.getLogger(__name__) # 定义属性面板类 class BasePropertiesPanel(QWidget): propertyChanged = pyqtSignal(str, object) # 定义信号 def __init__(self, parent=None): super().__init__(parent) self.region = None self.setup_ui() def setup_ui(self): """子类实现具体UI布局""" pass def set_region(self, region): """设置当前区域""" self.region = region self.update_ui_from_region() def update_ui_from_region(self): """根据区域更新UI""" pass def update_region_from_ui(self): """根据UI更新区域""" pass class TextPropertiesPanel(BasePropertiesPanel): def setup_ui(self): layout = QVBoxLayout(self) layout.addWidget(QLabel("文本属性面板 - 实际实现需要添加更多控件")) # 这里添加实际的文本属性控件 self.setLayout(layout) def update_ui_from_region(self): if self.region and isinstance(self.region, TextRegionItem): # 根据区域更新UI控件 pass def update_region_from_ui(self): if self.region: # 根据UI更新区域属性 pass class QRPropertiesPanel(BasePropertiesPanel): def setup_ui(self): layout = QVBoxLayout(self) layout.addWidget(QLabel("二维码属性面板 - 实际实现需要添加更多控件")) # 这里添加实际的二维码属性控件 self.setLayout(layout) def update_ui_from_region(self): if self.region and isinstance(self.region, QRRegionItem): # 根据区域更新UI控件 pass class LogoPropertiesPanel(BasePropertiesPanel): def setup_ui(self): layout = QVBoxLayout(self) layout.addWidget(QLabel("Logo属性面板 - 实际实现需要添加更多控件")) # 这里添加实际的Logo属性控件 self.setLayout(layout) def update_ui_from_region(self): if self.region and isinstance(self.region, LogoRegionItem): # 根据区域更新UI控件 pass class MainWindow(QMainWindow): def __init__(self, data_path=None): super().__init__() #简化初始化 self.central_widget = None self.logo_path = None self.project_manager = None self.btn_generate = None self.field_mapping_widget = None self.btn_add_text = None self.btn_add_qr = None self.current_project = None self.spot_color_path = None self.compositor = None self.font_manager = None self.left_layout = None self.btn_upload = None self.btn_import_data = None self.template_canvas = None self.status_bar = None self.btn_rerun = None self.btn_history = None self.data_mapper = None self.btn_save_config = None self.right_layout = None self.logo_props_panel = None self.text_props_panel = None self.props_tabs = None self.qr_props_panel = None self.history_combo = None self.logger = logging.getLogger(__name__) self.logger.info("主窗口初始化开始") self.data_path = data_path try: # 尝试简化初始化 self.logger.info("尝试基本初始化...") self.setWindowTitle("自动打印系统") self.resize(800, 600) # 延迟加载复杂组件 self.logger.info("延迟加载复杂组件") QTimer.singleShot(100, self.delayed_init) except Exception as e: self.logger.exception("主窗口初始化期间发生错误") raise def delayed_init(self): try: self.logger.info("开始延迟初始化") self.init_components() self.logger.info("组件初始化完成") # 其他初始化... except Exception as e: self.logger.exception("延迟初始化期间发生错误") # 显示错误消息 QMessageBox.critical( self, "初始化错误", f"初始化过程中发生错误:\n{str(e)}", QMessageBox.StandardButton.Ok ) self.close() self.data_path = self.data_path or os.path.join(os.path.dirname(__file__), "../../../data") self.setWindowTitle("自动拼版系统") self.setGeometry(100, 100, 1200, 800) # 声明所有实例变量 self.project_manager = None self.font_manager = None self.compositor = None self.status_bar = None self.current_project = None self.logo_path = None self.spot_color_path = None # 布局和控件变量 self.left_layout = None self.btn_upload = None self.btn_add_text = None self.btn_add_qr = None self.template_canvas = None self.props_tabs = None self.text_props_panel = None self.qr_props_panel = None self.logo_props_panel = None self.right_layout = None self.btn_import_data = None self.btn_save_config = None self.data_mapper = None self.field_mapping_widget = None self.history_combo = None self.btn_history = None self.btn_generate = None self.btn_rerun = None # 初始化核心组件 self.init_components() # 创建项目管理器 self.project_manager = ProjectManager(self.data_path) # 创建字体管理器 self.font_manager = FontManager(self.data_path) # 创建合成引擎 self.compositor = FileCompositor(self.data_path, self.font_manager) # 设置状态栏 self.status_bar = QStatusBar() self.setStatusBar(self.status_bar) self.status_bar.showMessage("就绪") # 当前项目 self.current_project = None self.logo_path = None self.spot_color_path = None # 加载样式 self.apply_styles() def init_components(self): """初始化主界面组件""" self.logger.info("开始初始化组件") # 逐步初始化各个组件,添加日志 self.logger.info("创建中央部件...") self.central_widget = QWidget() self.setCentralWidget(self.central_widget) self.logger.info("创建布局...") self.layout = QVBoxLayout(self.central_widget) # 逐步添加其他组件,每一步都添加日志 # 例如先注释掉停靠窗口的创建 # self.logger.info("创建左侧停靠窗口...") # self.create_left_dock() # 只添加一个简单的标签用于测试 test_label = QLabel("应用程序启动成功,基本UI已加载") self.layout.addWidget(test_label) self.logger.info("基本组件初始化完成") # def create_left_dock(self): # # 暂时注释掉停靠窗口创建 # pass # 创建菜单栏 self.create_menus() # 创建工具栏 self.create_toolbar() # 创建主中心区域 central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) # 左侧模板编辑区(占40%) left_dock = QDockWidget("模板编辑区", self) left_dock.setFeatures(QDockWidget.DockWidgetFeature.DockWidgetMovable | QDockWidget.DockWidgetFeature.DockWidgetFloatable) left_widget = QWidget() self.left_layout = QVBoxLayout(left_widget) # 模板操作按钮 btn_layout = QHBoxLayout() self.btn_upload = QPushButton("上传模板") self.btn_upload.clicked.connect(self.upload_template) self.btn_add_text = QPushButton("添加文本框") self.btn_add_text.clicked.connect(self.add_text_region) self.btn_add_qr = QPushButton("添加二维码区") self.btn_add_qr.clicked.connect(self.add_qr_region) btn_layout.addWidget(self.btn_upload) btn_layout.addWidget(self.btn_add_text) btn_layout.addWidget(self.btn_add_qr) self.left_layout.addLayout(btn_layout) # 模板编辑画布 self.template_canvas = TemplateCanvas(data_path=self.data_path) self.template_canvas.setAlignment(Qt.AlignmentFlag.AlignCenter) self.template_canvas.setStyleSheet("background-color: #f0f0f0; border: 1px solid #ccc;") self.left_layout.addWidget(self.template_canvas) # 属性面板(标签页) self.props_tabs = QTabWidget() self.text_props_panel = TextPropertiesPanel() self.qr_props_panel = QRPropertiesPanel() self.logo_props_panel = LogoPropertiesPanel() self.props_tabs.addTab(self.text_props_panel, "文本属性") self.props_tabs.addTab(self.qr_props_panel, "二维码属性") self.props_tabs.addTab(self.logo_props_panel, "Logo属性") # 连接信号 if hasattr(self.text_props_panel, 'propertyChanged'): self.text_props_panel.propertyChanged.connect(self.handle_property_change) if hasattr(self.qr_props_panel, 'propertyChanged'): self.qr_props_panel.propertyChanged.connect(self.handle_property_change) if hasattr(self.logo_props_panel, 'propertyChanged'): self.logo_props_panel.propertyChanged.connect(self.handle_property_change) self.left_layout.addWidget(self.props_tabs) left_dock.setWidget(left_widget) self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, left_dock) # 右侧数据区域(占60%) right_dock = QDockWidget("数据映射区", self) right_widget = QWidget() self.right_layout = QVBoxLayout(right_widget) # 数据操作按钮 data_btn_layout = QHBoxLayout() self.btn_import_data = QPushButton("导入数据") self.btn_import_data.clicked.connect(self.import_data) self.btn_save_config = QPushButton("保存配置") self.btn_save_config.clicked.connect(self.save_config) data_btn_layout.addWidget(self.btn_import_data) data_btn_layout.addWidget(self.btn_save_config) self.right_layout.addLayout(data_btn_layout) # 数据映射表格 self.data_mapper = DataMapperWidget(self) self.right_layout.addWidget(self.data_mapper, 1) # 字段映射区域 self.right_layout.addWidget(QLabel("字段映射配置:")) self.field_mapping_widget = QLabel("拖拽字段到模板区域进行映射") self.field_mapping_widget.setStyleSheet("min-height: 100px; background-color: #fff; border: 1px dashed #999;") self.right_layout.addWidget(self.field_mapping_widget) right_dock.setWidget(right_widget) self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, right_dock) # 底部操作区 bottom_widget = QWidget() bottom_layout = QHBoxLayout(bottom_widget) # 翻单历史选择 bottom_layout.addWidget(QLabel("翻单历史:")) self.history_combo = QComboBox() self.history_combo.setMinimumWidth(200) self.history_combo.currentIndexChanged.connect(self.on_history_selected) self.refresh_history_combo() # 历史管理按钮 self.btn_history = QPushButton("管理") self.btn_history.setToolTip("管理历史记录") self.btn_history.clicked.connect(self.manage_history) bottom_layout.addWidget(self.history_combo) bottom_layout.addWidget(self.btn_history) bottom_layout.addSpacing(20) # 合成按钮 self.btn_generate = QPushButton("开始合成") self.btn_generate.setStyleSheet( "background-color: #4CAF50; color: white; font-weight: bold; padding: 8px 16px;") self.btn_generate.clicked.connect(self.generate_output) bottom_layout.addWidget(self.btn_generate) # 翻单按钮 self.btn_rerun = QPushButton("执行翻单") self.btn_rerun.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold; padding: 8px 16px;") self.btn_rerun.clicked.connect(self.rerun_project) self.btn_rerun.setEnabled(False) bottom_layout.addWidget(self.btn_rerun) main_layout.addWidget(bottom_widget) def create_menus(self): """创建菜单栏""" menubar = self.menuBar() # 文件菜单 file_menu = menubar.addMenu('文件') # 使用 QAction 创建菜单项 new_action = QAction('新建项目', self) open_action = QAction('打开项目', self) save_action = QAction('保存项目', self) exit_action = QAction('退出', self) exit_action.triggered.connect(self.close) save_project_action = QAction('保存项目', self) save_project_action.triggered.connect(self.save_project) file_menu.addAction(save_project_action) open_project_action = QAction('打开项目', self) open_project_action.triggered.connect(self.open_project) file_menu.addAction(open_project_action) file_menu.addAction(new_action) file_menu.addAction(open_action) file_menu.addAction(save_action) file_menu.addSeparator() file_menu.addAction(exit_action) # 编辑菜单 edit_menu = menubar.addMenu('编辑') edit_menu.addAction('撤销') edit_menu.addAction('重做') edit_menu.addSeparator() font_action = QAction('字体管理', self) font_action.triggered.connect(self.open_font_manager) edit_menu.addAction(font_action) edit_menu.addAction('专色设置') # 视图菜单 view_menu = menubar.addMenu('视图') view_menu.addAction('放大') view_menu.addAction('缩小') view_menu.addAction('实际大小') # 帮助菜单 help_menu = menubar.addMenu('帮助') help_menu.addAction('用户手册') about_action = QAction('关于', self) about_action.triggered.connect(self.show_about) help_menu.addAction(about_action) def create_toolbar(self): """创建工具栏""" toolbar = QToolBar("主工具栏") self.addToolBar(toolbar) # 添加工具按钮 toolbar.addAction("打开模板", self.upload_template) toolbar.addAction("导入数据", self.import_data) toolbar.addSeparator() toolbar.addAction("添加文本", self.add_text_region) toolbar.addAction("添加二维码", self.add_qr_region) toolbar.addSeparator() toolbar.addAction("开始合成", self.generate_output) def apply_styles(self): """应用基本样式""" self.setStyleSheet(""" QMainWindow { background-color: #f5f5f5; } QPushButton { padding: 5px 10px; border: 1px solid #ccc; border-radius: 4px; } QPushButton:hover { background-color: #e9e9e9; } QDockWidget { background-color: #f0f0f0; border: 1px solid #ddd; titlebar-close-icon: url(resources/icons/close.png); titlebar-normal-icon: url(resources/icons/restore.png); } QDockWidget::title { background: #e0e0e0; padding: 4px; text-align: center; font-weight: bold; } QTabWidget::pane { border: 1px solid #ddd; top: 1px; background: white; } QTabBar::tab { background: #e0e0e0; border: 1px solid #ccc; padding: 6px 12px; border-top-left-radius: 4px; border-top-right-radius: 4px; } QTabBar::tab:selected { background: white; border-bottom-color: white; } """) # ===== 功能槽函数 ===== def upload_template(self): """上传模板图片""" file_path, _ = QFileDialog.getOpenFileName( self, "选择模板图片", self.data_path, "图像文件 (*.png *.jpg *.jpeg *.bmp);;PDF文件 (*.pdf)" ) if file_path: # 加载模板到画布 if self.template_canvas.load_template(file_path): self.status_bar.showMessage(f"已加载模板: {os.path.basename(file_path)}") # 更新区域信息 self.update_regions() def import_data(self): """导入数据文件""" file_path, _ = QFileDialog.getOpenFileName( self, "选择数据文件", self.data_path, "CSV文件 (*.csv);;Excel文件 (*.xlsx *.xls)" ) if file_path: self.status_bar.showMessage(f"已导入数据: {file_path}") # 解析并显示数据 self.data_mapper.import_data(file_path) def add_text_region(self): """添加文本区域""" self.template_canvas.set_mode(TemplateCanvas.MODE_TEXT) self.status_bar.showMessage("请在模板上框选文本区域") def add_qr_region(self): """添加二维码区域""" self.template_canvas.set_mode(TemplateCanvas.MODE_QR) self.status_bar.showMessage("请在模板上框选二维码区域") def save_config(self): """保存当前配置到项目文件""" if not self.template_canvas or not self.template_canvas.template_path: self.status_bar.showMessage("请先加载模板") return # 获取所有区域数据 regions_data = self.template_canvas.get_regions() # 获取映射关系 mapping = self.data_mapper.get_mapping() # 创建项目数据 project_data = { 'template_path': self.template_canvas.template_path, 'regions': regions_data, 'mapping': mapping } # 保存到文件 project_name = os.path.basename(self.template_canvas.template_path).split('.')[0] self.status_bar.showMessage(f"配置已保存: {project_name}") # 实际保存到文件 config_path = self.project_manager.save_project(project_data, project_name) if config_path: QMessageBox.information(self, "保存成功", f"项目已保存:\n{config_path}") self.refresh_history_combo() def generate_output(self): """开始合成""" self.status_bar.showMessage("合成中...") # 检查必要组件 if not self.template_canvas or not self.template_canvas.template_path: self.status_bar.showMessage("请先加载模板") QMessageBox.warning(self, "错误", "请先加载模板") return if not self.data_mapper.data_frame: self.status_bar.showMessage("请先导入数据") QMessageBox.warning(self, "错误", "请先导入数据") return # 获取区域和映射 regions = self.template_canvas.get_regions() mapping = self.data_mapper.get_mapping() # 生成输出文件名 timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") output_dir = os.path.join(self.data_path, "outputs") os.makedirs(output_dir, exist_ok=True) output_path = os.path.join(output_dir, f"output_{timestamp}.pdf") # 执行合成 success, logs = self.compositor.composite( template_path=self.template_canvas.template_path, regions=regions, mapping=mapping, data_source=self.data_mapper.data_frame, output_path=output_path, logo_path=self.logo_path if hasattr(self, 'logo_path') else "", spot_color_path=self.spot_color_path if hasattr(self, 'spot_color_path') else "" ) # 显示结果 if success: self.status_bar.showMessage(f"合成完成! 输出文件: {output_path}") # 显示预览对话框 preview_dialog = PreviewDialog(output_path, self) preview_dialog.exec() else: # 显示错误日志 error_msg = "\n".join(logs[-5:]) if logs else "未知错误" QMessageBox.critical(self, "合成错误", f"合成过程中发生错误:\n{error_msg}") def preview_current(self): """预览当前配置""" if not self.template_canvas or not self.template_canvas.template_path: QMessageBox.warning(self, "预览", "请先加载模板") return if not self.data_mapper.data_frame or len(self.data_mapper.data_frame) == 0: QMessageBox.warning(self, "预览", "请先导入数据") return # 获取第一行数据 data_row = self.data_mapper.data_frame.iloc[0].to_dict() # 获取区域和映射 regions = self.template_canvas.get_regions() mapping = self.data_mapper.get_mapping() # 创建临时预览文件 preview_path = os.path.join(self.compositor.temp_dir, "preview.png") # 执行合成 if hasattr(self.compositor, 'composite_single'): success, logs = self.compositor.composite_single( template_path=self.template_canvas.template_path, regions=regions, mapping=mapping, data_row=data_row, output_path=preview_path, logo_path=self.logo_path if hasattr(self, 'logo_path') else "", spot_color_path=self.spot_color_path if hasattr(self, 'spot_color_path') else "" ) else: QMessageBox.warning(self, "预览错误", "composite_single方法未实现") return if success: # 显示预览对话框 preview_dialog = PreviewDialog(preview_path, self) preview_dialog.exec() else: error_msg = "\n".join(logs) if logs else "未知错误" QMessageBox.critical(self, "预览错误", f"预览生成失败:\n{error_msg}") def open_font_manager(self): """打开字体管理器""" self.status_bar.showMessage("打开字体管理器") QMessageBox.information(self, "字体管理", "字体管理功能将在后续版本中实现") def update_regions(self): """更新模板区域信息""" if not self.template_canvas: return # 获取所有区域 regions = [region.get_name() for region in self.template_canvas.regions] # 更新数据映射模块 self.data_mapper.update_regions(regions) def update_property_panel(self, region): """更新属性面板显示""" if isinstance(region, TextRegionItem): self.props_tabs.setCurrentIndex(0) self.text_props_panel.set_region(region) elif isinstance(region, QRRegionItem): self.props_tabs.setCurrentIndex(1) self.qr_props_panel.set_region(region) elif isinstance(region, LogoRegionItem): self.props_tabs.setCurrentIndex(2) self.logo_props_panel.set_region(region) def handle_property_change(self, key, value): """处理属性变化""" self.template_canvas.set_region_property(key, value) def refresh_history_combo(self): """刷新历史记录下拉框""" self.history_combo.clear() self.history_combo.addItem("选择历史项目...", None) history = self.project_manager.get_history() for item in history: self.history_combo.addItem(item['name'], item['path']) def on_history_selected(self, index): """历史项目选择事件""" if index <= 0: self.current_project = None self.btn_rerun.setEnabled(False) return project_path = self.history_combo.itemData(index) if project_path: self.current_project = self.project_manager.load_project(project_path) self.btn_rerun.setEnabled(True) def manage_history(self): """打开历史记录管理对话框""" dialog = HistoryDialog(self.project_manager, self) if dialog.exec() == QDialog.DialogCode.Accepted: self.refresh_history_combo() def save_project(self): """保存当前项目配置""" if not self.template_canvas or not self.template_canvas.template_path: QMessageBox.warning(self, "保存项目", "请先加载模板") return if not self.data_mapper.data_frame: QMessageBox.warning(self, "保存项目", "请先导入数据") return # 获取项目数据 project_data = { 'template_path': self.template_canvas.template_path, 'regions': self.template_canvas.get_regions(), 'mapping': self.data_mapper.get_mapping(), 'data_source': os.path.basename(self.data_mapper.data_path) if hasattr(self.data_mapper, 'data_path') else "", 'logo_path': self.logo_path if hasattr(self, 'logo_path') else "", 'spot_color_path': self.spot_color_path if hasattr(self, 'spot_color_path') else "", 'timestamp': datetime.datetime.now().isoformat(), 'description': "" } # 输入项目名称 name, ok = QInputDialog.getText( self, "保存项目", "输入项目名称:", text=f"项目_{datetime.datetime.now().strftime('%Y%m%d')}" ) if ok and name: # 保存项目 config_path = self.project_manager.save_project(project_data, name) if config_path: QMessageBox.information(self, "保存成功", f"项目已保存:\n{config_path}") self.refresh_history_combo() def open_project(self): """打开项目""" # 使用历史记录对话框选择项目 dialog = HistoryDialog(self.project_manager, self) if dialog.exec() == QDialog.DialogCode.Accepted: selected_project = dialog.get_selected_project() if selected_project: self.load_project(selected_project['path']) def load_project(self, project_dir): """加载项目到界面""" project_data = self.project_manager.load_project(project_dir) if not project_data: QMessageBox.warning(self, "加载项目", "项目加载失败") return # 加载模板 if self.template_canvas.load_template(project_data['template_path']): self.status_bar.showMessage(f"已加载模板: {os.path.basename(project_data['template_path'])}") # 清除现有区域 self.template_canvas.clear_regions() # 添加区域 for region_data in project_data['regions']: self._create_region_from_data(region_data) # 更新区域显示 self.update_regions() # 加载Logo和专色路径 if 'logo_path' in project_data and os.path.exists(project_data['logo_path']): self.logo_path = project_data['logo_path'] if 'spot_color_path' in project_data and os.path.exists(project_data['spot_color_path']): self.spot_color_path = project_data['spot_color_path'] # 加载映射关系 self.data_mapper.mapping = project_data['mapping'] self.data_mapper.update_mapping_list() # 提示加载数据源 data_source = project_data.get('data_source', '') if data_source: data_path = self._get_data_source_path(project_data) if data_path: self.data_mapper.import_data(data_path) # 设置当前项目 self.current_project = project_data self.btn_rerun.setEnabled(True) # 更新历史记录下拉框 self.refresh_history_combo() # 选中当前项目 for i in range(self.history_combo.count()): if self.history_combo.itemData(i) == project_dir: self.history_combo.setCurrentIndex(i) break def _create_region_from_data(self, region_data): """根据区域数据创建区域项""" rect = QRectF( region_data['rect']['x'], region_data['rect']['y'], region_data['rect']['width'], region_data['rect']['height'] ) region = None if region_data['type'] == 'text': region = TextRegionItem(rect) region.set_name(region_data.get('name', '文本区域')) if 'font' in region_data: font = QFont() font.fromString(region_data['font']) region.set_font(font) if 'align' in region_data: region.set_align(region_data['align']) if 'color' in region_data: region.set_color(QColor(region_data['color'])) elif region_data['type'] == 'qr': region = QRRegionItem(rect) region.set_name(region_data.get('name', '二维码区域')) if 'code_type' in region_data: region.code_type = region_data['code_type'] elif region_data['type'] == 'logo': # 创建空区域,实际Logo在合成时加载 region = LogoRegionItem(rect, QPixmap()) region.set_name(region_data.get('name', 'Logo区域')) if 'opacity' in region_data: region.set_opacity(region_data['opacity']) # 添加到场景 if region: self.template_canvas.scene.addItem(region) self.template_canvas.regions.append(region) def rerun_project(self): """执行翻单操作""" if not self.current_project: QMessageBox.warning(self, "翻单", "请先选择项目") return # 确认数据源 data_source_path = self._get_data_source_path(self.current_project) if not data_source_path: return # 生成输出文件名 timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") output_dir = os.path.join(self.data_path, "outputs") os.makedirs(output_dir, exist_ok=True) output_path = os.path.join(output_dir, f"翻单_{self.current_project.get('name', 'project')}_{timestamp}.pdf") # 执行合成 success, logs = self.compositor.composite( template_path=self.current_project['template_path'], regions=self.current_project['regions'], mapping=self.current_project['mapping'], data_source=data_source_path, output_path=output_path, logo_path=self.current_project.get('logo_path', ''), spot_color_path=self.current_project.get('spot_color_path', '') ) # 显示结果 if success: self.status_bar.showMessage(f"翻单完成! 输出文件: {output_path}") # 显示预览对话框 preview_dialog = PreviewDialog(output_path, self) preview_dialog.exec() else: # 显示错误日志 error_msg = "\n".join(logs[-5:]) if logs else "未知错误" QMessageBox.critical(self, "翻单错误", f"翻单过程中发生错误:\n{error_msg}") def _get_data_source_path(self, project): """获取数据源文件路径""" # 1. 检查项目是否指定了数据源 data_source_name = project.get('data_source', '') if data_source_name: # 尝试在项目目录中查找 project_dir = os.path.dirname(project['template_path']) project_data_path = os.path.join(project_dir, data_source_name) if os.path.exists(project_data_path): return project_data_path # 尝试在数据目录中查找 data_dir = os.path.join(self.data_path, "data") data_path = os.path.join(data_dir, data_source_name) if os.path.exists(data_path): return data_path # 2. 提示用户选择数据文件 file_path, _ = QFileDialog.getOpenFileName( self, "选择数据文件", self.data_path, "CSV文件 (*.csv);;Excel文件 (*.xlsx *.xls)" ) return file_path if file_path else None def show_about(self): """显示关于对话框""" about_text = ( "<h2>自动拼版系统</h2>" "<p>版本: 1.0.0</p>" "<p>基于 PyQt6 构建</p>" "<p>© 2023 自动拼版系统团队</p>" ) QMessageBox.about(self, "关于", about_text) # 测试运行 if __name__ == "__main__": app = QApplication(sys.argv) window = MainWindow() window.show() sys.exit(app.exec()) # 检查内存模块 def log_memory(): try: snapshot = tracemalloc.take_snapshot() top_stats = snapshot.statistics('lineno') logger.info("内存使用统计:") for stat in top_stats[:10]: logger.info(stat) except Exception as e: logger.error(f"内存监控失败: {str(e)}") import atexit atexit.register(log_memory) 请帮我分析该代码块,并修改调整不合理的地方,以及排查出刚才分析的AttributeError: ‘NoneType’ object has no attribute ‘setEnabled’问题,并输出完整的修改后的main_window.py的代码
07-11
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值