代码逻辑问题求教

AI摘要
使用PyQt5和matplotlib开发族谱吊线图生成器时遇到子节点与父节点水平居中对齐问题及多子节点组重叠问题。代码实现了四种布局方向(竖式正翻/反翻、横式正翻/反翻)和多种样式配置,但节点位置计算逻辑存在缺陷,导致对齐不准和层级重叠。需要优化节点位置递归计算算法,确保父节点精确居中于子节点组并消除空间冲突。

竖式正翻效果
竖式反翻效果
横式正翻效果
横式反翻效果
以上是我想通过程序来实现的四种效果。
以下代码在位置对齐上一直达不到居中效果。
import sys
import os
import numpy as np
import pandas as pd
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
import matplotlib.patches as patches
from matplotlib.patches import FancyBboxPatch, Rectangle
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QFileDialog, QMessageBox, QScrollArea, QGroupBox,
QLabel, QComboBox, QSpinBox, QDoubleSpinBox, QColorDialog, QSplitter,
QFrame, QAction, QToolBar, QToolButton, QStyle)
from PyQt5.QtGui import QColor, QPainter, QPixmap, QFont, QCursor, QIcon
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtPrintSupport import QPrintDialog, QPrinter
from pathlib import Path

class Node:
def init(self, name, note=None):
self.name = name
self.note = note
self.parent = None
self.children = []
self.x = 0
self.y = 0
self.width = 0
self.height = 0

class FamilyTree:
def init(self):
self.nodes = {}
self.root = None

def build_tree(self, df):
    # 首先创建所有节点
    for _, row in df.iterrows():
        name = row['子名']
        note = row.get('备注', None)
        if name not in self.nodes:
            self.nodes[name] = Node(name, note)
        else:
            # 更新已有节点的备注
            if note:
                self.nodes[name].note = note

    # 建立父子关系
    for _, row in df.iterrows():
        name = row['子名']
        parent_name = row.get('父名', None)

        if parent_name and parent_name != name and parent_name in self.nodes:
            node = self.nodes[name]
            parent_node = self.nodes[parent_name]

            if node not in parent_node.children:
                parent_node.children.append(node)
                node.parent = parent_node

    # 查找根节点(没有父节点的节点)
    roots = [node for node in self.nodes.values() if node.parent is None]
    if roots:
        self.root = roots[0]
    else:
        # 如果没有根节点,选择一个任意节点作为根
        self.root = next(iter(self.nodes.values()), None)

class TreeCanvas(FigureCanvas):
def init(self, parent=None):
self.fig = Figure(figsize=(10, 8), dpi=100)
super().init(self.fig)
self.setParent(parent)
self.ax = self.fig.add_subplot(111)
self.ax.axis(‘off’)
self.fig.tight_layout()

    self.family_tree = None
    self.zoom_factor = 1.0
    # 移除平移相关的变量,因为现在使用滚动条控制

    # 样式设置
    self.box_fill = '#ffffff'
    self.border_color = '#000000'
    self.border_width = 1
    self.border_style = '-'
    self.box_style = 'square'  # 默认为直角,与UI面板保持一致
    self.line_color = '#000000'
    self.line_width = 1
    self.line_style = '-'
    self.font_name = 'SimHei'
    self.font_size = 12

    # 计算和绘制状态
    self.figure_initialized = False
    self.font_color = '#000000'
    self.horizontal_spacing_mm = 3
    self.vertical_spacing_mm = 4
    self.layout_direction = 'vertical_normal'
    self.text_direction = 'horizontal'
    # 添加边框宽高参数
    self.box_width_mm = 10  # 默认宽度10mm
    self.box_height_mm = 5  # 默认高度5mm
    # 圆角半径参数
    self.box_radius_mm = 1.0  # 默认圆角半径1mm

    # 添加样式设置字典,用于存储所有样式参数
    self.style_settings = {}

    # 初始化字间距属性
    self.letter_spacing = 0.0  # 默认字间距为0(无额外间距)

    # 只保留缩放事件,移除平移相关事件
    self.mpl_connect('scroll_event', self.on_scroll)

def set_tree(self, family_tree):
    self.family_tree = family_tree
    self.reset_view()
    self.draw_tree()

def set_style(self, **kwargs):
    # 更新实例属性
    for key, value in kwargs.items():
        if hasattr(self, key):
            setattr(self, key, value)
    # 更新style_settings字典,确保所有样式都在一个地方
    self.style_settings = kwargs.copy()
    # 完全重置视图,确保所有节点位置都重新计算
    self.reset_view()
    # 强制重绘树图以应用新的样式设置
    self.draw_tree()

def draw_end_shape(self, x, y, scaled_line_width):
    """绘制连接线末端图形"""
    end_shape = self.style_settings.get('end_shape', '无图形')
    shape_color = self.style_settings.get('shape_color', self.line_color)  # 获取图形颜色,默认为线条颜色
    shape_size = self.style_settings.get('shape_size', 5.0)  # 获取用户设置的图形大小,默认为5.0
    # 增加缩放因子使图形大小更加明显,同时保留与线条宽度的关联性
    size = scaled_line_width * shape_size * 5  # 增加5倍的缩放因子

    if end_shape == '实心圆':
        self.ax.scatter(x, y, s=size, color=shape_color, zorder=5)
    elif end_shape == '空心圆':
        self.ax.scatter(x, y, s=size, facecolors='none', edgecolors=shape_color, linewidth=1, zorder=5)

def calculate_box_size(self, text):
    """计算节点框的大小,确保纸张大小不影响绘图,只在导出和打印时使用"""
    # 使用用户设置的边框宽高,如果未设置或为0则自动计算
    if self.box_width_mm > 0 and self.box_height_mm > 0:
        # 当文字方向为竖排时,交换宽高值
        if self.text_direction == 'vertical':
            return self.box_height_mm, self.box_width_mm
        return self.box_width_mm, self.box_height_mm

    # 简单估算节点框的大小(当用户未设置或设置为0时使用)
    # 确保按照1:1比例计算,不考虑纸张大小
    lines = text.split('\n')
    # 使用默认行高1.0倍
    spacing_factor = 1.0

    # 基础字符宽度(毫米)
    base_char_width_mm = 12

    if self.text_direction == 'vertical':
        width_mm = base_char_width_mm * 2  # 每个字宽约12mm
        # 考虑垂直线上的字符数量
        height_mm = base_char_width_mm * len(max(lines, key=len)) * spacing_factor
        # 当文字方向为竖排时,交换宽高值
        return height_mm, width_mm
    else:
        # 考虑字间距对框宽度的影响,但不改变字体大小
        max_line_length = len(max(lines, key=len))
        # 基础宽度
        width_mm = base_char_width_mm * max_line_length

        # 字间距参数不再直接影响框宽度,而是影响节点之间的间距
        # 框宽度仍按原始计算,确保子名字能完整显示

        # 高度基于行数计算
        height_mm = base_char_width_mm * len(lines) * spacing_factor

        return width_mm, height_mm

def mm_to_inch(self, mm):
    return mm / 25.4

def inch_to_mm(self, inch):
    return inch * 25.4

