用pyqt做个日期输入控件,实现公农历转换及干支纪时功能
这段时间学了下紫微斗数,断命没学会,倒是对编程排命盘产生了兴趣。今天先完成了第一步,用pyqt做了个支持日期输入的控件,可以输入公历或农历日期及时间,并根据输入的公历或农历日期查到对应的农历或公历日期以及用干支计时法表示的当日年月日时信息。试用该控件的一个程序显示的界面如下:
如果输入了不存在的农历日期:
下面是日期输入控件代码:
from datetime import datetime
import pandas as pd
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QLabel, QComboBox, QDateEdit, \
QGroupBox, QMessageBox
from PyQt5.QtCore import QDate
from lunarcalendar import Lunar, DateNotExist, Solar, Converter
heavenly_stems = ['甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸']
earthly_branches = ['子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥']
time_name = ['00:00~00:59早子', '01:00~02:59丑', '03:00~04:59寅', '05:00~06:59卯', '07:00~08:59辰',
'09:00~10:59巳', '11:00~12:59午', '13:00~14:59未', '15:00~16:59申', '17:00~18:59酉',
'19:00~20:59戌', '21:00~22:59亥', '23:00~23:59晚子']
# 五虎遁诀由年干定月干表
month_stem_from_year = pd.DataFrame([
['年干', '子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'],
['甲', '丙', '丁', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸', '甲', '乙'],
['乙', '戊', '己', '戊', '己', '庚', '辛', '壬', '癸', '甲', '乙', '丙', '丁'],
['丙', '庚', '辛', '庚', '辛', '壬', '癸', '甲', '乙', '丙', '丁', '戊', '己'],
['丁', '壬', '癸', '壬', '癸', '甲', '乙', '丙', '丁', '戊', '己', '庚', '辛'],
['戊', '甲', '乙', '甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'],
['己', '丙', '丁', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸', '甲', '乙'],
['庚', '戊', '己', '戊', '己', '庚', '辛', '壬', '癸', '甲', '乙', '丙', '丁'],
['辛', '庚', '辛', '庚', '辛', '壬', '癸', '甲', '乙', '丙', '丁', '戊', '己'],
['壬', '壬', '癸', '壬', '癸', '甲', '乙', '丙', '丁', '戊', '己', '庚', '辛'],
['癸', '甲', '乙', '甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸']])
# 五鼠遁诀由日干定时干表
time_stem_from_day = pd.DataFrame([
['日干', '子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'],
['甲', '甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸', '甲', '乙'],
['乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸', '甲', '乙', '丙', '丁'],
['丙', '戊', '己', '庚', '辛', '壬', '癸', '甲', '乙', '丙', '丁', '戊', '己'],
['丁', '庚', '辛', '壬', '癸', '甲', '乙', '丙', '丁', '戊', '己', '庚', '辛'],
['戊', '壬', '癸', '甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'],
['己', '甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸', '甲', '乙'],
['庚', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸', '甲', '乙', '丙', '丁'],
['辛', '戊', '己', '庚', '辛', '壬', '癸', '甲', '乙', '丙', '丁', '戊', '己'],
['壬', '庚', '辛', '壬', '癸', '甲', '乙', '丙', '丁', '戊', '己', '庚', '辛'],
['癸', '壬', '癸', '甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸']])
class DateConverterWidget(QWidget):
def __init__(self):
super().__init__()
self.solar_year = 0 # 公历年
self.solar_month = 0 # 公历月
self.solar_day = 0 # 公历日
self.lunar_year = 0 # 农历年
self.lunar_month = 0 # 农历月
self.lunar_day = 0 # 农历日
self.year_stem = '' # 年干
self.year_branch = '' # 年支
self.month_stem = '' # 月干
self.month_branch = '' # 月支
self.day_stem = '' # 日干
self.day_branch = '' # 日支
self.hour_stem = '' # 时干
self.hour_branch = '' # 时支
self.is_leap_month = False # 是否农历闰月
line_height = 50
# 控件采取水平布局
date_layout = QHBoxLayout()
# 公/农历标签和选择框
calendar_label = QLabel('公/农历:')
self.calendar_combo = QComboBox()
self.calendar_combo.addItems(['公历', '农历'])
self.calendar_combo.setCurrentIndex(0) # 默认公历
group_calendar_type = QGroupBox()
group_calendar_type.setFixedHeight(line_height)
layout_calendar_type = QHBoxLayout()
layout_calendar_type.addWidget(calendar_label)
layout_calendar_type.addWidget(self.calendar_combo)
group_calendar_type.setLayout(layout_calendar_type)
# 农历闰月标签和选择框
leap_month_label = QLabel('农历闰月:')
self.leap_month_combo = QComboBox()
self.leap_month_combo.addItems(['否', '是'])
self.leap_month_combo.setCurrentIndex(0) # 默认否
group_leap_month = QGroupBox()
group_leap_month.setFixedHeight(line_height)
layout_leap_month_type = QHBoxLayout()
layout_leap_month_type.addWidget(leap_month_label)
layout_leap_month_type.addWidget(self.leap_month_combo)
group_leap_month.setLayout(layout_leap_month_type)
# 日期标签和日期编辑控件
date_label = QLabel('日期:')
self.date_edit = QDateEdit()
# 根据工农历转换所用的lunarcalendar库能够支持的时间范围设定输入的时间范围
self.date_edit.setMinimumDate(QDate(1900, 1, 31))
self.date_edit.setMaximumDate(QDate(2100, 12, 31))
self.date_edit.setDate(QDate.currentDate())
group_date_edit = QGroupBox()
group_date_edit.setFixedHeight(line_height)
layout_date_edit = QHBoxLayout()
layout_date_edit.addWidget(date_label)
layout_date_edit.addWidget(self.date_edit)
group_date_edit.setLayout(layout_date_edit)
# 时辰标签和选择框
hour_label = QLabel('时辰:')
self.hour_combo = QComboBox()
self.hour_combo.addItems(s for s in time_name)
self.hour_combo.setCurrentIndex(0)
group_hour = QGroupBox()
layout_hour = QHBoxLayout()
group_hour.setFixedHeight(line_height)
layout_hour.addWidget(hour_label)
layout_hour.addWidget(self.hour_combo)
group_hour.setLayout(layout_hour)
date_layout.addWidget(group_calendar_type)
date_layout.addWidget(group_leap_month)
date_layout.addWidget(group_date_edit)
date_layout.addWidget(group_hour)
# 设置控件布局
self.setLayout(date_layout)
def calculate_days_to_basic_day(self, date):
"""
计算给定日期离支持的最早日期之前最近的一个甲子日1899年12月22日之间的天数差,
用这个天数差确定给定日期的日干支
:param date: 以短横杠隔开的日期字符串
:return: 给定日期与1899年12月22日之间的天数差
"""
# 将字符串日期转换为datetime对象
day = datetime.strptime(date, '%Y-%m-%d')
base_day = datetime.strptime('1899-12-22', '%Y-%m-%d')
# 计算两个日期之间的差值并返回间隔天数
difference = day - base_day
return difference.days
def safe_lunar(self, year, month, day, isleap=False):
"""
QDateEdit控件不能保证输入的农历日期存在,故进行异常捕捉。
发生异常则返回None
"""
try:
lunar = Lunar(year, month, day, isleap=isleap)
return lunar
except DateNotExist as e:
print(e)
return None
def get_date_info(self):
date = self.date_edit.date()
year = date.year()
month = date.month()
day = date.day()
if self.calendar_combo.currentText() == '公历':
# QDateEdit控件可以确保输入的公历日期存在
solar = Solar(year, month, day)
lunar = Converter.Solar2Lunar(solar)
self.solar_year, self.solar_month, self.solar_day = solar.year, solar.month, solar.day
self.lunar_year, self.lunar_month, self.lunar_day = lunar.year, lunar.month, lunar.day
self.is_leap_month = lunar.isleap
else: # 农历
is_leap = self.leap_month_combo.currentText() == '是'
# 农历需判断日期的合法性,即给出的日期是否存在。lunardate库目前此功能不正常。
lunar = self.safe_lunar(year, month, day, is_leap)
if lunar is None:
QMessageBox.warning(self, '注意', '输入的日期不存在,改为默认日期。请重新输入合法日期。')
lunar = Lunar(1900, 1, 2)
self.date_edit.setDate(QDate(1900, 2, 1))
solar = Converter.Lunar2Solar(lunar)
self.solar_year, self.solar_month, self.solar_day = solar.year, solar.month, solar.day
self.lunar_year, self.lunar_month, self.lunar_day = lunar.year, lunar.month, lunar.day
self.is_leap_month = lunar.isleap
# 计算年干支
stem_index = (self.lunar_year - 4) % 10
branch_index = (self.lunar_year - 4) % 12
self.year_stem = heavenly_stems[stem_index]
self.year_branch = earthly_branches[branch_index]
# 计算月干支
self.month_branch = earthly_branches[(self.lunar_month + 1) % 12]
# 从csv文件中保存的五虎遁诀数据根据年干与月支查月干
# month_heavenly_from_year = 'month_heavenly_from_year.csv'
# month_stem_from_year = pd.read_csv(month_heavenly_from_year)
# row = month_stem_from_year[month_stem_from_year[0] == self.year_stem].values.tolist()[0]
# 直接用五虎遁诀表根据年干与月支查月干,先取出年干对应的行,再根据月支在该行的索引取得天干
row = month_stem_from_year[month_stem_from_year[0] == self.year_stem].values.tolist()[0]
# 五虎遁诀表比地支表多出了第一列的天干,故应将地支表中查出的地支索引加1以对应五虎遁诀表
index = earthly_branches.index(self.month_branch) + 1
self.month_stem = row[index]
# 计算日干支
days = self.calculate_days_to_basic_day(f'{self.solar_year}-{self.solar_month}-{self.solar_day}')
heavenly_day = days % 10
earthly_day = days % 12
self.day_stem = heavenly_stems[heavenly_day]
self.day_branch = earthly_branches[earthly_day]
# 计算时干支
index = self.hour_combo.currentIndex()
self.hour_branch = earthly_branches[index if index < 12 else 0]
# 从csv文件中保存的五鼠遁诀数据根据日干与时支查时干
# time_heavenly_from_day = 'time_heavenly_from_day.csv'
# time_stem_from_day = pd.read_csv(time_heavenly_from_day)
# row = time_stem_from_day[time_stem_from_day[0] == self.day_stem].values.tolist()[0]
# 直接用五鼠遁诀表根据日干与时支查时干,先取出日干对应的行,再根据时支在该行的索引取得天干
row = time_stem_from_day[time_stem_from_day[0] == self.day_stem].values.tolist()[0]
# 五鼠遁诀表比地支表多出了第一列的天干,故将地支表中查出的地支索引加1以对应五鼠遁诀表
index += 1
# 晚子时天干同下一日子时
self.hour_stem = row[index if index < 12 else index - 10]
对上述代码要点的简单说明:
1、自定义控件应当继承QWidget类;
2、工农历日期转换使用了lunarcalendar库,lunardate库也能够完成工农历转换,但使用时发现lunardate库在输入的农历日期不存在(例如输入不存在的农历闰月)时,无法捕捉错误并处理,直接导致程序崩溃(409错误)。lunarcalendar库中需要用到的类主要有Lunar、Solar和Converter,分别对应农历、公历和公农历转换器,Converter类的方法Solar2Lunar()接收一个Solar对象(包含公历信息)作为参数,并转换为Lunar对象(包含农历信息),Lunar2Solar()方法则接收一个Lunar对象(包含农历信息)作为参数,并转换为Solar对象(包含公历信息)。Solar对象和Lunar对象都有属性year、month和day属性记录公历或农历的年、月、日,Lunar对象还有isleap属性记录当前农历月份是不是闰月。
3、lunarcalendar库目前只能处理公历1900年1月31日(农历1900年正月初一)到2100年12月31日之间的日期,因此,调用QDateEdit控件的setMinimumDate(QDate(1900, 1, 31))方法和setMaximumDate(QDate(2100, 12, 31))方法将自定义控件中的日期输入控件可用时间限制在1900年1月31日到2100年12月31日之间。QDateEdit控件自身即可确保输入的公历日期合法,但不能确保输入农历日期时是合法的。如果输入了非法的农历日期,构造Lunar对象时会抛出DateNotExist异常,程序中利用这一机制定义了一个安全Lunar对象构造函数safe_lunar(self, year, month, day, isleap=False),避免了输入错误的农历日期时引起程序崩溃。
4、在计算年份干支时,必须使用农历的年份,因为干支纪年是从农历正月初一至除夕使用同一个干支。
5、按传统干支计时法,月份地支正月对应寅,按十二地支顺序一一对应到亥(十月),十一月对应子,十二月对应丑,因此只需用农历月份调整错位值后作为索引在地支数组中(地支数组从子排到寅,月份从寅到亥再接子丑,因此有错位)即可查询得到。月支的天干按所谓五虎遁口诀由年的天干与月支确定。程序中直接将五虎遁口诀得出的结果用一个pandas.DateFrame记录了下来,也可以用文件记录(程序中从csv文件中加载的代码被注释掉了)。查询天干时先通过下面一行代码:
month_stem_from_year[month_stem_from_year[0] == self.year_stem].values.tolist()[0]
找到年份天干所在行的数据并转换为一个一维数组,在直接按月支的索引(需要加1,因为五虎遁诀表比地支数组多出了第一列的天干)找到相应天干。
5、计算日期干支时使用了1899年12月22日作为基准日,这一天的干支分别是天干和地支的起始符号“甲”和“子”,并且是离1900年1月31日最近的甲子日。利用datetime.datetime的功能计算出指定日期与基准日之间的天数差,然后分别对10和12求余,即可作为天干数组与地支数组的索引分别取得日期的干支。
6、按传统干支计时法,一天分为十二个时辰,每个时辰为2个小时。时辰的地支可以从程序中的time_name数组中看出(按传统命理理论,多数派别主张早子时和晚子时分别采用当日五鼠遁口诀和次日五鼠遁口诀天干,此程序采用了这一主张,区分了早子时和晚子时)。时辰天干的确定与月份天干的确定类似,用所谓五鼠遁口诀由日干与时支确定。程序中将五鼠遁口诀得出的结果也用一个pandas.DateFrame记录了,查询方法类似月干查询的方法,在查找晚子时天干时将索引值回退10位,即与下一日早子时天干相同,观察DateFrame保存的二维表即可发现此规律。
下面是本文开头截图的测试程序的代码:
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QPushButton
from date_converter import DateConverterWidget
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
# 创建垂直布局
layout = QVBoxLayout()
# 创建自定义日期控件
self.date = DateConverterWidget()
# 创建按钮
self.show_date_button = QPushButton("显示选择的日期公农历及干支信息")
self.show_date_button.clicked.connect(self.update_date_info)
self.label_info = QLabel()
self.update_date_info()
# 将自定义日期控件和按钮添加到布局中
layout.addWidget(self.date)
layout.addWidget(self.label_info)
layout.addWidget(self.show_date_button)
# 设置布局
self.setLayout(layout)
# 设置窗口属性
self.setWindowTitle('日期输入控件测试')
self.setGeometry(300, 300, 300, 200)
def update_date_info(self):
self.date.get_date_info()
solar = f'公历:{self.date.solar_year}年{self.date.solar_month}月{self.date.solar_day}日'
lunar = f'农历:{self.date.lunar_year}年{"闰" if self.date.is_leap_month else ""}' + \
f'{self.date.lunar_month}月{self.date.lunar_day}日'
stem_branches = f'{self.date.year_stem}{self.date.year_branch}年{self.date.month_stem}' + \
f'{self.date.month_branch}月{self.date.day_stem}{self.date.day_branch}日' + \
f'{self.date.hour_stem}{self.date.hour_branch}时'
self.label_info.setText(f'{solar}\n{lunar}\n{stem_branches}')
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
这段程序中可以看到使用自定义控件的方法与使用pyqt自带控件的方法并没有区别,这段代码中值得注意的是pyqt的事件处理机制,在给connect方法传递事件处理方法时应传递方法名,而不是方法调用(即方法名后不要带参数表括号)。