统信系统小程序(七)关系系统

'''
一直梦想制作一个不受制于人,方便保存的家谱软件,永久免费、方便保存。以后,即使这个软件丢失了,也有xls存档可用。
因为没有对应版本,cairo在linux环境下的库改为cairosvg。
Linux系统的字体很难与windows统一,设置了多种字体。
软件开始运行时会自动弹出窗口,用于打开家谱xls文件,如果没有历史软件,关闭窗口即可。
支持成员按代分级别,支持按家族父子、同辈兄弟关系排序,并能导出形成一张A3的彩色png树状图,只要打印机够大,成员关系都能看的清。
支持xls保存,只要保存这个xls文件,软件丢了也能看成员信息。
能自动根据公历生日转换农历生日,根据生日推测属相。能根据农历生日排序,方便看看家族谁快过生日了。
支持备注,记录家族成员个人资料、历史。
能备注配偶子女关系。也可以作为关系图。
'''
import os
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from datetime import datetime
from tkcalendar import DateEntry
from openpyxl import Workbook, load_workbook
from lunarcalendar import Converter, Solar, Lunar

import cairosvg
import math
from PIL import Image, ImageDraw
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties
import networkx as nx
from svgwrite import Drawing
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas

from scipy.cluster import hierarchy


# from graphviz import Digraph
# from openpyxl.LineProperties import Line
class LunarDate:
    """农历日期工具类,用于将公历日期转换为农历日期"""
    # 生肖对应表
    ZODIAC = ["鼠", "牛", "虎", "兔", "龙", "蛇", "马", "羊", "猴", "鸡", "狗", "猪"]

    @staticmethod
    def to_zodiac(date):
        """根据公历日期获取生肖"""
        if not date:
            return ""
        solar = Solar(date.year, date.month, date.day)
        lunar = Converter.Solar2Lunar(solar)
        zodiac_index = (lunar.year - 1876) % 12  # 1900年为鼠年
        return LunarDate.ZODIAC[zodiac_index]

    @staticmethod
    def to_lunar(date):
        """将公历日期转换为农历日期"""
        if not date:
            return ""
        solar = Solar(date.year, date.month, date.day)
        lunar = Converter.Solar2Lunar(solar)
        return f"{lunar.year}年{lunar.month}月{lunar.day}日"


class Member:
    """成员类,用于存储家谱成员的详细信息"""

    def __init__(self, name, gender, generation):
        self.name = name
        self.gender = gender
        self.generation = generation
        self.birth_date = None
        self.death_date = None
        self.default_font = ("fangsong ti", 12)
        self.father = None  # 父亲
        self.mother = None  # 母亲
        self.spouse = ""  # 配偶初始化为空字符串
        self.notes = ""
        self.focus = False  # 关注状态
        # 设置默认打开路径为 /home/huanghe/Desktop/
        self.default_path = "/home/huanghe/Desktop/"

    # 添加属相属性
    def get_zodiac(self):
        """获取生肖"""
        return LunarDate.to_zodiac(self.birth_date)

    # 添加年龄属性
    def get_age(self, current_date_str=None):
        """获取年龄"""
        current_date = datetime.now().date() if current_date_str is None else datetime.strptime(current_date_str,
                                                                                                "%Y-%m-%d").date()
        return self.death_date.year - self.birth_date.year if self.death_date != None else current_date.year - self.birth_date.year

    @property
    def birth_date(self):
        return self._birth_date

    @birth_date.setter
    def birth_date(self, value):
        if value:
            if isinstance(value, str):
                self._birth_date = datetime.strptime(value, "%Y-%m-%d").date()
            elif isinstance(value, datetime):
                self._birth_date = value.date()
            else:
                raise ValueError("birth_date 必须是字符串或 datetime 对象")
        else:
            self._birth_date = None

    @property
    def death_date(self):
        return self._death_date

    @death_date.setter
    def death_date(self, value):
        if value:
            if isinstance(value, str):
                self._death_date = datetime.strptime(value, "%Y-%m-%d").date()
            elif isinstance(value, datetime):
                self._death_date = value.date()
            else:
                raise ValueError("death_date 必须是字符串或 datetime 对象")
        else:
            self._death_date = None

    def get_lunar_birth_date(self):
        """获取农历生日"""
        if not self.birth_date:
            return ""

        try:
            solar = Solar(self.birth_date.year, self.birth_date.month, self.birth_date.day)
            lunar = Converter.Solar2Lunar(solar)
            # solar = Solar(date.year, date.month, date.day)
            # lunar = Converter.Solar2Lunar(solar)
            return f"{lunar.year}年{lunar.month}月{lunar.day}日"
        except Exception as e:

            return "无效日期"

    def get_lunar_death_date(self):
        """获取农历忌日"""
        return LunarDate.to_lunar(self.death_date)

    def get_lunar_month_day(self):
        """获取农历月日 (month, day) 元组"""
        if not self.birth_date:
            return (13, 32)  # 无效日期排最后
        solar = Solar(self.birth_date.year, self.birth_date.month, self.birth_date.day)
        lunar = Converter.Solar2Lunar(solar)
        return (lunar.month, lunar.day)


class FamilyTreeManager:
    """家谱数据管理类"""

    def __init__(self):
        self.members = {}  # 存储所有成员,键为姓名,值为 Member 对象
        self.relationships = set()  # 存储父子关系,例如 ("父亲", "儿子")
        self.spouses = {}  # 存储配偶关系,例如 {("丈夫", "妻子"): 结婚日期}
        self.sorted_members = []  # 新增排序状态存储

    def add_member(self, member):
        """添加成员"""
        if member.name in self.members:
            raise ValueError(f"成员 {member.name} 已存在")
        self.members[member.name] = member

    def delete_member(self, name):
        """删除成员"""
        if name not in self.members:
            raise ValueError(f"成员 {name} 不存在")
        del self.members[name]

    def add_relationship(self, parent, child):
        """添加父子关系"""
        if parent not in self.members or child not in self.members:
            raise ValueError("父或子成员不存在")
        if (parent, child) in self.relationships:
            raise ValueError("关系已存在")
        self.relationships.add((parent, child))

    def delete_relationship(self, parent, child):
        """删除父子关系"""
        if (parent, child) not in self.relationships:
            raise ValueError("关系不存在")
        self.relationships.remove((parent, child))

    def add_spouse(self, spouse1, spouse2):
        """添加配偶关系"""
        if spouse1 not in self.members or spouse2 not in self.members:
            raise ValueError("配偶成员不存在")
        if (spouse1, spouse2) in self.spouses or (spouse2, spouse1) in self.spouses:
            raise ValueError("配偶关系已存在")
        self.spouses[(spouse1, spouse2)] = True
        self.members[spouse1].spouse = spouse2
        self.members[spouse2].spouse = spouse1

    def delete_spouse(self, spouse1, spouse2):
        """删除配偶关系"""
        if (spouse1, spouse2) not in self.spouses and (spouse2, spouse1) not in self.spouses:
            raise ValueError("配偶关系不存在")
        if (spouse1, spouse2) in self.spouses:
            del self.spouses[(spouse1, spouse2)]
        else:
            del self.spouses[(spouse2, spouse1)]
        self.members[spouse1].spouse = None
        self.members[spouse2].spouse = None

    def get_structured_tree(self):
        """获取结构化家谱树(包含配偶和子女)"""
        roots = [m for m in self.members.values() if not m.father and not m.mother]
        trees = []

        def build_family(member, tree):
            # 添加配偶
            if member.spouse:
                spouse = self.members.get(member.spouse)
                if spouse:
                    tree["spouse"] = {
                        "name": spouse.name,
                        "birth": spouse.birth_date,
                        "zodiac": spouse.get_zodiac(),
                        "death": spouse.death_date
                    }

            # 添加子女(按出生日期排序)
            children = []
            for parent, child in self.relationships:
                if parent == member.name:
                    child_member = self.members[child]
                    children.append((child_member.birth_date or datetime.max, child))

            # 按出生日期排序
            for _, child in sorted(children, key=lambda x: x[0]):
                child_tree = {}
                build_family(self.members[child], child_tree)
                tree.setdefault("children", []).append(child_tree)

        for root in roots:
            tree = {"name": root.name, "birth": root.birth_date}
            build_family(root, tree)
            trees.append(tree)

        return trees

    def _build_tree(self, name, tree):
        """递归构建家谱树"""
        member = self.members[name]
        tree[name] = {
            "generation": member.generation,
            "children": [],
            "spouse": member.spouse
        }

        # 添加子女
        for parent, child in self.relationships:
            if parent == name:
                tree[name]["children"].append(child)
                self._build_tree(child, tree)


