前已在Python公历转换农历及简易万年历一文中实现农历计算的基本功能(内附农历计算原理),本篇利用这些功能完成UI小程序。
完整代码下载:GitHub - Luni-solar-Calendar: 万年历(含农历、节气等)
5积分支持作者:CSDN - Python万年历源码
以下为显示月历部分的代码示例。
def getSolorTerms(year):
jqb = [[i] for i in range(12)] # [月序,[日序, 节气序] * n]
for i in range(24):
jq = JD2date(SolarTerms(year, i * 15), 8)
jqn, jqy, jqr = jq.triple()
if jqn != year:
jq = JD2date(SolarTerms(year + (year - jqn), i * 15), 8)
jqn, jqy, jqr = jq.triple()
if jqn == year:
jqb[jqy-1].append([int(jqr), (i + 6) % 24]) # 按月存储
for j in range(len(jqb)): jqb[j].pop(0)
return jqb # [[日序, 节气序] * n]
def setNYE(festival, ymb, shuoJD): # 重设节日日期
yx = ymb.index(festival[0])
if DateDiffer(shuoJD[yx + 1], shuoJD[yx]) == 29:
festival[1] = '廿九'
else:
festival[1] = '三十'
return festival
def getYearMonth(ui, wheel=0): # 根据输入重设年月
edit = ui.cblYear.currentText()
try:
year = int(re.search(r'-?\d+', edit).group())
except:
return 0, 0 # 异常输入
if year == 0: return 0, 0
if edit[:2].lower() == 'bc': year = -year
year += wheel
century = year // 100
if start_century * 100 <= year < endCentury * 100:
ui.cblCentury.setCurrentIndex(century - start_century)
if century == 0: ui.cblYear.setCurrentIndex(year % 100 - 1)
else: ui.cblYear.setCurrentIndex(year % 100)
else:
ui.cblCentury.setCurrentIndex(-1)
ui.cblYear.clear()
if wheel == 0: ui.cblYear.setCurrentText(edit)
else: ui.cblYear.setCurrentText(str(year))
if ui.sender() == ui.cblFindFestival: month = -1
else: month = ui.cblMonth.currentIndex()
global setYear
if year != setYear or setYear == '':
setYear = year
updateYear()
return year, month
def updateYear():
global yearYMB, yearSJD, yearST
yearYMB, yearSJD = LunarCalendar(setYear, 0)
yearST = getSolorTerms(setYear)
def displayMonth(ui):
year, month = getYearMonth(ui)
if year == 0: return 0, 0, 0
ymb, shuoJD, jqb = yearYMB, yearSJD, yearST
# ymb, shuoJD = LunarCalendar(year, 0)
# jqb = getSolorTerms(year)
if DateCompare(ephem.julian_date((year, 12, 31)), shuoJD[-2] + 29):
ymb1, shuoJD1 = LunarCalendar(year + 1)
ymb = ymb[:-2] + ymb1[:2]
shuoJD = shuoJD[:-2] + shuoJD1[:3]
currentFes, fesDay = '', 0
if ui.sender() == ui.cblFindFestival:
currentFes = ui.cblFindFestival.currentText()
month = jumpLCF(currentFes, ymb, shuoJD)
ui.cblMonth.setCurrentIndex(month)
days[1] = 29 if (year % 4 == 0 and year % 100 != 0) or year % 400 == 0 else 28
if year < 1582 and year % 4 == 0: days[1] = 29
i = month
ysJD = ephem.julian_date((year, i + 1))
szy = findSZY(ysJD, shuoJD) # 每月1日对应的农历月
ysRQ = DateDiffer(ysJD, shuoJD[szy]) # 每月1日的农历日期
yue0 = DateDiffer(shuoJD[szy + 1], shuoJD[szy])
yue1 = DateDiffer(shuoJD[szy + 2], shuoJD[szy + 1])
blank = int((ysJD + 0.5) % 7)
for j in range(6):
for k in range(7):
# 计算日期
day = j * 7 + k - blank + 1
rqx = ysRQ + (j+1) * 7 - 7 + k - blank
if rqx < 0: # 月首日所在农历月上月
yue = DateDiffer(shuoJD[szy], shuoJD[szy - 1])
rq = nlrq[rqx % yue]
yx = szy - 1
elif 0 <= rqx < yue0:
rq = nlrq[rqx]
yx = szy
elif yue0 <= rqx < yue0 + yue1:
rq = nlrq[rqx - yue0]
yx = szy + 1
elif rqx >= yue0 + yue1:
rq = nlrq[rqx - yue0 - yue1]
yx = szy + 2
if day == 1:
yx1 = yx
rq1 = rq
if day == days[i]:
yx2 = yx
rq2 = rq
dateInfo[j * 7 + k] = [day, ymb[yx], rq, ysJD+day-1, jqb[i]]
if year == 1582 and i == 9: dateInfo[j * 7 + k][-1] = [jqb[i][0], jqb[i-1][1]] # 该月无节气,月干支序从上月获取
if rq == '初一': rq = ymb[yx]
# 显示月历
if j == 0 and k < blank: # 上月
ui.labs[j][k].setText(font(days[i-1]-blank+k+1, 20) + font(rq))
else: # 本月
if day <= days[i]:
if year == 1582 and i == 9 and day > 4:
if day < 15: continue
else: ui.labs[j+(k-10)//7][k-3].setText(font(day, 20, "black", 800) + font(rq))
else:
ui.labs[j][k].setText(font(day, 20, "black", 800) + font(rq)) # "500;font-family:微软雅黑"
else: # 次月
if year == 1582 and i == 9:
ui.labs[j+(k-10)//7][k-3].setText(font(day - days[i], 20) + font(rq))
if day - days[i] == 11:
for m in range(10): ui.labs[j-1+(m+k-2)//7][(m+k-2)%7].setText("")
else: ui.labs[j][k].setText(font(day-days[i], 20) + font(rq))
# 显示节日
qmDay = 0
for jq in jqb[i]: # 节气
jqrx = jq[0] + blank - 1
if year == 1582 and i == 9: jqrx -= 10
ui.labs[jqrx//7][jqrx%7].setText(font(jq[0], 20, "black", 800) + font(jieqi[jq[1]], 12, "red"))
if jq[1] == 7: qmDay = jq[0]
if year >= 1949: # 公历节日起始年
if qmDay: ui.labs[(qmDay+blank-1)//7][(qmDay+blank-1)%7].setText(font(qmDay, 20, "red", 800) + font("清明", 12, "red"))
for fes in scfestivals:
if fes[0] == month + 1:
jqrx = fes[1] + blank - 1
ui.labs[jqrx // 7][jqrx % 7].setText(font(fes[1], 20, "red", 800) + font(fes[2], 12, "red"))
if year > 1911: # 农历节日起始年
rqx1 = nlrq.index(rq1)
for fes in lcfestivals:
if fes[2] == '除夕': fes = setNYE(fes, ymb, shuoJD)
jqrx = nlrq.index(fes[1])
if fes[0] == ymb[yx1] and jqrx >= rqx1: # 该月农历首日
jqrx += -rqx1 + blank
ui.labs[jqrx // 7][jqrx % 7].setText(font(jqrx-blank+1, 20, "red", 800) + font(fes[2], 12, "red"))
elif fes[0] == ymb[yx1+1] and jqrx <= nlrq.index(rq2): # 该月农历末日
jqrx += DateDiffer(shuoJD[yx1 + 1], shuoJD[yx1]) - rqx1 + blank
ui.labs[jqrx // 7][jqrx % 7].setText(font(jqrx - blank + 1, 20, "red", 800) + font(fes[2], 12, "red"))
elif yx2 - yx1 == 2 and fes[0] == ymb[yx2]: # 跨2月
jqrx += DateDiffer(shuoJD[yx1 + 2], shuoJD[yx1]) - rqx1 + blank
if 0 < jqrx <= days[i] + blank: ui.labs[jqrx // 7][jqrx % 7].setText(font(jqrx - blank + 1, 20, "red", 800) + font(fes[2], 12, "red"))
if fes[2] == currentFes: fesDay = jqrx - blank + 1
return year, month, fesDay
def displayDate(ui):
global selected
try:
selected.setStyleSheet("")
except:
pass
if ui.sender() in [None, ui.btnToday]: # 设为今日
year, month, day = time.localtime(time.time())[0:3]
month -= 1
ui.cblCentury.setCurrentIndex(-start_century + year // 100)
ui.cblYear.setCurrentIndex(year % 100)
ui.cblMonth.setCurrentIndex(month % 12)
displayMonth(ui)
borderDate(ui, month, day)
else:
if ui.sender() in [ui.cblCentury, ui.cblYear, ui.cblMonth, ui.btnLastMonth, ui.btnNextMonth, ui.btnLastYear, ui.btnNextYear, ui.cblFindFestival]: # 设为原公历日
year, month, day = displayMonth(ui)
if year == 0: return 0
if ui.sender() != ui.cblFindFestival: day = int(re.findall(r'(\d+)', ui.labInfo.text())[0]) # 公历日期
if day > days[month]: day = days[month] # 跳到上月底
borderDate(ui, month, day)
else: # 点击日期跳转
year, month = getYearMonth(ui)
if year == 0: return 0
selected = ui.sender()
if selected.text() == "": return 0 # 1582年被删除的日期
day = int(re.findall(r'(\d+)', selected.text())[0]) # 公历日期
if not re.search(r"= 10: nian -= 1
if nian < 0: ngz = gz[(nian - 3) % 60]
else: ngz = gz[(nian - 4) % 60]
nm = gyjn(year)
sxm = zodiac[(nian - 4) % 12]
if year < 0: year += 1
jqr = 99
for i in range(len(jqrq)):
if jqrq[i][1] % 2 == 1: jqr = jqrq[i][0]
if day >= jqr: ygz = gz[(year * 12 + 13 + month) % 60]
else: ygz = gz[(year * 12 + 12 + month) % 60]
rgz = gz[math.floor(JD + 8/24 + 0.5 + 49) % 60]
# JDN、距今、年名、月、星期、日、农历月日、年干支、生肖名、月干支、日干支
ui.labInfo.setText("
JDN {}
{}
{}
{}月 星期{}
{}{}{}年 〖{}〗
{}月 {}日
".format(
jdn, font(difference, 12, "black"), nm, month+1, week, font(day, 50, "black"), font(ym+rq, 17, "black"), ngz, sxm, ygz, rgz))
UI(以PyQt5为例)中使用Label控件显示月历,为完成点击后显示该日信息功能,需要对QLabel类添加单击函数。QComboBox的鼠标滚动功能默认滚动到最前或最末项后不再改变,需要重写wheelEvent事件使其在某年1月向上滚动时跳转到上一年12月。
Qt中每次设置样式表会清空原先的样式,故而选择使用html实现字体相关设置,保证设置样式表后字体不会变化,并且可通过获取Label文本判断日期类型。
虽然前已实现任意日期的公历转农历函数,但这里并不使用。如果每次点击标签都重新计算,会极大地增大计算量。本程序在每年内只计算一次,使用全局变量存储,跳转到年内其他月可以直接读取,无需重新计算。
程序中世纪列表提供前13世纪至30世纪,因为PyEphem库在计算BC13世纪至BC30世纪间的平均误差达2小时,导致确定农历月份容易出错,在早于BC13世纪前,误差更大。虽然在30世纪以后的较长时间内,ephem库的误差还很小,但也并非一般所需的功能。同时,对于特殊需求,程序中的年列表控件也开放了输入任意年份的功能。