Qt安装包里藏着不少宝贝,其中最容易被低估的,就是官方自带的示例程序。今天想聊聊一个看着不起眼、实际上很耐琢磨的小例子:Qt 5.14.2自带Examples里的Calendar Widget。很多刚开始接触Qt的朋友,照着教程学会了QLabel、QPushButton、QLineEdit,就觉得自己入门了。但真到要用日历、日期选择这类功能时,往往会卡住——要么不知道有个现成的QCalendarWidget能用,要么稀里糊涂拖了个控件进界面,发现样式、交互、格式化一堆问题不知道怎么处理。
这个示例刚好好就好在:它用最传统的方式,把一个日历控件应该具备的能力完整地展示了出来。没有花哨的架构,没有繁琐的组件化封装,就是老老实实地摆控件、连信号、处理逻辑。你把这个Demo跑起来,玩明白,再自己动手改造一遍,就能摸清Qt桌面开发里数据绑定、界面刷新、信号槽通信这些基本功的门道。这篇就顺着我实际调试这个示例的路径,把里面值得看的代码、值得踩的坑、值得扩展的思路全部梳理一遍,给打算拿Qt做工具类软件、桌面管理应用,或者想深入研究控件自绘的朋友做个参考。
1. 先让示例跑起来:环境准备与实际运行
1.1 找到示例项目与打开方式
Qt的Examples目录在安装时是跟着组件一起走的,我用的5.14.2默认装在了安装盘的根目录下,路径大约是Qt/Qt5.14.2/Examples/Qt-5.14.2/widgets/widgets/calendarlwidget。你如果装的时候没改过路径,基本都能在这个结构里找到。需要注意区分:widgets这个大分类下,还有一个叫calendarwidget(不带l)的老版本例子,你们要注意区分,前者才是我们要看的那个带农历显示、多面板切换的Calendar Widget。
打开项目文件很简单,找到后缀为.pro的文件,直接用Qt Creator打开就行。但如果你的电脑上同时装了多个版本的Qt,比如老项目要5.9,新项目要5.15,这里就要特别小心——尽量把整个示例目录拷贝一份到你的工作目录里再打开,避免直接在安装目录里改,否则编译的时候不小心点错套件,会在源码安装目录里留下一堆临时文件,以后想卸载重装都麻烦。
提示:我的习惯是建一个专门放示例的文件夹,比如D:/MyQtSamples,把官方Examples里需要研究的项目整个复制过去再打开。这样做还有个额外好处:官方目录是只读的倾向,复制到自己地盘后,想加调试输出、改代码都不受权限限制,也不会污染原装文件。
1.2 编译运行的全过程与常见环境坑
这个示例用到的模块并不复杂,核心就是widgets和gui,所以编译成功率和环境关系非常大。我自己用5.14.2自带的MinGW 32位版本编译,打开后直接点左下角的绿色三角按钮,大概十几秒就能跑起来,界面是一个比较简洁的日历窗口,左侧是主日历网格,右侧列了一排开发和演示用的控制选项。
如果遇到编译报错,最典型的场景是Kit选择不当。5.14.2安装时会让你勾选编译器,常见的有MSVC 2017 64bit和MinGW 7.3.0 32bit两种。MinGW版对应的是QMake生成的Makefile,编译时如果报找不到g++,多半是环境变量里没有加入MinGW的bin目录。这时候不要急着重装Qt,打开Qt安装目录下的Tools/***/bin目录,确认一下g++.exe是否还在,然后把路径加到系统Path环境变量,重启Qt Creator基本就能解决。
还有一类报错是找不到头文件,比如提示QCalendarWidget: No such file or directory。这基本可以断定是pro文件里少了QT += widgets这一行。这属于典型的工程配置问题,不是环境坏了。检查.pro文件时,我建议把guimodule也留着,因为实例里用到了QIcon、QPalette这些界面相关的类,没有gui模块,加载的时候会报很多继承链上的符号错误。这个示例自己带的pro文件已经把模块写好了,所以一般不会遇到,但如果你们自己新建工程参考这段代码,这个坑是逃不掉的。
跑起来之后,别急着写代码,先花半小时把这个Demo的所有控件点一遍。它已经把QCalendarWidget的绝大多数API暴露成了右侧的可操作选项,怎么用、怎么改属性,直接界面点点点就能直观看到效果。这种“以Demo为说明书”的学习方式,比翻一整本API文档高效得多。
2. 拆开看看:Calendar Widget的界面布局与交互逻辑
2.1 界面构成与主要控件
一打开Calendar Widget示例,映入眼帘的左右分栏结构,实际上就是Qt里很常见的QSplitter布局。左侧是一个常规的QCalendarWidget,右侧是一个承载各类控制控件的QWidget容器。这种结构设计得很典型——左侧是业务展示区,右侧是参数控制区,任何想在运行时实时调整控件外观特性的Demo,都几乎会用这么一套布局。看懂了它,以后你做图像处理工具的参数调节面板、报表系统的筛选区时,也能直接复用这套设计。
在主日历上,你能看到月份标题栏、周几表头、日期网格这三大部分。月份标题栏左侧右侧各有一个箭头按钮,中间显示当前年月;点一下箭头,页面会整体翻到上个月或下个月。如果你再仔细点看中间那个年月文字,会发现它不是死文本——整个区域本质是一个可交互的导航栏,点击左右箭头或者切换到特定角度拖动时,会联动刷新底部网格的日期分布。
再看右侧的控制区,从上到下挤着好几个控件:一组单选框用来切换选择模式,三个下拉列表分别控制水平表头格式、垂直表头格式和日期文本格式,两个复选框控制是否显示格网和是否启用导航栏,最底下还有一个文本编辑框用来输出当前选中日期的具体信息。理解每个控件的功能后,你会发现它其实就是把QCalendarWidget的公开属性面板化——每改一个属性,左侧日历立刻响应。这个“属性-控件”一一对应的设计,强烈建议新手完整过一遍,能极大缩短你对陌生控件从陌生到上手的时间。
2.2 交互背后的数据链路
这个示例最能体现Qt信号槽机制的地方,就在于右侧那堆控制项和左侧日历之间的联动关系。拿“水平表头格式”这个下拉框来说,它的选项有三个:无表头、短格式(比如显示“周一”)、长格式(比如显示“星期一”)。这个下拉框在切换选项的时候,触发的是currentIndexChanged信号,而槽函数里调用的就是calendarWidget->setHorizontalHeaderFormat()。这里要注意:下拉框选项的顺序必须和API枚举的索引一一对应——如果对应错了,就会出现你选了短格式,界面却变成无表头的奇怪现象。
我调试时发现,示例里还藏着一个比较隐蔽的数据同步逻辑:当你在日历上点击选中某一天,QCalendarWidget会发出selectionChanged信号,右侧文本编辑框里显示的日期就会跟着变化。这个逻辑之所以值得单独拿出来说,是因为它不是用connect连接的一个简单lambda表达式,而是先连接了selectionChanged到某个更新函数上,再由那个函数同时完成文本拼接和setPlainText。这种“一次信号,多个动作”的处理方式,非常符合日常开发中“修改数据后统一刷新界面”的需求。新手总喜欢在每个控件里直接调用UI的setText来更新界面,但示例里这段代码反映出的思路是:把界面刷新收敛到一个函数里,数据变化走统一通道,对后期维护极其友好。
另外一个值得琢磨的交互细节是“最小日期”和“最大日期”两个控制项,虽然在默认界面里有可能被放在第二页或者靠下的位置,但它的存在其实是QCalendarWidget非常实用的功能之一。比如做预约系统时,你希望用户只能在未来一个月内选日期,就可以通过这两个接口限制可选范围;如果设置最小日期晚于最大日期,布尔型返回值会告诉你设置失败,这在开发时也是很容易踩的边界问题。
2.3 从示例里学到的控件布局经验
这个示例的布局并不复杂,但很多细节能做到“看起来清晰舒服”,全靠两个点:一是统一了控件之间的间距,二是对按钮和下拉框使用了合理的最小宽度。我们在自己写代码时,最怕的就是界面堆在一起,按钮挤按钮。这里右侧面板的核心是使用了QVBoxLayout来做垂直排列,再用QGroupBox对相关功能做了分组,视觉上就能很自然地划分开功能区。QGroupBox带一个边框和标题栏,老手喜欢用,新手往往嫌它占地方。但这个场景下,我不建议大家为了好看而删掉分组框,因为它能在视觉上降低右侧参数面板的“扁平感”,让用户一眼扫过去就能看出哪些控件管的是同一类事。
如果你想要进一步调整间距,可以在Qt Designer里双击示例界面,或者直接看源代码里的addWidget和setSpacing数值。把间距从默认的6改成8到10左右,观感会舒服很多。我是强烈建议大家在跑这个Demo时,顺便把右侧那一列QGroupBox的宽度和内部控件的排列方式截图下来,之后做任何工具软件时都能拿出来参考。
3. 背后原理:看懂QCalendarWidget的核心机制
3.1 从日期计算到网格绘制
QCalendarWidget看起来只是一个控件,但去掉所有外观装饰后,它的本质是一个按星期排布的日期网格。要理解这个网格怎么生成,先要看懂它的内部运算逻辑。选定任意一个月份,第一步是得到这个月第一天是星期几,第二步要知道这个月一共有多少天,然后按照从第0列到第6列(对应周日到周六)的位置,把1号填进对应的格子,接着逐天往后填充。这个逻辑听起来简单,但真正实现时有两个关键点:
第一个是每周起始日的计算。QCalendarWidget内部会依赖一个日期时间库,把“年月日”转换为“星期几”时,底层算法涉及日历的推算规则,尤其要考虑闰年、月份天数不同等因素。QCalendarWidget自己就封装了这套机制,你不需要自己写模运算。第二个是跨月填充。当1号不是从第一列开始时,1号前方的空白区域实际上是上个月的尾巴日期;当本月最后一天没排满一周时,末尾空白区域又放下个月的月初日期。很多自定义日历控件在做这块时都会做出两种风格:一种是把空白区域留白,另一种是把相邻月份的日期用灰色调显示出来。官方示例默认会把跨月日期正常显示,但你可以通过自定义绘制函数paintCell去改变它的表现——如果你以后要做一个包含农历信息的日历控件,就必须在这里动手脚。
多提一句,QCalendarWidget还有一个容易被忽略的刷新增量:当它检测到年份或月份变化时,会直接触发内部网格的重新计算。它内部维护了显示日期区间、当前选中日期、显示月份这三个状态。你在代码里调用setCurrentPage(int year, int month),它会立刻把网格内所有日期文字更新;但如果只调用setSelectedDate,而当前页和选中日期不在同一页时,它不会自动翻页,只会发信号。这个区别很多人容易搞混,以为选中一个日期后日历就应该自动跳到那个月去。实际不会的,你自己开发日历应用时要根据业务需求决定,用户选的日期跨月份时,需不需要setCurrentPage联动切换。
3.2 日期文本格式与本地化
Calendar Widget示例里有一个下拉框,专门用来切日期文本格式。它实际映射的是QCalendarWidget::setDateTextFormat方法,而日期显示还会有另一个维度,那就是Qt::DateFormat枚举,比如Qt::TextDate会把日期显示成“2025年2月14日”的长文本,Qt::ISODate则是“2025-02-14”这种机器友好的格式。很多朋友在做国际化软件时会在日期显示上踩坑——明明系统是中文环境,控件右下方文本却显示英文格式。这多半是因为程序里没设置QLocale,或者用了默认的C locale。
具体到QCalendarWidget,它的表头显示和日期网格中“周一”“二月”这类文字,是可以跟随系统语言环境自动切换的。但前提是你在main函数里得做两件事:第一,通过QTranslator加载对应的qt_zh_CN.qm翻译文件;第二,设置QLocale::setDefault(QLocale(QLocale::Chinese, QLocale::China))。如果你做的是跨平台软件,还要注意Windows和Linux的本地化目录结构有差异,WIndows上qm文件一般在Qt安装目录的translations文件夹里,而Linux发行版有的放在/usr/share/qt5/translations。示例自带的Demo没做这个处理,因为演示程序可以默认英文,但我们自己开发应用时要记得补上。
再延伸一个细节:QCalendarWidget的日期文本格式还可以区分“上旬/中旬/下旬”的前景、背景色。示例里有一块代码演示了怎么用QTextCharFormat给日期设置不同的前景色、背景色,甚至加粗。这个特性不单单是“好看”,排班表、打卡记录、股票K线日历标记都用得上。比如你在一个考勤系统里,把周末的日期的背景色设为浅灰色,把旷工的日期设成浅红色,用户一眼就能看出来这个月哪几天有问题。这个能力背后的原理是:QCalendarWidget在绘制每个日期格子时,会去查一张“日期格式映射表”,如果当前日期在表里,就用指定格式去绘制,否则就用默认格式。所以你要做的就是往这张表里填数据:QMap,key是QDate,value是QTextCharFormat。需要注意QMap的键值排序特性,当你批量标记一个月的大量日期时,最好使用QHash或者按时间顺序插入,虽然QMap也能用,但使用QHash的效率会稍微好一些。
3.3 选择模式与日期范围限制
示例里右侧的单选框组,分别对应三个选择模式:单选、无选择、多选。实际开发中单选用得最多,无选择适合只做展示不允许交互的日历视图,多选模式适合需要一次圈定多个日期范围的场景,比如酒店预订要选择连续多个夜晚,或者报表系统要选择多个统计日期。
日期范围限制是QCalendarWidget非常典型的业务能力,它对应setMinimumDate和setMaximumDate两个接口。如果你想做“只能选今天之前”或“只能选今天之后”的效果,用这两个接口最简单。但要注意,最小和最大日期之间包含首尾两端,判断逻辑是闭区间。换言之:如果你设了minimumDate为今天,那么今天也是可以被选中的;如果希望只能选明天以后,最小值应该改成QDate::currentDate().addDays(1)。这种边界条件在需求评审时就必须跟产品讲清楚,否则就会出现“昨晚我明明能选今天,上线后选不了了”这种看似奇怪的现象。
配合日期范围的还有内部日历网格翻页边界的控制:如果你设置了最小值,网格的翻页不会阻止用户翻到比最小值更早的月份,只是那几个月里的日期全部不可选中。对某些要求严格的业务场景,比如不能开放查询系统运行日志之前的日期,这种表现可能不够理想,这时需要自行监听页面变更事件,再根据当前页与范围限制的关系,决定是否把页面强制拉回合法范围内。
4. 动手改造:围绕示例做三个可落地的小项目
看示例、跑示例,最终目的还是为了内化成自己的开发能力。我建议每个读过这篇的朋友,做完上面的基础操作后,再花半天时间给它做三个方向的改造。一个方向是完善选择时的反馈,一个方向是视觉层面的定制,第三是业务层面的存储联动。这三个方向做完,你对QCalendarWidget的掌握程度会有本质的提升。
4.1 改造一:做一个自定义日期选择器
示例自带的是一个单日历面板,但实际办公系统里经常需要带“开始日期”和“结束日期”的双日历选择。用QCalendarWidget来做这套并不复杂:把两个日历控件水平排开,左侧选开始日期,右侧选结束日期,再在两者之间画一个箭头表示区间。难点在于处理两个日历之间的逻辑约束:结束日期不能早于开始日期。我的做法是,在左侧日历的selectionChanged信号里,判断当前选中日期是否晚于右侧的当前值,如果是则忽略这次选择,或者在选择后自动把右侧日期同步成同一天。同时把左侧日历的maximumDate临时设置为右侧日历当前选中日期的前一天,这样用户就算想点后面的日期也点不到了。
// 伪代码示意:左侧日历不能选择晚于右侧日期的天数 connect(calendarLeft, &QCalendarWidget::selectionChanged, this, [this]() { QDate leftDate = calendarLeft->selectedDate(); QDate rightDate = calendarRight->selectedDate(); if (leftDate > rightDate) { calendarLeft->setSelectedDate(rightDate); return; } calendarRight->setMinimumDate(leftDate); });实际做的时候,要注意防抖:用户在左侧日历上快速连续点击,可能会触发多次selectionChanged,如果每次都去设置右侧日历的minimumDate,会引发一系列无谓的界面刷新。我的解决方案是加一个标志位,或者比较QDate对象是否变化了再决定要不要往下走。这个小改造做完,你对“跨控件数据联动”和“防止信号风暴”这两个概念就会特别有感觉。
4.2 改造二:给日期着色实现排班表
排班表和打卡记录最需要视觉化呈现。QCalendarWidget的setDateTextFormat就是为此设计的。操作步骤很直白:把员工的排班规则存到QMap里,然后编程遍历这个月的每一天,根据规则给对应的日期设置QTextCharFormat。比如白班是蓝色文本加白底,夜班是黄色文本加深色底,休息日直接置灰。
QMap<QDate, QTextCharFormat> formats; QTextCharFormat nightShiftFormat; nightShiftFormat.setBackground(QColor(230, 184, 138)); nightShiftFormat.setForeground(QColor(128, 0, 0)); nightShiftFormat.setFontWeight(QFont::Bold); formats.insert(date, nightShiftFormat); calendarWidget->setDateTextFormat(formats);这里有个坑要提醒:setDateTextFormat这个接口的作用不是累加式添加,而是覆盖式赋值。每次调用这个方法,内部日期格式映射表会整体替换成你传入的QMap。如果你今天传的是白班格式表,明天传的是夜班格式表,被覆盖掉的日期就会恢复成默认样式。所以正确做法是:每次刷新时,用当前月份所有需要特殊标注的日期重新构建一个完整的QMap,再一次性set进去。我做排班模块时踩过一次这个坑,排查了好久才发现前面设置的周末颜色莫名消失了,就是因为这个覆盖机制。
除了颜色,QCalendarWidget还允许隐藏垂直表头(显示周数的那一列),对排班表来说,这一列会占宽度,也容易让人误以为是工号,建议直接用setVerticalHeaderFormat(QCalendarWidget::NoVerticalHeader)干掉。
4.3 改造三:一个极简日历记事本
把QCalendarWidget和一款纯文本备忘录结合,是很经典的应用模式。核心需求是:用户选中某一天,可以在右侧文本框里写内容,写完自动保存;再次切换到某一天时,如果当天有记录,文本框中自动载入。实现思路如下:
- 用QHash存储“日期-内容”映射,QDate做键,QString做内容。
- 日历的selectionChanged信号触发读取函数,从哈希表里找出选中日期的内容,填充到QTextEdit。
- 在QTextEdit的textChanged信号,或者保存按钮的clicked信号里,把当前内容写回哈希表。
- 退出程序时,把哈希表用QJsonDocument序列化,写到本地JSON文件。
这个Demo写起来不超过150行代码,但里面已经覆盖了状态管理、界面刷新、本地持久化三个核心模块。如果你还想升级,可以把QTextEdit换成QListView,每选中一天就列出当天的多条记录,这就无限接近一个完整的日程管理软件雏形了。
注意:用QHash存日期时,建议统一用QDate对象本身做key,不要用toString("yyyy-MM-dd")的字符串。字符串做key在查询时有格式化开销,还会遇到时间格式不统一引发的隐性问题。QDate做key直接用,匹配稳、性能好,跨平台行为也一致。
5. 调试经验与常见问题排查实录
5.1 个性化样式失效怎么办
很多人从示例里看到了QCalendarWidget有改日期颜色的能力,就想着用QSS(Qt样式表)把所有日子统一改颜色。实际操作时往往会碰到:设置了QCalendarWidget QAbstractItemView { background-color: ... },结果只有边框变了,格子颜色没变。原因在于QCalendarWidget内部多级结构很复杂,日期网格本身是由一个Viewport和一个内部Model共同完成绘制的,用QSS去直接改背景色,动的只是最外层QAbstractScrollArea的视口背景,真正绘制日期文字的cell区域并不会被这个QSS影响。
在QCalendarWidget里设置样式,正确方案是优先用setDateTextFormat配合QTextCharFormat,它才是针对“单个日期格子”的视觉定制接口。想改整个控件大背景,可以用setGridVisible来显示网格线条,网格的样式由palette(调色板)控制。真到了需要完全重绘的地步,那就得走子类化路线,重写paintCell方法。这个方法的签名是:void paintCell(QPainter *painter, const QRect &rect, const QDate &date) const; 在里面你可以完全掌控日期格子的绘制逻辑,背景、边框、文字、甚至过期日期的置灰效果都由你自己画。示例里是没有展示这个高级玩法的,但作为爱好者和专业人士,值得去了解。
5.2 日期格式显示不对与本地化问题
如果你按示例代码自己新建工程,发现月份标题显示的是英文“February”,而不是“二月”,第一反应不要猜是系统语言问题,要看程序里有没有加载翻译文件。Qt自带的翻译文件路径,在Windows下通常在“Qt安装目录/translations/qt_zh_CN.qm”。加载的代码就三行:
QTranslator translator; translator.load(":/qt_zh_CN.qm"); app.installTranslator(&translator);值得注意的是,translator对象必须保持存活,如果定义在main函数内部并且作用域很小,可能加载动画一闪即失,而某些控件(尤其是已经实例化过的QCalendarWidget)不会主动重绘翻译后的文字。正确的做法是把translator定义在main函数作用域靠外的地方、或者作为全局变量/静态成员,然后重新设置一次QLocale才不会出现切换无效的诡异现象。我自己在Linux环境部署时遇到过:加载了qt_zh_CN.qm后周几仍是英文,最终排查发现是编译时没有把qm文件放进资源文件,程序运行时找不到当地文件。这种情况可以用绝对路径拼一个QLibraryInfo::location(QLibraryInfo::TranslationsPath)来定位,Windows和Linux上这个路径差异挺大。
5.3 日期越界与最小最大日期坑
一个很常见的需求是禁止用户选择今天之前的日期。新手通常这么写:
ui->calendarWidget->setMinimumDate(QDate::currentDate());这么写的结果是今天可以选中,但这引出一个问题:如果你是做“截止日期修改”的模块,今天这个日期本身就需要被锁定,那就得换成addDays(1)。还有,如果你动态调整minimumDate,比如用户选择了开始日期后就联动调整结束日期的最小值,务必注意调整完日期范围后,再主动调用一次setSelectedDate,把当前不合法选区移到一个合法位置上。否则,会发生这样的情况:当前选中日期已经早于新的minimumDate,控件会显示选中了某个不可用日期,内部状态和界面显示不一致,后续信号的触发逻辑会变得很怪。
另外,如果设置的最小日期晚于当前显示页面里的日期,QCalendarWidget并不会自动把页面翻到最小日期所在的月份。这个问题我之前在做一个内部借书系统时遇到,后来不得不手动调用setCurrentPage(minimumDate.year(), minimumDate.month()),这个行为官方文档其实写得很含糊,非常容易被忽略。
5.4 信号重复触发的排查套路
QCalendarWidget的信号问题也值得一提。selectionChanged和currentPageChanged都会在某些操作下连续触发多次。比如你调用setSelectedDate时,如果日期已经就是当前选中日期,那不会触发;但只要日期变化一次,必然触发一次selectionChanged。如果某个槽函数里又去调用了setMinimumDate或setMaximumDate,而这些方法内部又会经过选择状态的合法性检测,就可能再次触发selectionChanged。如果你在槽里做了弹窗或者日期重设,就很容易陷入递归套娃。
面对这种问题,我的排查惯例是:在槽函数第一行打印一行日志,记录触发值和当前控件内部状态。确认出现重复触发后,在槽外设一个成员变量m_updatingFlag,在关键操作前置true,槽函数入口判断这个标志,为真就直接return。这是一种防止重入的经典做法,写Qt程序经常会用到,不只是日历控件。
5.5 垂直表头与周数的显示
日历左侧的垂直表头,显示的是周数。默认情况下它会占据一部分宽度,不少开发者觉得这是多余的。设置setVerticalHeaderFormat(QCalendarWidget::NoVerticalHeader)能把宽度完全释放出来,但有人发现这样设置后,表头确实没了,整个网格的宽度也并没有变明显。原因在于QCalendarWidget内部布局中垂直表头仍然在占用最小空间,除非你同时处理viewport的宽度或者调整日历控件的最小宽度。实际上我的做法是把日历控件的minimumWidth设置成比原始尺寸大几十像素,让网格部分真正受益。如果你连表头空间都不想残留,可以试试把verticalHeaderFormat设为ISOWeekNumber,这样和周数文本同时显示,界面上看反而更整齐。
6. 写在最后:看完示例之后还能怎么练
这个标题下的项目本身不大,但强调举一反三之后,它对进阶的价值就非常高了。建议你按我前面说的三个改造方向,自己挑一个动手写一写。代码量不多,但足以把QCalendarWidget从“会用”变成“能用明白”。
按我个人的经验,最容易被忽略的不是API本身,而是它和业务逻辑结合时的各种边界情况——日期时间处理永远是最容易出隐藏BUG的领域。回想早期我在一个内部工具里用QCalendarWidget做截止日期选择,结果用户反馈说“选了28号以后日期就消失了”,排查了半天才发现是触发了日期范围限制和页面刷新之间的次序问题。后来我把所有跟日期相关的设置集中在了一个统一刷新函数里,这个BUG就再也没出现过。所以这里也特别想提醒读到这里的你:如果你在改造Calendar Widget的过程中遇到了日期跳跃、选中状态丢失、面板宽度塌缩这类的怪问题,优先往“日期范围变更后没有主动刷新当前页”这个方向去排查。
如果你跑通这个示例之后,还想往更深的方向探索,下一步可以试试做一个带农历显示的自绘日历,或者把日期网格替换成可拖拽事件块的任务面板。前者难度在于农历数据和paintCell绘制的配合,后者则需要在QCalendarWidget基础上自定义内部布局,这已经是另一个量级的技术挑战了。
最后分享一个我坚持了很久的总结习惯:每次看完Qt官方示例,都在工程目录里写一个README,记录这个示例能不能迁移到老版本、哪些API已经被标记为过时、哪些交互细节不符合实际业务习惯。这些笔记在几个月后回头翻看,往往比重新查一遍文档更有用。日历这个控件不复杂,但正是这类“不复杂”的组件,能帮你把Qt的布局系统、信号槽机制、本地化处理和自绘逻辑全部串起来,值得沉下心来消化。