def draw_tree(self):
    """绘制整个家族树,严格按照1:1比例绘制,不考虑纸张大小,让图表自由扩展"""
    if not self.family_tree or not self.family_tree.root:
        return

    # 清除当前画布
    self.ax.clear()
    self.ax.axis('off')

    # 计算节点位置
    self._calculate_positions()

    # 记录绘制标志,确保按照1:1绘制
    self._drawn_at_1_to_1 = True

    # 绘制连接线 - 添加drawn_parents集合跟踪已绘制过父节点的父节点
    drawn_parents = set()
    # 收集每个父节点的所有子节点
    parent_children_map = {}
    for node in self.family_tree.nodes.values():
        if node.parent:
            parent = node.parent
            if parent not in parent_children_map:
                parent_children_map[parent] = []
            parent_children_map[parent].append(node)

    # 先绘制所有连接线
    for parent, children in parent_children_map.items():
        if parent in drawn_parents:
            continue
        drawn_parents.add(parent)

        parent_center_x = parent.x
        parent_center_y = parent.y

        # 计算父节点边界
        parent_top_y = parent.y + parent.height / 2
        parent_bottom_y = parent.y - parent.height / 2
        parent_left_x = parent.x - parent.width / 2
        parent_right_x = parent.x + parent.width / 2

        # 按照默认参数1:1绘制,不随缩放级别变化
        scaled_line_width = max(self.line_width, 0.5)  # 确保最小宽度

        # 竖式正翻
        if self.layout_direction == 'vertical_normal':
                # 计算所有子节点的垂直中间点
                if children:
                    # 获取第一个子节点的顶部Y坐标(所有子节点应该在同一水平线上)
                    node_top_y = children[0].y + children[0].height / 2
                    # 计算父子节点之间的垂直中间点
                    vertical_mid_point_y = parent_bottom_y + (node_top_y - parent_bottom_y) * 0.5

                # 绘制父节点的垂直线
                self.ax.plot([parent_center_x, parent_center_x], [parent_bottom_y, vertical_mid_point_y],
                            color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)

                # 找出最左侧和最右侧的子节点中心
                min_child_x = min(children, key=lambda n: n.x).x
                max_child_x = max(children, key=lambda n: n.x).x

                # 获取当前的框样式,判断是否需要使用圆角连接线
                box_style = self.style_settings.get('box_style', 'square')
                use_rounded_corners = box_style == 'rounded'

                # 获取用户设置的圆角半径,默认值为5mm
                radius_mm = self.style_settings.get('box_radius_mm', 5)
                # 直接使用用户设置的圆角半径,不考虑垂直间距的限制
                # 将毫米转换为英寸,因为matplotlib使用英寸作为单位
                corner_radius = self.mm_to_inch(radius_mm)
                # 定义一个固定的垂直偏移量,确保垂直线不穿过子名框
                vertical_offset = self.mm_to_inch(2)  # 2mm的偏移量

                if use_rounded_corners:
                    # 使用圆角绘制连接线
                    import numpy as np
                    from matplotlib.path import Path
                    from matplotlib.patches import PathPatch

                    # 创建路径数据
                    verts = []
                    codes = []

                    # 从父节点到最左子节点
                    # 在圆角模式下,首先绘制完整的水平导线
                    self.ax.plot([min_child_x, max_child_x], [vertical_mid_point_y, vertical_mid_point_y],
                                color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)

                    # 为每个子节点绘制连接线
                    for i, node in enumerate(children):
                        node_center_x = node.x
                        node_top_y = node.y + node.height / 2

                        # 如果是第一个子节点,在与水平导线交点处绘制圆角
                        if i == 0 and len(children) > 1:
                            # 水平导线与垂直导线交点的圆角(左上角方向)

                            # 计算竖向导线高度
                            vertical_line_height = abs(node_top_y - vertical_mid_point_y)
                            # 确保圆角半径不大于竖向导线高度的一半
                            adjusted_corner_radius = min(corner_radius, vertical_line_height / 2)

                            # 绘制左上圆角(正圆四分之一圆弧)- 以半个圆角值距离的竖线为轴水平翻转
                            # 圆弧X轴端点与水平导线相接,Y轴端点与垂直导线相接
                            corner_verts = [
                                (node_center_x - adjusted_corner_radius, vertical_mid_point_y),  # 起点:以半个圆角值距离的竖线为轴翻转后的起点
                                (node_center_x, vertical_mid_point_y),  # X轴端点:向右移动一个半径距离
                                (node_center_x, vertical_mid_point_y - adjusted_corner_radius)  # Y轴端点:向上移动一个半径距离
                            ]
                            corner_codes = [
                                Path.MOVETO,
                                Path.CURVE3,
                                Path.CURVE3
                            ]
                            corner_path = Path(corner_verts, corner_codes)
                            corner_patch = PathPatch(corner_path, facecolor='none', edgecolor=self.line_color,
                                                linestyle=self.line_style, linewidth=scaled_line_width)
                            self.ax.add_patch(corner_patch)

                            # 绘制从圆角到节点的垂直线(刚好与子名框顶部相交)
                            self.ax.plot([node_center_x, node_center_x], [vertical_mid_point_y - adjusted_corner_radius, node_top_y],
                                        color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)

                            # 绘制节点顶部与垂直线交点的圆角
                            node_corner_verts = [
                                (node_center_x, node_top_y - 0.5 * adjusted_corner_radius),
                                (node_center_x + 0.5 * adjusted_corner_radius, node_top_y - 0.5 * adjusted_corner_radius),
                                (node_center_x, node_top_y)
                            ]
                            node_corner_codes = [
                                Path.MOVETO,
                                Path.CURVE3,
                                Path.CURVE3
                            ]
                            node_corner_path = Path(node_corner_verts, node_corner_codes)
                            node_corner_patch = PathPatch(node_corner_path, facecolor='none', edgecolor=self.line_color,
                                                     linestyle=self.line_style, linewidth=scaled_line_width)
                            self.ax.add_patch(node_corner_patch)

                            # 绘制节点顶部与垂直线交点的圆角
                            node_corner_verts = [
                                (node_center_x, node_top_y - 0.5 * adjusted_corner_radius),
                                (node_center_x + 0.5 * adjusted_corner_radius, node_top_y - 0.5 * adjusted_corner_radius),
                                (node_center_x, node_top_y)
                            ]
                            node_corner_codes = [
                                Path.MOVETO,
                                Path.CURVE3,
                                Path.CURVE3
                            ]
                            node_corner_path = Path(node_corner_verts, node_corner_codes)
                            node_corner_patch = PathPatch(node_corner_path, facecolor='none', edgecolor=self.line_color,
                                                    linestyle=self.line_style, linewidth=scaled_line_width)
                            self.ax.add_patch(node_corner_patch)
                        # 如果是最后一个子节点,在与水平导线交点处绘制圆角
                        elif i == len(children) - 1 and len(children) > 1:
                            # 水平导线与垂直导线交点的圆角(右上角方向)
                            # 计算竖向导线高度
                            vertical_line_height = abs(node_top_y - vertical_mid_point_y)
                            # 确保圆角半径不大于竖向导线高度的一半
                            adjusted_corner_radius = min(corner_radius, vertical_line_height / 2)

                            # 绘制右上圆角(正圆四分之一圆弧)- 以半个圆角值距离的竖线为轴水平翻转
                            # 圆弧X轴端点与水平导线相接,Y轴端点与垂直导线相接
                            corner_verts = [
                                (node_center_x + adjusted_corner_radius, vertical_mid_point_y),  # 起点:以半个圆角值距离的竖线为轴翻转后的起点
                                (node_center_x, vertical_mid_point_y),  # X轴端点:向左移动一个半径距离
                                (node_center_x, vertical_mid_point_y - adjusted_corner_radius)  # Y轴端点:向上移动一个半径距离
                            ]
                            corner_codes = [
                                Path.MOVETO,
                                Path.CURVE3,
                                Path.CURVE3
                            ]
                            corner_path = Path(corner_verts, corner_codes)
                            corner_patch = PathPatch(corner_path, facecolor='none', edgecolor=self.line_color,
                                                linestyle=self.line_style, linewidth=scaled_line_width)
                            self.ax.add_patch(corner_patch)

                            # 绘制从圆角到节点的垂直线(刚好与子名框顶部相交)
                            self.ax.plot([node_center_x, node_center_x], [vertical_mid_point_y - adjusted_corner_radius, node_top_y],
                                        color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)

                            # 绘制节点顶部与垂直线交点的圆角
                            node_corner_verts = [
                                (node_center_x, node_top_y),
                                (node_center_x - corner_radius, node_top_y),
                                (node_center_x, node_top_y - corner_radius)
                            ]
                            node_corner_codes = [
                                Path.MOVETO,
                                Path.CURVE3,
                                Path.CURVE3
                            ]
                            node_corner_path = Path(node_corner_verts, node_corner_codes)
                            node_corner_patch = PathPatch(node_corner_path, facecolor='none', edgecolor=self.line_color,
                                                    linestyle=self.line_style, linewidth=scaled_line_width)
                            self.ax.add_patch(node_corner_patch)
                        # 如果是中间子节点或唯一子节点,直接绘制垂直线
                        else:
                            # 统一处理:所有子节点使用相同的垂直线绘制逻辑
                            # 计算竖向导线高度
                            vertical_line_height = abs(node_top_y - vertical_mid_point_y)
                            # 确保圆角半径不大于竖向导线高度的一半
                            adjusted_corner_radius = min(corner_radius, vertical_line_height / 2)

                            # 绘制从圆角到节点顶部的垂直线
                            self.ax.plot([node_center_x, node_center_x], [vertical_mid_point_y - adjusted_corner_radius, node_top_y],
                                        color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)

                            # 绘制节点顶部与垂直线交点的圆角
                            node_corner_verts = [
                                (node_center_x, node_top_y - 0.5 * adjusted_corner_radius),
                                (node_center_x + 0.5 * adjusted_corner_radius, node_top_y - 0.5 * adjusted_corner_radius),
                                (node_center_x, node_top_y)
                            ]
                            node_corner_codes = [
                                Path.MOVETO,
                                Path.CURVE3,
                                Path.CURVE3
                            ]
                            node_corner_path = Path(node_corner_verts, node_corner_codes)
                            node_corner_patch = PathPatch(node_corner_path, facecolor='none', edgecolor=self.line_color,
                                                    linestyle=self.line_style, linewidth=scaled_line_width)
                            self.ax.add_patch(node_corner_patch)

                            # 在子节点顶部绘制末端图形
                            self.draw_end_shape(node_center_x, node_top_y, scaled_line_width)
                else:
                    # 绘制一条完整的水平导线,覆盖从最左到最右的子节点
                    self.ax.plot([min_child_x, max_child_x], [vertical_mid_point_y, vertical_mid_point_y],
                                color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)

                    # 为每个子节点绘制垂直线连接到水平导线
                    for node in children:
                        node_center_x = node.x
                        node_top_y = node.y + node.height / 2
                        # 绘制从水平导线到节点顶部的垂直线
                        self.ax.plot([node_center_x, node_center_x], [vertical_mid_point_y, node_top_y],
                                    color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)

                        # 在子节点顶部绘制末端图形
                        self.draw_end_shape(node_center_x, node_top_y, scaled_line_width)
        # 竖式反翻
        elif self.layout_direction == 'vertical_reverse':
            if children:
                node_top_y = children[0].y + children[0].height / 2
                # 计算父子节点之间的垂直中间点
                vertical_mid_point_y = parent_bottom_y + (node_top_y - parent_bottom_y) * 0.5

                # 绘制父节点的垂直线
                self.ax.plot([parent_center_x, parent_center_x], [parent_bottom_y, vertical_mid_point_y],
                            color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)

                # 找出最左侧和最右侧的子节点中心
                min_child_x = min(children, key=lambda n: n.x).x
                max_child_x = max(children, key=lambda n: n.x).x

                # 获取当前的框样式,判断是否需要使用圆角连接线
                box_style = self.style_settings.get('box_style', 'square')
                use_rounded_corners = box_style == 'rounded'

                # 获取用户设置的圆角半径,默认值为5mm
                radius_mm = self.style_settings.get('box_radius_mm', 5)
                # 直接使用用户设置的圆角半径,不考虑垂直间距的限制
                # 将毫米转换为英寸,因为matplotlib使用英寸作为单位
                corner_radius = self.mm_to_inch(radius_mm)
                # 定义一个固定的垂直偏移量,确保垂直线不穿过子名框
                vertical_offset = self.mm_to_inch(2)  # 2mm的偏移量

                if use_rounded_corners:
                    # 使用圆角绘制水平导线
                    import numpy as np

                    # 在圆角模式下,首先绘制完整的水平导线
                    self.ax.plot([min_child_x, max_child_x], [vertical_mid_point_y, vertical_mid_point_y],
                                color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)
                    from matplotlib.path import Path
                    from matplotlib.patches import PathPatch

                    # 创建水平导线路径数据
                    verts = []
                    codes = []

                    # 从父节点垂直线连接到水平导线
                    verts.append((parent_center_x, vertical_mid_point_y))
                    codes.append(Path.MOVETO)

                    # 只创建基本的水平线段,不添加任何圆角
                    # 圆角将在处理每个子节点时单独绘制
                    self.ax.plot([min_child_x, max_child_x], [vertical_mid_point_y, vertical_mid_point_y],
                                color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)

                    # 为每个子节点绘制连接线
                    for i, node in enumerate(children):
                        node_center_x = node.x
                        node_top_y = node.y + node.height / 2

                        # 如果是第一个子节点,在与水平导线交点处绘制圆角
                        if i == 0 and len(children) > 1:
                            # 水平导线与垂直导线交点的圆角(左上角方向)

                            # 计算竖向导线高度
                            vertical_line_height = abs(node_top_y - vertical_mid_point_y)
                            # 确保圆角半径不大于竖向导线高度的一半
                            adjusted_corner_radius = min(corner_radius, vertical_line_height / 2)

                            # 绘制左上圆角(正圆四分之一圆弧)- 以半个圆角值距离的竖线为轴水平翻转
                            # 圆弧X轴端点与水平导线相接,Y轴端点与垂直导线相接
                            corner_verts = [
                                (node_center_x - adjusted_corner_radius, vertical_mid_point_y),  # 起点:以半个圆角值距离的竖线为轴翻转后的起点
                                (node_center_x, vertical_mid_point_y),  # X轴端点:向右移动一个半径距离
                                (node_center_x, vertical_mid_point_y - adjusted_corner_radius)  # Y轴端点:向上移动一个半径距离
                            ]
                            corner_codes = [
                                Path.MOVETO,
                                Path.CURVE3,
                                Path.CURVE3
                            ]
                            corner_path = Path(corner_verts, corner_codes)
                            corner_patch = PathPatch(corner_path, facecolor='none', edgecolor=self.line_color,
                                                linestyle=self.line_style, linewidth=scaled_line_width)
                            self.ax.add_patch(corner_patch)

                            # 绘制从圆角到节点顶部的垂直线
                            self.ax.plot([node_center_x, node_center_x], [vertical_mid_point_y - adjusted_corner_radius, node_top_y],
                                        color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)

                            # 绘制节点顶部与垂直线交点的圆角
                            node_corner_verts = [
                                (node_center_x, node_top_y - vertical_offset),
                                (node_center_x + vertical_offset, node_top_y - vertical_offset),
                                (node_center_x, node_top_y)
                            ]
                            node_corner_codes = [
                                Path.MOVETO,
                                Path.CURVE3,
                                Path.CURVE3
                            ]
                            node_corner_path = Path(node_corner_verts, node_corner_codes)
                            node_corner_patch = PathPatch(node_corner_path, facecolor='none', edgecolor=self.line_color,
                                                     linestyle=self.line_style, linewidth=scaled_line_width)
                            self.ax.add_patch(node_corner_patch)
                        # 如果是最后一个子节点,在与水平导线交点处绘制圆角
                        elif i == len(children) - 1 and len(children) > 1:
                            # 水平导线与垂直导线交点的圆角(右上角方向)
                            # 计算竖向导线高度
                            vertical_line_height = abs(node_top_y - vertical_mid_point_y)
                            # 确保圆角半径不大于竖向导线高度的一半
                            adjusted_corner_radius = min(corner_radius, vertical_line_height / 2)

                            # 绘制右上圆角(正圆四分之一圆弧)- 以半个圆角值距离的竖线为轴水平翻转
                            # 圆弧X轴端点与水平导线相接,Y轴端点与垂直导线相接
                            corner_verts = [
                                (node_center_x + adjusted_corner_radius, vertical_mid_point_y),  # 起点:以半个圆角值距离的竖线为轴翻转后的起点
                                (node_center_x, vertical_mid_point_y),  # X轴端点:向左移动一个半径距离
                                (node_center_x, vertical_mid_point_y - adjusted_corner_radius)  # Y轴端点:向上移动一个半径距离
                            ]
                            corner_codes = [
                                Path.MOVETO,
                                Path.CURVE3,
                                Path.CURVE3
                            ]
                            corner_path = Path(corner_verts, corner_codes)
                            corner_patch = PathPatch(corner_path, facecolor='none', edgecolor=self.line_color,
                                                linestyle=self.line_style, linewidth=scaled_line_width)
                            self.ax.add_patch(corner_patch)

                            # 绘制从圆角到节点的垂直线(不穿过子名框顶部)
                            self.ax.plot([node_center_x, node_center_x], [vertical_mid_point_y - adjusted_corner_radius, node_top_y - vertical_offset],
                                        color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)
                        # 如果是中间子节点或唯一子节点,添加圆角处理使其与水平导线连接更平滑
                        else:
                            # 计算竖向导线高度
                            vertical_line_height = abs(node_top_y - vertical_mid_point_y)
                            # 确保圆角半径不大于竖向导线高度的一半
                            adjusted_corner_radius = min(corner_radius, vertical_line_height / 2)

                            # 绘制与水平导线连接的圆角
                            corner_verts = [
                                (node_center_x, vertical_mid_point_y),  # 起点:水平导线位置
                                (node_center_x, vertical_mid_point_y - adjusted_corner_radius),  # Y轴端点:向上移动一个半径距离
                                (node_center_x, vertical_mid_point_y - adjusted_corner_radius)  # 终点:圆角结束位置
                            ]
                            corner_codes = [
                                Path.MOVETO,
                                Path.CURVE3,
                                Path.CURVE3
                            ]
                            corner_path = Path(corner_verts, corner_codes)
                            corner_patch = PathPatch(corner_path, facecolor='none', edgecolor=self.line_color,
                                                linestyle=self.line_style, linewidth=scaled_line_width)
                            self.ax.add_patch(corner_patch)

                            # 绘制从圆角到节点顶部的垂直线
                            self.ax.plot([node_center_x, node_center_x], [vertical_mid_point_y - adjusted_corner_radius, node_top_y - vertical_offset],
                                        color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)

                            # 绘制节点顶部与垂直线交点的圆角
                            node_corner_verts = [
                                (node_center_x, node_top_y - vertical_offset),
                                (node_center_x + vertical_offset, node_top_y - vertical_offset),
                                (node_center_x, node_top_y)
                            ]
                            node_corner_codes = [
                                Path.MOVETO,
                                Path.CURVE3,
                                Path.CURVE3
                            ]
                            node_corner_path = Path(node_corner_verts, node_corner_codes)
                            node_corner_patch = PathPatch(node_corner_path, facecolor='none', edgecolor=self.line_color,
                                                     linestyle=self.line_style, linewidth=scaled_line_width)
                            self.ax.add_patch(node_corner_patch)
                else:
                    # 绘制一条完整的水平导线,覆盖从最左到最右的子节点
                    self.ax.plot([min_child_x, max_child_x], [vertical_mid_point_y, vertical_mid_point_y],
                                color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)

                    # 为每个子节点绘制垂直线连接到水平导线
                    for node in children:
                        node_center_x = node.x
                        node_top_y = node.y + node.height / 2
                        # 绘制从水平导线到节点顶部的垂直线
                        self.ax.plot([node_center_x, node_center_x], [vertical_mid_point_y, node_top_y],
                                    color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)

                        # 只在子节点垂直导线结束端绘制末端图形
                        self.draw_end_shape(node_center_x, node_top_y - vertical_offset, scaled_line_width)
        # 横式正翻和横式反翻
        elif self.layout_direction == 'horizontal_normal':
            for node in children:
                node_center_x = node.x
                node_center_y = node.y
                parent_right_x = parent.x + parent.width/2
                node_left_x = node.x - node.width/2

                # 确定水平线和垂直线的交点
                horizontal_mid_point_x = (parent_right_x + node_left_x) / 2

                # 获取框样式,判断是否需要使用圆角连接线
                box_style = self.style_settings.get('box_style', 'square')
                use_rounded_corners = box_style == 'rounded'

                if use_rounded_corners:
                    # 使用圆角样式,使用贝塞尔曲线绘制圆角连接线
                    # 使用用户设置的圆角半径,默认值为5mm
                    radius_mm = self.style_settings.get('box_radius_mm', 5)
                    # 将毫米转换为英寸,因为matplotlib使用英寸作为单位
                    radius = self.mm_to_inch(radius_mm)

                    # 创建贝塞尔曲线路径
                    from matplotlib.path import Path
                    import matplotlib.patches as patches

                    # 从父节点右侧到水平线与垂直线的交点(带圆角)
                    verts1 = [
                        (parent_right_x, parent_center_y),
                        (parent_right_x + radius, parent_center_y),
                        (horizontal_mid_point_x, parent_center_y - radius),
                        (horizontal_mid_point_x, parent_center_y)
                    ]
                    codes1 = [
                        Path.MOVETO,
                        Path.LINETO,
                        Path.CURVE3,
                        Path.CURVE3
                    ]
                    path1 = Path(verts1, codes1)
                    patch1 = patches.PathPatch(path1, facecolor='none', edgecolor=self.line_color, 
                                            linestyle=self.line_style, linewidth=scaled_line_width)
                    self.ax.add_patch(patch1)

                    # 从交点垂直到子节点中心位置(修改为连接到中心)
                    # 使用子节点中心Y坐标作为连接点
                    vertical_end_y = node_center_y

                    # 计算圆角适配的连接点,确保不穿过子名框
                    node_top_y = node.y + node.height / 2
                    node_bottom_y = node.y - node.height / 2

                    # 确保垂直连接线在子名框范围内
                    if vertical_end_y > node_top_y:
                        vertical_end_y = node_top_y - radius
                    elif vertical_end_y < node_bottom_y:
                        vertical_end_y = node_bottom_y + radius

                    verts2 = [
                        (horizontal_mid_point_x, parent_center_y),
                        (horizontal_mid_point_x, parent_center_y + radius),
                        (horizontal_mid_point_x + radius, vertical_end_y),
                        (horizontal_mid_point_x, vertical_end_y)
                    ]
                    codes2 = [
                        Path.MOVETO,
                        Path.LINETO,
                        Path.CURVE3,
                        Path.CURVE3
                    ]
                    path2 = Path(verts2, codes2)
                    patch2 = patches.PathPatch(path2, facecolor='none', edgecolor=self.line_color, 
                                            linestyle=self.line_style, linewidth=scaled_line_width)
                    self.ax.add_patch(patch2)

                    # 从交点到子节点左侧的水平线
                    # 连接到子节点中心位置
                    self.ax.plot([horizontal_mid_point_x, node_left_x], [vertical_end_y, vertical_end_y],
                                color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)
                else:
                    # 原始直角连接线
                    # 使用子节点中心Y坐标作为连接点
                    vertical_end_y = node_center_y

                    # 确保垂直连接线在子名框范围内
                    node_top_y = node.y + node.height / 2
                    node_bottom_y = node.y - node.height / 2
                    if vertical_end_y > node_top_y:
                        vertical_end_y = node_top_y - self.mm_to_inch(2)
                    elif vertical_end_y < node_bottom_y:
                        vertical_end_y = node_bottom_y + self.mm_to_inch(2)

                    # 绘制从父节点右侧的水平线
                    self.ax.plot([parent_right_x, horizontal_mid_point_x], [parent_center_y, parent_center_y],
                                color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)
                    # 绘制垂直线连接
                    self.ax.plot([horizontal_mid_point_x, horizontal_mid_point_x], [parent_center_y, vertical_end_y],
                                color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)
                    # 绘制到子节点左侧的水平线
                    self.ax.plot([horizontal_mid_point_x, node_left_x], [vertical_end_y, vertical_end_y],
                                color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)

                # 只在子节点连接处绘制末端图形
                self.draw_end_shape(horizontal_mid_point_x, vertical_end_y, scaled_line_width)
        elif self.layout_direction == 'horizontal_reverse':
            for node in children:
                node_center_x = node.x
                node_center_y = node.y
                parent_left_x = parent.x - parent.width/2
                node_right_x = node.x + node.width/2

                # 确定水平线和垂直线的交点
                horizontal_mid_point_x = (parent_left_x + node_right_x) / 2

                # 获取框样式,判断是否需要使用圆角连接线
                box_style = self.style_settings.get('box_style', 'square')
                use_rounded_corners = box_style == 'rounded'

                if use_rounded_corners:
                    # 使用圆角样式,使用贝塞尔曲线绘制圆角连接线
                    # 使用用户设置的圆角半径,默认值为5mm
                    radius_mm = self.style_settings.get('box_radius_mm', 5)
                    # 将毫米转换为英寸,因为matplotlib使用英寸作为单位
                    radius = self.mm_to_inch(radius_mm)

                    # 创建贝塞尔曲线路径
                    from matplotlib.path import Path
                    import matplotlib.patches as patches

                    # 从父节点左侧到水平线与垂直线的交点(带圆角)
                    verts1 = [
                        (parent_left_x, parent_center_y),
                        (parent_left_x - radius, parent_center_y),
                        (horizontal_mid_point_x, parent_center_y - radius),
                        (horizontal_mid_point_x, parent_center_y)
                    ]
                    codes1 = [
                        Path.MOVETO,
                        Path.LINETO,
                        Path.CURVE3,
                        Path.CURVE3
                    ]
                    path1 = Path(verts1, codes1)
                    patch1 = patches.PathPatch(path1, facecolor='none', edgecolor=self.line_color, 
                                            linestyle=self.line_style, linewidth=scaled_line_width)
                    self.ax.add_patch(patch1)

                    # 从交点垂直到子节点中心位置(修改为连接到中心)
                    # 使用子节点中心Y坐标作为连接点
                    vertical_end_y = node_center_y

                    # 计算圆角适配的连接点,确保不穿过子名框
                    node_top_y = node.y + node.height / 2
                    node_bottom_y = node.y - node.height / 2

                    # 确保垂直连接线在子名框范围内
                    if vertical_end_y > node_top_y:
                        vertical_end_y = node_top_y - radius
                    elif vertical_end_y < node_bottom_y:
                        vertical_end_y = node_bottom_y + radius

                    verts2 = [
                        (horizontal_mid_point_x, parent_center_y),
                        (horizontal_mid_point_x, parent_center_y + radius),
                        (horizontal_mid_point_x - radius, vertical_end_y),
                        (horizontal_mid_point_x, vertical_end_y)
                    ]
                    codes2 = [
                        Path.MOVETO,
                        Path.LINETO,
                        Path.CURVE3,
                        Path.CURVE3
                    ]
                    path2 = Path(verts2, codes2)
                    patch2 = patches.PathPatch(path2, facecolor='none', edgecolor=self.line_color, 
                                            linestyle=self.line_style, linewidth=scaled_line_width)
                    self.ax.add_patch(patch2)

                    # 从交点到子节点右侧的水平线
                    # 使用相同的终点Y坐标,确保不穿过子名框顶部
                    self.ax.plot([horizontal_mid_point_x, node_right_x], [vertical_end_y, vertical_end_y],
                                color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)
                else:
                    # 原始直角连接线
                    # 使用子节点中心Y坐标作为连接点
                    vertical_end_y = node_center_y

                    # 确保垂直连接线在子名框范围内
                    node_top_y = node.y + node.height / 2
                    node_bottom_y = node.y - node.height / 2
                    if vertical_end_y > node_top_y:
                        vertical_end_y = node_top_y - self.mm_to_inch(2)
                    elif vertical_end_y < node_bottom_y:
                        vertical_end_y = node_bottom_y + self.mm_to_inch(2)

                    # 绘制从父节点左侧的水平线
                    self.ax.plot([parent_left_x, horizontal_mid_point_x], [parent_center_y, parent_center_y],
                                color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)
                    # 绘制垂直线连接
                    self.ax.plot([horizontal_mid_point_x, horizontal_mid_point_x], [parent_center_y, vertical_end_y],
                                color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)
                    # 绘制到子节点右侧的水平线
                    self.ax.plot([horizontal_mid_point_x, node_right_x], [vertical_end_y, vertical_end_y],
                                color=self.line_color, linestyle=self.line_style, linewidth=scaled_line_width)

                # 只在子节点连接处绘制末端图形
                self.draw_end_shape(horizontal_mid_point_x, vertical_end_y, scaled_line_width)

    # 绘制节点
    for node in self.family_tree.nodes.values():
        x, y = node.x, node.y

        # 计算框的实际尺寸
        text = node.name
        if node.note:
            text += f"\n({node.note})"

        box_width_mm, box_height_mm = self.calculate_box_size(text)
        box_width_inch = self.mm_to_inch(box_width_mm)
        box_height_inch = self.mm_to_inch(box_height_mm)

        # 按照默认参数1:1绘制,不随缩放级别变化
        scaled_border_width = max(self.border_width, 0.5)  # 确保最小宽度

        # 从样式设置中获取box_style
        box_style = self.style_settings.get('box_style', 'square')

        # 绘制框
        if box_style == 'rounded':
            # 问题分析:当使用rounding_size参数时,它期望的是一个非常小的相对值,
            # 而不是像我们之前计算的那样基于英寸的相对比例

            # 将圆角半径从毫米转换为英寸
            box_radius_inch = self.mm_to_inch(self.box_radius_mm)

            # 获取框的最小尺寸(宽和高中较小的值)
            min_dimension_inch = min(box_width_inch, box_height_inch)

            # 计算更适合的相对圆角值
            # 对于mm_to_inch转换后的尺寸,需要使用更小的相对比例
            # 直接使用box_radius_inch值而不是相对值,因为这似乎是FancyBboxPatch实际期望的
            rounding_size = min(box_radius_inch, min_dimension_inch / 2)  # 确保不超过最小尺寸的一半

            # 创建圆角框,使用正确的boxstyle参数格式
            box = FancyBboxPatch((x - box_width_inch/2, y - box_height_inch/2), 
                                box_width_inch, box_height_inch,
                                boxstyle=f"round,pad=0.0,rounding_size={rounding_size:.6f}", 
                                facecolor=self.box_fill, 
                                edgecolor=self.border_color,
                                linestyle=self.border_style,
                                linewidth=scaled_border_width)
        elif box_style == 'square':
            box = Rectangle((x - box_width_inch/2, y - box_height_inch/2), 
                           box_width_inch, box_height_inch,
                           facecolor=self.box_fill, 
                           edgecolor=self.border_color,
                           linestyle=self.border_style,
                           linewidth=scaled_border_width)
        else:
            box = Rectangle((x - box_width_inch/2, y - box_height_inch/2), 
                           box_width_inch, box_height_inch,
                           facecolor=self.box_fill, 
                           edgecolor=self.box_fill)

        self.ax.add_patch(box)

        # 添加文本
        if self.text_direction == 'vertical':
            # 竖排文字
            lines = []
            for char in text:
                if char == '\n':
                    continue
                lines.append(char)

            # 计算每个字符的位置
            char_height = box_height_inch / len(lines) * 1.0  # 使用默认行高1.0
            for i, char in enumerate(lines):
                char_y = y - box_height_inch/2 + (i + 0.5) * char_height
                self.ax.text(x, char_y, char, ha='center', va='center', 
                            fontfamily=self.font_name, color=self.font_color,
                            fontsize=self.font_size, rotation=0)
        else:
            # 横排文字
            # 为了支持字间距,需要使用字体属性
            from matplotlib.font_manager import FontProperties
            font_prop = FontProperties(family=self.font_name, size=self.font_size)

            # 使用transform参数结合字间距设置
            # 当字间距大于0时,适当增加字符间的距离
            if hasattr(self, 'letter_spacing') and self.letter_spacing > 0:
                # 改进的字间距实现,确保只调整间距不改变字体大小
                from matplotlib.font_manager import FontProperties

                # 使用固定字体大小,不随间距变化
                font_prop = FontProperties(family=self.font_name, size=self.font_size)

                # 先测量文本的总宽度(无间距)
                test_text = self.ax.text(0, 0, text, fontproperties=font_prop)
                self.fig.canvas.draw()  # 必须绘制才能获取正确的边界框
                bbox = test_text.get_window_extent()
                text_width = bbox.width
                test_text.remove()  # 移除测试文本

                # 计算每个字符的平均宽度
                avg_char_width = text_width / len(text) if text else 0

                # 保持原有的文本渲染方式,不再拆分单个字符
                # 字符间距设置将影响节点之间的间距,而非单个字符之间的间距
                self.ax.text(x, y, text, ha='center', va='center', 
                            fontfamily=self.font_name, color=self.font_color,
                            fontsize=self.font_size)
            else:
                # 默认情况下使用简单的text方法
                self.ax.text(x, y, text, ha='center', va='center', 
                            fontfamily=self.font_name, color=self.font_color,
                            fontsize=self.font_size)

    # 只在首次绘制或明确需要重置视图时才设置完整的视图范围
    # 这样可以避免覆盖用户通过滚动条设置的视图位置
    if not hasattr(self, '_has_initial_view') or not self._has_initial_view:
        # 设置坐标轴范围,确保所有节点都可见
        all_x = [node.x for node in self.family_tree.nodes.values()]
        all_y = [node.y for node in self.family_tree.nodes.values()]

        if all_x and all_y:
            # 计算原始边界,不考虑缩放
            min_x_original = min(all_x)
            max_x_original = max(all_x)
            min_y_original = min(all_y)
            max_y_original = max(all_y)

            # 根据缩放因子调整边界
            range_x = max_x_original - min_x_original
            range_y = max_y_original - min_y_original

            # 中心位置
            center_x = (min_x_original + max_x_original) / 2
            center_y = (min_y_original + max_y_original) / 2

            # 固定原始边界,不随缩放自动调整
            # 添加适当的padding
            padding_factor = 0.1  # 10%的额外空间
            x_size = (range_x * (1 + padding_factor)) / 2
            y_size = (range_y * (1 + padding_factor)) / 2

            # 保持原始范围,不应用缩放调整
            new_min_x = center_x - x_size
            new_max_x = center_x + x_size
            new_min_y = center_y - y_size
            new_max_y = center_y + y_size

            # 设置新的坐标轴范围
            self.ax.set_xlim(new_min_x, new_max_x)
            self.ax.set_ylim(new_min_y, new_max_y)

            # 记录完整视图范围,用于计算画布大小
            self._full_view_min_x = new_min_x
            self._full_view_max_x = new_max_x
            self._full_view_min_y = new_min_y
            self._full_view_max_y = new_max_y

        # 标记已设置初始视图
        self._has_initial_view = True

    self.ax.set_aspect('equal')
    self.ax.axis('off')

    # 禁用tight_layout以避免自动调整裁剪区域
    # self.fig.tight_layout()

    # 使用更强大的方法确保所有内容可见
    # 清除所有裁剪设置
    for artist in self.ax.get_children():
        artist.set_clip_on(False)

    self.draw()

    # 设置初始化标志
    self.figure_initialized = True

    # 确保绘制完成后更新滚动区域,正确反映内容大小
    if hasattr(self.parent(), 'parent') and hasattr(self.parent().parent(), 'scroll_area'):
        # 强制更新滚动区域
        self.parent().parent().scroll_area.update()
        # 重新计算视图范围并设置最小大小
        if hasattr(self, '_full_view_min_x'):
            full_width = self._full_view_max_x - self._full_view_min_x
            full_height = self._full_view_max_y - self._full_view_min_y
            dpi = self.fig.get_dpi()
            required_width = int((full_width * self.zoom_factor * dpi) * 1.1)
            required_height = int((full_height * self.zoom_factor * dpi) * 1.1)
            required_width = max(required_width, 500)
            required_height = max(required_height, 400)
            self.setMinimumSize(required_width, required_height)

