553429363 发表于 2024-11-13 15:06

新鲜出炉的用python写的族谱软件-第三版

本帖最后由 553429363 于 2024-11-14 10:29 编辑

废话不多说,先上图,上成品,最后上源码!



我用夸克网盘分享了「FamilyTreeAppV3.exe」,点击链接即可保存。打开「夸克APP」,无需下载在线播放视频,畅享原画5倍速,支持电视投屏。
链接:https://pan.quark.cn/s/5ed9f7a15b61

FamilyTreeAppV3.exe https://www.alipan.com/s/RAg797FnAiB 点击链接保存,或者复制本段内容,打开「阿里云盘」APP ,无需下载极速在线查看,视频原画倍速播放。
import sys
import pickle
import csv
from importlib.resources.readers import remove_duplicates
from warnings import catch_warnings

from PyQt5.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QListWidget, QMessageBox, QFileDialog, QRadioButton, QButtonGroup, QInputDialog
)
from PyQt5.QtGui import QIcon
from graphviz import Digraph

# 定义FamilyMember类,表示族谱中的一个成员
class FamilyMember:
    def __init__(self, name, father=None, mother=None, spouse=None, gender='男'):
      self.name, self.father, self.mother, self.spouse, self.gender = name, father, mother, spouse, gender
      self.children = []
      self.generation = 1

      # 使用辅助集合保持顺序地去重

    def remove_duplicates(lst):
      seen = set()
      unique_list = []
      for item in lst:
            if item not in seen:
                seen.add(item)
                unique_list.append(item)
      return unique_list

    def add_child(self, child):
      li=[]
      for i in self.children:
            li.append(i)
      li.append(child)
      li=remove_duplicates(li)
      return li



    def calculate_generation(self, family_tree):
      # 计算代数,递归父亲代数加1,直至没有父亲时返回1
      if self.father and self.father in family_tree:
            self.generation = family_tree.generation + 1
      else:
            if self.spouse in family_tree:
                self.generation=family_tree.generation
            else:
                self.generation = 1