class FamilyTreeApp(tk.Tk):
    """家谱管理应用程序主界面"""

    # 在FamilyTreeApp类中添加字体设置方法
    def setup_font(self):
        """设置可用字体"""
        import matplotlib.pyplot as plt
        from matplotlib.font_manager import FontManager
        # 直接设置fangsong ti字体,因为它在系统中是可用的
        self.preferred_font = 'fangsong ti'
        # 设置matplotlib字体 - 使用更安全的方式
        plt.rcParams['font.sans-serif'] = ['fangsong ti', 'FangSong', 'SimSun', 'DejaVu Sans', 'sans-serif']
        plt.rcParams['axes.unicode_minus'] = False

        # 获取系统可用字体
        try:
            fm = FontManager()
            available_fonts = {f.name for f in fm.ttflist}

            # 找到第一个可用的中文字体
            self.preferred_font = 'sans-serif'  # 默认字体
            for font in chinese_fonts:
                if font in available_fonts:
                    self.preferred_font = font
                    break
        except:
            # 如果无法获取字体列表,使用默认设置
            self.preferred_font = 'sans-serif'

        # 设置matplotlib字体
        plt.rcParams['font.sans-serif'] = [self.preferred_font]
        plt.rcParams['axes.unicode_minus'] = False
    def __init__(self):
        super().__init__()
        self.title("家谱管理系统")
        self.geometry("1200x800")
        self.manager = FamilyTreeManager()
        self.bind("<Control-f>", lambda e: self.search_member())  # 添加快捷键绑定
        # 设置默认导出路径
        self.default_path = "/home/huanghe/Desktop/"
        # 检测可用字体
        self.setup_font()
        self.bind("<Control-f>", lambda e: self.search_member())  # 添加快捷键绑定

        style = ttk.Style(self)
        style.configure("Treeview", background="#FFFFF0", fieldbackground="#FFFFF0", foreground="Blue",
                        wraplength=500)  # 设置了换行宽度
        style.map("Treeview", background=[("selected", "#347083")])
        # 创建界面布局
        self.create_widgets()
        self.current_members = []  # 新增变量
        self.after(100, self.auto_open_file)  # 延迟100ms执行打开

    def auto_open_file(self):
        """启动自动打开文件"""
        # if messagebox.askyesno("启动选项", "是否要打开现有家谱文件?"):
        self.open_file()

    def create_widgets(self):
        """创建界面控件"""
        # 操作按钮
        button_frame = ttk.Frame(self)
        button_frame.pack(fill=tk.X, padx=5, pady=5)

        # 合并添加、编辑、删除功能的按钮
        ttk.Button(button_frame, text="导出", command=self.export_image).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="保存", command=self.save_file).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="打开", command=self.open_file).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="添加", command=self.add_member).pack(side=tk.LEFT, padx=5)
        # 添加刷新按钮
        ttk.Button(button_frame, text="刷新", command=self.update_member_list).pack(side=tk.LEFT, padx=5)
        # 设置Treeview的字体大小
        style = ttk.Style()
        style.configure("Treeview", font=("fangsong ti", 12))
        style.configure("Treeview.Heading", font=("fangsong ti", 12))
        # 成员列表
        self.tree = ttk.Treeview(
            self,
            columns=(
            "gender", "generation", "birth", "zodiac", "age", "lunar_birth", "father", "mother", "spouse", "notes",
            "focus", "death", "lunar_death"),
            show="tree headings"
        )
        self.tree.heading("gender", text="性别")
        self.tree.heading("generation", text="代数")
        self.tree.heading("birth", text="生日")
        self.tree.heading("zodiac", text="属")  # 新增属相列
        self.tree.heading("age", text="年龄")  # 新增年龄列
        self.tree.heading("lunar_birth", text="农历生日")

        self.tree.heading("father", text="父")
        self.tree.heading("mother", text="母")
        self.tree.heading("spouse", text="配偶")

        self.tree.heading("notes", text="备注", anchor=tk.CENTER)  # 标题左对齐, sticky='w'
        self.tree.heading("focus", text="关注")

        self.tree.heading("death", text="寿数")
        self.tree.heading("lunar_death", text="天年")
        self.tree.bind('<ButtonRelease-1>', self.on_tree_click)  # 增加点击按钮
        self.tree.bind('<Double-1>', self.on_double_click)  # 增加点击按钮
        # 设置列宽
        self.tree.column("#0", width=80, anchor=tk.CENTER)  # 名字列80刚好够用
        for col in self.tree["columns"]:
            # self.tree.column(col, width=500, anchor=tk.CENTER)
            self.tree.column("notes", width=500, anchor=tk.NW, stretch=True)  # 左对齐

        self.tree.pack(fill=tk.BOTH, expand=True)

        # 右键菜单
        self.context_menu = tk.Menu(self, tearoff=0)
        self.context_menu.add_command(label="添加成员", command=self.add_member)
        self.context_menu.add_command(label="编辑成员", command=self.edit_member)
        self.context_menu.add_command(label="删除成员", command=self.delete_member)
        self.context_menu.add_command(label="查找成员", command=self.search_member)  # 新增查找功能
        self.context_menu.add_command(label="排序", command=self.sort_members)
        self.context_menu.add_command(label="显示所有", command=self.show_all_members)
        self.context_menu.add_command(label="仅显示关注", command=self.show_focused_members)
        self.context_menu.add_command(label="家庭树排序", command=self.sort_by_family_tree)
        # 绑定右键事件
        self.tree.bind("<Button-3>", self.show_context_menu)

    # 在FamilyTreeApp类中新增导出选项对话框方法
    def get_export_choice(self):
        """显示导出选项对话框"""
        dialog = tk.Toplevel(self)
        dialog.title("选择导出方式")
        dialog.geometry("300x200")

        choice = tk.StringVar(value="Matplotlib")

        ttk.Label(dialog, text="请选择导出引擎:").pack(pady=10)

        engines = [
            ("Matplotlib (推荐)", "Matplotlib"),
            ("Pillow (快速)", "Pillow"),
            ("PyQt (高质量)", "PyQt"),
            ("SVG (矢量)", "SVG"),
            ("Cairo (专业)", "Cairo")
        ]

        for text, mode in engines:
            ttk.Radiobutton(dialog, text=text, variable=choice, value=mode).pack(anchor=tk.W)

        def confirm():
            # dialog.result = choice.get("Matplotlib")
            dialog.result = choice.get()
            dialog.destroy()

        # 确保使用fangsong ti字体
        import matplotlib.pyplot as plt
        plt.rcParams['font.sans-serif'] = ['fangsong ti', 'FangSong', 'SimSun', 'DejaVu Sans', 'sans-serif']
        plt.rcParams['axes.unicode_minus'] = False
        ttk.Button(dialog, text="确定", command=confirm).pack(pady=10)

        dialog.wait_window()
        return getattr(dialog, 'result', 'Matplotlib')

    # 修改export_image方法
    def export_image(self):
        file_path = filedialog.asksaveasfilename(
            initialdir=self.default_path,  # 设置默认路径
            defaultextension=".png",
            filetypes=[("PNG 文件", "*.png")]
        )
        if not file_path:
            return
        import matplotlib.pyplot as plt
        plt.rcParams['font.sans-serif'] = ['fangsong ti', 'FangSong', 'SimSun', 'DejaVu Sans', 'sans-serif']
        plt.rcParams['axes.unicode_minus'] = False
        exporter = PNGExporter(self.manager)
        try:
            choice = self.get_export_choice()

            if choice == "Pillow":
                exporter.export_pillow(file_path)
            elif choice == "Matplotlib":
                exporter.export_matplotlib(file_path)
            elif choice == "PyQt":
                exporter.export_pyqt(file_path)
            elif choice == "SVG":
                exporter.export_svg(file_path)
            elif choice == "Cairo":
                exporter.export_cairo(file_path)
        except Exception as e:
            messagebox.showerror("错误", f"导出失败: {str(e)}")

    def on_double_click(self, event):
        """双击编辑事件处理"""
        item = self.tree.identify_row(event.y)
        if item:
            self.tree.selection_set(item)
            self.edit_member()
        # 关注按钮点了之后更新,下边还要改变显示方式

    def on_tree_click(self, event):
        region = self.tree.identify_region(event.x, event.y)
        if region != "cell":
            return
        column = self.tree.identify_column(event.x)
        item = self.tree.identify_row(event.y)
        if column == "#11":  # 关注列是第11列
            member_name = self.tree.item(item, "text")
            member = self.manager.members.get(member_name)
            if member:
                member.focus = not member.focus
                self.update_member_list()

    def show_context_menu(self, event):
        """显示右键菜单"""
        item = self.tree.identify_row(event.y)
        if item:
            self.tree.selection_set(item)
        self.context_menu.post(event.x_root, event.y_root)

    def update_member_list(self, members=None):
        """更新成员列表"""
        self.tree.delete(*self.tree.get_children())
        # 清空前先保存当前展开状态
        expanded_items = [self.tree.item(item, "text") for item in self.tree.get_children() if
                          self.tree.item(item, "open")]

        # self.update_idletasks()
        members = members or list(self.manager.members.values())
        self.current_members = members.copy()  # 更新当前显示成员

        # 配置自动换行,最后配置成功的
        self.tree.column("notes", width=500, anchor=tk.NW, stretch=True)
        self.tree.heading("notes", text="备注", anchor=tk.CENTER)
        # 必须在插入数据前配置标签(核心修复)
        self.tree.tag_configure("focused", background="#C1FFC1")  # pink淡绿色
        # self.tree.tag_configure("deceased", background="brown")
        self.tree.tag_configure("deceased", background="#CDC9C9")  # 使用更柔和的棕色,浅灰色
        self.tree.tag_configure("both", background="#FFEFDB")  # 新增叠加样式,肉红色
        # 标记关注人员底色为粉色
        # 添加强制样式刷新(关键补充)注意:这个决定了显示框中的显示内容,不能缺少或者错乱!
        for member in members:
            # 格式化日期数据(关键修复点)
            # birth_date = member.birth_date.strftime("%Y-%m-%d") if member.birth_date else ""
            # death_date = member.death_date.strftime("%Y-%m-%d") if member.death_date else ""
            birth_str = member.birth_date.strftime("%Y-%m-%d") if member.birth_date else ""
            death_str = member.death_date.strftime("%Y-%m-%d") if member.death_date else ""
            lunar_birth = member.get_lunar_birth_date() or ""
            lunar_death = member.get_lunar_death_date() or ""

            values = (
                member.gender,
                member.generation,
                birth_str,  # 使用格式化字符串
                member.get_zodiac(),  # 添加属相数据
                # member.death_date if member.death_date else None,
                member.get_age(),
                lunar_birth,
                # member.get_lunar_birth_date(),
                # member.get_lunar_death_date(),
                member.father if member.father else "",
                member.mother if member.mother else "",
                member.spouse if member.spouse else "",
                member.notes, "☑" if member.focus else "☐",  # 更改为复选框符号
                death_str,
                lunar_death,
                # "是" if member.focus else "否"

            )
            # item = self.tree.insert("", tk.END, text=member.name, values=values)
            # 插入数据时应用标签
            tags = []
            if member.focus and member.death_date:
                tags.append("both")
            elif member.focus:
                tags.append("focused")
            elif member.death_date:
                tags.append("deceased")
            item = self.tree.insert("", tk.END, text=member.name, values=values, tags=tags)
        self.manager.sorted_members = members.copy()  # 更新排序状态
        # 自动调整列宽
        self.after(500, self.auto_resize_columns)

    def auto_resize_columns(self):
        for col in self.tree["columns"]:
            # min_width = min(
            # [self.tree.column(col, "width")] +
            # [tk.font.Font().measure(str(self.tree.set(item, col))) for item in self.tree.get_children()]
            # )
            # self.tree.column(col, width=min_width + 60)  # 增加10像素边距
            max_width = max(
                [tk.font.Font().measure(str(self.tree.heading(col, "text")))] +  # 包含表头宽度
                [tk.font.Font().measure(str(self.tree.set(item, col)))
                 for item in self.tree.get_children()]
            )
            self.tree.column(col, width=min(max_width + 10, 500))  # 设置最大宽度限制

    def sort_members(self):
        """按生日或忌日排序"""

        def on_sort():
            key = sort_key.get()
            reverse = sort_order.get() == "降序"
            only_focused = only_focused_var.get()

            # 获取成员列表
            members = list(self.manager.members.values())
            if only_focused:
                members = [m for m in members if m.focus]

            if key == "农历生日":
                # 按农历生日排序(忽略年份,仅按月和日排序)lunar_birth
                members = sorted(
                    members,
                    key=lambda m: (m.get_lunar_month_day()) if m.birth_date else (13, 32),
                    # if m.lunar_birth else ""  # 将空值排到最后

                    reverse=reverse
                )
            elif key == "birth_date":
                # 按生日排序
                members = sorted(
                    members,
                    key=lambda m: m.birth_date if m.birth_date else datetime.max,
                    reverse=reverse
                )
            elif key == "death_date":
                # 按忌日排序
                members = sorted(
                    [m for m in members if m.death_date],  # 仅过滤有忌日的成员
                    key=lambda m: m.death_date,
                    reverse=reverse
                )

            self.update_member_list(members)
            dialog.destroy()  # 关闭排序对话框

        # 创建排序对话框
        dialog = tk.Toplevel(self)
        dialog.title("排序")
        dialog.grab_set()

        sort_key = tk.StringVar(value="农历生日")
        sort_order = tk.StringVar(value="升序")
        only_focused_var = tk.BooleanVar(value=False)

        ttk.Label(dialog, text="排序依据:").grid(row=0, column=0, padx=5, pady=5)
        ttk.Combobox(dialog, textvariable=sort_key, values=["农历生日", "birth_date", "death_date"],
                     state="readonly").grid(row=0, column=1, padx=5, pady=5)

        ttk.Label(dialog, text="排序顺序:").grid(row=1, column=0, padx=5, pady=5)
        ttk.Combobox(dialog, textvariable=sort_order, values=["升序", "降序"], state="readonly").grid(row=1, column=1,
                                                                                                      padx=5, pady=5)

        ttk.Checkbutton(dialog, text="仅关注", variable=only_focused_var).grid(row=2, columnspan=2, pady=5)
        ttk.Button(dialog, text="确定", command=on_sort).grid(row=3, columnspan=2, pady=10)

    def search_member(self):
        """查找成员"""

        def on_search():
            search_text = search_entry.get().strip().lower()
            if not search_text:
                messagebox.showwarning("提示", "请输入查找内容")
                return

            # 清空之前的查找结果
            for item in self.tree.get_children():
                self.tree.item(item, tags=())

            # 查找包含搜索文本的成员和配偶
            found_items = []
            for item in self.tree.get_children():
                member_name = self.tree.item(item, "text")
                member = self.manager.members.get(member_name)
                if not member:
                    continue

                # 检查姓名或配偶是否包含搜索文本
                # if (search_text in member.name) or (member.spouse and search_text in member.spouse):
                # found_items.append(item)
                # 检查所有相关字段是否包含搜索文本(不区分大小写)
                fields_to_search = [
                    member.name.lower(),
                    member.father.lower() if member.father else "",
                    member.mother.lower() if member.mother else "",
                    member.spouse.lower() if member.spouse else "",
                    member.notes.lower() if member.notes else ""
                ]
                if any(search_text in field for field in fields_to_search):
                    found_items.append(item)  # 这句话很重要,有了他能查找备注
            # 临时改变找到的成员的显示状态
            for item in found_items:
                self.tree.item(item, tags=("found",))
            self.tree.tag_configure("found", background="yellow")  # 设置找到的成员背景色

            if not found_items:
                messagebox.showinfo("提示", "未找到匹配的成员")
            dialog.destroy()

        # 创建查找对话框
        dialog = tk.Toplevel(self)
        dialog.title("查找成员")
        dialog.grab_set()

        ttk.Label(dialog, text="姓名、父母、配偶、备注:").grid(row=0, column=0, padx=5, pady=5)
        search_entry = ttk.Entry(dialog)
        search_entry.grid(row=0, column=1, padx=5, pady=5)

        ttk.Button(dialog, text="查找", command=on_search).grid(row=1, columnspan=2, pady=10)
        search_entry.focus_set()

    # 确保所有数据修改操作后调用update_member_list,例如:
    def add_spouse_relationship(self, spouse1, spouse2):
        try:
            self.manager.add_spouse(spouse1, spouse2)
            self.update_member_list()  # 添加后刷新列表
        except ValueError as e:
            messagebox.showerror("错误", str(e))

    def show_focused_members(self):
        """显示关注人员"""
        focused_members = [m for m in self.manager.members.values() if m.focus]
        # 获取排序状态
        current_sort_column = self.tree.heading("lunar_birth")["text"]  # 假设按照生日
        reverse = False  # 默认升序
        # 根据当前排序状态进行排序
        if current_sort_column == "农历生日":
            focused_members = sorted(
                focused_members,
                key=lambda m: (m.lunar_birth.month, m.lunar_birth.day) if m.death_date else datetime.min,
                reverse=False
            )
        elif current_sort_column == "生日":
            focused_members = sorted(
                focused_members,
                key=lambda m: (m.birth_date.month, m.birth_date.day) if m.birth_date else datetime.min,
                reverse=reverse
            )
        # 其他排序条件可以根据需要添加
        self.update_member_list(focused_members)
        foc = sorted(
            # self.manager.focused_members.values(),
            key=lambda m: (m.birth_date.month, m.birth_date.day) if m.birth_date else datetime.min
        )
        self.update_member_list(focused_members)

    def show_all_members(self):
        """显示所有人员信息,并按生日从早到晚排序"""
        members = sorted(
            self.manager.members.values(),
            key=lambda m: m.birth_date if m.birth_date else datetime.min
        )
        self.update_member_list(members)

    def sort_by_family_tree(self):
        """按家庭树结构排序(递归排序)"""
        members = list(self.manager.members.values())
        processed = set()
        family_trees = []

        def build_family_tree(member):
            if member.name in processed:
                return None
            processed.add(member.name)
            tree = {
                "member": member,
                "spouse": None,
                "children": []
            }

            # 添加配偶
            if member.spouse:
                spouse = self.manager.members.get(member.spouse)
                if spouse and spouse.name not in processed:
                    processed.add(spouse.name)
                    tree["spouse"] = spouse

            # 找到所有子女
            for m in members:
                if (m.father == member.name or m.mother == member.name) and m.name not in processed:
                    child_tree = build_family_tree(m)
                    if child_tree:
                        tree["children"].append(child_tree)
                    print(child_tree)
            # 按子女的出生日排序
            tree["children"].sort(key=lambda x: x["member"].birth_date if x["member"].birth_date else "")
            return tree

        # 构建根家庭树
        for member in members:
            if member.name not in processed:
                tree = build_family_tree(member)
                if tree:
                    family_trees.append(tree)

        # 遍历树结构,生成有序成员列表
        sorted_members = []

        def traverse_tree(tree_node):
            if not tree_node:
                return
            sorted_members.append(tree_node["member"])
            # 添加配偶
            if tree_node["spouse"]:
                sorted_members.append(tree_node["spouse"])
            # 递归处理子女
            for child in tree_node["children"]:
                traverse_tree(child)

        for family_tree in family_trees:
            traverse_tree(family_tree)
            # print('========================')

        self.manager.sorted_members = sorted_members
        self.update_member_list(sorted_members)

    def add_member(self):
        """添加成员"""
        dialog = tk.Toplevel(self)
        dialog.title("添加成员")
        dialog.grab_set()

        # 控件变量
        entries = {
            'name': ttk.Entry(dialog),
            # 'gender': tk.StringVar(value="男"),  # 使用 StringVar 存储性别
            'gender': ttk.Combobox(dialog, values=["男", "女"], state="readonly"),
            'generation': ttk.Spinbox(dialog, from_=1, to=20),
            'birth': DateEntry(dialog, date_pattern='yyyy-mm-dd'),
            'death': DateEntry(dialog, date_pattern='yyyy-mm-dd'),
            # 'father': ttk.Combobox(dialog, values=list(self.manager.members.keys()), state="readonly"),
            # 'mother': ttk.Combobox(dialog, values=list(self.manager.members.keys()), state="readonly"),
            'father': ttk.Combobox(dialog, state="readonly"),
            'mother': ttk.Combobox(dialog, state="readonly"),
            'spouse': ttk.Entry(dialog),
            'notes': ttk.Entry(dialog),
            'focus': tk.BooleanVar()
        }
        # entries['gender'].current(0)
        entries['generation'].set(13)  # 设置默认代数为1
        entries['birth'].set_date(datetime.now().date())
        # 设置忌日默认值为3000年5月5日
        default_death_date_str = '3000-05-05'
        entries['death'].set_date(default_death_date_str)

        # entries['gender'].grid(row=1, column=1)
        # 以下改变男女输入方式
        gender_frame = ttk.Frame(dialog)
        gender_var = tk.StringVar(value="男")
        ttk.Radiobutton(gender_frame, text="男", variable=gender_var, value="男").pack(side=tk.LEFT)
        ttk.Radiobutton(gender_frame, text="女", variable=gender_var, value="女").pack(side=tk.LEFT)

        # 布局
        ttk.Label(dialog, text="姓  名:*").grid(row=0, column=0, padx=5, pady=2)
        entries['name'].grid(row=0, column=1, columnspan=2, padx=5, pady=2, sticky='w')

        ttk.Label(dialog, text="性  别:").grid(row=1, column=0)
        entries['gender'] = gender_var
        gender_frame.grid(row=1, column=1, columnspan=2, sticky='w')  # 空白是居中,w是左对齐,

        ttk.Label(dialog, text="代  数:").grid(row=2, column=0)
        entries['generation'].grid(row=2, column=1, columnspan=2, padx=10, pady=10, sticky='w')

        ttk.Label(dialog, text="生日:").grid(row=3, column=0)
        entries['birth'].grid(row=3, column=1, columnspan=2, padx=10, pady=10, sticky='w')

        ttk.Label(dialog, text="忌日:").grid(row=4, column=0)
        entries['death'].grid(row=4, column=1, columnspan=2, padx=10, pady=10, sticky='w')

        ttk.Label(dialog, text="父  亲:").grid(row=5, column=0)
        entries['father'].grid(row=5, column=1, columnspan=2, padx=10, pady=10, sticky='w')

        ttk.Label(dialog, text="母  亲:").grid(row=6, column=0)
        entries['mother'].grid(row=6, column=1, columnspan=2, padx=10, pady=10, sticky='w')

        ttk.Label(dialog, text="配偶:").grid(row=7, column=0)
        entries['spouse'].grid(row=7, column=1, columnspan=2, padx=10, pady=10, sticky='w')

        ttk.Label(dialog, text="备  注:").grid(row=8, column=0)
        entries['notes'].grid(row=8, column=1, columnspan=2, rowspan=2, padx=10, pady=10, sticky='w')

        ttk.Checkbutton(dialog, text="重点关注", variable=entries['focus']).grid(row=10, columnspan=2, sticky='')

        # 更新父母候选列表
        def update_parent_candidates():
            current_gen = int(entries['generation'].get())
            father_candidates = [name for name, m in self.manager.members.items() if
                                 m.generation == current_gen - 1 and m.gender == "男"]
            mother_candidates = [name for name, m in self.manager.members.items() if
                                 m.generation == current_gen - 1 and m.gender == "女"]
            fatherspouse_candidates = [s.spouse for s, s in self.manager.members.items() if
                                       s.generation == current_gen - 1 and s.gender == "女"]
            motherspouse_candidates = [s.spouse for s, s in self.manager.members.items() if
                                       s.generation == current_gen - 1 and s.gender == "男"]
            entries['father']['values'] = fatherspouse_candidates + father_candidates
            entries['mother']['values'] = motherspouse_candidates + mother_candidates

            entries['gender'].trace_add('write', lambda *args: update_parent_candidates())
            # update_parent_candidates()  # 初始更新

        update_parent_candidates()  # 初始更新
        # 绑定代数值变化时的回调函数
        entries['generation'].configure(command=update_parent_candidates)

        def save():
            try:
                # 验证必填字段
                name = entries['name'].get().strip()
                if not name:
                    raise ValueError("姓名不能为空")

                # 验证代数
                try:
                    generation = int(entries['generation'].get())
                except ValueError:
                    raise ValueError("代数必须为有效整数")

                # 处理忌日
                death_date_str = entries['death'].get()
                death_date = None if death_date_str == default_death_date_str else death_date_str

                # 创建成员对象
                member = Member(
                    name=name,
                    gender=entries['gender'].get(),
                    generation=generation
                )
                member.birth_date = entries['birth'].get()
                member.death_date = entries['death'].get() if entries['death'].get() != '3000-05-05' else None
                member.father = entries['father'].get() or None
                member.mother = entries['mother'].get() or None
                member.spouse = entries['spouse'].get()
                member.notes = entries['notes'].get()
                member.focus = entries['focus'].get()

                # 验证父母是否存在
                # if member.father not in self.manager.members and member.father not in self.manager.members.items().spouse:
                # raise ValueError(f"父亲 '{member.father}' 不存在")
                # if member.mother not in self.manager.members and member.mother not in self.manager.members.items().spouse:
                # raise ValueError(f"母亲 '{member.mother}' 不存在")

                self.manager.add_member(member)
                self.update_member_list()
                dialog.destroy()
                # messagebox.showinfo("成功", "成员添加成功!")
            except Exception as e:
                messagebox.showerror("错误", f"添加失败:{str(e)}")

        ttk.Button(dialog, text="保 存", command=save).grid(row=11, column=1, columnspan=1, padx=10, pady=10,
                                                            sticky='w')  # 空白是居中,w是左对齐,
        # dialog.bind("<Return>", lambda e: save())
        # 添加对话框内容区域的弹性布局(关键优化)
        dialog.grid_columnconfigure(1, weight=1)
        for i in range(12):  # 为所有行配置弹性
            dialog.grid_rowconfigure(i, weight=1)

    def edit_member(self):
        """编辑成员"""
        selected = self.tree.selection()
        if not selected:
            messagebox.showwarning("提示", "请先选择要编辑的成员")
            return

        name = self.tree.item(selected[0], 'text')
        member = self.manager.members.get(name)
        if not member:
            return

        dialog = tk.Toplevel(self)
        dialog.title(f"编辑成员 - {name}")
        dialog.grab_set()

        # 控件变量
        entries = {
            'name': ttk.Entry(dialog),
            # 'gender': ttk.Combobox(dialog, values=["男", "女"], state="readonly"),#这是下拉菜单方式,以下改为点选
            # 'gender': tk.StringVar(value=member.gender),  # 使用 StringVar 存储性别
            # 'gender': ttk.Combobox(dialog, values=member.gender),#这是下拉菜单方式,以下改为点选
            # 填充数据时无需额外设置性别(已通过 StringVar 绑定)
            'generation': ttk.Spinbox(dialog, from_=1, to=10),
            'birth': DateEntry(dialog, date_pattern='yyyy-mm-dd'),
            'death': DateEntry(dialog, date_pattern='yyyy-mm-dd'),
            'father': ttk.Combobox(dialog, values=list(self.manager.members.keys()), state="readonly"),
            'mother': ttk.Combobox(dialog, values=list(self.manager.members.keys()), state="readonly"),
            'spouse': ttk.Entry(dialog),
            'notes': ttk.Entry(dialog),
            'focus': tk.BooleanVar()
        }
        # 性别选择
        # 以下改变男女输入方式
        gender_frame = ttk.Frame(dialog)
        gender_var = tk.StringVar(value=member.gender)
        ttk.Radiobutton(gender_frame, text="男", variable=gender_var, value="男").pack(side=tk.LEFT)
        ttk.Radiobutton(gender_frame, text="女", variable=gender_var, value="女").pack(side=tk.LEFT)
        # entries['gender'] = member.gender
        # entries['gender'].current(0)
        # 填充现有数据
        entries['name'].insert(0, member.name)
        # entries['gender'].set(member.gender)#改为点选后顺便省去了填充
        entries['gender'] = gender_var  #
        entries['generation'].set(member.generation)
        entries['birth'].set_date(member.birth_date)
        entries['death'].set_date(
            f'3000-05-05' if member.death_date == None else member.death_date)  # 填充忌日时如果为空,则3000,不为空则输入实际日期
        # entries['death'].set_date(member.death_date if member.death_date else '3000-05-05'另一种存储忌日方式,有空试试
        entries['father'].set(member.father if member.father else "")
        entries['mother'].set(member.mother if member.mother else "")
        entries['spouse'].insert(0, member.spouse if member.spouse else "")
        entries['notes'].insert(0, member.notes if member.notes else "")
        entries['focus'].set(member.focus)

        # 'gender': ttk.Combobox(dialog, values=["男", "女"], state="readonly"),

        # 布局
        ttk.Label(dialog, text="姓  名:*").grid(row=0, column=0, padx=5, pady=2)
        entries['name'].grid(row=0, column=1, padx=5, pady=2)

        ttk.Label(dialog, text="性  别:").grid(row=1, column=0)
        # entries['gender'].grid(row=1, column=1)
        gender_frame.grid(row=1, column=1)

        ttk.Label(dialog, text="代  数:").grid(row=2, column=0)
        entries['generation'].grid(row=2, column=1)

        ttk.Label(dialog, text="生日:").grid(row=3, column=0)
        entries['birth'].grid(row=3, column=1)

        ttk.Label(dialog, text="忌日:").grid(row=4, column=0)
        entries['death'].grid(row=4, column=1)

        ttk.Label(dialog, text="父  亲:").grid(row=5, column=0)
        entries['father'].grid(row=5, column=1)

        ttk.Label(dialog, text="母  亲:").grid(row=6, column=0)
        entries['mother'].grid(row=6, column=1)

        ttk.Label(dialog, text="配偶:").grid(row=7, column=0)
        entries['spouse'].grid(row=7, column=1)

        ttk.Label(dialog, text="备  注:").grid(row=8, column=0)
        entries['notes'].grid(row=8, column=1)

        ttk.Checkbutton(dialog, text="重点关注", variable=entries['focus']).grid(row=9, columnspan=2)

        # 更新父母候选列表
        def update_parent_candidates():
            current_gen = int(entries['generation'].get())
            father_candidates = [name for name, m in self.manager.members.items() if
                                 m.generation == current_gen - 1 and m.gender == "男"]
            mother_candidates = [name for name, m in self.manager.members.items() if
                                 m.generation == current_gen - 1 and m.gender == "女"]
            fatherspouse_candidates = [s.spouse for s, s in self.manager.members.items() if
                                       s.generation == current_gen - 1 and s.gender == "女"]
            motherspouse_candidates = [s.spouse for s, s in self.manager.members.items() if
                                       s.generation == current_gen - 1 and s.gender == "男"]
            entries['father']['values'] = father_candidates + fatherspouse_candidates + [""]
            entries['mother']['values'] = motherspouse_candidates + mother_candidates + [""]

            entries['gender'].trace_add('write', lambda *args: update_parent_candidates())
            # update_parent_candidates()  # 初始更新

        update_parent_candidates()  # 初始更新
        # 绑定代数值变化时的回调函数
        entries['generation'].configure(command=update_parent_candidates)

        def save():
            try:

                member.name = entries['name'].get().strip()
                member.gender = gender_var.get()
                member.generation = int(entries['generation'].get())
                member.birth_date = entries['birth'].get() or None
                member.death_date = entries['death'].get() if entries['death'].get() != '3000-05-05' else None
                # entries['death'].set_date(default_death_date_str)
                # member.death_date = entries['death'].get() if entries['death'].get() != '3000-05-05' else None另一种
                member.father = entries['father'].get() or None  # if entries['father'].get()
                member.mother = entries['mother'].get() or None
                member.spouse = entries['spouse'].get() or None
                member.notes = entries['notes'].get() or None
                member.focus = entries['focus'].get()
                self.update_member_list()
                dialog.destroy()

                # messagebox.showinfo("成功", "修改已保存!")
            except Exception as e:
                messagebox.showerror("错误", f"保存失败:{str(e)}")

        ttk.Button(dialog, text="保 存", command=save).grid(row=11, columnspan=2, pady=10)

    def delete_member(self):
        """删除成员"""
        selected = self.tree.selection()
        if not selected:
            messagebox.showwarning("提示", "请先选择要删除的成员")
            return

        name = self.tree.item(selected[0], 'text')
        if not messagebox.askyesno("确认删除", f"确定要永久删除 {name} 吗?\n此操作不可恢复!"):
            return

        try:
            self.manager.delete_member(name)
            self.update_member_list()
            messagebox.showinfo("成功", f"{name} 已删除")
        except Exception as e:
            messagebox.showerror("错误", f"删除失败:{str(e)}")

    def export_structure(self):
        """导出家庭树结构为excel图形"""
        file_path = filedialog.asksaveasfilename(
            defaultextension=".xlsx",
            filetypes=[("Excel 文件", "*.xlsx"), ("CSV 文件", "*.csv")]
        )
        if not file_path:
            return

        try:
            exporter = FamilyTreeExporter(self.manager)
            exporter.export_to_excel(file_path)
            messagebox.showinfo("成功", "家谱结构图导出完成!")
        except Exception as e:
            messagebox.showerror("错误", f"导出失败:{str(e)}")

    def save_file(self):
        """保存文件"""

        file_path = filedialog.asksaveasfilename(
            initialdir=self.default_path,  # 设置默认路径
            defaultextension=".xlsx",
            filetypes=[("Excel 文件", "*.xlsx")]
        )
        if not file_path:
            return
        try:
            wb = Workbook()
            ws = wb.active
            ws.append(["姓名", "性别", "代数", "生日", "忌日", "父亲", "母亲", "配偶", "备注", "重点关注"])

            # for member in self.manager.members.values():
            # ws.append([
            # member.name,
            # member.gender,
            # member.generation,
            # member.birth_date,
            # member.death_date if member.death_date!='3000-05-05' else None,
            # member.father if member.father else "",
            # member.mother if member.mother else "",
            # member.spouse if member.spouse else "",
            # member.notes if member.notes else "",
            # "☑" if member.focus else "☐"
            # ])
            for member in self.manager.sorted_members:  # current_members:
                ws.append([
                    member.name,
                    member.gender,
                    member.generation,
                    member.birth_date.strftime("%Y-%m-%d") if member.birth_date else "",
                    member.death_date.strftime("%Y-%m-%d") if member.death_date else "",
                    member.father if member.father else "",
                    member.mother if member.mother else "",
                    member.spouse if member.spouse else "",
                    member.notes if member.notes else "",
                    "☑" if member.focus else "☐"
                ])

            wb.save(file_path)

            # messagebox.showinfo("成功", "文件保存成功!")
        except Exception as e:
            messagebox.showerror("错误", f"保存失败:{str(e)}")

    def open_file(self):
        """打开文件"""
        # 使用默认路径而不是当前文件目录
        file_path = filedialog.askopenfilename(
            initialdir=self.default_path,  # 使用桌面路径
            filetypes=[("Excel 文件", "*.xlsx")]
        )
        if not file_path:
            return
        try:
            wb = load_workbook(file_path)
            ws = wb.active
            self.manager.members.clear()
            for row in ws.iter_rows(min_row=2, values_only=True):
                # 检查行数据是否完整
                if len(row) < 10:  # 确保每一行至少有 10 列数据
                    continue  # 跳过不完整的行
                member = Member(row[0], row[1], row[2])
                member.birth_date = row[3] if row[3] else None
                member.death_date = row[4] if row[4] else None
                member.father = row[5] if row[5] else None
                member.mother = row[6] if row[6] else None
                member.spouse = row[7] if row[7] else None
                member.notes = row[8] if row[8] else ""
                member.focus = row[9] == "☑" if row[9] else False
                self.manager.members[member.name] = member
            self.update_member_list()
            # messagebox.showinfo("成功", "文件加载成功!")
            self.tree.update_idletasks()  # 强制GUI刷新
        except Exception as e:
            messagebox.showerror("错误", f"加载失败:{str(e)}")