def on_scroll(self, event):
    """处理鼠标滚轮事件以实现放大缩小,确保以鼠标当前位置为中心点"""
    # 获取鼠标当前位置(以鼠标事件位置为中心)
    x = event.x
    y = event.y

    # 确保事件位置有效
    if x < 0 or y < 0:
        return

    # 将屏幕坐标转换为数据坐标
    try:
        # 确保坐标转换的正确性
        data_x, data_y = self.ax.transData.inverted().transform([x, y])
    except:
        # 如果转换失败,使用当前视图中心作为备选
        xlim = self.ax.get_xlim()
        ylim = self.ax.get_ylim()
        data_x = (xlim[0] + xlim[1]) / 2
        data_y = (ylim[0] + ylim[1]) / 2

    # 保存当前的视图范围
    xlim = self.ax.get_xlim()
    ylim = self.ax.get_ylim()

    # 计算缩放因子
    old_zoom = self.zoom_factor

    # 确保缩放基于1:1绘图进行
    # 严格按照1:1为基础进行缩放

    # 应用缩放
    if event.button == 'up':
        self.zoom_factor *= 1.1
    else:
        self.zoom_factor /= 1.1

    # 限制缩放范围
    self.zoom_factor = max(0.1, min(self.zoom_factor, 10.0))

    # 计算缩放比例变化
    zoom_ratio = self.zoom_factor / old_zoom

    # 计算新的视图范围,精确确保鼠标位置保持不变
    # 先调整视图大小
    x_width = (xlim[1] - xlim[0]) / zoom_ratio
    y_height = (ylim[1] - ylim[0]) / zoom_ratio

    # 然后计算新的边界,使鼠标位置保持在屏幕上的相同位置
    # 这确保了缩放操作是以鼠标所在位置为中心的
    new_xlim = [data_x - (data_x - xlim[0]) / zoom_ratio,
                data_x + (xlim[1] - data_x) / zoom_ratio]
    new_ylim = [data_y - (data_y - ylim[0]) / zoom_ratio,
                data_y + (ylim[1] - data_y) / zoom_ratio]

    # 应用新的视图范围
    self.ax.set_xlim(new_xlim)
    self.ax.set_ylim(new_ylim)

    # 重绘
    self.draw()

    # 更新状态栏显示缩放比例
    if hasattr(self, 'parent') and hasattr(self.parent(), 'statusBar'):
        zoom_percentage = self.zoom_factor * 100
        if zoom_percentage >= 100:
            status_text = f"就绪 (1:{self.zoom_factor:.1f})"
        else:
            status_text = f"就绪 ({self.zoom_factor:.1f}:1)"
        self.parent().statusBar().showMessage(status_text)

    # 调整画布大小以适应缩放,确保滚动条能够显示所有内容
    if self.figure_initialized and hasattr(self, '_full_view_min_x'):
        # 计算完整视图的宽度和高度
        full_width = self._full_view_max_x - self._full_view_min_x
        full_height = self._full_view_max_y - self._full_view_min_y

        # 获取当前画布的DPI
        dpi = self.fig.get_dpi()

        # 计算画布需要的最小大小,基于完整视图范围和缩放因子
        # 当缩放大时,画布需要更大才能显示所有内容
        required_width = int((full_width * self.zoom_factor * dpi) * 1.1)  # 添加10%的余量
        required_height = int((full_height * self.zoom_factor * dpi) * 1.1)

        # 确保最小大小
        required_width = max(required_width, 500)
        required_height = max(required_height, 400)

        # 设置画布的最小大小,这样滚动条就能正确反映内容大小
        self.setMinimumSize(required_width, required_height)

    # 确保滚动区域正确更新
    if hasattr(self.parent(), 'parent') and hasattr(self.parent().parent(), 'scroll_area'):
        # 强制更新滚动区域
        self.parent().parent().scroll_area.update()
        # 调整视图大小策略以确保滚动条在需要时显示
        self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
        self.updateGeometry()