# 定义FamilyTreeApp类,继承自QWidget,负责创建图形用户界面和处理用户交互
class FamilyTreeApp(QWidget):
    def __init__(self):
      super().__init__()
      self.family_tree = {}
      self.initUI()

    def initUI(self):
      self.setWindowTitle('族谱软件')
      self.setGeometry(100, 100, 600, 400)
      self.setWindowIcon(QIcon('icon.ico'))

      layout = QVBoxLayout(self)

      input_layout = QHBoxLayout()
      input_layout.addWidget(QLabel('姓名:'))
      self.name_input = QLineEdit(self)
      input_layout.addWidget(self.name_input)
      input_layout.addWidget(QLabel('父亲:'))
      self.father_input = QLineEdit(self)
      input_layout.addWidget(self.father_input)
      input_layout.addWidget(QLabel('母亲:'))
      self.mother_input = QLineEdit(self)
      input_layout.addWidget(self.mother_input)
      input_layout.addWidget(QLabel('配偶:'))
      self.spouse_input = QLineEdit(self)
      input_layout.addWidget(self.spouse_input)
      input_layout.addWidget(QLabel('儿子:'))
      self.son_input = QLineEdit(self)
      input_layout.addWidget(self.son_input)
      input_layout.addWidget(QLabel('女儿:'))
      self.daughter_input = QLineEdit(self)
      input_layout.addWidget(self.daughter_input)
      layout.addLayout(input_layout)

      gender_layout = QHBoxLayout()
      self.gender_group = QButtonGroup(self)
      self.male_radio = QRadioButton('男', self)
      self.female_radio = QRadioButton('女', self)
      self.male_radio.setChecked(True)
      self.gender_group.addButton(self.male_radio)
      self.gender_group.addButton(self.female_radio)
      gender_layout.addWidget(QLabel('性别:'))
      gender_layout.addWidget(self.male_radio)
      gender_layout.addWidget(self.female_radio)
      layout.addLayout(gender_layout)

      button_layout = QHBoxLayout()
      self.add_button = QPushButton('添加成员', self)
      self.save_button = QPushButton('保存族谱', self)
      self.load_button = QPushButton('加载族谱', self)
      self.clear_button = QPushButton('清除族谱', self)
      self.print_button = QPushButton('打印族谱', self)
      self.new_button = QPushButton('新建族谱', self)
      self.export_button = QPushButton('导出CSV', self)
      button_layout.addWidget(self.add_button)
      button_layout.addWidget(self.save_button)
      button_layout.addWidget(self.load_button)
      button_layout.addWidget(self.clear_button)
      button_layout.addWidget(self.print_button)
      button_layout.addWidget(self.new_button)
      button_layout.addWidget(self.export_button)
      layout.addLayout(button_layout)

      self.family_list = QListWidget(self)
      layout.addWidget(self.family_list)

      self.add_button.clicked.connect(self.add_member)
      self.save_button.clicked.connect(self.save_family_tree)
      self.load_button.clicked.connect(self.load_family_tree)
      self.clear_button.clicked.connect(self.clear_family_tree)
      self.print_button.clicked.connect(self.print_family_tree)
      self.new_button.clicked.connect(self.new_family_tree)
      self.export_button.clicked.connect(self.export_family_tree)

    def add_member(self):
      name, father, mother, spouse = self.name_input.text(), self.father_input.text(), self.mother_input.text(), self.spouse_input.text()
      if father =='':father=None
      if mother == '': mother = None
      if spouse == '': spouse = None
      sons = self.son_input.text().split(',') if self.son_input.text() else []
      daughters = self.daughter_input.text().split(',') if self.daughter_input.text() else []
      gender = '男' if self.male_radio.isChecked() else '女'

      if not name:
            QMessageBox.warning(self, '错误', '姓名不能为空')
            return

      # 检查并添加父亲、母亲、配偶、儿子和女儿
      if father and father not in self.family_tree:
            self.family_tree = FamilyMember(father,gender='男')
      if mother and mother not in self.family_tree:
            self.family_tree = FamilyMember(mother,gender='女')
      if spouse and spouse not in self.family_tree:
            spouse_gender = '女' if gender == '男' else '男'
            self.family_tree = FamilyMember(spouse, gender=spouse_gender)

      for son in sons:
            if son and son not in self.family_tree:
                self.family_tree = FamilyMember(son, father=name, gender='男')
                if spouse in self.family_tree:self.family_tree.add_child(son)
      for daughter in daughters:
            if daughter and daughter not in self.family_tree:
                self.family_tree = FamilyMember(daughter, father=name, gender='女')
                if spouse in self.family_tree:self.family_tree.add_child(daughter)

      if name in self.family_tree:
            reply = QMessageBox.question(self, '重名提示', f'成员 {name} 已存在,是否更新信息?', QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
            if reply == QMessageBox.Yes:
                member = self.family_tree
                member.father, member.mother, member.spouse, member.gender = father, mother, spouse, gender
                member.children=remove_duplicates(sons + daughters)
            else:
                name = self.get_unique_name(name)
                self.family_tree = FamilyMember(name, father, mother, spouse, gender)
                self.family_tree.children = remove_duplicates(sons + daughters)
      else:
            self.family_tree = FamilyMember(name, father, mother, spouse, gender)
            self.family_tree.children = remove_duplicates(sons + daughters)

      if father in self.family_tree:
            self.family_tree.add_child(name)
      if mother in self.family_tree:
            self.family_tree.add_child(name)
      if spouse in self.family_tree:
            self.family_tree.spouse = name

      # 确保父亲和母亲是配偶
      if father and mother and father in self.family_tree and mother in self.family_tree:
            self.family_tree.spouse = mother
            self.family_tree.spouse = father

      for son in sons:
            if son in self.family_tree:
                if gender=='男':
                  self.family_tree.father = name
                  self.family_tree.mother = spouse
                else:
                  self.family_tree.father = spouse
                  self.family_tree.mother = name
      for daughter in daughters:
            if daughter in self.family_tree:
                if gender == '男':
                  self.family_tree.father = name
                  self.family_tree.mother = spouse
                else:
                  self.family_tree.father = spouse
                  self.family_tree.mother = name

      self.update_generations()
      self.update_list()

    def get_unique_name(self, name):
      count = 1
      unique_name = f"{name}_{count}"
      while unique_name in self.family_tree:
            count += 1
            unique_name = f"{name}_{count}"
      return unique_name

    def update_generations(self):
      # 先计算所有成员的代数
      for member in self.family_tree.values():
            member.calculate_generation(self.family_tree)

      # 确保夫妻的代数相同,并且以丈夫的代数为准
      for member in self.family_tree.values():
            if member.gender == '男' and member.spouse and member.spouse in self.family_tree:
                self.family_tree.generation = member.generation

      # 联动更新所有相关成员的代数
      self.propagate_generations()

    def propagate_generations(self):
      # 使用广度优先搜索(BFS)更新所有相关成员的代数
      queue = list(self.family_tree.values())
      while queue:
            member = queue.pop(0)
            old_generation = member.generation
            member.calculate_generation(self.family_tree)
            if member.generation != old_generation:
                for child in member.children:
                  if child in self.family_tree:
                        queue.append(self.family_tree)

    def update_list(self):
      self.family_list.clear()
      for member in self.family_tree.values():
            spouse_info = f' (配偶: {member.spouse})' if member.spouse else ''
            self.family_list.addItem(f'{member.name}{spouse_info} (代数: {member.generation})')

    def save_family_tree(self):
      file_name, _ = QFileDialog.getSaveFileName(self, '保存族谱', '', 'Pickle Files (*.pkl);;All Files (*)')
      if file_name:
            with open(file_name, 'wb') as f:
                pickle.dump(self.family_tree, f)

    def load_family_tree(self):
      file_name, _ = QFileDialog.getOpenFileName(self, '加载族谱', '', 'Pickle Files (*.pkl);;All Files (*)')
      if file_name:
            with open(file_name, 'rb') as f:
                self.family_tree = pickle.load(f)
            self.update_generations()
            self.update_list()

    def clear_family_tree(self):
      self.family_tree.clear()
      self.update_list()

    def print_family_tree(self):
      if not self.family_tree:
            QMessageBox.warning(self, '错误', '族谱为空')
            return

      dot = Digraph(comment='Family Tree', format='png')
      dot.attr('graph', rankdir='TB')
      dot.attr('node', fontname='FangSong_GB2312')

      for member in self.family_tree.values():
            color=self.get_generation_color(member.generation)
            #当前家庭夫妻俩节点
            if member.gender == '男':
                if member.spouse is None:
                  dot.node(f"第{member.generation}代 夫:" + str(member.name) + " 妻:未知", style='filled', fillcolor=color)
                else:
                  dot.node(f"第{member.generation}代 夫:"+str(member.name) + " 妻:" + str(member.spouse), style='filled', fillcolor=color)
            else:
                if member.spouse is None:
                  dot.node(f"第{member.generation}代 夫:未知" + " 妻:" + str(member.name), style='filled',fillcolor=color)
                else:
                  dot.node(f"第{member.generation}代 夫:" + str(member.spouse) + " 妻:"+str(member.name), style='filled',fillcolor=color)


            if member.spouse is not None:
                if member.father is not None:
                  if member.mother is not None:#有父亲、母亲和配偶
                        if member.gender == '男':
                            dot.edge(f"第{self.family_tree.generation}代 夫:" + str(member.father) + " 妻:" + str(member.mother),f"第{member.generation}代 夫:" + str(member.name) + " 妻:"+str(member.spouse))
                        else:
                            dot.edge(f"第{self.family_tree.generation}代 夫:" + str(member.father) + " 妻:" + str(member.mother),f"第{member.generation}代 夫:" +str(member.spouse)+ " 妻:" + str(member.name))
                  else:#有配偶、父亲没有母亲
                        if member.gender == '男':
                            dot.edge(f"第{self.family_tree.generation}代 夫:"+str(member.father)+" 妻:未知", f"第{member.generation}代 夫:"+str(member.name)+" 妻:"+str(member.spouse))
                        else:
                            dot.edge(f"第{self.family_tree.generation}代 夫:" + str(member.father) + " 妻:未知",f"第{member.generation}代 夫:"+str(member.spouse)+" 妻:"+str(member.name) )
                else:
                  if member.mother is not None:#有母亲、配偶,没有父亲
                        if member.gender == '男':
                            dot.edge(f"第{self.family_tree.generation}代 夫:未知" + " 妻:" + str(member.mother),f"第{member.generation}代 夫:" + str(member.name) + " 妻:" + str(member.spouse))
                        else:
                            dot.edge(f"第{self.family_tree.generation}代 夫:未知" + " 妻:" + str(member.mother),f"第{member.generation}代 夫:" + str(member.spouse) + " 妻:" + str(member.name))
                  else:
                        print("无父无母")
            else:
                if member.father is not None:
                  if member.mother is not None:#有父亲、母亲没有配偶
                        if member.gender == '男':
                            dot.edge(f"第{self.family_tree.generation}代 夫:" + str(member.father) + " 妻:" + str(member.mother),f"第{member.generation}代 夫:" + str(member.name) + " 妻:未知")
                        else:
                            dot.edge(f"第{self.family_tree.generation}代 夫:" + str(member.father) + " 妻:" + str(member.mother),f"第{member.generation}代 夫:未知" + " 妻:" + str(member.name))
                  else:#有父亲、配偶没有母亲
                        if member.gender == '男':
                            dot.edge(f"第{self.family_tree.generation}代 夫:"+str(member.father)+" 妻:未知", f"第{member.generation}代 夫:" + str(member.name) + " 妻:未知")
                        else:
                            dot.edge(f"第{self.family_tree.generation}代 夫:" + str(member.father) + " 妻:未知",f"第{member.generation}代 夫:未知" + " 妻:" + str(member.name))
                else:
                  if member.mother is not None:#有母亲,没父亲、配偶
                        if member.gender == '男':
                            dot.edge(f"第{self.family_tree.generation}代 夫:未知" + " 妻:" + str(member.mother),f"第{member.generation}代 夫:" + str(member.name) + " 妻:未知" )
                        else:
                            dot.edge(f"第{self.family_tree.generation}代 夫:未知" + " 妻:" + str(member.mother),f"第{member.generation}代 夫:未知" + " 妻:" + str(member.name))
                  else:#全没有
                        print("无父无母无对象")

      file_name, _ = QFileDialog.getSaveFileName(self, '打印族谱', '', 'PNG Files (*.png);;All Files (*)')
      if file_name:
            dot.render(file_name, view=True)
      dot.clear()


    def get_generation_color(self, generation):
      colors = ['#FFD700', '#FFA500', '#FF8C00', '#FF6347', '#FF4500', '#FF0000']
      return colors

    def new_family_tree(self):
      self.family_tree.clear()
      self.update_list()

    def export_family_tree(self):
      if not self.family_tree:
            QMessageBox.warning(self, '错误', '族谱为空')
            return

      file_name, _ = QFileDialog.getSaveFileName(self, '导出族谱', '', 'CSV Files (*.csv);;All Files (*)')
      try:
            if file_name:
                with open(file_name, 'w', newline='', encoding='utf-8') as f:
                  writer = csv.writer(f)
                  writer.writerow(['姓名', '父亲', '母亲', '配偶', '性别', '代数'])
                  for member in self.family_tree.values():
                        writer.writerow()
      except :
            print('')
if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = FamilyTreeApp()
    ex.show()
    sys.exit(app.exec_())

欢迎大家,继续提提意见,之前因为不熟悉版规,也给管理大大造成了困扰,万分抱歉!

553429363 发表于 2024-11-14 11:15

scp201 发表于 2024-11-14 11:08
是不是也能通过EXCEL导入搭建呢?

可以可以哈哈

lxyx 发表于 2024-11-14 11:35

https://www.52pojie.cn/thread-1978103-1-1.html

这个大佬也写了,感觉两位大佬各有所长啊,针不戳

553429363 发表于 2024-11-13 15:14

@风之暇想 版主大大 求审核

scp201 发表于 2024-11-14 11:08

是不是也能通过EXCEL导入搭建呢?{:301_998:}

553429363 发表于 2024-11-14 11:18

scp201 发表于 2024-11-14 11:08
是不是也能通过EXCEL导入搭建呢?

以后改一下

tuhutuhu 发表于 2024-11-14 18:54

这辈子还没见过族谱{:1_925:}

ZnOSpin 发表于 2024-11-17 11:10

用开源代码制作族谱,很有特色,族谱分支条件讨论得很详细👍
感觉在每一代都对齐的布局中可以把辈分标在画布边缘,使每个节点小巧一些
另外就是代际的箭头是斜的不像很多族谱那样横平竖直再分岔,不过表达意思已经很清楚了,看习惯了就行
(仅供参照哈,我是还没编过族谱的Python和Graphiz小白)
页: [1]
查看完整版本: 新鲜出炉的用python写的族谱软件-第三版