Files
niumasoftware/ui/dialog_style.py
T
2026-04-08 10:11:16 +08:00

304 lines
9.0 KiB
Python

"""标准对话框(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 ""