def reset_view(self):
    """重置视图到原始状态"""
    self.zoom_factor = 1.0
    # 重置初始视图标志,使draw_tree方法重新计算完整的视图范围
    self._has_initial_view = False
    self.draw_tree()

def resizeEvent(self, event):
    """处理画布大小变化事件,确保滚动条在放大后正常工作"""
    # 调用父类的resizeEvent方法
    super().resizeEvent(event)

    # 只有当初始化完成后才进行调整
    if self.figure_initialized:
        # 调整画布大小以适应视图
        width, height = self.size().width(), self.size().height()
        dpi = self.fig.get_dpi()
        self.fig.set_size_inches(width/dpi, height/dpi)

        # 更新滚动区域
        if hasattr(self.parent(), 'parent') and hasattr(self.parent().parent(), 'scroll_area'):
            self.parent().parent().scroll_area.update()

def _calculate_positions(self):
    """根据用户定义的吊线图生成规律计算节点位置"""
    if not self.family_tree or not self.family_tree.root:
        return

    # 首先计算每个节点的宽度和高度
    for node in self.family_tree.nodes.values():
        text = node.name
        if node.note:
            text += f"\n({node.note})"
        box_width_mm, box_height_mm = self.calculate_box_size(text)
        node.width = self.mm_to_inch(box_width_mm)
        node.height = self.mm_to_inch(box_height_mm)

    # 设置根节点位置,从绘图区顶部开始
    self.family_tree.root.x = 0
    self.family_tree.root.y = 0  # 保持y坐标为0,从顶部开始绘制

    # 多次迭代计算以确保复杂树结构的节点位置收敛
    # 增加迭代次数处理更深层级的树结构,解决第十三层开始的节点重叠问题
    max_iterations = 8  # 增加到8次迭代,足够处理非常深的树结构
    for _ in range(max_iterations):
        # 递归计算所有节点位置 - 改进为宝塔式布局
        self._calculate_node_positions_recursive(self.family_tree.root, 0)

        # 在每次迭代中,调用调整祖先节点位置的方法确保对称性
        if self.layout_direction.startswith('vertical'):
            self._adjust_parent_positions_for_symmetry(self.family_tree.root)

    # 最后再进行一次完整的递归计算确保所有位置准确
    self._calculate_node_positions_recursive(self.family_tree.root, 0)

def _adjust_parent_positions_for_symmetry(self, node):
    """调整祖先节点的位置,确保整个树的对称性"""
    # 这是一个简单的实现,仅确保在竖式布局中父节点位置正确
    if not node.parent or self.layout_direction.startswith('horizontal'):
        return

    # 找到当前节点的所有同级节点
    siblings = node.parent.children
    if len(siblings) <= 1:
        return

    # 计算同级节点的中心点
    min_x = min(siblings, key=lambda n: n.x).x
    max_x = max(siblings, key=lambda n: n.x).x
    center_x = (min_x + max_x) / 2

    # 如果父节点的位置与子节点中心点不匹配,则调整父节点位置
    if abs(node.parent.x - center_x) > 0.001:  # 添加一个小的容差
        dx = center_x - node.parent.x
        node.parent.x = center_x

        # 递归调整所有祖先节点
        self._adjust_parent_positions_for_symmetry(node.parent)

