From 178c7a75d0c407d995eb735aa138099fcd2438f4 Mon Sep 17 00:00:00 2001 From: jimmy-sketch Date: Sun, 30 Nov 2025 19:02:10 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(ui):=20=E7=BB=9F=E4=B8=80=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E6=8E=A7=E4=BB=B6=E5=AE=BD=E5=BA=A6=E4=BB=A5=E9=80=82?= =?UTF-8?q?=E5=BA=94=E6=96=87=E6=9C=AC=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除了多个按钮和下拉框的固定宽度设置 - 添加了 _adjustControlWidgetWidths 方法动态计算并设置控件宽度 - 在界面初始化和数据更新后调用宽度调整逻辑 - 实现了导航栏按钮宽度自适应多语言文本的功能 - 修复了 Nuitka 打包时文件目标路径的问题 - 改进了版本号处理逻辑,确保兼容不同格式 - 更新了编译器检查逻辑以支持 Python 3.13 及以上版本 --- app/view/main/lottery.py | 68 +++++++++++-- app/view/main/roll_call.py | 65 ++++++++++-- app/view/main/window.py | 78 ++++++++++++++ app/view/settings/settings.py | 78 ++++++++++++++ build_nuitka.py | 187 ++++++++++++++-------------------- 5 files changed, 353 insertions(+), 123 deletions(-) diff --git a/app/view/main/lottery.py b/app/view/main/lottery.py index c88da300..ab1eee64 100644 --- a/app/view/main/lottery.py +++ b/app/view/main/lottery.py @@ -108,7 +108,7 @@ def initUI(self): get_content_pushbutton_name_async("lottery", "reset_button") ) self._set_widget_font(self.reset_button, 15) - self.reset_button.setFixedSize(165, 45) + self.reset_button.setFixedHeight(45) self.reset_button.clicked.connect(lambda: self.reset_count()) self.minus_button = PushButton("-") @@ -150,12 +150,12 @@ def initUI(self): get_content_pushbutton_name_async("lottery", "start_button") ) self._set_widget_font(self.start_button, 15) - self.start_button.setFixedSize(165, 45) + self.start_button.setFixedHeight(45) self.start_button.clicked.connect(lambda: self.start_draw()) self.pool_list_combobox = ComboBox() self._set_widget_font(self.pool_list_combobox, 12) - self.pool_list_combobox.setFixedSize(165, 45) + self.pool_list_combobox.setFixedHeight(45) self.pool_list_combobox.setPlaceholderText( get_content_name_async("lottery", "default_empty_item") ) @@ -164,19 +164,19 @@ def initUI(self): self.list_combobox = ComboBox() self._set_widget_font(self.list_combobox, 12) - self.list_combobox.setFixedSize(165, 45) + self.list_combobox.setFixedHeight(45) # 延迟填充班级列表,避免启动时进行文件IO self.list_combobox.currentTextChanged.connect(self.on_class_changed) self.range_combobox = ComboBox() self._set_widget_font(self.range_combobox, 12) - self.range_combobox.setFixedSize(165, 45) + self.range_combobox.setFixedHeight(45) # 延迟填充范围选项 self.range_combobox.currentTextChanged.connect(self.on_filter_changed) self.gender_combobox = ComboBox() self._set_widget_font(self.gender_combobox, 12) - self.gender_combobox.setFixedSize(165, 45) + self.gender_combobox.setFixedHeight(45) # 延迟填充性别选项 self.gender_combobox.currentTextChanged.connect(self.on_filter_changed) @@ -184,7 +184,7 @@ def initUI(self): get_content_pushbutton_name_async("lottery", "remaining_button") ) self._set_widget_font(self.remaining_button, 12) - self.remaining_button.setFixedSize(165, 45) + self.remaining_button.setFixedHeight(45) self.remaining_button.clicked.connect(lambda: self.show_remaining_list()) # 初始时不进行昂贵的数据加载,改为延迟填充 @@ -197,7 +197,6 @@ def initUI(self): self.many_count_label = BodyLabel(formatted_text) self.many_count_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self._set_widget_font(self.many_count_label, 10) - self.many_count_label.setFixedWidth(165) self.control_widget = QWidget() self.control_layout = QVBoxLayout(self.control_widget) @@ -285,6 +284,9 @@ def initUI(self): main_layout.addWidget(scroll, 1) main_layout.addWidget(self.control_widget) + # 统一调整控件宽度以适应文本内容 + self._adjustControlWidgetWidths() + # 在事件循环中延迟填充下拉框和初始统计,减少启动阻塞 QTimer.singleShot(0, self.populate_lists) @@ -301,6 +303,53 @@ def add_control_widget_if_enabled( # 出错时默认添加控件 layout.addWidget(widget, alignment=Qt.AlignmentFlag.AlignCenter) + def _adjustControlWidgetWidths(self): + """统一调整控件宽度以适应文本内容""" + try: + # 收集所有需要调整宽度的控件 + widgets_to_adjust = [ + self.reset_button, + self.start_button, + self.pool_list_combobox, + self.list_combobox, + self.range_combobox, + self.gender_combobox, + self.remaining_button, + self.many_count_label, + ] + + # 计算所有控件文本所需的最大宽度 + max_text_width = 0 + for widget in widgets_to_adjust: + fm = widget.fontMetrics() + # 检查按钮/标签文本 + if hasattr(widget, "text") and widget.text(): + text_width = fm.horizontalAdvance(widget.text()) + max_text_width = max(max_text_width, text_width) + # 检查占位符文本 + if hasattr(widget, "placeholderText") and widget.placeholderText(): + text_width = fm.horizontalAdvance(widget.placeholderText()) + max_text_width = max(max_text_width, text_width) + # 检查下拉框所有选项的宽度 + if hasattr(widget, "count"): + for i in range(widget.count()): + item_text = widget.itemText(i) + if item_text: + text_width = fm.horizontalAdvance(item_text) + max_text_width = max(max_text_width, text_width) + + # 计算统一宽度(文本宽度 + 边距 + 下拉框箭头空间) + padding = 60 # 左右边距 + 下拉箭头空间 + min_width = 165 # 最小宽度 + unified_width = max(min_width, max_text_width + padding) + + # 设置所有控件的固定宽度 + for widget in widgets_to_adjust: + widget.setFixedWidth(int(unified_width)) + + except Exception as e: + logger.debug(f"调整控件宽度时出错: {e}") + def on_pool_changed(self): """当奖池选择改变时,更新奖数显示""" try: @@ -961,6 +1010,9 @@ def populate_lists(self): LotteryUtils.update_start_button_state(self.start_button, total_count) + # 重新调整控件宽度以适应下拉框内容 + self._adjustControlWidgetWidths() + except Exception as e: logger.error(f"延迟填充列表失败: {e}") diff --git a/app/view/main/roll_call.py b/app/view/main/roll_call.py index 560ecaf6..cd94d69b 100644 --- a/app/view/main/roll_call.py +++ b/app/view/main/roll_call.py @@ -108,7 +108,7 @@ def initUI(self): get_content_pushbutton_name_async("roll_call", "reset_button") ) self._set_widget_font(self.reset_button, 15) - self.reset_button.setFixedSize(165, 45) + self.reset_button.setFixedHeight(45) self.reset_button.clicked.connect(lambda: self.reset_count()) self.minus_button = PushButton("-") @@ -150,12 +150,12 @@ def initUI(self): get_content_pushbutton_name_async("roll_call", "start_button") ) self._set_widget_font(self.start_button, 15) - self.start_button.setFixedSize(165, 45) + self.start_button.setFixedHeight(45) self.start_button.clicked.connect(lambda: self.start_draw()) self.list_combobox = ComboBox() self._set_widget_font(self.list_combobox, 12) - self.list_combobox.setFixedSize(165, 45) + self.list_combobox.setFixedHeight(45) self.list_combobox.setPlaceholderText( get_content_name_async("roll_call", "default_empty_item") ) @@ -164,13 +164,13 @@ def initUI(self): self.range_combobox = ComboBox() self._set_widget_font(self.range_combobox, 12) - self.range_combobox.setFixedSize(165, 45) + self.range_combobox.setFixedHeight(45) # 延迟填充范围选项 self.range_combobox.currentTextChanged.connect(self.on_filter_changed) self.gender_combobox = ComboBox() self._set_widget_font(self.gender_combobox, 12) - self.gender_combobox.setFixedSize(165, 45) + self.gender_combobox.setFixedHeight(45) # 延迟填充性别选项 self.gender_combobox.currentTextChanged.connect(self.on_filter_changed) @@ -178,7 +178,7 @@ def initUI(self): get_content_pushbutton_name_async("roll_call", "remaining_button") ) self._set_widget_font(self.remaining_button, 12) - self.remaining_button.setFixedSize(165, 45) + self.remaining_button.setFixedHeight(45) self.remaining_button.clicked.connect(lambda: self.show_remaining_list()) # 初始时不进行昂贵的数据加载,改为延迟填充 @@ -193,7 +193,6 @@ def initUI(self): self.many_count_label = BodyLabel(formatted_text) self.many_count_label.setAlignment(Qt.AlignmentFlag.AlignCenter) self._set_widget_font(self.many_count_label, 10) - self.many_count_label.setFixedWidth(165) self.control_widget = QWidget() self.control_layout = QVBoxLayout(self.control_widget) @@ -271,6 +270,9 @@ def initUI(self): main_layout.addWidget(scroll, 1) main_layout.addWidget(self.control_widget) + # 统一调整控件宽度以适应文本内容 + self._adjustControlWidgetWidths() + # 在事件循环中延迟填充下拉框和初始统计,减少启动阻塞 QTimer.singleShot(0, self.populate_lists) @@ -287,6 +289,52 @@ def add_control_widget_if_enabled( # 出错时默认添加控件 layout.addWidget(widget, alignment=Qt.AlignmentFlag.AlignCenter) + def _adjustControlWidgetWidths(self): + """统一调整控件宽度以适应文本内容""" + try: + # 收集所有需要调整宽度的控件 + widgets_to_adjust = [ + self.reset_button, + self.start_button, + self.list_combobox, + self.range_combobox, + self.gender_combobox, + self.remaining_button, + self.many_count_label, + ] + + # 计算所有控件文本所需的最大宽度 + max_text_width = 0 + for widget in widgets_to_adjust: + fm = widget.fontMetrics() + # 检查按钮/标签文本 + if hasattr(widget, "text") and widget.text(): + text_width = fm.horizontalAdvance(widget.text()) + max_text_width = max(max_text_width, text_width) + # 检查占位符文本 + if hasattr(widget, "placeholderText") and widget.placeholderText(): + text_width = fm.horizontalAdvance(widget.placeholderText()) + max_text_width = max(max_text_width, text_width) + # 检查下拉框所有选项的宽度 + if hasattr(widget, "count"): + for i in range(widget.count()): + item_text = widget.itemText(i) + if item_text: + text_width = fm.horizontalAdvance(item_text) + max_text_width = max(max_text_width, text_width) + + # 计算统一宽度(文本宽度 + 边距 + 下拉框箭头空间) + padding = 60 # 左右边距 + 下拉箭头空间 + min_width = 165 # 最小宽度 + unified_width = max(min_width, max_text_width + padding) + + # 设置所有控件的固定宽度 + for widget in widgets_to_adjust: + widget.setFixedWidth(int(unified_width)) + + except Exception as e: + logger.debug(f"调整控件宽度时出错: {e}") + def on_class_changed(self): """当班级选择改变时,更新范围选择、性别选择和人数显示""" self.range_combobox.blockSignals(True) @@ -884,6 +932,9 @@ def populate_lists(self): # 根据总人数是否为0,启用或禁用开始按钮 RollCallUtils.update_start_button_state(self.start_button, total_count) + # 重新调整控件宽度以适应下拉框内容 + self._adjustControlWidgetWidths() + except Exception as e: logger.error(f"延迟填充列表失败: {e}") diff --git a/app/view/main/window.py b/app/view/main/window.py index b8034f78..82ccad12 100644 --- a/app/view/main/window.py +++ b/app/view/main/window.py @@ -242,6 +242,84 @@ def initNavigation(self): ) settings_item.clicked.connect(lambda: self.switchTo(self.roll_call_page)) + # 调整侧边栏宽度以适应多语言文本 + self._adjustNavigationBarWidth() + + def _adjustNavigationBarWidth(self): + """调整导航栏宽度以适应多语言文本""" + try: + nav = self.navigationInterface + if not nav or not hasattr(nav, "buttons"): + return + + # 计算所有按钮文本所需的最大宽度 + max_text_width = 0 + buttons = nav.buttons() + for button in buttons: + if hasattr(button, "text") and button.text(): + # 使用按钮的字体度量计算文本宽度 + fm = button.fontMetrics() + text_width = fm.horizontalAdvance(button.text()) + max_text_width = max(max_text_width, text_width) + + # 计算所需的按钮宽度(文本宽度 + 左右边距) + # NavigationBarPushButton 默认为 64x58,图标在上,文本在下 + button_padding = 16 # 左右边距 + min_button_width = 64 # 最小按钮宽度 + required_width = max(min_button_width, max_text_width + button_padding) + + # 设置所有按钮的宽度,并重写图标绘制方法使图标居中 + for button in buttons: + button.setFixedWidth(int(required_width)) + # 重写 _drawIcon 方法使图标居中 + self._patchButtonDrawIcon(button, int(required_width)) + + # 设置导航栏宽度(按钮宽度 + 滚动区域边距) + nav_padding = 8 # 导航栏左右边距 + nav_width = int(required_width + nav_padding) + nav.setFixedWidth(nav_width) + + except Exception as e: + logger.debug(f"调整导航栏宽度时出错: {e}") + + def _patchButtonDrawIcon(self, button, button_width): + """修补按钮的图标绘制方法,使图标居中""" + from PySide6.QtCore import QRectF + from PySide6.QtGui import QPainter + from qfluentwidgets.common.icon import drawIcon, FluentIconBase + from qfluentwidgets.common.color import autoFallbackThemeColor + + original_draw_icon = button._drawIcon + + def centered_draw_icon(painter: QPainter): + if (button.isPressed or not button.isEnter) and not button.isSelected: + painter.setOpacity(0.6) + if not button.isEnabled(): + painter.setOpacity(0.4) + + # 计算居中的图标位置 + icon_size = 20 + icon_x = (button_width - icon_size) / 2 + icon_y = 13 + if hasattr(button, "iconAni") and not button._isSelectedTextVisible: + icon_y += button.iconAni.offset + + rect = QRectF(icon_x, icon_y, icon_size, icon_size) + + selectedIcon = button._selectedIcon or button._icon + + if isinstance(selectedIcon, FluentIconBase) and button.isSelected: + color = autoFallbackThemeColor( + button.lightSelectedColor, button.darkSelectedColor + ) + selectedIcon.render(painter, rect, fill=color.name()) + elif button.isSelected: + drawIcon(selectedIcon, painter, rect) + else: + drawIcon(button._icon, painter, rect) + + button._drawIcon = centered_draw_icon + def _toggle_float_window(self): if self.float_window.isVisible(): self.float_window.hide() diff --git a/app/view/settings/settings.py b/app/view/settings/settings.py index 6f6cf806..2a94829d 100644 --- a/app/view/settings/settings.py +++ b/app/view/settings/settings.py @@ -737,6 +737,9 @@ def initNavigation(self): position=NavigationItemPosition.BOTTOM, ) + # 调整侧边栏宽度以适应多语言文本 + self._adjustNavigationBarWidth() + self.splashScreen.finish() # 连接信号 @@ -823,6 +826,81 @@ def show_settings_window_about(self): self.switchTo(self.aboutInterface) + def _adjustNavigationBarWidth(self): + """调整导航栏宽度以适应多语言文本""" + try: + nav = self.navigationInterface + if not nav or not hasattr(nav, "buttons"): + return + + # 计算所有按钮文本所需的最大宽度 + max_text_width = 0 + buttons = nav.buttons() + for button in buttons: + if hasattr(button, "text") and button.text(): + # 使用按钮的字体度量计算文本宽度 + fm = button.fontMetrics() + text_width = fm.horizontalAdvance(button.text()) + max_text_width = max(max_text_width, text_width) + + # 计算所需的按钮宽度(文本宽度 + 左右边距) + # NavigationBarPushButton 默认为 64x58,图标在上,文本在下 + button_padding = 16 # 左右边距 + min_button_width = 64 # 最小按钮宽度 + required_width = max(min_button_width, max_text_width + button_padding) + + # 设置所有按钮的宽度,并重写图标绘制方法使图标居中 + for button in buttons: + button.setFixedWidth(int(required_width)) + # 重写 _drawIcon 方法使图标居中 + self._patchButtonDrawIcon(button, int(required_width)) + + # 设置导航栏宽度(按钮宽度 + 滚动区域边距) + nav_padding = 8 # 导航栏左右边距 + nav_width = int(required_width + nav_padding) + nav.setFixedWidth(nav_width) + + except Exception as e: + logger.debug(f"调整导航栏宽度时出错: {e}") + + def _patchButtonDrawIcon(self, button, button_width): + """修补按钮的图标绘制方法,使图标居中""" + from PySide6.QtCore import QRectF + from PySide6.QtGui import QPainter + from qfluentwidgets.common.icon import drawIcon, FluentIconBase + from qfluentwidgets.common.color import autoFallbackThemeColor + + original_draw_icon = button._drawIcon + + def centered_draw_icon(painter: QPainter): + if (button.isPressed or not button.isEnter) and not button.isSelected: + painter.setOpacity(0.6) + if not button.isEnabled(): + painter.setOpacity(0.4) + + # 计算居中的图标位置 + icon_size = 20 + icon_x = (button_width - icon_size) / 2 + icon_y = 13 + if hasattr(button, "iconAni") and not button._isSelectedTextVisible: + icon_y += button.iconAni.offset + + rect = QRectF(icon_x, icon_y, icon_size, icon_size) + + selectedIcon = button._selectedIcon or button._icon + + if isinstance(selectedIcon, FluentIconBase) and button.isSelected: + color = autoFallbackThemeColor( + button.lightSelectedColor, button.darkSelectedColor + ) + selectedIcon.render(painter, rect, fill=color.name()) + elif button.isSelected: + drawIcon(selectedIcon, painter, rect) + else: + drawIcon(button._icon, painter, rect) + + button._drawIcon = centered_draw_icon + def _apply_sidebar_settings(self): """应用侧边栏设置""" # 由于导航项已在initNavigation中根据设置处理,这里不再需要重复处理 diff --git a/build_nuitka.py b/build_nuitka.py index 922e46eb..fda01c74 100644 --- a/build_nuitka.py +++ b/build_nuitka.py @@ -4,6 +4,7 @@ import subprocess import sys +import re from pathlib import Path # 设置Windows控制台编码为UTF-8 @@ -17,20 +18,15 @@ ADDITIONAL_HIDDEN_IMPORTS, ICON_FILE, PROJECT_ROOT, - VERSION_FILE, collect_data_includes, collect_language_modules, collect_view_modules, normalize_hidden_imports, ) -# 导入项目配置信息 from app.tools.variable import APPLY_NAME, VERSION, APP_DESCRIPTION, AUTHOR, WEBSITE - -# 导入deb包构建工具 from packaging_utils_deb import DebBuilder - PACKAGE_INCLUDE_NAMES = { "app.Language.modules", "app.view", @@ -39,34 +35,24 @@ } -def _read_version() -> str: - try: - return VERSION_FILE.read_text(encoding="utf-8").strip() - except FileNotFoundError: - return "0.0.0" - - def _print_packaging_summary() -> None: data_includes = collect_data_includes() hidden_names = normalize_hidden_imports( collect_language_modules() + collect_view_modules() + ADDITIONAL_HIDDEN_IMPORTS ) - package_names = sorted( {name for name in hidden_names if "." not in name} | PACKAGE_INCLUDE_NAMES ) module_names = [name for name in hidden_names if "." in name] - print("\nSelected data includes ({} entries):".format(len(data_includes))) + print(f"\nSelected data includes ({len(data_includes)} entries):") for item in data_includes: kind = "dir " if item.is_dir else "file" print(f" - {kind} {item.source} -> {item.target}") - - print("\nRequired packages ({} entries):".format(len(package_names))) + print(f"\nRequired packages ({len(package_names)} entries):") for pkg in package_names: print(f" - {pkg}") - - print("\nHidden modules ({} entries):".format(len(module_names))) + print(f"\nHidden modules ({len(module_names)} entries):") for mod in module_names: print(f" - {mod}") @@ -75,7 +61,12 @@ def _gather_data_flags() -> list[str]: flags: list[str] = [] for include in collect_data_includes(): flag = "--include-data-dir" if include.is_dir else "--include-data-file" - flags.append(f"{flag}={include.source}={include.target}") + source = include.source + target = include.target + # FIX: Nuitka 不允许 file 目标为 "." + if not include.is_dir and target == ".": + target = Path(source).name + flags.append(f"{flag}={source}={target}") return flags @@ -83,25 +74,35 @@ def _gather_module_and_package_flags() -> tuple[list[str], list[str]]: hidden_names = normalize_hidden_imports( collect_language_modules() + collect_view_modules() + ADDITIONAL_HIDDEN_IMPORTS ) - package_names = set(PACKAGE_INCLUDE_NAMES) module_names: list[str] = [] - for name in hidden_names: if "." not in name: package_names.add(name) else: module_names.append(name) - package_flags = [f"--include-package={pkg}" for pkg in sorted(package_names)] module_flags = [f"--include-module={mod}" for mod in module_names] return module_flags, package_flags -def get_nuitka_command(): - """生成 Nuitka 打包命令""" +def _sanitize_version(ver_str: str) -> str: + if not ver_str: + return "0.0.0.0" + ver_str = ver_str.lstrip("vV").strip() + match = re.match(r"^(\d+(\.\d+)*)", ver_str) + if match: + clean_ver = match.group(1) + if "." not in clean_ver: + clean_ver += ".0" + return clean_ver + return "0.0.0.0" - version = _read_version() + +def get_nuitka_command(): + raw_version = VERSION if VERSION else "0.0.0" + clean_version = _sanitize_version(raw_version) + print(f"\n版本号处理: '{raw_version}' -> '{clean_version}'") module_flags, package_flags = _gather_module_and_package_flags() @@ -114,164 +115,134 @@ def get_nuitka_command(): "--onefile", "--enable-plugin=pyside6", "--assume-yes-for-downloads", - # 输出目录 "--output-dir=dist", - # 应用程序信息 "--product-name=SecRandom", "--file-description=公平随机抽取系统", - f"--product-version={version}", + f"--product-version={clean_version}", + f"--file-version={clean_version}", "--copyright=Copyright (c) 2025", - # **修复 QFluentWidgets 方法签名检测问题** - # Nuitka 在 standalone 模式下会改变代码执行环境, - # 导致 QFluentWidgets 的 overload.py 签名检测失败 "--no-deployment-flag=self-execution", ] - # 根据平台添加特定参数 + # === 编译器选择逻辑 === if sys.platform == "win32": - # Windows 特定参数 - cmd.append("--mingw64") # 使用 MinGW64 编译器 + # 检测是否为 Python 3.13 及以上 + if sys.version_info >= (3, 13): + print("\n[注意] 检测到 Python 3.13+") + print(" Nuitka 暂不支持在此版本使用 MinGW64。") + print( + " 将自动切换为 MSVC (Visual Studio)。请确保已安装 C++ 生成工具。" + ) + cmd.append("--msvc=latest") + else: + # Python 3.12 及以下使用 MinGW64 + cmd.append("--mingw64") else: - # Linux 特定参数 cmd.append("--linux-onefile-icon") cmd.extend(_gather_data_flags()) cmd.extend(package_flags) cmd.extend(module_flags) - # 根据平台添加图标参数 if sys.platform == "win32" and ICON_FILE.exists(): cmd.append(f"--windows-icon-from-ico={ICON_FILE}") elif sys.platform == "linux" and ICON_FILE.exists(): cmd.append(f"--linux-icon={ICON_FILE}") - # 主入口文件 cmd.append("main.py") - return cmd -def check_mingw64(): - """检查 MinGW64 是否可用""" - print("\n检查 MinGW64 环境...") +def check_compiler_env(): + """检查编译器环境""" + if sys.platform != "win32": + return True + + # 如果是 Python 3.13+,需要检查 MSVC(这里简单略过,交给 Nuitka 报错,因为检测 MSVC 比较复杂) + if sys.version_info >= (3, 13): + return True - # 检查是否在 PATH 中 - gcc_path = None + # 如果是 Python < 3.13,检查 MinGW64 + print("\n检查 MinGW64 环境...") try: result = subprocess.run( - ["gcc", "--version"], capture_output=True, text=True, check=False + ["gcc", "--version"], + capture_output=True, + text=True, + check=False, + encoding="utf-8", + errors="replace", ) if result.returncode == 0: - gcc_path = "gcc (在 PATH 中)" - print(f"✓ 找到 GCC: {gcc_path}") - print(f" 版本信息: {result.stdout.splitlines()[0]}") + print( + f"✓ 找到 GCC: {result.stdout.splitlines()[0] if result.stdout else 'Unknown'}" + ) return True except FileNotFoundError: pass - # 检查常见的 MinGW64 安装位置 + # 简单检查路径 common_paths = [ r"C:\msys64\mingw64\bin", r"C:\mingw64\bin", r"C:\Program Files\mingw64\bin", - r"C:\msys64\ucrt64\bin", ] - for path in common_paths: - gcc_exe = Path(path) / "gcc.exe" - if gcc_exe.exists(): + if (Path(path) / "gcc.exe").exists(): print(f"✓ 找到 MinGW64: {path}") - print(f" 提示: 请确保 {path} 在系统 PATH 环境变量中") return True - print("⚠ 警告: 未找到 MinGW64") - print("\n请按照以下步骤安装 MinGW64:") - print("1. 下载 MSYS2: https://www.msys2.org/") - print("2. 安装后运行: pacman -S mingw-w64-x86_64-gcc") - print("3. 将 C:\\msys64\\mingw64\\bin 添加到系统 PATH") - print("\n或者使用 Nuitka 自动下载 MinGW64 (首次运行会自动下载)") - - response = input("\n是否继续? Nuitka 可以自动下载 MinGW64 (y/n): ") - return response.lower() == "y" + print("⚠ 警告: 未找到 MinGW64,Nuitka 可能会尝试自动下载。") + return input("是否继续? (y/n): ").lower() == "y" def build_deb() -> None: - """构建deb包(适用于Nuitka单文件输出)""" if sys.platform != "linux": return - - print("\n" + "=" * 60) - print("开始构建deb包...") - print("=" * 60) - + print("\n" + "=" * 60 + "\n开始构建deb包...\n" + "=" * 60) try: - # 使用DebBuilder构建deb包 DebBuilder.build_from_nuitka( - project_root=PROJECT_ROOT, - app_name=APPLY_NAME, - version=VERSION, - description=APP_DESCRIPTION, - author=AUTHOR, - website=WEBSITE, + PROJECT_ROOT, APPLY_NAME, VERSION, APP_DESCRIPTION, AUTHOR, WEBSITE ) - print("=" * 60) - except Exception as e: print(f"构建deb包失败: {e}") sys.exit(1) def main(): - """执行打包""" print("=" * 60) - if sys.platform == "win32": - print("开始使用 Nuitka + MinGW64 + uv 打包 SecRandom") - else: - print("开始使用 Nuitka + uv 打包 SecRandom") + print( + f"开始打包 SecRandom (Python {sys.version_info.major}.{sys.version_info.minor} on {sys.platform})" + ) print("=" * 60) - # 检查 MinGW64(仅在Windows平台) - if sys.platform == "win32": - if not check_mingw64(): - print("\n取消打包") - sys.exit(1) + if sys.platform == "win32" and not check_compiler_env(): + sys.exit(1) _print_packaging_summary() - - # 生成命令 cmd = get_nuitka_command() - # 打印命令 print("\n执行命令:") print(" ".join(cmd)) print("\n" + "=" * 60) - # 执行打包 try: - result = subprocess.run( + # capture_output=False 允许看到实时进度 + subprocess.run( cmd, check=True, cwd=PROJECT_ROOT, - capture_output=True, - text=True, + capture_output=False, encoding="utf-8", + errors="replace", ) - print("\n" + "=" * 60) - print("Nuitka打包成功!") - print("=" * 60) - - # 构建deb包(仅在Linux平台) + print("\n" + "=" * 60 + "\nNuitka打包成功!\n" + "=" * 60) build_deb() - except subprocess.CalledProcessError as e: - print("\n" + "=" * 60) - print(f"打包失败: {e}") - print(f"返回码: {e.returncode}") - if e.stdout: - print(f"标准输出:\n{e.stdout}") - if e.stderr: - print(f"错误输出:\n{e.stderr}") - print("=" * 60) + print(f"\n打包失败 (返回码: {e.returncode})") + sys.exit(1) + except KeyboardInterrupt: + print("\n用户取消打包") sys.exit(1) From 0097ff948fa577a35bf067f47c34c312918a24a0 Mon Sep 17 00:00:00 2001 From: jimmy-sketch Date: Sun, 30 Nov 2025 19:32:34 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat(ui):=20=E4=BC=98=E5=8C=96=E5=AF=BC?= =?UTF-8?q?=E8=88=AA=E6=A0=8F=E6=8C=89=E9=92=AE=E5=B8=83=E5=B1=80=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=A4=9A=E8=AF=AD=E8=A8=80=E6=96=87=E6=9C=AC=E6=8D=A2?= =?UTF-8?q?=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 统一主窗口和设置窗口的导航栏按钮为固定正方形尺寸 - 重新实现按钮绘制逻辑,支持图标与文本整体垂直居中 - 添加文本自动换行功能,提升多语言适配体验 - 优化按钮尺寸计算方式,去除动态宽度调整逻辑 - 修复长文本显示不全的问题,确保文本在按钮内完整展示 - 改进绘制方法,增加文本位置缓存提高渲染效率 --- app/view/main/window.py | 102 +++++++++++++++++++++++----------- app/view/settings/settings.py | 102 +++++++++++++++++++++++----------- 2 files changed, 140 insertions(+), 64 deletions(-) diff --git a/app/view/main/window.py b/app/view/main/window.py index 82ccad12..1c5996c3 100644 --- a/app/view/main/window.py +++ b/app/view/main/window.py @@ -246,50 +246,36 @@ def initNavigation(self): self._adjustNavigationBarWidth() def _adjustNavigationBarWidth(self): - """调整导航栏宽度以适应多语言文本""" + """调整导航栏宽度以适应多语言文本,按钮保持正方形,长文本换行""" try: nav = self.navigationInterface if not nav or not hasattr(nav, "buttons"): return - # 计算所有按钮文本所需的最大宽度 - max_text_width = 0 + # 设置按钮的正方形尺寸 + button_size = 80 # 正方形按钮的边长 + buttons = nav.buttons() for button in buttons: - if hasattr(button, "text") and button.text(): - # 使用按钮的字体度量计算文本宽度 - fm = button.fontMetrics() - text_width = fm.horizontalAdvance(button.text()) - max_text_width = max(max_text_width, text_width) - - # 计算所需的按钮宽度(文本宽度 + 左右边距) - # NavigationBarPushButton 默认为 64x58,图标在上,文本在下 - button_padding = 16 # 左右边距 - min_button_width = 64 # 最小按钮宽度 - required_width = max(min_button_width, max_text_width + button_padding) - - # 设置所有按钮的宽度,并重写图标绘制方法使图标居中 - for button in buttons: - button.setFixedWidth(int(required_width)) - # 重写 _drawIcon 方法使图标居中 - self._patchButtonDrawIcon(button, int(required_width)) + button.setFixedSize(button_size, button_size) + # 重写绘制方法使图标和文本整体居中 + self._patchButtonDraw(button, button_size) - # 设置导航栏宽度(按钮宽度 + 滚动区域边距) - nav_padding = 8 # 导航栏左右边距 - nav_width = int(required_width + nav_padding) + # 设置导航栏宽度 + nav_padding = 8 + nav_width = button_size + nav_padding nav.setFixedWidth(nav_width) except Exception as e: logger.debug(f"调整导航栏宽度时出错: {e}") - def _patchButtonDrawIcon(self, button, button_width): - """修补按钮的图标绘制方法,使图标居中""" - from PySide6.QtCore import QRectF - from PySide6.QtGui import QPainter + def _patchButtonDraw(self, button, button_size): + """修补按钮的绘制方法,使图标和文本整体垂直居中""" + from PySide6.QtCore import QRectF, Qt, QRect + from PySide6.QtGui import QPainter, QFontMetrics from qfluentwidgets.common.icon import drawIcon, FluentIconBase from qfluentwidgets.common.color import autoFallbackThemeColor - - original_draw_icon = button._drawIcon + from qfluentwidgets.common.config import isDarkTheme def centered_draw_icon(painter: QPainter): if (button.isPressed or not button.isEnter) and not button.isSelected: @@ -297,13 +283,34 @@ def centered_draw_icon(painter: QPainter): if not button.isEnabled(): painter.setOpacity(0.4) - # 计算居中的图标位置 + # 计算文本需要的行数和高度 + text = button.text() + fm = QFontMetrics(button.font()) + text_width = button_size - 8 # 文本区域宽度 + + # 计算文本换行后的高度 + text_rect = QRect(0, 0, text_width, 1000) + bounding = fm.boundingRect( + text_rect, Qt.AlignHCenter | Qt.TextWordWrap, text + ) + text_height = bounding.height() + + # 计算整体内容高度(图标 + 间距 + 文本) icon_size = 20 - icon_x = (button_width - icon_size) / 2 - icon_y = 13 + spacing = 4 # 图标和文本之间的间距 + total_height = icon_size + spacing + text_height + + # 计算垂直居中的起始位置 + start_y = (button_size - total_height) / 2 + icon_x = (button_size - icon_size) / 2 + icon_y = start_y + if hasattr(button, "iconAni") and not button._isSelectedTextVisible: icon_y += button.iconAni.offset + # 保存计算结果供 _drawText 使用 + button._calculated_text_top = start_y + icon_size + spacing + rect = QRectF(icon_x, icon_y, icon_size, icon_size) selectedIcon = button._selectedIcon or button._icon @@ -318,7 +325,38 @@ def centered_draw_icon(painter: QPainter): else: drawIcon(button._icon, painter, rect) + def wrapped_draw_text(painter: QPainter): + if button.isSelected and not button._isSelectedTextVisible: + return + + if button.isSelected: + painter.setPen( + autoFallbackThemeColor( + button.lightSelectedColor, button.darkSelectedColor + ) + ) + else: + painter.setPen(Qt.white if isDarkTheme() else Qt.black) + + painter.setFont(button.font()) + + text = button.text() + + # 使用之前计算的文本顶部位置 + text_top = getattr(button, "_calculated_text_top", 36) + text_rect = QRect( + 4, int(text_top), button_size - 8, button_size - int(text_top) + ) + + # 使用 Qt 的自动换行功能 + painter.drawText( + text_rect, + Qt.AlignHCenter | Qt.AlignTop | Qt.TextWordWrap, + text, + ) + button._drawIcon = centered_draw_icon + button._drawText = wrapped_draw_text def _toggle_float_window(self): if self.float_window.isVisible(): diff --git a/app/view/settings/settings.py b/app/view/settings/settings.py index 2a94829d..0eaa6887 100644 --- a/app/view/settings/settings.py +++ b/app/view/settings/settings.py @@ -827,50 +827,36 @@ def show_settings_window_about(self): self.switchTo(self.aboutInterface) def _adjustNavigationBarWidth(self): - """调整导航栏宽度以适应多语言文本""" + """调整导航栏宽度以适应多语言文本,按钮保持正方形,长文本换行""" try: nav = self.navigationInterface if not nav or not hasattr(nav, "buttons"): return - # 计算所有按钮文本所需的最大宽度 - max_text_width = 0 + # 设置按钮的正方形尺寸 + button_size = 80 # 正方形按钮的边长 + buttons = nav.buttons() for button in buttons: - if hasattr(button, "text") and button.text(): - # 使用按钮的字体度量计算文本宽度 - fm = button.fontMetrics() - text_width = fm.horizontalAdvance(button.text()) - max_text_width = max(max_text_width, text_width) - - # 计算所需的按钮宽度(文本宽度 + 左右边距) - # NavigationBarPushButton 默认为 64x58,图标在上,文本在下 - button_padding = 16 # 左右边距 - min_button_width = 64 # 最小按钮宽度 - required_width = max(min_button_width, max_text_width + button_padding) - - # 设置所有按钮的宽度,并重写图标绘制方法使图标居中 - for button in buttons: - button.setFixedWidth(int(required_width)) - # 重写 _drawIcon 方法使图标居中 - self._patchButtonDrawIcon(button, int(required_width)) + button.setFixedSize(button_size, button_size) + # 重写绘制方法使图标和文本整体居中 + self._patchButtonDraw(button, button_size) - # 设置导航栏宽度(按钮宽度 + 滚动区域边距) - nav_padding = 8 # 导航栏左右边距 - nav_width = int(required_width + nav_padding) + # 设置导航栏宽度 + nav_padding = 8 + nav_width = button_size + nav_padding nav.setFixedWidth(nav_width) except Exception as e: logger.debug(f"调整导航栏宽度时出错: {e}") - def _patchButtonDrawIcon(self, button, button_width): - """修补按钮的图标绘制方法,使图标居中""" - from PySide6.QtCore import QRectF - from PySide6.QtGui import QPainter + def _patchButtonDraw(self, button, button_size): + """修补按钮的绘制方法,使图标和文本整体垂直居中""" + from PySide6.QtCore import QRectF, Qt, QRect + from PySide6.QtGui import QPainter, QFontMetrics from qfluentwidgets.common.icon import drawIcon, FluentIconBase from qfluentwidgets.common.color import autoFallbackThemeColor - - original_draw_icon = button._drawIcon + from qfluentwidgets.common.config import isDarkTheme def centered_draw_icon(painter: QPainter): if (button.isPressed or not button.isEnter) and not button.isSelected: @@ -878,13 +864,34 @@ def centered_draw_icon(painter: QPainter): if not button.isEnabled(): painter.setOpacity(0.4) - # 计算居中的图标位置 + # 计算文本需要的行数和高度 + text = button.text() + fm = QFontMetrics(button.font()) + text_width = button_size - 8 # 文本区域宽度 + + # 计算文本换行后的高度 + text_rect = QRect(0, 0, text_width, 1000) + bounding = fm.boundingRect( + text_rect, Qt.AlignHCenter | Qt.TextWordWrap, text + ) + text_height = bounding.height() + + # 计算整体内容高度(图标 + 间距 + 文本) icon_size = 20 - icon_x = (button_width - icon_size) / 2 - icon_y = 13 + spacing = 4 # 图标和文本之间的间距 + total_height = icon_size + spacing + text_height + + # 计算垂直居中的起始位置 + start_y = (button_size - total_height) / 2 + icon_x = (button_size - icon_size) / 2 + icon_y = start_y + if hasattr(button, "iconAni") and not button._isSelectedTextVisible: icon_y += button.iconAni.offset + # 保存计算结果供 _drawText 使用 + button._calculated_text_top = start_y + icon_size + spacing + rect = QRectF(icon_x, icon_y, icon_size, icon_size) selectedIcon = button._selectedIcon or button._icon @@ -899,7 +906,38 @@ def centered_draw_icon(painter: QPainter): else: drawIcon(button._icon, painter, rect) + def wrapped_draw_text(painter: QPainter): + if button.isSelected and not button._isSelectedTextVisible: + return + + if button.isSelected: + painter.setPen( + autoFallbackThemeColor( + button.lightSelectedColor, button.darkSelectedColor + ) + ) + else: + painter.setPen(Qt.white if isDarkTheme() else Qt.black) + + painter.setFont(button.font()) + + text = button.text() + + # 使用之前计算的文本顶部位置 + text_top = getattr(button, "_calculated_text_top", 36) + text_rect = QRect( + 4, int(text_top), button_size - 8, button_size - int(text_top) + ) + + # 使用 Qt 的自动换行功能 + painter.drawText( + text_rect, + Qt.AlignHCenter | Qt.AlignTop | Qt.TextWordWrap, + text, + ) + button._drawIcon = centered_draw_icon + button._drawText = wrapped_draw_text def _apply_sidebar_settings(self): """应用侧边栏设置""" From 4a6d81fcfaeba8daf4a4b17a6bee490a67dbcb6d Mon Sep 17 00:00:00 2001 From: jimmy-sketch Date: Fri, 5 Dec 2025 22:21:21 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat(ui):=20=E4=BC=98=E5=8C=96=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E5=B8=83=E5=B1=80=E4=B8=8E=E6=8C=89=E9=92=AE=E7=BB=98?= =?UTF-8?q?=E5=88=B6=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 调整抽奖和点名页面计数器组件的水平布局对齐方式 - 使用弹性空间实现控件左、中、右对齐效果 - 在主窗口和设置窗口中添加按钮背景居中绘制逻辑 - 优化按钮选中状态下的指示条垂直居中显示效果 - 根据按钮按下状态动态调整指示条位置与高度 - 将count_widget加入宽度自适应调整列表 --- app/tools/button_draw_utils.py | 28 ++++++++++++++++++++++++++++ app/view/main/lottery.py | 10 +++++++--- app/view/main/roll_call.py | 10 +++++++--- app/view/main/window.py | 5 +++++ app/view/settings/settings.py | 5 +++++ 5 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 app/tools/button_draw_utils.py diff --git a/app/tools/button_draw_utils.py b/app/tools/button_draw_utils.py new file mode 100644 index 00000000..779a666a --- /dev/null +++ b/app/tools/button_draw_utils.py @@ -0,0 +1,28 @@ +"""按钮绘制工具函数""" + +from PySide6.QtCore import Qt +from PySide6.QtGui import QPainter, QColor +from qfluentwidgets.common.color import autoFallbackThemeColor +from qfluentwidgets.common.config import isDarkTheme + + +def centered_draw_background(button, painter: QPainter, button_size: int): + """绘制按钮背景,使选中指示条(小蓝条)垂直居中""" + if button.isSelected: + painter.setBrush(QColor(255, 255, 255, 42) if isDarkTheme() else Qt.white) + painter.drawRoundedRect(button.rect(), 5, 5) + + # 绘制指示条(小蓝条),垂直居中 + painter.setBrush( + autoFallbackThemeColor(button.lightSelectedColor, button.darkSelectedColor) + ) + indicator_height = 24 if not button.isPressed else 18 + indicator_y = (button_size - indicator_height) // 2 + if button.isPressed: + indicator_y += 3 # 按下时稍微下移 + painter.drawRoundedRect(0, indicator_y, 4, indicator_height, 2, 2) + elif button.isPressed or button.isEnter: + c = 255 if isDarkTheme() else 0 + alpha = 9 if button.isEnter else 6 + painter.setBrush(QColor(c, c, c, alpha)) + painter.drawRoundedRect(button.rect(), 5, 5) diff --git a/app/view/main/lottery.py b/app/view/main/lottery.py index ab1eee64..a82529d3 100644 --- a/app/view/main/lottery.py +++ b/app/view/main/lottery.py @@ -141,9 +141,12 @@ def initUI(self): self.count_widget = QWidget() horizontal_layout = QHBoxLayout() horizontal_layout.setContentsMargins(0, 0, 0, 0) - horizontal_layout.addWidget(self.minus_button, 0, Qt.AlignmentFlag.AlignLeft) - horizontal_layout.addWidget(self.count_label, 0, Qt.AlignmentFlag.AlignLeft) - horizontal_layout.addWidget(self.plus_button, 0, Qt.AlignmentFlag.AlignLeft) + horizontal_layout.setSpacing(0) + horizontal_layout.addWidget(self.minus_button) + horizontal_layout.addStretch() + horizontal_layout.addWidget(self.count_label) + horizontal_layout.addStretch() + horizontal_layout.addWidget(self.plus_button) self.count_widget.setLayout(horizontal_layout) self.start_button = PrimaryPushButton( @@ -309,6 +312,7 @@ def _adjustControlWidgetWidths(self): # 收集所有需要调整宽度的控件 widgets_to_adjust = [ self.reset_button, + self.count_widget, self.start_button, self.pool_list_combobox, self.list_combobox, diff --git a/app/view/main/roll_call.py b/app/view/main/roll_call.py index cd94d69b..aa73c7bf 100644 --- a/app/view/main/roll_call.py +++ b/app/view/main/roll_call.py @@ -141,9 +141,12 @@ def initUI(self): self.count_widget = QWidget() horizontal_layout = QHBoxLayout() horizontal_layout.setContentsMargins(0, 0, 0, 0) - horizontal_layout.addWidget(self.minus_button, 0, Qt.AlignmentFlag.AlignLeft) - horizontal_layout.addWidget(self.count_label, 0, Qt.AlignmentFlag.AlignLeft) - horizontal_layout.addWidget(self.plus_button, 0, Qt.AlignmentFlag.AlignLeft) + horizontal_layout.setSpacing(0) + horizontal_layout.addWidget(self.minus_button) + horizontal_layout.addStretch() + horizontal_layout.addWidget(self.count_label) + horizontal_layout.addStretch() + horizontal_layout.addWidget(self.plus_button) self.count_widget.setLayout(horizontal_layout) self.start_button = PrimaryPushButton( @@ -295,6 +298,7 @@ def _adjustControlWidgetWidths(self): # 收集所有需要调整宽度的控件 widgets_to_adjust = [ self.reset_button, + self.count_widget, self.start_button, self.list_combobox, self.range_combobox, diff --git a/app/view/main/window.py b/app/view/main/window.py index 1c5996c3..c82398ac 100644 --- a/app/view/main/window.py +++ b/app/view/main/window.py @@ -355,6 +355,11 @@ def wrapped_draw_text(painter: QPainter): text, ) + from app.tools.button_draw_utils import centered_draw_background + + button._drawBackground = lambda painter: centered_draw_background( + button, painter, button_size + ) button._drawIcon = centered_draw_icon button._drawText = wrapped_draw_text diff --git a/app/view/settings/settings.py b/app/view/settings/settings.py index 0eaa6887..e96788a2 100644 --- a/app/view/settings/settings.py +++ b/app/view/settings/settings.py @@ -936,6 +936,11 @@ def wrapped_draw_text(painter: QPainter): text, ) + from app.tools.button_draw_utils import centered_draw_background + + button._drawBackground = lambda painter: centered_draw_background( + button, painter, button_size + ) button._drawIcon = centered_draw_icon button._drawText = wrapped_draw_text