"""标准对话框(QMessageBox / QInputDialog / QFileDialog)与当前主题一致。""" from __future__ import annotations from PyQt6.QtCore import Qt, QTimer from PyQt6.QtGui import QTextOption from PyQt6.QtWidgets import ( QFileDialog, QInputDialog, QMessageBox, QWidget, QDialog, QLabel, QSizePolicy, ) import ui.theme as theme def stylesheet() -> str: t = theme.current() bg = t["menu_bg"] fg = t["menu_color"] border = t["menu_border"] hover = t["menu_selected"] inp = t["search_bg"] inp_b = t["search_border"] focus = t["search_focus"] return f""" QMessageBox {{ background-color: {bg}; color: {fg}; }} QMessageBox QLabel {{ color: {fg}; background: transparent; min-width: 0px; }} QMessageBox QLabel#qt_msgbox_label {{ min-width: 200px; }} QMessageBox QLabel#qt_msgboxex_icon_label {{ min-width: 28px; max-width: 28px; }} QMessageBox QPushButton {{ min-width: 64px; padding: 6px 14px; border: 1px solid {border}; border-radius: 5px; background: {inp}; color: {fg}; }} QMessageBox QPushButton:hover {{ background: {hover}; }} QMessageBox QPushButton:default {{ border-color: {focus}; }} QInputDialog {{ background-color: {bg}; color: {fg}; }} QInputDialog QLabel {{ color: {fg}; background: transparent; }} QInputDialog QLineEdit {{ background: {inp}; border: 1px solid {inp_b}; border-radius: 5px; padding: 6px 8px; color: {fg}; min-width: 260px; }} QInputDialog QLineEdit:focus {{ border-color: {focus}; }} QInputDialog QPushButton {{ min-width: 72px; padding: 6px 14px; border: 1px solid {border}; border-radius: 5px; background: {inp}; color: {fg}; }} QInputDialog QPushButton:hover {{ background: {hover}; }} QFileDialog {{ background-color: {bg}; color: {fg}; }} QFileDialog QLabel {{ color: {fg}; background: transparent; }} QFileDialog QLineEdit, QFileDialog QComboBox {{ background: {inp}; border: 1px solid {inp_b}; border-radius: 4px; padding: 4px 6px; color: {fg}; }} QFileDialog QLineEdit:focus, QFileDialog QComboBox:focus {{ border-color: {focus}; }} QFileDialog QTreeView, QFileDialog QListView, QFileDialog QTableView {{ background: {inp}; border: 1px solid {inp_b}; border-radius: 4px; color: {fg}; outline: none; }} QFileDialog QTreeView::item:selected, QFileDialog QListView::item:selected {{ background: {hover}; }} QFileDialog QPushButton {{ min-width: 72px; padding: 6px 12px; border: 1px solid {border}; border-radius: 5px; background: {inp}; color: {fg}; }} QFileDialog QPushButton:hover {{ background: {hover}; }} QFileDialog QComboBox QAbstractItemView {{ background: {bg}; color: {fg}; border: 1px solid {border}; }} """ def _apply(w: QWidget | None) -> None: if w is not None: w.setStyleSheet(stylesheet()) def _prepare_qmessagebox(msg: QMessageBox) -> None: """让提示框按内容换行并自适应宽高。""" def _tune_labels() -> None: # QMessageBox 里通常有两段文字: # - qt_msgbox_label(主文本) # - qt_msgbox_informativelabel(辅助文本/更长的提示) # 部分平台会在显示/布局后才创建这些 label,因此这里支持延迟执行一次。 for obj_name in ("qt_msgbox_label", "qt_msgbox_informativelabel"): w = msg.findChild(QWidget, obj_name) if w is None: continue if hasattr(w, "setWordWrap"): try: w.setWordWrap(True) # type: ignore[attr-defined] except Exception: pass if hasattr(w, "setTextFormat"): try: w.setTextFormat(Qt.TextFormat.PlainText) # type: ignore[attr-defined] except Exception: pass if hasattr(w, "setWordWrapMode"): try: w.setWordWrapMode(QTextOption.WrapMode.WrapAnywhere) # type: ignore[attr-defined] except Exception: pass if hasattr(w, "setTextElideMode"): try: w.setTextElideMode(Qt.TextElideMode.ElideNone) # type: ignore[attr-defined] except Exception: pass if hasattr(w, "setMinimumWidth"): try: w.setMinimumWidth(0) # type: ignore[attr-defined] except Exception: pass if hasattr(w, "setMaximumWidth"): try: w.setMaximumWidth(16777215) # type: ignore[attr-defined] except Exception: pass if hasattr(w, "setSizePolicy"): try: w.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.MinimumExpanding, ) # type: ignore[attr-defined] except Exception: pass msg.setSizePolicy( QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred, ) msg.setMinimumSize(0, 0) msg.setMaximumSize(16777215, 16777215) lay = msg.layout() if lay is not None: # QMessageBox 内部通常是 QGridLayout:第 1 列是文本列 # 某些主题/平台会让文本列偏窄从而触发省略,这里显式拉伸该列并设置最小宽度。 try: lay.setColumnStretch(1, 1) except Exception: pass lay.activate() _tune_labels() msg.adjustSize() QTimer.singleShot(0, lambda: (_tune_labels(), msg.adjustSize())) def question( parent: QWidget | None, title: str, text: str, *, buttons: QMessageBox.StandardButton = ( QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No ), default_button: QMessageBox.StandardButton | None = None, icon: QMessageBox.Icon = QMessageBox.Icon.Question, ) -> QMessageBox.StandardButton: msg = QMessageBox(parent) msg.setWindowTitle(title) msg.setText(text) msg.setIcon(icon) msg.setStandardButtons(buttons) if default_button is not None: msg.setDefaultButton(default_button) _apply(msg) _prepare_qmessagebox(msg) return QMessageBox.StandardButton(msg.exec()) def warning(parent: QWidget | None, title: str, text: str) -> None: msg = QMessageBox(parent) msg.setWindowTitle(title) msg.setText(text) msg.setIcon(QMessageBox.Icon.Warning) msg.setStandardButtons(QMessageBox.StandardButton.Ok) _apply(msg) _prepare_qmessagebox(msg) msg.exec() def information(parent: QWidget | None, title: str, text: str) -> None: msg = QMessageBox(parent) msg.setWindowTitle(title) msg.setText(text) msg.setIcon(QMessageBox.Icon.Information) msg.setStandardButtons(QMessageBox.StandardButton.Ok) _apply(msg) _prepare_qmessagebox(msg) msg.exec() def get_text( parent: QWidget | None, title: str, label: str, text: str = "", ) -> tuple[str, bool]: d = QInputDialog(parent) d.setWindowTitle(title) d.setLabelText(label) d.setTextValue(text) _apply(d) ok = d.exec() == QDialog.DialogCode.Accepted return d.textValue(), ok def get_open_file_name( parent: QWidget | None, caption: str, directory: str, filter_str: str, ) -> tuple[str, str]: fd = QFileDialog(parent, caption, directory, filter_str) fd.setOption(QFileDialog.Option.DontUseNativeDialog, True) fd.setFileMode(QFileDialog.FileMode.ExistingFile) _apply(fd) if fd.exec() == QDialog.DialogCode.Accepted: files = fd.selectedFiles() if files: return files[0], fd.selectedNameFilter() return "", "" def get_existing_directory( parent: QWidget | None, caption: str, directory: str = "", ) -> str: fd = QFileDialog(parent, caption, directory) fd.setOption(QFileDialog.Option.DontUseNativeDialog, True) fd.setFileMode(QFileDialog.FileMode.Directory) fd.setOption(QFileDialog.Option.ShowDirsOnly, True) _apply(fd) if fd.exec() == QDialog.DialogCode.Accepted: files = fd.selectedFiles() if files: return files[0] return ""