def _calculate_node_positions_recursive(self, parent_node, level=0):
    """递归计算节点及其子节点的位置,实现宝塔式布局"""
    if not parent_node or not parent_node.children:
        return

    # 获取毫米转换为英寸的间距 - 每次递归调用都重新获取,确保参数立即生效
    horizontal_spacing_inch = self.mm_to_inch(self.horizontal_spacing_mm)
    vertical_spacing_inch = self.mm_to_inch(self.vertical_spacing_mm)

    # 应用字符间距设置(子名字之间的间距)
    # 将letter_spacing参数转换为英寸并加到水平间距中
    # letter_spacing是0-2.0个字符宽度的间距值
    if hasattr(self, 'letter_spacing'):
        # 进一步增加字符宽度估算值和放大系数,使字间距效果更明显
        avg_char_width_mm = 5.0  # 进一步增加平均字符宽度估算
        spacing_multiplier = 3.0  # 进一步增加放大系数
        letter_spacing_inch = self.mm_to_inch(avg_char_width_mm * self.letter_spacing * spacing_multiplier)
        # 应用到水平间距(子名字之间的间距)
        horizontal_spacing_inch += letter_spacing_inch
        print(f"调试信息: 原始水平间距={self.horizontal_spacing_mm}mm, 字符间距={self.letter_spacing}, 转换后字符间距={letter_spacing_inch:.6f}英寸, 应用后水平间距={horizontal_spacing_inch:.6f}英寸")

    # 使用统一的垂直间距和水平间距,确保所有层级间的导线长度一致且对齐准确
    level_based_vertical_spacing = vertical_spacing_inch
    level_based_horizontal_spacing = horizontal_spacing_inch

    # 对子节点排序
    children = parent_node.children.copy()  # 创建副本避免修改原列表
    # 在竖式正翻模式下,保持子节点的原始顺序(按照数据表中的顺序从左到右)
    if self.layout_direction != 'vertical_normal':
        # 其他模式下按名称字母顺序排序
        children.sort(key=lambda n: n.name)

    # 首先递归计算所有子节点的子节点位置,这是关键的自底向上布局方法
    for child in children:
        self._calculate_node_positions_recursive(child, level + 1)

    # 计算所有子节点的总宽度/高度
    total_width = 0
    total_height = 0
    for child in children:
        total_width += child.width
        total_height += child.height
    # 确保所有子节点之间的间距都等于水平间距参数值
    # 即使只有一个子节点,也要考虑间距,以避免与其他子节点组重叠
    spacing_count = max(1, len(children) - 1)  # 至少保留一个间距的位置
    total_width += horizontal_spacing_inch * spacing_count
    total_height += vertical_spacing_inch * spacing_count

    # 根据布局类型计算子节点位置
    if self.layout_direction == 'vertical_normal':
        # 竖式正翻:子节点在父节点下方
        start_x = parent_node.x - total_width / 2
        current_x = start_x
        for child in children:
            child_x = current_x + child.width / 2
            child.x = child_x
            # 计算子节点Y坐标,确保与父节点对齐
            child.y = parent_node.y - parent_node.height / 2 - level_based_vertical_spacing - child.height / 2
            # 增强版:确保子节点之间有足够的间距,解决节点组重叠问题
            current_x += child.width + horizontal_spacing_inch
            # 增加额外的间距以确保不同子节点组之间有足够的空间
            current_x += horizontal_spacing_inch * 0.5
    elif self.layout_direction == 'vertical_reverse':
        # 竖式反翻:子节点在父节点下方,以Y轴为准水平翻转
        start_x = parent_node.x - total_width / 2
        current_x = start_x
        for child in children:
            child_x = current_x + child.width / 2
            # 修复垂直翻转布局的计算
            child.x = 2 * parent_node.x - child_x  # 相对于父节点进行水平翻转
            # 为当前层级添加垂直偏移,确保不同层级之间有足够空间
            child.y = parent_node.y - parent_node.height / 2 - level_based_vertical_spacing - child.height / 2
            # 增强版:确保子节点之间有足够的间距,解决节点组重叠问题
            current_x += child.width + horizontal_spacing_inch
            # 增加额外的间距以确保不同子节点组之间有足够的空间
            current_x += horizontal_spacing_inch * 0.5
    elif self.layout_direction == 'horizontal_normal':
        # 横式正翻:子节点在父节点右侧,为每个层级添加水平偏移
        start_y = parent_node.y - total_height / 2
        current_y = start_y
        for child in children:
            # 子节点在父节点右侧,使用层级确定X坐标位置
            child_x = parent_node.x + parent_node.width / 2 + level_based_horizontal_spacing + child.width / 2
            # 在垂直方向上均匀分布同一层级的子节点
            child.y = current_y + child.height / 2
            child.x = child_x
            # 增强版:确保子节点之间有足够的间距,解决节点组重叠问题
            current_y += child.height + vertical_spacing_inch
            # 增加额外的间距以确保不同子节点组之间有足够的空间
            current_y += vertical_spacing_inch * 0.5
    elif self.layout_direction == 'horizontal_reverse':
        # 横式反翻:子节点在父节点左侧,为每个层级添加水平偏移
        start_y = parent_node.y - total_height / 2
        current_y = start_y
        for child in children:
            # 子节点在父节点左侧,使用层级确定X坐标位置
            child_x = parent_node.x - parent_node.width / 2 - level_based_horizontal_spacing - child.width / 2
            # 在垂直方向上均匀分布同一层级的子节点
            child.y = current_y + child.height / 2
            child.x = child_x
            # 增强版:确保子节点之间有足够的间距,解决节点组重叠问题
            current_y += child.height + vertical_spacing_inch
            # 增加额外的间距以确保不同子节点组之间有足够的空间
            current_y += vertical_spacing_inch * 0.5

    # 确保父节点居中于其子节点组 - 适用于所有布局类型
    if len(children) > 0:
        # 先处理单子女节点对齐问题
        if len(children) == 1:
            # 只有一个子节点时,子节点直接对齐父节点
            child = children[0]
            if self.layout_direction in ['vertical_normal', 'vertical_reverse']:
                # 竖式布局:子节点水平居中于父节点(精确对齐)
                child.x = parent_node.x
                # 确保垂直方向位置正确
                child.y = parent_node.y - parent_node.height / 2 - level_based_vertical_spacing - child.height / 2
                # 增强版:确保所有单节点子节点组(如立仕与本周)准确水平居中对齐
                # 移除特定名称的硬编码,使其通用化
                child.x = parent_node.x  # 强制精确对齐
            elif self.layout_direction in ['horizontal_normal', 'horizontal_reverse']:
                # 横式布局:子节点垂直居中于父节点
                child.y = parent_node.y
                # 确保水平方向位置正确
                if self.layout_direction == 'horizontal_normal':
                    child.x = parent_node.x + parent_node.width / 2 + level_based_vertical_spacing + child.width / 2
                else:
                    child.x = parent_node.x - parent_node.width / 2 - level_based_vertical_spacing - child.width / 2
        else:
            # 处理多子节点居中对齐问题
            if self.layout_direction in ['vertical_normal', 'vertical_reverse']:
                # 竖式布局:计算子节点组的水平中心点
                # 更准确的计算方式:先计算所有子节点覆盖的总宽度范围
                min_child_x = min(child.x - child.width/2 for child in children)
                max_child_x = max(child.x + child.width/2 for child in children)
                children_center_x = (min_child_x + max_child_x) / 2

                # 增强版:确保多子节点组(如洪日与洪寿、洪福与洪禄、守义与守礼)准确水平居中对齐
                # 调整父节点位置使其居中于子节点组
                if abs(parent_node.x - children_center_x) > 0.001:  # 降低阈值以确保更精确的居中
                    # 先调整父节点位置
                    parent_node.x = children_center_x
                    # 然后同时调整所有子节点的位置,确保子节点组与父节点精确对齐
                    # 计算子节点组的理想宽度
                    total_width = max_child_x - min_child_x
                    # 计算子节点组的理想起始位置
                    ideal_start_x = parent_node.x - total_width / 2
                    # 计算当前起始位置
                    current_start_x = min_child_x
                    # 计算需要调整的偏移量
                    delta_x = ideal_start_x - current_start_x

                    # 调整所有子节点的位置
                    for child in children:
                        child.x += delta_x

                    # 调整父节点的所有祖先节点位置
                    self._adjust_ancestor_positions(parent_node, 0, 0)
            elif self.layout_direction in ['horizontal_normal', 'horizontal_reverse']:
                # 横式布局:计算子节点组的垂直中心点
                min_child_y = min(child.y - child.height/2 for child in children)
                max_child_y = max(child.y + child.height/2 for child in children)
                children_center_y = (min_child_y + max_child_y) / 2

                # 调整父节点位置使其居中于子节点组
                if abs(parent_node.y - children_center_y) > 0.001:  # 降低阈值以确保更精确的居中
                    delta_y = children_center_y - parent_node.y
                    parent_node.y = children_center_y
                    # 调整父节点的所有祖先节点位置
                    self._adjust_ancestor_positions(parent_node, 0, delta_y)

def _adjust_ancestor_positions(self, node, delta_x, delta_y):
    """调整节点的所有祖先节点位置,确保父子关系正确显示"""
    # 向上递归调整所有祖先节点的位置
    parent = node.parent
    while parent:
        parent.x += delta_x
        parent.y += delta_y

        # 对于每个祖先节点,需要确保它也居中于其子节点组
        # 计算祖先节点的子节点组中心点
        if self.layout_direction in ['vertical_normal', 'vertical_reverse']:
            # 竖式布局:计算水平居中
            siblings = parent.parent.children if parent.parent else []
            if len(siblings) > 1:
                min_x = min(s.x - s.width/2 for s in siblings)
                max_x = max(s.x + s.width/2 for s in siblings)
                siblings_center_x = (min_x + max_x) / 2

                # 如果祖先节点的父节点位置与子节点组中心点不匹配,则进一步调整
                if abs(parent.parent.x - siblings_center_x) > 0.001:
                    grand_delta_x = siblings_center_x - parent.parent.x
                    parent.parent.x = siblings_center_x
                    # 递归调整更高层级的祖先节点
                    self._adjust_ancestor_positions(parent.parent, grand_delta_x, 0)
        elif self.layout_direction in ['horizontal_normal', 'horizontal_reverse']:
            # 横式布局:计算垂直居中
            siblings = parent.parent.children if parent.parent else []
            if len(siblings) > 1:
                min_y = min(s.y - s.height/2 for s in siblings)
                max_y = max(s.y + s.height/2 for s in siblings)
                siblings_center_y = (min_y + max_y) / 2

                # 如果祖先节点的父节点位置与子节点组中心点不匹配,则进一步调整
                if abs(parent.parent.y - siblings_center_y) > 0.001:
                    grand_delta_y = siblings_center_y - parent.parent.y
                    parent.parent.y = siblings_center_y
                    # 递归调整更高层级的祖先节点
                    self._adjust_ancestor_positions(parent.parent, 0, grand_delta_y)

        # 继续调整父节点的父节点
        parent = parent.parent

def _adjust_descendants_position(self, node, delta_x, delta_y):
    """调整节点及其所有后代节点的位置,避免递归重新计算"""
    if not node or not node.children:
        return

    # 调整所有子节点的位置
    for child in node.children:
        # 在竖式正翻布局中,我们只调整水平位置,不改变垂直位置
        # 这样可以确保垂直方向上的布局保持正确,避免节点重叠
        child.x += delta_x
        if self.layout_direction != 'vertical_normal':
            child.y += delta_y
        # 递归调整孙子节点
        self._adjust_descendants_position(child, delta_x, delta_y)

class StylePanel(QScrollArea):
def init(self, parent=None):
super().init(parent)
self.setFrameStyle(QFrame.StyledPanel)
self.setMinimumWidth(190) # 固定宽度,与应用按钮一致
self.setMaximumWidth(190)
self.setWidgetResizable(True)

    # 确保滚动条不会覆盖内容
    self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
    # 设置垂直滚动条始终显示(保留空间),但只有在需要时才可操作
    self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)

    # 创建内容窗口
    self.content_widget = QWidget()
    self.setWidget(self.content_widget)

    self.setup_ui()

