基于Python開發(fā)日歷記事本的完整教程
項(xiàng)目簡(jiǎn)介
本文將詳細(xì)講解如何使用Python的wxPython GUI框架開發(fā)一個(gè)功能完整的日歷記事本應(yīng)用。該應(yīng)用支持選擇年月、記錄每日待辦事項(xiàng)、美觀預(yù)覽、背景自定義以及PDF導(dǎo)出等功能。
特別說明:本教程使用自繪日歷方式,完全不依賴 wx.calendar 模塊,避免了安裝問題,更加靈活可控。
技術(shù)棧
- wxPython: 跨平臺(tái)GUI框架,用于構(gòu)建用戶界面
- ReportLab: PDF生成庫(kù)
- PIL (Pillow): 圖像處理庫(kù)
- Python標(biāo)準(zhǔn)庫(kù): calendar、json、datetime等
一、項(xiàng)目架構(gòu)設(shè)計(jì)
1.1 核心類結(jié)構(gòu)
項(xiàng)目包含兩個(gè)主要類:
CalendarDiary (主窗口類)
├── 數(shù)據(jù)管理 (JSON讀寫)
├── UI界面構(gòu)建
├── 自繪日歷實(shí)現(xiàn)
├── 事件處理
└── PDF導(dǎo)出功能
PreviewFrame (預(yù)覽窗口類)
├── 日歷圖像生成
└── 可視化展示
1.2 數(shù)據(jù)存儲(chǔ)設(shè)計(jì)
使用JSON格式存儲(chǔ)數(shù)據(jù),結(jié)構(gòu)如下:
{
"2025-10-01": {
"morning": "晨跑 30分鐘",
"noon": "團(tuán)隊(duì)會(huì)議",
"evening": "學(xué)習(xí)Python"
},
"2025-10-02": {
"morning": "",
"noon": "午餐約會(huì)",
"evening": "看電影"
}
}
二、主窗口類詳解
2.1 初始化方法
def __init__(self):
super().__init__(None, title="美觀日歷記事本", size=(1200, 800))
self.data_file = "diary_data.json"
self.diary_data = self.load_data()
self.background_image = None
# 初始化日期
today = datetime.now()
self.current_year = today.year
self.current_month = today.month
self.selected_date = None
self.init_ui()
self.Centre()
關(guān)鍵點(diǎn):
- 調(diào)用父類構(gòu)造函數(shù)創(chuàng)建窗口框架
- 定義數(shù)據(jù)文件路徑
- 加載歷史數(shù)據(jù)
- 初始化當(dāng)前年月和選中日期
- 構(gòu)建UI并居中顯示
2.2 UI界面構(gòu)建
2.2.1 布局管理器
wxPython使用Sizer進(jìn)行布局管理,主要類型:
- BoxSizer: 水平或垂直排列控件
- GridSizer: 網(wǎng)格布局
- FlexGridSizer: 靈活網(wǎng)格布局
本項(xiàng)目使用BoxSizer的嵌套結(jié)構(gòu):
main_sizer = wx.BoxSizer(wx.VERTICAL) # 主垂直布局
├── toolbar_sizer (wx.HORIZONTAL) # 頂部工具欄
├── content_sizer (wx.HORIZONTAL) # 主內(nèi)容區(qū)
├── left_panel (自繪日歷)
└── right_panel (記事區(qū)域)
2.2.2 工具欄設(shè)計(jì)
toolbar_sizer = wx.BoxSizer(wx.HORIZONTAL)
# 年份選擇
self.year_choice = wx.Choice(panel, choices=[str(y) for y in range(2020, 2031)])
self.year_choice.SetSelection(self.current_year - 2020)
self.year_choice.Bind(wx.EVT_CHOICE, self.on_year_month_change)
# 月份選擇
self.month_choice = wx.Choice(panel, choices=[f"{m}月" for m in range(1, 13)])
self.month_choice.SetSelection(self.current_month - 1)
self.month_choice.Bind(wx.EVT_CHOICE, self.on_year_month_change)
wx.Choice控件特點(diǎn):
- 下拉選擇框,占用空間小
SetSelection()設(shè)置默認(rèn)選中項(xiàng)(索引從0開始)GetStringSelection()獲取當(dāng)前選中的文本Bind(wx.EVT_CHOICE, handler)綁定選擇變化事件
三、自繪日歷實(shí)現(xiàn)(核心技術(shù))
3.1 創(chuàng)建日歷面板
# 創(chuàng)建自定義日歷 self.calendar_panel = wx.Panel(left_panel, size=(400, 400)) self.calendar_panel.SetBackgroundColour(wx.Colour(255, 255, 255)) self.calendar_panel.Bind(wx.EVT_PAINT, self.on_paint_calendar) self.calendar_panel.Bind(wx.EVT_LEFT_DOWN, self.on_calendar_click)
關(guān)鍵點(diǎn):
- 使用
wx.Panel作為畫布 - 綁定
wx.EVT_PAINT事件進(jìn)行繪制 - 綁定
wx.EVT_LEFT_DOWN處理點(diǎn)擊事件 - 設(shè)置白色背景色
3.2 繪制日歷(核心算法)
def on_paint_calendar(self, event):
dc = wx.PaintDC(self.calendar_panel)
dc.Clear()
width, height = self.calendar_panel.GetSize()
# 1. 繪制標(biāo)題
dc.SetFont(wx.Font(16, wx.FONTFAMILY_DEFAULT,
wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD))
title = f"{self.current_year}年{self.current_month}月"
tw, th = dc.GetTextExtent(title)
dc.DrawText(title, (width - tw) // 2, 10)
# 2. 獲取日歷數(shù)據(jù)
cal = calendar.monthcalendar(self.current_year, self.current_month)
# 3. 計(jì)算單元格大小
start_y = 50
cell_width = width // 7
cell_height = (height - start_y) // (len(cal) + 1)
# 4. 繪制星期標(biāo)題
dc.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT,
wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD))
weekdays = ['一', '二', '三', '四', '五', '六', '日']
for i, day in enumerate(weekdays):
x = i * cell_width + cell_width // 2
tw, th = dc.GetTextExtent(day)
dc.DrawText(day, x - tw // 2, start_y)
# 5. 繪制日期單元格
dc.SetFont(wx.Font(12, wx.FONTFAMILY_DEFAULT,
wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL))
for week_idx, week in enumerate(cal):
for day_idx, day in enumerate(week):
if day != 0:
x = day_idx * cell_width
y = start_y + 30 + week_idx * cell_height
# 繪制單元格(見下節(jié)詳解)
self.draw_calendar_cell(dc, x, y, cell_width,
cell_height, day)
wx.PaintDC 核心方法:
Clear(): 清空畫布SetFont(): 設(shè)置字體SetBrush(): 設(shè)置填充畫刷SetPen(): 設(shè)置邊框畫筆DrawText(): 繪制文本DrawRectangle(): 繪制矩形DrawCircle(): 繪制圓形GetTextExtent(): 獲取文本尺寸
3.3 單元格樣式繪制
def draw_calendar_cell(self, dc, x, y, cell_width, cell_height, day):
# 檢查是否有記事
date_str = f"{self.current_year}-{self.current_month:02d}-{day:02d}"
has_events = (date_str in self.diary_data and
any(self.diary_data[date_str].values()))
# 檢查是否是選中的日期
is_selected = (self.selected_date and
self.selected_date == date_str)
# 根據(jù)狀態(tài)設(shè)置不同樣式
if is_selected:
# 選中:藍(lán)色背景 + 藍(lán)色粗邊框
dc.SetBrush(wx.Brush(wx.Colour(100, 149, 237)))
dc.SetPen(wx.Pen(wx.Colour(0, 0, 255), 2))
elif has_events:
# 有記事:黃色背景 + 橙色粗邊框
dc.SetBrush(wx.Brush(wx.Colour(255, 250, 205)))
dc.SetPen(wx.Pen(wx.Colour(255, 165, 0), 2))
else:
# 普通:白色背景 + 灰色細(xì)邊框
dc.SetBrush(wx.Brush(wx.Colour(255, 255, 255)))
dc.SetPen(wx.Pen(wx.Colour(200, 200, 200), 1))
# 繪制矩形
dc.DrawRectangle(x, y, cell_width - 2, cell_height - 2)
# 繪制日期數(shù)字
day_str = str(day)
tw, th = dc.GetTextExtent(day_str)
if is_selected:
dc.SetTextForeground(wx.Colour(255, 255, 255)) # 白色文字
else:
dc.SetTextForeground(wx.Colour(0, 0, 0)) # 黑色文字
dc.DrawText(day_str, x + 5, y + 5)
# 如果有記事,顯示紅色小圓點(diǎn)標(biāo)記
if has_events and not is_selected:
dc.SetBrush(wx.Brush(wx.Colour(255, 0, 0)))
dc.DrawCircle(x + cell_width - 10, y + 10, 3)
顏色設(shè)計(jì)理念:
- 選中狀態(tài):藍(lán)色(Cornflower Blue)突出當(dāng)前操作
- 有記事:黃色(Light Goldenrod Yellow)醒目提醒
- 普通日期:白色(White)干凈簡(jiǎn)潔
- 記事標(biāo)記:紅色小圓點(diǎn)(Red Dot)快速識(shí)別
3.4 處理點(diǎn)擊事件
def on_calendar_click(self, event):
width, height = self.calendar_panel.GetSize()
x, y = event.GetPosition()
# 獲取日歷數(shù)據(jù)
cal = calendar.monthcalendar(self.current_year, self.current_month)
# 計(jì)算單元格尺寸(與繪制時(shí)一致)
start_y = 80
cell_width = width // 7
cell_height = (height - 80) // (len(cal) + 1)
if y < start_y:
return # 點(diǎn)擊在標(biāo)題區(qū)域,忽略
# 計(jì)算點(diǎn)擊位置對(duì)應(yīng)的周索引和天索引
week_idx = (y - start_y) // cell_height
day_idx = x // cell_width
# 驗(yàn)證索引有效性
if 0 <= week_idx < len(cal) and 0 <= day_idx < 7:
day = cal[week_idx][day_idx]
if day != 0: # 0表示非當(dāng)月日期
# 構(gòu)造日期字符串
self.selected_date = f"{self.current_year}-{self.current_month:02d}-{day:02d}"
self.date_label.SetLabel(f"日期: {self.selected_date}")
# 加載該日期的數(shù)據(jù)
if self.selected_date in self.diary_data:
data = self.diary_data[self.selected_date]
self.morning_text.SetValue(data.get("morning", ""))
self.noon_text.SetValue(data.get("noon", ""))
self.evening_text.SetValue(data.get("evening", ""))
else:
self.morning_text.SetValue("")
self.noon_text.SetValue("")
self.evening_text.SetValue("")
# 重繪日歷以顯示選中狀態(tài)
self.calendar_panel.Refresh()
坐標(biāo)計(jì)算原理:
點(diǎn)擊坐標(biāo) (x, y)
↓
week_idx = (y - start_y) // cell_height # 第幾周
day_idx = x // cell_width # 星期幾
↓
day = cal[week_idx][day_idx] # 獲取日期數(shù)字
3.5 年月切換處理
def on_year_month_change(self, event):
# 從下拉框獲取新的年月
self.current_year = int(self.year_choice.GetStringSelection())
self.current_month = self.month_choice.GetSelection() + 1
# 觸發(fā)重繪
self.calendar_panel.Refresh()
Refresh() 方法說明:
- 觸發(fā)
wx.EVT_PAINT事件 - 自動(dòng)調(diào)用
on_paint_calendar()方法 - 實(shí)現(xiàn)日歷內(nèi)容更新
四、核心功能實(shí)現(xiàn)
4.1 數(shù)據(jù)保存功能
def on_save(self, event):
if not self.selected_date:
wx.MessageBox("請(qǐng)先選擇日期", "提示", wx.OK | wx.ICON_WARNING)
return
# 保存到字典
self.diary_data[self.selected_date] = {
"morning": self.morning_text.GetValue(),
"noon": self.noon_text.GetValue(),
"evening": self.evening_text.GetValue()
}
# 持久化到文件
self.save_data()
# 重繪日歷(顯示紅點(diǎn)標(biāo)記)
self.calendar_panel.Refresh()
wx.MessageBox("保存成功!", "提示", wx.OK | wx.ICON_INFORMATION)
4.2 數(shù)據(jù)持久化
def load_data(self):
if os.path.exists(self.data_file):
with open(self.data_file, 'r', encoding='utf-8') as f:
return json.load(f)
return {}
def save_data(self):
with open(self.data_file, 'w', encoding='utf-8') as f:
json.dump(self.diary_data, f, ensure_ascii=False, indent=2)
JSON參數(shù)解析:
ensure_ascii=False: 保存中文而非Unicode轉(zhuǎn)義indent=2: 格式化輸出,縮進(jìn)2個(gè)空格
五、PDF導(dǎo)出功能
5.1 PDF生成基礎(chǔ)
def export_to_pdf(self, filename, year, month):
c = pdf_canvas.Canvas(filename, pagesize=A4)
width, height = A4
# 嘗試注冊(cè)中文字體
try:
pdfmetrics.registerFont(TTFont('SimSun', 'simsun.ttc'))
font_name = 'SimSun'
except:
font_name = 'Helvetica'
# 標(biāo)題
c.setFont(font_name, 20)
title = f"{year} Year {month} Month Calendar"
c.drawCentredString(width / 2, height - 50, title)
ReportLab核心概念:
Canvas: PDF畫布對(duì)象pagesize: 頁(yè)面尺寸(A4、Letter等)- 字體注冊(cè):支持中文需要TrueType字體
5.2 繪制PDF日歷網(wǎng)格
# 獲取日歷
cal = calendar.monthcalendar(year, month)
# 繪制日歷網(wǎng)格
start_x = 50
start_y = height - 100
cell_width = (width - 100) / 7
cell_height = 80
# 星期標(biāo)題
c.setFont(font_name, 12)
weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
for i, day in enumerate(weekdays):
c.drawCentredString(start_x + i * cell_width + cell_width / 2,
start_y, day)
start_y -= 20
# 繪制日期和記事
c.setFont(font_name, 10)
for week_idx, week in enumerate(cal):
for day_idx, day in enumerate(week):
if day != 0:
x = start_x + day_idx * cell_width
y = start_y - week_idx * cell_height
# 繪制邊框
c.rect(x, y - cell_height, cell_width, cell_height)
# 繪制日期
c.setFont(font_name, 14)
c.drawString(x + 5, y - 20, str(day))
# 獲取當(dāng)天記事并繪制
date_str = f"{year}-{month:02d}-{day:02d}"
if date_str in self.diary_data:
data = self.diary_data[date_str]
c.setFont(font_name, 8)
y_offset = 35
if data.get("morning"):
text = data["morning"][:20] + "..." if len(data["morning"]) > 20 else data["morning"]
c.drawString(x + 5, y - y_offset, f"M: {text}")
y_offset += 12
if data.get("noon"):
text = data["noon"][:20] + "..." if len(data["noon"]) > 20 else data["noon"]
c.drawString(x + 5, y - y_offset, f"N: {text}")
y_offset += 12
if data.get("evening"):
text = data["evening"][:20] + "..." if len(data["evening"]) > 20 else data["evening"]
c.drawString(x + 5, y - y_offset, f"E: {text}")
c.save()
ReportLab坐標(biāo)系統(tǒng):
- 原點(diǎn)(0,0)在左下角
- Y軸向上增長(zhǎng)
- 單位是點(diǎn)(point),1英寸=72點(diǎn)
六、預(yù)覽功能實(shí)現(xiàn)
6.1 預(yù)覽窗口架構(gòu)
class PreviewFrame(wx.Frame):
def __init__(self, parent, year, month, diary_data, background_image):
super().__init__(parent, title=f"{year}年{month}月日歷預(yù)覽",
size=(1000, 800))
# 創(chuàng)建日歷圖像
img = self.create_calendar_image()
# 轉(zhuǎn)換為wx.Image
wx_img = wx.Image(io.BytesIO(img), wx.BITMAP_TYPE_PNG)
bitmap = wx.Bitmap(wx_img)
# 顯示
img_ctrl = wx.StaticBitmap(panel, bitmap=bitmap)
圖像處理流程:
PIL創(chuàng)建圖像 → 2. 保存到內(nèi)存(BytesIO) → 3. 轉(zhuǎn)換為wx.Image → 4. 轉(zhuǎn)換為wx.Bitmap → 5. 顯示
6.2 使用PIL繪制日歷
def create_calendar_image(self):
img_width, img_height = 1400, 1000
# 處理背景圖片
if self.background_image and os.path.exists(self.background_image):
img = Image.open(self.background_image).convert('RGBA')
img = img.resize((img_width, img_height))
overlay = Image.new('RGBA', img.size, (255, 255, 255, 180))
img = Image.alpha_composite(img, overlay)
else:
img = Image.new('RGB', (img_width, img_height),
color=(240, 248, 255))
draw = ImageDraw.Draw(img)
PIL圖像模式:
RGB: 紅綠藍(lán)三通道RGBA: 紅綠藍(lán)+Alpha透明通道alpha_composite(): 混合兩個(gè)RGBA圖像
6.3 字體處理
try:
title_font = ImageFont.truetype("msyh.ttc", 48)
date_font = ImageFont.truetype("msyh.ttc", 24)
text_font = ImageFont.truetype("msyh.ttc", 16)
except:
title_font = ImageFont.load_default()
date_font = ImageFont.load_default()
text_font = ImageFont.load_default()
常見中文字體文件:
- Windows:
msyh.ttc(微軟雅黑),simsun.ttc(宋體) - macOS:
PingFang.ttc(蘋方) - Linux:
WenQuanYi*.ttf(文泉驛)
6.4 繪制美觀日歷
# 標(biāo)題
title = f"{self.year}年{self.month}月"
bbox = draw.textbbox((0, 0), title, font=title_font)
title_width = bbox[2] - bbox[0]
draw.text((img_width // 2 - title_width // 2, 30), title,
fill=(50, 50, 150), font=title_font)
# 獲取日歷
cal = calendar.monthcalendar(self.year, self.month)
# 繪制日歷網(wǎng)格
start_x = 50
start_y = 120
cell_width = (img_width - 100) // 7
cell_height = (img_height - 200) // len(cal)
# 星期標(biāo)題
weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
for i, day in enumerate(weekdays):
x = start_x + i * cell_width + cell_width // 2
bbox = draw.textbbox((0, 0), day, font=date_font)
text_width = bbox[2] - bbox[0]
draw.text((x - text_width // 2, start_y), day,
fill=(100, 100, 100), font=date_font)
start_y += 50
# 繪制日期單元格
for week_idx, week in enumerate(cal):
for day_idx, day in enumerate(week):
if day != 0:
x = start_x + day_idx * cell_width
y = start_y + week_idx * cell_height
# 判斷是否有記事
date_str = f"{self.year}-{self.month:02d}-{day:02d}"
has_events = (date_str in self.diary_data and
any(self.diary_data[date_str].values()))
# 不同樣式繪制
if has_events:
draw.rectangle([x, y, x + cell_width - 5, y + cell_height - 5],
fill=(255, 250, 205), # 淺黃色填充
outline=(255, 165, 0), # 橙色邊框
width=2)
else:
draw.rectangle([x, y, x + cell_width - 5, y + cell_height - 5],
outline=(200, 200, 200),
width=1)
# 繪制日期
draw.text((x + 10, y + 10), str(day), fill=(0, 0, 0), font=date_font)
# 顯示記事預(yù)覽
if has_events:
data = self.diary_data[date_str]
y_offset = 45
if data.get("morning"):
text = "??" + (data["morning"][:8] + "..."
if len(data["morning"]) > 8
else data["morning"])
draw.text((x + 10, y + y_offset), text,
fill=(255, 100, 0), font=text_font)
y_offset += 25
if data.get("noon"):
text = "??" + (data["noon"][:8] + "..."
if len(data["noon"]) > 8
else data["noon"])
draw.text((x + 10, y + y_offset), text,
fill=(255, 165, 0), font=text_font)
y_offset += 25
if data.get("evening"):
text = "??" + (data["evening"][:8] + "..."
if len(data["evening"]) > 8
else data["evening"])
draw.text((x + 10, y + y_offset), text,
fill=(0, 0, 255), font=text_font)
# 保存到內(nèi)存
buffer = io.BytesIO()
img.save(buffer, format='PNG')
buffer.seek(0)
return buffer.read()
效果圖


以上就是基于Python開發(fā)日歷記事本的完整教程的詳細(xì)內(nèi)容,更多關(guān)于Python日歷記事本的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Python+wxPython實(shí)現(xiàn)合并多個(gè)文本文件
在?Python?編程中,我們經(jīng)常需有時(shí)候,我們可能需要將多個(gè)文本文件合并成一個(gè)文件,要處理文本文件,本文就來介紹下如何使用?wxPython?模塊編寫一個(gè)簡(jiǎn)單的程序,能夠讓用戶選擇多個(gè)文本文件,感興趣的可以了解下2023-08-08
利用Python實(shí)現(xiàn)網(wǎng)絡(luò)測(cè)試的腳本分享
這篇文章主要給大家介紹了關(guān)于利用Python實(shí)現(xiàn)網(wǎng)絡(luò)測(cè)試的方法,文中給出了詳細(xì)的示例代碼供大家參考學(xué)習(xí),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起看看吧。2017-05-05
Python?VisPy庫(kù)高性能科學(xué)可視化圖形處理用法實(shí)例探究
VisPy是一個(gè)用于高性能科學(xué)可視化的Python庫(kù),它建立在現(xiàn)代圖形處理單元(GPU)上,旨在提供流暢、交互式的數(shù)據(jù)可視化體驗(yàn),本文將深入探討VisPy的基本概念、核心特性以及實(shí)際應(yīng)用場(chǎng)景,并通過豐富的示例代碼演示其強(qiáng)大的可視化能力2023-12-12
python?aeon庫(kù)進(jìn)行時(shí)間序列算法預(yù)測(cè)分類實(shí)例探索
這篇文章主要介紹了python?aeon庫(kù)進(jìn)行時(shí)間序列算法預(yù)測(cè)分類實(shí)例探索,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-02-02
python?使用?with?open()?as?讀寫文件的操作方法
這篇文章主要介紹了python?使用?with?open()as?讀寫文件的操作代碼,寫文件和讀文件是一樣的,唯一區(qū)別是調(diào)用open()函數(shù)時(shí),傳入標(biāo)識(shí)符'w'或者'wb'表示寫文本文件或?qū)懚M(jìn)制文件,需要的朋友可以參考下2022-11-11