# 新增导出器类


# 新增 Graphviz 导出类,通过按钮选择如下三种导出方式的类
# class GraphvizExporter:
# class MatplotlibExporter:
# class PillowExporter:
class PNGExporter:
    def __init__(self, manager):
        self.manager = manager
        self.A3_size = (4960, 3508)  # @300dpi
        self.colors = {
            'male': '#6FA8DC',
            'female': '#FF9999',
            'spouse': '#FF6666',
            'parent': '#8FCE00',
            'text': '#2C3E50',
            'bg': '#FFFFFF'
        }
        # 不再依赖app.preferred_font,使用默认字体设置
        self.default_font = ("sans-serif", 12)
        # 通用格式化方法
    def _format_member(self, member):
        # 处理日期格式
        def fmt(d):
            if not d: return ""
            return f"{(d.year % 100):02}{d.month}{d.day}"

        text = [
            f"{member.name}",
            # f"{member.birth_date.strftime('%Y%m%d')}",#
            fmt(member.birth_date),
            f"{member.get_zodiac()}"  # 属
        ]
        if member.death_date:
            text.append(fmt(member.death_date))
        if member.spouse:
            text.append(f" {member.spouse}")
        return "\n".join(text)

    # 方案2:Matplotlib实现
    def export_matplotlib(self, filename):
        """改进的Matplotlib导出方法"""
        import matplotlib.pyplot as plt
        from matplotlib.font_manager import FontProperties
        # 直接使用fangsong ti字体
        plt.rcParams['font.sans-serif'] = ['fangsong ti', 'FangSong', 'SimSun', 'DejaVu Sans', 'sans-serif']
        plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题
        plt.figure(figsize=(16.53, 11.69), dpi=600)  # A3横向尺寸
        G, pos = self._common_layout()
        # 以下取自网络
        # Z = hierarchy.linkage(pos.items(),'ward')
        plt.title('关系树')
        plt.ylabel('代')
        # hierrarchy.dendrogram(Z,labels=df.index,lef_rotation=90)
        # 颜色配置
        # COLOR_SCHEME = {
        # 'male': ['#6FA8DC', '#4169E1', '#00BFFF'],
        # 'female': ['#FFB6C1', '#FF69B4', '#DB7093'],
        # 'dead': '#808080',
        # 'lines': {
        # 'spouse': '#FF4500',
        # 'parent': '#228B22'
        # }
        # }
        # # 方形节点参数
        # node_size = 8000  # 调整节点大小
        # font_size = 1

        # 在export_matplotlib的绘图部分添加:

        # for u, v, data in G.edges(data=True):
        # x1, y1 = pos[u]
        # x2, y2 = pos[v]
        # if data['relation'] == 'parent':
        # plt.plot([x1, x2], [y1, y2], color='green', linestyle='--', linewidth=1,zorder=1)

        # elif data['relation'] == 'spouse':
        # plt.plot([x1, x2], [y1, y2], color='red', linestyle='--', linewidth=1)
        # 设置矩形尺寸
        rect_height = 80  # 高度
        rect_width = rect_height * 5  # 宽度2:1

        # 绘制方形节点
        for node, (x, y) in pos.items():
            member = self.manager.members[node]
            color = '#6FA8DC' if member.gender == '男' else '#FFB6C1'
            if member.death_date: color = '#808080'
            # 绘制矩形
            rect = plt.Rectangle((x - rect_width / 2, y - rect_height / 2),
                                 rect_width, rect_height,
                                 facecolor=color, edgecolor='black', lw=0.8, zorder=2)
            plt.gca().add_patch(rect)
            # # 绘制方形
            # plt.scatter(x, y,
            # marker='s',  # 关键修正点:使用方形标记
            # s=node_size,
            # c=color,
            # edgecolors='black',
            # zorder=2)

            # # 添加文字
            # text = f"{member.name}\n{member.birth_date.year}"
            # if member.death_date:
            # text += f"-{member.death_date.year}"
            # plt.text(x, y, text,
            # ha='center', va='center',
            # fontsize=font_size,
            # fontproperties=FontProperties(fname='fangsong ti.ttf'))
            # 绘制双节点配偶框
            if member.spouse and member.spouse in pos:
                spouse_x, spouse_y = pos[member.spouse]
                if spouse_x:  # 仅处理一次spouse_x > x
                    plt.plot([x, spouse_x], [y, spouse_y],  # 添加配偶连接线
                             color='#FF6666',  # 红色连线
                             linewidth=20,
                             linestyle='-',  # 实线
                             zorder=0)  # zorder是显示层数

            if member.father and member.father in pos:
                father_x, father_y = pos[member.father]
                if father_x:  # 仅处理一次
                    plt.plot([x, father_x], [y + 40, father_y - 40],  # 添加连接线
                             color='#000000',  # 1E90FF
                             linewidth=1,
                             zorder=0)  # zorder是显示层数
            if member.mother and member.mother in pos:
                mother_x, mother_y = pos[member.mother]
                if mother_x:  # 仅处理一次
                    plt.plot([x, mother_x], [y + 40, mother_y - 40],  # 添加连接线
                             color='#FF6666',
                             linewidth=1,
                             zorder=0)  # zorder是显示层数

                    # 以下这段打开就是正方形,关闭就是矩形
            # plt.scatter(x, y,
            # marker='s',
            # s=800,#5000减少,越少框越小
            # edgecolor='black',
            # facecolor=color,
            # zorder=2)#zorder是显示层数

            # 中文文本渲染
            text = self._format_member(member)
            plt.text(x, y,
                     self._format_member(member),
                     ha='center',
                     va='center',
                     fontsize=12,
                     fontfamily=self.manager.app.preferred_font if hasattr(self.manager, 'app') else 'sans-serif')

            #fontproperties=FontProperties(fname='C:\Windows\Fonts\fangsong ti.ttf'))
        # 先绘制所有连线,这个就是配偶之间的黑线来源。打开就有黑线
        # for u, v, data in G.edges(data=True):
        # x1, y1 = pos[u]
        # x2, y2 = pos[v]
        # plt.plot([x1, x2], [y1, y2], 'k-', lw=1)

        # plt.xlim(0, self.A3_size[0])
        # plt.ylim(0, self.A3_size[1])
        plt.axis('off')
        plt.savefig(filename, bbox_inches='tight')
        plt.close()

    def _common_layout(self):
        """改进的通用布局算法"""
        try:
            G = nx.DiGraph()
            # positions = {}
            pos = {}
            roots = [name for name, m in self.manager.members.items() if not m.father and not m.mother]
            layer_height = self.A3_size[1] // 12  # 分10层
            # 构建网络图结构(增加错误处理)
            for name, member in self.manager.members.items():
                G.add_node(name, generation=member.generation)  #
                if member.spouse and member.spouse in self.manager.members:
                    G.add_edge(name, member.spouse, relation='spouse')
            # 父子关系
            for parent, child in self.manager.relationships:
                G.add_edge(name, child, relation='parent')

            # 层次布局核心算法改进
            # 按代数分层布局
            generations = {}
            for node in G.nodes():
                gen = self.manager.members[node].generation
                generations.setdefault(gen, []).append(node)

            if not generations:
                return G, pos

            max_gen = max(generations.keys())
            layer_height = self.A3_size[1] / (max_gen + 2)  # 原为//12,现改为动态分母
            x_spacing = self.A3_size[0] / 10  # 节点间横向间距
            # 每代水平间距基数
            base_x_spacing = self.A3_size[0] / 10  # 基准间距
            # 初始化各代节点位置
            for gen in generations:
                y = self.A3_size[1] - gen * layer_height
                nodes = generations[gen]
                num_nodes = len(nodes)
                start_x = (self.A3_size[0] - (num_nodes - 1) * x_spacing) / 2
                for i, node in enumerate(nodes):
                    pos[node] = (start_x + i * x_spacing, y)

            # 调整子节点到父节点下方居中
            for _ in range(3):  # 多次迭代优化
                for parent in G.nodes():
                    children = [child for (p, child) in self.manager.relationships if p == parent]
                    if not children:
                        continue

                    # 计算父节点位置
                    if parent not in pos:
                        continue
                    px, py = pos[parent]

                    # 计算子节点理想位置(父下方居中)
                    child_y = py - layer_height
                    child_x_start = px - (len(children) - 1) * x_spacing / 2

                    # 更新子节点位置
                    for i, child in enumerate(children):
                        ideal_x = child_x_start + i * x_spacing
                        if child not in pos:
                            pos[child] = (ideal_x, child_y)
                        else:
                            # 平滑移动至目标位置
                            curr_x, curr_y = pos[child]
                            new_x = curr_x + (ideal_x - curr_x) * 0.3
                            pos[child] = (new_x, child_y)

            # return G, pos
            # 从最顶层开始布局
            for gen in sorted(generations.keys()):
                y = self.A3_size[1] - gen * layer_height
                nodes = generations[gen]

                # 按父节点分组子节点
                parent_children = {}
                for node in nodes:
                    children = [child for _, child in self.manager.relationships if _ == node]
                    if children:
                        parent_children[node] = children

                # 分配父节点位置(居中)
                x_step = self.A3_size[0] / (len(nodes) + 1)
                x_positions = {}
                for i, node in enumerate(nodes):
                    x = (i + 1) * x_step
                    x_positions[node] = x
                    pos[node] = (x, y)

                # 计算父节点位置并分配子节点到其下方居中
                for parent, children in parent_children.items():
                    if parent not in pos:
                        continue
                    px, py = pos[parent]
                    child_y = py - layer_height  # 子节点层y坐标
                    child_x_start = px - (len(children) - 1) * x_spacing / 2  # 子节点起始x坐标
                    for i, child in enumerate(children):
                        pos[child] = (child_x_start + i * x_spacing, child_y)

                return G, pos
        except Exception as e:
            print(f"布局错误: {str(e)}")
            return G, {}


if __name__ == "__main__":
    app = FamilyTreeApp()
    app.mainloop()


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值