def setup_ui(self):
    layout = QVBoxLayout(self.content_widget)
    layout.setContentsMargins(5, 5, 5, 5)        
    # 保存对主窗口的引用,用于调用apply_style方法
    self.main_window = None

    # 生成方式
    reading_group = QGroupBox("生成方式")
    reading_layout = QVBoxLayout()

    # 生成方向设置(新增)
    generation_direction_layout = QHBoxLayout()
    label_gen_dir = QLabel("生成方向:")
    label_gen_dir.setFixedWidth(60)  # 固定标签宽度
    generation_direction_layout.addWidget(label_gen_dir)
    self.generation_direction_combo = QComboBox()
    self.generation_direction_combo.addItems(["竖式", "横式"])
    self.generation_direction_combo.setFixedWidth(80)  # 固定选择框宽度
    generation_direction_layout.addWidget(self.generation_direction_combo)
    generation_direction_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    reading_layout.addLayout(generation_direction_layout)

    # 阅读方向设置
    layout_direction_layout = QHBoxLayout()
    label_layout_dir = QLabel("阅读方向:")
    label_layout_dir.setFixedWidth(60)  # 固定标签宽度
    layout_direction_layout.addWidget(label_layout_dir)
    self.layout_direction_combo = QComboBox()
    self.layout_direction_combo.addItems(["正翻", "反翻"])
    self.layout_direction_combo.setFixedWidth(80)  # 固定选择框宽度
    layout_direction_layout.addWidget(self.layout_direction_combo)
    layout_direction_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    reading_layout.addLayout(layout_direction_layout)

    # 文字方向设置
    text_direction_layout = QHBoxLayout()
    label_text_dir = QLabel("文字方向:")
    label_text_dir.setFixedWidth(60)  # 固定标签宽度
    text_direction_layout.addWidget(label_text_dir)
    self.text_direction_combo = QComboBox()
    self.text_direction_combo.addItems(["横排", "竖排"])
    self.text_direction_combo.setFixedWidth(80)  # 固定选择框宽度
    text_direction_layout.addWidget(self.text_direction_combo)
    text_direction_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    reading_layout.addLayout(text_direction_layout)

    reading_group.setLayout(reading_layout)
    layout.addWidget(reading_group)

    # 图形设置
    box_style_group = QGroupBox("图形设置")
    box_style_layout = QVBoxLayout()

    # 框填充颜色
    box_fill_layout = QHBoxLayout()
    label_box_fill = QLabel("填充颜色:")
    label_box_fill.setFixedWidth(60)  # 固定标签宽度
    box_fill_layout.addWidget(label_box_fill)
    self.box_fill_button = QPushButton()
    self.box_fill_button.setFixedSize(80, 20)
    self.box_fill_button.setStyleSheet("background-color: #ffffff")
    self.box_fill_button.clicked.connect(lambda: self.select_color(self.box_fill_button))
    box_fill_layout.addWidget(self.box_fill_button)
    box_fill_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    box_style_layout.addLayout(box_fill_layout)

    # 边框颜色
    border_color_layout = QHBoxLayout()
    label_border_color = QLabel("边框颜色:")
    label_border_color.setFixedWidth(60)  # 固定标签宽度
    border_color_layout.addWidget(label_border_color)
    self.border_color_button = QPushButton()
    self.border_color_button.setFixedSize(80, 20)
    self.border_color_button.setStyleSheet("background-color: #000000")
    self.border_color_button.clicked.connect(lambda: self.select_color(self.border_color_button))
    border_color_layout.addWidget(self.border_color_button)
    border_color_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    box_style_layout.addLayout(border_color_layout)

    # 边框粗细
    border_width_layout = QHBoxLayout()
    label_border_width = QLabel("边框粗细:")
    label_border_width.setFixedWidth(60)  # 固定标签宽度
    border_width_layout.addWidget(label_border_width)
    self.border_width_spin = QDoubleSpinBox()
    self.border_width_spin.setRange(0.1, 5.0)
    self.border_width_spin.setSingleStep(0.1)
    self.border_width_spin.setValue(1.0)
    self.border_width_spin.setFixedWidth(80)  # 固定选择框宽度
    border_width_layout.addWidget(self.border_width_spin)
    border_width_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    box_style_layout.addLayout(border_width_layout)

    # 图形宽度
    box_width_layout = QHBoxLayout()
    label_box_width = QLabel("图形宽度:")
    label_box_width.setFixedWidth(60)  # 固定标签宽度
    box_width_layout.addWidget(label_box_width)
    self.box_width_spin = QDoubleSpinBox()
    self.box_width_spin.setRange(0.0, 200.0)
    self.box_width_spin.setSingleStep(1.0)
    self.box_width_spin.setValue(10.0)
    self.box_width_spin.setSuffix(" mm")
    self.box_width_spin.setFixedWidth(80)  # 固定选择框宽度
    box_width_layout.addWidget(self.box_width_spin)
    box_width_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    box_style_layout.addLayout(box_width_layout)

    # 图形高度
    box_height_layout = QHBoxLayout()
    label_box_height = QLabel("图形高度:")
    label_box_height.setFixedWidth(60)  # 固定标签宽度
    box_height_layout.addWidget(label_box_height)
    self.box_height_spin = QDoubleSpinBox()
    self.box_height_spin.setRange(0.0, 200.0)
    self.box_height_spin.setSingleStep(1.0)
    self.box_height_spin.setValue(5.0)
    self.box_height_spin.setSuffix(" mm")
    self.box_height_spin.setFixedWidth(80)  # 固定选择框宽度
    box_height_layout.addWidget(self.box_height_spin)
    box_height_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    box_style_layout.addLayout(box_height_layout)

    # 边框线型
    border_style_layout = QHBoxLayout()
    label_border_style = QLabel("边框线型:")
    label_border_style.setFixedWidth(60)  # 固定标签宽度
    border_style_layout.addWidget(label_border_style)
    self.border_style_combo = QComboBox()
    self.border_style_combo.addItems(["实线", "虚线", "点线", "无框"])
    self.border_style_combo.setFixedWidth(80)  # 固定选择框宽度
    border_style_layout.addWidget(self.border_style_combo)
    border_style_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    box_style_layout.addLayout(border_style_layout)

    # 边框样式
    box_shape_layout = QHBoxLayout()
    label_box_shape = QLabel("边框样式:")
    label_box_shape.setFixedWidth(60)  # 固定标签宽度
    box_shape_layout.addWidget(label_box_shape)
    self.box_shape_combo = QComboBox()
    self.box_shape_combo.addItems(["直角", "圆角"])
    self.box_shape_combo.setFixedWidth(80)  # 固定选择框宽度
    box_shape_layout.addWidget(self.box_shape_combo)
    box_shape_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    box_style_layout.addLayout(box_shape_layout)

    # 圆角半径(只在圆角样式时生效)
    box_radius_layout = QHBoxLayout()
    label_box_radius = QLabel("圆角半径:")
    label_box_radius.setFixedWidth(60)  # 固定标签宽度
    box_radius_layout.addWidget(label_box_radius)
    self.box_radius_spin = QDoubleSpinBox()
    self.box_radius_spin.setRange(0.0, 100.0)
    self.box_radius_spin.setSingleStep(1.0)
    self.box_radius_spin.setValue(10.0)
    self.box_radius_spin.setSuffix(" mm")
    self.box_radius_spin.setFixedWidth(80)  # 固定选择框宽度
    # 初始时根据当前选择的边框样式设置圆角半径控件的启用状态
    self.box_radius_spin.setEnabled(self.box_shape_combo.currentText() == "圆角")
    # 连接信号,当边框样式改变时更新圆角半径控件的启用状态
    self.box_shape_combo.currentTextChanged.connect(
        lambda text: self.box_radius_spin.setEnabled(text == "圆角")
    )
    box_radius_layout.addWidget(self.box_radius_spin)
    box_radius_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    box_style_layout.addLayout(box_radius_layout)

    # 水平间距
    horizontal_spacing_layout = QHBoxLayout()
    label_horizontal_spacing = QLabel("水平间距:")
    label_horizontal_spacing.setFixedWidth(60)  # 固定标签宽度
    horizontal_spacing_layout.addWidget(label_horizontal_spacing)
    self.horizontal_spacing_spin = QDoubleSpinBox()
    self.horizontal_spacing_spin.setRange(1.0, 100.0)
    self.horizontal_spacing_spin.setSingleStep(1.0)
    self.horizontal_spacing_spin.setValue(3.0)
    self.horizontal_spacing_spin.setSuffix(" mm")
    self.horizontal_spacing_spin.setFixedWidth(80)  # 固定选择框宽度
    horizontal_spacing_layout.addWidget(self.horizontal_spacing_spin)
    horizontal_spacing_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    box_style_layout.addLayout(horizontal_spacing_layout)

    # 垂直间距
    vertical_spacing_layout = QHBoxLayout()
    label_vertical_spacing = QLabel("垂直间距:")
    label_vertical_spacing.setFixedWidth(60)  # 固定标签宽度
    vertical_spacing_layout.addWidget(label_vertical_spacing)
    self.vertical_spacing_spin = QDoubleSpinBox()
    self.vertical_spacing_spin.setRange(1.0, 100.0)
    self.vertical_spacing_spin.setSingleStep(1.0)
    self.vertical_spacing_spin.setValue(4.0)
    self.vertical_spacing_spin.setSuffix(" mm")
    self.vertical_spacing_spin.setFixedWidth(80)  # 固定选择框宽度
    vertical_spacing_layout.addWidget(self.vertical_spacing_spin)
    vertical_spacing_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    box_style_layout.addLayout(vertical_spacing_layout)

    box_style_group.setLayout(box_style_layout)
    layout.addWidget(box_style_group)

    # 连接线样式设置
    line_style_group = QGroupBox("导线样式")
    line_style_layout = QVBoxLayout()

    # 连接线颜色
    line_color_layout = QHBoxLayout()
    label_line_color = QLabel("线条颜色:")
    label_line_color.setFixedWidth(60)  # 固定标签宽度
    line_color_layout.addWidget(label_line_color)
    self.line_color_button = QPushButton()
    self.line_color_button.setFixedSize(80, 20)
    self.line_color_button.setStyleSheet("background-color: #000000")
    self.line_color_button.clicked.connect(lambda: self.select_color(self.line_color_button))
    line_color_layout.addWidget(self.line_color_button)
    line_color_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    line_style_layout.addLayout(line_color_layout)

    # 连接线宽度
    line_width_layout = QHBoxLayout()
    label_line_width = QLabel("线条宽度:")
    label_line_width.setFixedWidth(60)  # 固定标签宽度
    line_width_layout.addWidget(label_line_width)
    self.line_width_spin = QDoubleSpinBox()
    self.line_width_spin.setRange(0.1, 5.0)
    self.line_width_spin.setSingleStep(0.1)
    self.line_width_spin.setValue(1.0)
    self.line_width_spin.setFixedWidth(80)  # 固定选择框宽度
    line_width_layout.addWidget(self.line_width_spin)
    line_width_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    line_style_layout.addLayout(line_width_layout)

    # 连接线样式
    line_style_layout_layout = QHBoxLayout()
    label_line_style = QLabel("线条样式:")
    label_line_style.setFixedWidth(60)  # 固定标签宽度
    line_style_layout_layout.addWidget(label_line_style)
    self.line_style_combo = QComboBox()
    self.line_style_combo.addItems(["实线", "虚线", "点线"])
    self.line_style_combo.setFixedWidth(80)  # 固定选择框宽度
    line_style_layout_layout.addWidget(self.line_style_combo)
    line_style_layout_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    line_style_layout.addLayout(line_style_layout_layout)

    # 末端图形参数
    end_shape_layout = QHBoxLayout()
    label_end_shape = QLabel("末端图形:")
    label_end_shape.setFixedWidth(60)  # 固定标签宽度
    end_shape_layout.addWidget(label_end_shape)
    self.end_shape_combo = QComboBox()
    self.end_shape_combo.addItems(["无图形", "空心圆", "实心圆"])
    self.end_shape_combo.setFixedWidth(80)  # 固定选择框宽度
    end_shape_layout.addWidget(self.end_shape_combo)
    end_shape_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    line_style_layout.addLayout(end_shape_layout)

    # 末端图形颜色
    end_shape_color_layout = QHBoxLayout()
    label_end_shape_color = QLabel("图形颜色:")
    label_end_shape_color.setFixedWidth(60)  # 固定标签宽度
    end_shape_color_layout.addWidget(label_end_shape_color)
    self.end_shape_color_button = QPushButton()
    self.end_shape_color_button.setFixedSize(80, 20)
    self.end_shape_color_button.setStyleSheet("background-color: #000000")
    # 信号连接移至connect_signals方法中统一处理
    end_shape_color_layout.addWidget(self.end_shape_color_button)
    end_shape_color_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    line_style_layout.addLayout(end_shape_color_layout)

    # 末端图形大小
    end_shape_size_layout = QHBoxLayout()
    label_end_shape_size = QLabel("图形大小:")
    label_end_shape_size.setFixedWidth(60)  # 固定标签宽度
    end_shape_size_layout.addWidget(label_end_shape_size)
    self.end_shape_size_spin = QDoubleSpinBox()
    self.end_shape_size_spin.setRange(1.0, 10.0)
    self.end_shape_size_spin.setSingleStep(0.5)
    self.end_shape_size_spin.setValue(5.0)  # 默认值为原来的5倍
    self.end_shape_size_spin.setFixedWidth(80)  # 固定选择框宽度
    end_shape_size_layout.addWidget(self.end_shape_size_spin)
    end_shape_size_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    line_style_layout.addLayout(end_shape_size_layout)

    line_style_group.setLayout(line_style_layout)
    layout.addWidget(line_style_group)

    # 字体设置
    font_group = QGroupBox("字体设置")
    font_layout = QVBoxLayout()

    # 字体名称
    font_name_layout = QHBoxLayout()
    label_font_name = QLabel("字体名称:")
    label_font_name.setFixedWidth(60)  # 固定标签宽度
    font_name_layout.addWidget(label_font_name)
    self.font_name_combo = QComboBox()
    self.font_name_combo.addItems(["SimHei", "Stxinwei", "SimSun", "KaiTi"])
    self.font_name_combo.setFixedWidth(80)  # 固定选择框宽度
    font_name_layout.addWidget(self.font_name_combo)
    font_name_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    font_layout.addLayout(font_name_layout)

    # 字体大小
    font_size_layout = QHBoxLayout()
    label_font_size = QLabel("字体大小:")
    label_font_size.setFixedWidth(60)  # 固定标签宽度
    font_size_layout.addWidget(label_font_size)
    self.font_size_spin = QSpinBox()
    self.font_size_spin.setRange(8, 36)
    self.font_size_spin.setValue(12)
    self.font_size_spin.setSuffix(" pt")
    self.font_size_spin.setFixedWidth(80)  # 固定选择框宽度
    font_size_layout.addWidget(self.font_size_spin)
    font_size_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    font_layout.addLayout(font_size_layout)

    # 字体颜色
    font_color_layout = QHBoxLayout()
    label_font_color = QLabel("字体颜色:")
    label_font_color.setFixedWidth(60)  # 固定标签宽度
    font_color_layout.addWidget(label_font_color)
    self.font_color_button = QPushButton()
    self.font_color_button.setFixedSize(80, 20)
    self.font_color_button.setStyleSheet("background-color: #000000")
    self.font_color_button.clicked.connect(lambda: self.select_color(self.font_color_button))
    font_color_layout.addWidget(self.font_color_button)
    font_color_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    font_layout.addLayout(font_color_layout)

    # 字间距(字符间距)
    letter_spacing_layout = QHBoxLayout()
    label_letter_spacing = QLabel("字间距:")
    label_letter_spacing.setFixedWidth(60)  # 固定标签宽度
    letter_spacing_layout.addWidget(label_letter_spacing)
    self.letter_spacing_spin = QDoubleSpinBox()
    self.letter_spacing_spin.setRange(0.0, 2.0)  # 02倍字符宽度的间距
    self.letter_spacing_spin.setSingleStep(0.25)  # 步长为0.25个字符宽度
    self.letter_spacing_spin.setValue(0.0)  # 默认字间距为0
    self.letter_spacing_spin.setSuffix(" 字符")  # 显示单位
    self.letter_spacing_spin.setFixedWidth(90)  # 固定选择框宽度
    letter_spacing_layout.addWidget(self.letter_spacing_spin)
    letter_spacing_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    font_layout.addLayout(letter_spacing_layout)

    font_group.setLayout(font_layout)
    layout.addWidget(font_group)

    # 输出设置
    output_group = QGroupBox("输出设置")
    output_layout = QVBoxLayout()

    # 纸张宽度
    paper_width_layout = QHBoxLayout()
    label_paper_width = QLabel("纸张宽度:")
    label_paper_width.setFixedWidth(60)  # 固定标签宽度
    paper_width_layout.addWidget(label_paper_width)
    self.paper_width_spin = QDoubleSpinBox()
    self.paper_width_spin.setRange(10.0, 1000.0)
    self.paper_width_spin.setSingleStep(1.0)
    self.paper_width_spin.setValue(210.0)
    self.paper_width_spin.setSuffix(" mm")
    self.paper_width_spin.setFixedWidth(80)  # 固定选择框宽度
    paper_width_layout.addWidget(self.paper_width_spin)
    paper_width_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    output_layout.addLayout(paper_width_layout)

    # 纸张高度
    paper_height_layout = QHBoxLayout()
    label_paper_height = QLabel("纸张高度:")
    label_paper_height.setFixedWidth(60)  # 固定标签宽度
    paper_height_layout.addWidget(label_paper_height)
    self.paper_height_spin = QDoubleSpinBox()
    self.paper_height_spin.setRange(10.0, 1000.0)
    self.paper_height_spin.setSingleStep(1.0)
    self.paper_height_spin.setValue(297.0)
    self.paper_height_spin.setSuffix(" mm")
    self.paper_height_spin.setFixedWidth(80)  # 固定选择框宽度
    paper_height_layout.addWidget(self.paper_height_spin)
    paper_height_layout.setAlignment(Qt.AlignLeft)  # 左对齐
    output_layout.addLayout(paper_height_layout)

    output_group.setLayout(output_layout)
    layout.addWidget(output_group)

    # 添加伸展空间使内容顶部对齐
    layout.addStretch()

    # 添加StylePanel类中所有控件的信号连接,实现参数修改后立即生效
    # 导入必要的模块
    from functools import partial

    # 在StylePanel类中添加一个方法来处理参数变化
    def parameter_changed():
        """当参数发生变化时调用,检查是否有加载的数据并刷新视图"""
        if self.main_window and hasattr(self.main_window, 'family_tree') and self.main_window.family_tree:
            self.main_window.apply_style()

    # 将信号连接代码放在单独的方法中
    def connect_signals():
        """连接所有UI控件的信号,实现参数修改后立即刷新"""
        # 生成方式相关控件
        self.generation_direction_combo.currentIndexChanged.connect(parameter_changed)
        self.layout_direction_combo.currentIndexChanged.connect(parameter_changed)
        self.text_direction_combo.currentIndexChanged.connect(parameter_changed)

        # 图形设置相关控件
        self.border_style_combo.currentIndexChanged.connect(parameter_changed)
        self.box_shape_combo.currentIndexChanged.connect(parameter_changed)
        self.border_width_spin.valueChanged.connect(parameter_changed)
        self.box_width_spin.valueChanged.connect(parameter_changed)
        self.box_height_spin.valueChanged.connect(parameter_changed)
        self.box_radius_spin.valueChanged.connect(parameter_changed)

        # 线条设置相关控件
        self.line_style_combo.currentIndexChanged.connect(parameter_changed)
        self.line_width_spin.valueChanged.connect(parameter_changed)
        self.end_shape_combo.currentIndexChanged.connect(parameter_changed)
        self.end_shape_size_spin.valueChanged.connect(parameter_changed)

        # 字体设置相关控件
        self.font_name_combo.currentIndexChanged.connect(parameter_changed)
        self.font_size_spin.valueChanged.connect(parameter_changed)
        self.letter_spacing_spin.valueChanged.connect(parameter_changed)

        # 间距和纸张设置相关控件
        self.horizontal_spacing_spin.valueChanged.connect(parameter_changed)
        self.vertical_spacing_spin.valueChanged.connect(parameter_changed)
        self.paper_width_spin.valueChanged.connect(parameter_changed)
        self.paper_height_spin.valueChanged.connect(parameter_changed)

        # 颜色选择按钮(通过自定义的select_color方法触发刷新)
        # 修改select_color方法,在颜色选择后调用parameter_changed
        original_select_color = self.select_color
        def select_color_with_refresh(button):
            original_select_color(button)
            parameter_changed()

        # 重新连接颜色选择按钮的信号
        self.box_fill_button.clicked.connect(partial(select_color_with_refresh, self.box_fill_button))
        self.border_color_button.clicked.connect(partial(select_color_with_refresh, self.border_color_button))
        self.line_color_button.clicked.connect(partial(select_color_with_refresh, self.line_color_button))
        self.end_shape_color_button.clicked.connect(partial(select_color_with_refresh, self.end_shape_color_button))
        self.font_color_button.clicked.connect(partial(select_color_with_refresh, self.font_color_button))

    # 调用信号连接方法
    connect_signals()

def select_color(self, button):
    color = QColorDialog.getColor()
    if color.isValid():
        button.setStyleSheet(f"background-color: {color.name()}")

def get_settings(self):
    """获取所有样式设置"""
    # 结合生成方向和阅读方向确定最终的布局方向
    generation_direction = self.generation_direction_combo.currentText()  # 竖式或横式
    reading_direction = self.layout_direction_combo.currentText()  # 正翻或反翻

    # 根据组合确定布局方向值
    if generation_direction == "竖式":
        if reading_direction == "正翻":
            layout_direction_value = "vertical_normal"
        else:
            layout_direction_value = "vertical_reverse"
    else:  # 横式
        if reading_direction == "正翻":
            layout_direction_value = "horizontal_normal"
        else:
            layout_direction_value = "horizontal_reverse"

    # 处理边框样式映射
    border_style_mapping = {
        "实线": "-",
        "虚线": "--",
        "点线": ":",
        "无框": ""
    }
    border_style_value = border_style_mapping.get(self.border_style_combo.currentText(), "-")

    # 处理线条样式映射
    line_style_mapping = {
        "实线": "-",
        "虚线": "--",
        "点线": ":"
    }
    line_style_value = line_style_mapping.get(self.line_style_combo.currentText(), "-")

    # 获取末端图形值
    end_shape_value = self.end_shape_combo.currentText()

    # 处理框样式映射
    box_style_mapping = {
        "直角": "square",
        "圆角": "rounded"
    }
    box_style_value = box_style_mapping.get(self.box_shape_combo.currentText(), "square")

    # 处理文字方向映射
    text_direction_mapping = {
        "横排": "horizontal",
        "竖排": "vertical"
    }
    text_direction_value = text_direction_mapping.get(self.text_direction_combo.currentText(), "horizontal")

    # 当边框样式为圆角时,验证圆角半径是否过大
    box_radius_mm = self.box_radius_spin.value()
    if box_style_value == 'rounded':
        box_width_mm = self.box_width_spin.value()
        box_height_mm = self.box_height_spin.value()
        # 获取宽和高中较小的值
        min_dimension_mm = min(box_width_mm, box_height_mm)
        # 计算最大允许的圆角半径(宽高较小值的一半)
        max_allowed_radius = min_dimension_mm / 2
        # 如果圆角半径过大,显示提示
        if box_radius_mm > max_allowed_radius:
            QMessageBox.warning(self, "圆角半径过大", 
                               f"圆角半径不能大于框宽高较小值的一半\n" 
                               f"当前最大允许值: {max_allowed_radius:.1f} mm")
            # 自动调整圆角半径为最大允许值
            self.box_radius_spin.setValue(max_allowed_radius)
            box_radius_mm = max_allowed_radius

    return {
        "box_fill": self.box_fill_button.styleSheet().split(':')[1].strip(),
        "border_color": self.border_color_button.styleSheet().split(':')[1].strip(),
        "border_width": self.border_width_spin.value(),
        "border_style": border_style_value,
        "box_style": box_style_value,
        "line_color": self.line_color_button.styleSheet().split(':')[1].strip(),
        "line_width": self.line_width_spin.value(),
        "line_style": line_style_value,
        "end_shape": end_shape_value,
        "shape_color": self.end_shape_color_button.styleSheet().split(':')[1].strip(),
        "shape_size": self.end_shape_size_spin.value(),
        "font_name": self.font_name_combo.currentText(),
        "font_size": self.font_size_spin.value(),
        "font_color": self.font_color_button.styleSheet().split(':')[1].strip(),
        "letter_spacing": self.letter_spacing_spin.value(),
        "horizontal_spacing_mm": self.horizontal_spacing_spin.value(),
        "vertical_spacing_mm": self.vertical_spacing_spin.value(),
        "layout_direction": layout_direction_value,
        "text_direction": text_direction_value,
        "paper_width_mm": self.paper_width_spin.value(),
        "paper_height_mm": self.paper_height_spin.value(),
        "box_width_mm": self.box_width_spin.value(),
        "box_height_mm": self.box_height_spin.value(),
        "box_radius_mm": box_radius_mm
    }

class MainWindow(QMainWindow):
def init(self):
super().init()

    # 设置窗口图标
    self.setWindowIcon(QIcon("Icons/LOGO.png"))
    # 设置窗口标题,在图标和文字间添加空格
    self.setWindowTitle(" 江西民众谱志 · 族谱吊线图生成器")
    self.setGeometry(100, 100, 1200, 800)

    self.family_tree = None
    self.current_file = None

    self.setup_ui()

    # 在窗口完全初始化后应用正确的分割器大小
    from PyQt5.QtCore import QTimer
    QTimer.singleShot(200, self._apply_correct_splitter_size)

def setup_ui(self):
    # 创建菜单栏
    self.create_menu_bar()

    # 创建显示中文文字的快捷工具栏
    self.toolbar = self.addToolBar("快捷工具")
    self.toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)  # 文字在图标下方
    self.toolbar.setIconSize(QSize(24, 24))

    # 添加打开按钮
    self.open_tool_btn = QToolButton()
    # 使用自定义图标
    self.open_tool_btn.setIcon(QIcon("Icons/InPut.png"))
    self.open_tool_btn.setText("打开")
    self.open_tool_btn.setToolTip("导入Excel数据 (Ctrl+O)")
    self.open_tool_btn.clicked.connect(self.open_file)
    self.toolbar.addWidget(self.open_tool_btn)

    # 在第二个图标前增加一个分隔竖线
    self.toolbar.addSeparator()

    # 添加导出PDF按钮
    self.export_tool_btn = QToolButton()
    # 使用自定义图标
    self.export_tool_btn.setIcon(QIcon("Icons/PDF.png"))
    self.export_tool_btn.setText("PDF")
    self.export_tool_btn.setToolTip("导出为PDF (Ctrl+P)")
    self.export_tool_btn.clicked.connect(self.export_pdf)
    self.export_tool_btn.setEnabled(False)  # 初始禁用,加载数据后启用
    self.toolbar.addWidget(self.export_tool_btn)

    # 添加导出PNG按钮
    self.export_png_btn = QToolButton()
    # 使用自定义图标
    self.export_png_btn.setIcon(QIcon("Icons/PNG.png"))
    self.export_png_btn.setText("PNG")
    self.export_png_btn.setToolTip("导出为PNG")
    self.export_png_btn.clicked.connect(self.export_png)
    self.export_png_btn.setEnabled(False)  # 初始禁用,加载数据后启用
    self.toolbar.addWidget(self.export_png_btn)

    # 在PNG图标后增加一个JPG功能
    self.export_jpg_btn = QToolButton()
    # 使用自定义图标
    self.export_jpg_btn.setIcon(QIcon("Icons/JPEG.png"))
    self.export_jpg_btn.setText("JPG")
    self.export_jpg_btn.setToolTip("导出为JPG")
    self.export_jpg_btn.clicked.connect(self.export_jpg)
    self.export_jpg_btn.setEnabled(False)  # 初始禁用,加载数据后启用
    self.toolbar.addWidget(self.export_jpg_btn)

    # 添加打印按钮,使用更直观的打印机图标
    self.print_tool_btn = QToolButton()
    # 使用自定义图标
    self.print_tool_btn.setIcon(QIcon("Icons/Print.png"))
    self.print_tool_btn.setText("打印")
    self.print_tool_btn.setToolTip("打印吊线图 (Ctrl+Shift+P)")
    self.print_tool_btn.clicked.connect(self.print_tree)
    self.print_tool_btn.setEnabled(False)  # 初始禁用,加载数据后启用
    self.toolbar.addWidget(self.print_tool_btn)

    # 添加刷新按钮
    self.refresh_tool_btn = QToolButton()
    # 使用自定义图标
    self.refresh_tool_btn.setIcon(QIcon("Icons/刷新.png"))
    self.refresh_tool_btn.setText("刷新")
    self.refresh_tool_btn.setToolTip("应用设置并刷新视图")
    self.refresh_tool_btn.clicked.connect(self.apply_style)
    self.refresh_tool_btn.setEnabled(False)  # 初始禁用,加载数据后启用
    self.toolbar.addWidget(self.refresh_tool_btn)

    # 创建中心部件
    central_widget = QWidget()
    self.setCentralWidget(central_widget)

    main_layout = QHBoxLayout(central_widget)

    # 左侧容器 - 包含面板容器和应用按钮
    left_side_container = QWidget()
    left_side_container.setMinimumWidth(195)
    left_side_container.setMaximumWidth(195)
    left_side_layout = QVBoxLayout(left_side_container)
    left_side_layout.setContentsMargins(0, 0, 0, 0)
    left_side_layout.setSpacing(0)

    # 左侧面板容器 - 只包含样式面板
    left_panel = QWidget()
    left_panel.setMinimumWidth(195)
    left_panel.setMaximumWidth(195)
    left_layout = QVBoxLayout(left_panel)
    left_layout.setContentsMargins(0, 0, 0, 0)
    left_layout.setSpacing(0)

    # 左侧样式面板
    self.style_panel = StylePanel()
    # 设置主窗口引用,使StylePanel能够调用apply_style方法
    self.style_panel.main_window = self
    left_layout.addWidget(self.style_panel)

    # 添加面板容器到左侧容器
    left_side_layout.addWidget(left_panel)

    # 在面板容器和应用按钮之间添加一行行距
    left_side_layout.addSpacing(10)

    # 添加应用按钮到左侧容器的下方(面板容器外部)
    self.apply_btn = QPushButton("应用以上参数刷新")
    self.apply_btn.setFixedHeight(40)
    self.apply_btn.setFixedWidth(190)
    # 使用PyQt5支持的CSS属性实现点击效果
    self.apply_btn.setStyleSheet("""
        QPushButton {
            background-color: #4CAF50;
            color: white;
            border-radius: 8px;
            border: none;
            padding: 5px 10px;
            font-weight: bold;
        }
        QPushButton:pressed {
            background-color: #45a049;
            padding: 6px 9px;
            font-size: 98%;
        }
    """)
    self.apply_btn.clicked.connect(self.apply_style)
    left_side_layout.addWidget(self.apply_btn)     

    # 右侧画布区域
    right_widget = QWidget()
    right_layout = QVBoxLayout(right_widget)

    # 创建滚动区域
    self.scroll_area = QScrollArea()
    self.scroll_area.setWidgetResizable(False)  # 禁用自动调整内部部件大小,让滚动条能正确反映内容大小
    self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
    self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)

    # 画布
    self.canvas = TreeCanvas(self)
    self.scroll_area.setWidget(self.canvas)  # 将画布放入滚动区域
    right_layout.addWidget(self.scroll_area)

    # 使用分割器
    self.splitter = QSplitter(Qt.Horizontal)
    self.splitter.addWidget(left_side_container)
    self.splitter.addWidget(right_widget)
    # 设置初始分割器大小,确保左侧面板宽度约为195px,右侧绘图区域占用剩余空间
    self.splitter.setSizes([195, 1000])

    main_layout.addWidget(self.splitter)

    # 状态栏
    self.statusBar().showMessage("就绪 (1:1)")

    # 确保窗口显示后立即应用正确的分割器大小
    self.showEvent = self.ensure_correct_splitter_size

def create_menu_bar(self):
    """创建菜单栏"""
    # 获取菜单栏
    menubar = self.menuBar()

    # 文件菜单
    file_menu = menubar.addMenu('文件')

    # 打开文件菜单项
    open_action = QAction('导入Excel数据', self)
    open_action.setShortcut('Ctrl+O')
    open_action.triggered.connect(self.open_file)
    file_menu.addAction(open_action)

    # 导出子菜单
    export_menu = file_menu.addMenu('导出')

    # 导出PDF菜单项
    self.export_action = QAction('导出PDF', self)
    self.export_action.setShortcut('Ctrl+P')
    self.export_action.triggered.connect(self.export_pdf)
    self.export_action.setEnabled(False)
    export_menu.addAction(self.export_action)

    # 导出JPG菜单项
    self.export_jpg_action = QAction('导出JPG', self)
    self.export_jpg_action.triggered.connect(self.export_jpg)
    self.export_jpg_action.setEnabled(False)
    export_menu.addAction(self.export_jpg_action)

    # 导出PNG菜单项
    self.export_png_action = QAction('导出PNG', self)
    self.export_png_action.triggered.connect(self.export_png)
    self.export_png_action.setEnabled(False)
    export_menu.addAction(self.export_png_action)

    # 打印菜单项
    self.print_action = QAction('打印', self)
    self.print_action.setShortcut('Ctrl+Shift+P')
    self.print_action.triggered.connect(self.print_tree)
    self.print_action.setEnabled(False)
    file_menu.addAction(self.print_action)

    # 添加分隔线
    file_menu.addSeparator()

    # 退出菜单项
    exit_action = QAction('退出', self)
    exit_action.setShortcut('Ctrl+Q')
    exit_action.triggered.connect(self.close)
    file_menu.addAction(exit_action)

    # 视图菜单
    view_menu = menubar.addMenu('视图')

    # 重置视图菜单项
    self.reset_view_action = QAction('重置视图', self)
    self.reset_view_action.setShortcut('Ctrl+R')
    self.reset_view_action.triggered.connect(self.reset_view)
    self.reset_view_action.setEnabled(False)
    view_menu.addAction(self.reset_view_action)

    # 样式设置菜单项
    style_action = QAction('样式设置', self)
    style_action.setShortcut('Ctrl+S')
    style_action.triggered.connect(self.show_style_dialog)
    view_menu.addAction(style_action)

    # 帮助菜单
    help_menu = menubar.addMenu('帮助')

    # 关于菜单项
    about_action = QAction('关于', self)
    about_action.triggered.connect(self.show_about_dialog)
    help_menu.addAction(about_action)

    # 使用帮助菜单项
    help_action = QAction('使用帮助', self)
    help_action.triggered.connect(self.show_help_dialog)
    help_menu.addAction(help_action)

def open_file(self):
    file_path, _ = QFileDialog.getOpenFileName(
        self, "导入Excel数据", "", "Excel文件 (*.xlsx *.xls)"
    )

    if file_path:
        try:
            df = pd.read_excel(file_path)
            if '子名' not in df.columns or '父名' not in df.columns:
                QMessageBox.warning(self, "错误", "Excel文件中必须包含'子名'和'父名'列")
                return

            self.current_file = file_path
            self.family_tree = FamilyTree()
            self.family_tree.build_tree(df)

            self.canvas.set_tree(self.family_tree)
            # 立即应用UI面板中的默认参数,避免显示问题
            self.apply_style()
            self.export_action.setEnabled(True)
            self.export_jpg_action.setEnabled(True)
            self.export_png_action.setEnabled(True)
            self.print_action.setEnabled(True)  # 启用打印按钮
            self.reset_view_action.setEnabled(True)  # 启用重置视图按钮
            # 启用快捷工具栏按钮
            self.export_tool_btn.setEnabled(True)
            self.export_png_btn.setEnabled(True)
            self.export_jpg_btn.setEnabled(True)
            self.print_tool_btn.setEnabled(True)
            self.refresh_tool_btn.setEnabled(True)
            self.statusBar().showMessage(f"已加载文件: {file_path}")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"读取文件时出错: {str(e)}")

def apply_style(self):
    if not self.family_tree:
        QMessageBox.warning(self, "警告", "请先加载家谱数据")
        return

    try:
        settings = self.style_panel.get_settings()
        self.canvas.set_style(**settings)
    except Exception as e:
        QMessageBox.critical(self, "错误", f"应用样式时出错: {str(e)}")

def reset_view(self):
    """重置视图到原始状态"""
    if self.canvas:
        self.canvas.reset_view()

def export_pdf(self):
    if not self.family_tree:
        QMessageBox.warning(self, "警告", "请先加载家谱数据")
        return

    # 使用数据库文件名作为PDF文件名,保存地址默认为桌面
    if self.current_file:
        file_name = Path(self.current_file).stem + ".pdf"
        desktop_path = str(Path.home() / "Desktop")
        default_path = os.path.join(desktop_path, file_name)
    else:
        default_path = ""

    file_path, _ = QFileDialog.getSaveFileName(
        self, "导出PDF", default_path, "PDF文件 (*.pdf)"
    )

    if file_path:
        try:
            # 获取纸张设置
            settings = self.style_panel.get_settings()
            paper_width = settings["paper_width_mm"]
            paper_height = settings["paper_height_mm"]

            # 计算原始图形边界
            xlim = self.canvas.ax.get_xlim()
            ylim = self.canvas.ax.get_ylim()
            fig_width = xlim[1] - xlim[0]
            fig_height = ylim[1] - ylim[0]

            # 计算目标纸张可用空间(转换为英寸)
            paper_width_inch = paper_width / 25.4
            paper_height_inch = paper_height / 25.4

            # 根据长宽比确定纸张方向和计算缩放比例
            scale = min(paper_width_inch / fig_width, paper_height_inch / fig_height)

            # 直接使用QPixmap抓取当前画布内容并保存为PDF
            pixmap = self.canvas.grab()

            # 使用Qt的打印功能保存为PDF
            pdf_printer = QPrinter(QPrinter.HighResolution)
            pdf_printer.setOutputFormat(QPrinter.PdfFormat)
            pdf_printer.setOutputFileName(file_path)
            pdf_printer.setPageSize(QPrinter.Letter)  # 设置页面大小

            # 设置页面方向
            if paper_width > paper_height:
                pdf_printer.setOrientation(QPrinter.Landscape)
            else:
                pdf_printer.setOrientation(QPrinter.Portrait)

            # 创建绘画对象并绘制pixmap
            painter = QPainter()
            painter.begin(pdf_printer)

            # 计算缩放比例,使图像适合页面
            page_rect = pdf_printer.pageRect()
            scale_factor = min(page_rect.width() / pixmap.width(), page_rect.height() / pixmap.height())

            # 计算居中位置
            x = (page_rect.width() - pixmap.width() * scale_factor) / 2
            y = (page_rect.height() - pixmap.height() * scale_factor) / 2

            # 绘制缩放后的图像
            painter.scale(scale_factor, scale_factor)
            painter.drawPixmap(int(x/scale_factor), int(y/scale_factor), pixmap)
            painter.end()

            self.statusBar().showMessage(f"PDF已导出: {file_path}")
            QMessageBox.information(self, "成功", "PDF导出成功")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"导出PDF时出错: {str(e)}")

def export_jpg(self):
    if not self.family_tree:
        QMessageBox.warning(self, "警告", "请先加载家谱数据")
        return

    # 使用数据库文件名作为JPG文件名,保存地址默认为桌面
    if self.current_file:
        file_name = Path(self.current_file).stem + ".jpg"
        desktop_path = str(Path.home() / "Desktop")
        default_path = os.path.join(desktop_path, file_name)
    else:
        default_path = ""

    file_path, _ = QFileDialog.getSaveFileName(
        self, "导出JPG", default_path, "JPG文件 (*.jpg)"
    )

    if file_path:
        try:
            # 获取纸张设置
            settings = self.style_panel.get_settings()
            paper_width = settings["paper_width_mm"]
            paper_height = settings["paper_height_mm"]

            # 计算原始图形边界
            xlim = self.canvas.ax.get_xlim()
            ylim = self.canvas.ax.get_ylim()
            fig_width = xlim[1] - xlim[0]
            fig_height = ylim[1] - ylim[0]

            # 计算目标纸张可用空间(转换为英寸)
            paper_width_inch = paper_width / 25.4
            paper_height_inch = paper_height / 25.4

            # 根据长宽比确定纸张方向和计算缩放比例
            scale = min(paper_width_inch / fig_width, paper_height_inch / fig_height)

            # 直接使用QPixmap抓取当前画布内容并保存为JPG
            pixmap = self.canvas.grab()

            # 保存为JPG文件,设置高DPI
            pixmap.save(file_path, "JPG", quality=95)

            self.statusBar().showMessage(f"JPG已导出: {file_path}")
            QMessageBox.information(self, "成功", "JPG导出成功")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"导出JPG时出错: {str(e)}")

def export_png(self):
    if not self.family_tree:
        QMessageBox.warning(self, "警告", "请先加载家谱数据")
        return

    # 使用数据库文件名作为PNG文件名,保存地址默认为桌面
    if self.current_file:
        file_name = Path(self.current_file).stem + ".png"
        desktop_path = str(Path.home() / "Desktop")
        default_path = os.path.join(desktop_path, file_name)
    else:
        default_path = ""

    file_path, _ = QFileDialog.getSaveFileName(
        self, "导出PNG", default_path, "PNG文件 (*.png)"
    )

    if file_path:
        try:
            # 保存为透明背景的PNG文件
            # 使用Matplotlib的原生保存功能,设置transparent=True实现透明背景
            self.canvas.figure.savefig(
                file_path, 
                format='png', 
                transparent=True,  # 设置背景透明
                dpi=300,  # 高DPI
                bbox_inches='tight',  # 自动裁剪多余空间
                pad_inches=0.1  # 保留少量边距
            )

            self.statusBar().showMessage(f"PNG已导出: {file_path}")
            QMessageBox.information(self, "成功", "PNG导出成功")
        except Exception as e:
            QMessageBox.critical(self, "错误", f"导出PNG时出错: {str(e)}")

def print_tree(self):
    """打印家谱图 - 改进版:基于整个图形边界而非当前视图进行缩放"""

def show_style_dialog(self):
    """显示样式设置对话框"""
    # 这里可以打开一个独立的样式设置对话框
    # 目前我们只是显示一个提示
    QMessageBox.information(self, '样式设置', '请使用左侧面板进行样式设置。')

def show_about_dialog(self):
    """显示关于对话框"""
    QMessageBox.about(self, '关于', 
                      '江西民众谱志 · 族谱吊线图生成器\n\n'
                      '版本: 1.0\n\n'
                      '功能: 从Excel文件导入家谱数据,生成吊线图,并支持多种导出格式。\n\n'
                      '© 2023 江西民众谱志团队')

def print_tree(self):
    """打印家谱图 - 改进版:基于整个图形边界而非当前视图进行缩放"""
    if not self.family_tree:
        QMessageBox.warning(self, "警告", "请先加载家谱数据")
        return

    try:
        printer = QPrinter()
        print_dialog = QPrintDialog(printer, self)

        # 获取纸张设置
        settings = self.style_panel.get_settings()
        paper_width = settings["paper_width_mm"]
        paper_height = settings["paper_height_mm"]

        # 设置打印机纸张大小
        if paper_width > paper_height:
            # 横向
            printer.setOrientation(QPrinter.Landscape)
        else:
            # 纵向
            printer.setOrientation(QPrinter.Portrait)

        if print_dialog.exec_() == QPrintDialog.Accepted:
            # 保存当前视图状态
            current_zoom = self.canvas.zoom_factor

            # 临时重置视图以获取完整图形边界
            self.canvas.reset_view()

            # 创建打印画布
            painter = QPainter()
            painter.begin(printer)

            # 将matplotlib图形渲染到QPixmap
            pixmap = self.canvas.grab()

            # 获取页面矩形
            page_rect = printer.pageRect()

            # 计算居中位置
            x = (page_rect.width() - pixmap.width()) / 2
            y = (page_rect.height() - pixmap.height()) / 2

            # 绘制图像
            painter.drawPixmap(x, y, pixmap)
            painter.end()

            # 恢复原始视图状态
            self.canvas.zoom_factor = current_zoom
            self.canvas.draw_tree()

            self.statusBar().showMessage("打印完成")
            QMessageBox.information(self, "成功", "打印完成")
    except Exception as e:
        # 确保恢复原始视图状态
        try:
            self.canvas.zoom_factor = current_zoom
            self.canvas.draw_tree()
        except:
            pass
        QMessageBox.critical(self, "错误", f"打印时出错: {str(e)}")

def show_help_dialog(self):
    """显示使用帮助对话框"""
    QMessageBox.information(self, '使用帮助', 
                           '1. 点击  文件 > 导入Excel数据  导入家谱数据\n'
                           '2. 使用左侧面板调整样式设置\n'
                           '3. 点击  文件 > 导出 > PDF/JPG  导出吊线图\n'
                           '4. 点击  视图 > 重置视图  恢复默认视图')

def ensure_correct_splitter_size(self, event):
    """确保窗口显示时应用正确的分割器大小"""
    # 调用原始的showEvent方法
    super().showEvent(event)

    # 延迟一点时间,确保窗口已经完全显示和布局
    from PyQt5.QtCore import QTimer
    QTimer.singleShot(100, self._apply_correct_splitter_size)

def _apply_correct_splitter_size(self):
    """应用正确的分割器大小"""
    # 重新计算总宽度并设置正确的分割器大小
    total_width = self.splitter.width()
    if total_width > 0:
        left_width = 195
        right_width = total_width - left_width
        # 确保右侧至少有一些空间
        if right_width > 0:
            self.splitter.setSizes([left_width, right_width])

def resizeEvent(self, event):
    """重写resizeEvent方法,确保窗口大小变化时左侧参数容器宽度不变"""
    super().resizeEvent(event)

    # 在窗口大小变化时始终保持左侧面板宽度固定,右侧面板填充剩余空间
    # 获取当前分割器的大小
    sizes = self.splitter.sizes()
    total_width = sum(sizes)

    # 保持左侧容器宽度不变(约190px),让右侧绘图区域占用剩余空间
    # 这里使用195px而不是190px是为了考虑边框和间距
    left_width = 195
    right_width = total_width - left_width

    # 确保右侧至少有一些空间
    if right_width > 0:
        self.splitter.setSizes([left_width, right_width])

if name == “main“:
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())

问题:
生成结果中,子节点或子节点组需要与其父节点水平居中对齐,但怎么都对不齐。
多个子节点组发生重叠问题,也一直没有找到解决办法。
请老师指点一下,万分感谢!

讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!