继承 QWidget 是自定义控件的正确起点,需重写 paintEvent、sizeHint 等并设置 sizePolicy,避免 QPixmap 缓存陷阱,注意 Qt 6 高 DPI 坐标适配。

继承 QWidget 是最直接的起点
Qt 自定义控件不是靠“画布叠加”或“样式表魔改”,而是通过子类化 QWidget(或其子类如 QFrame、QLabel)并重写关键函数来实现。你控制的是整个控件的生命周期和渲染逻辑,不是在现有控件上贴图。
常见错误是试图只重写 paintEvent 却忽略尺寸策略和事件响应——结果控件不响应鼠标、不能缩放、在布局里塌陷成一条线。
- 必须重写
paintEvent:这是绘制入口,用QPainter在event->rect()范围内作画 - 建议重写
sizeHint和minimumSizeHint:否则布局管理器不知道该给你多大空间 - 如果需要交互,别忘了
mousePressEvent、enterEvent等,且需调用setMouseTracking(true)才能持续捕获移动 - 避免在
paintEvent里做耗时计算或分配内存——它可能每帧都调用
QPainter 绘制时别碰 QPixmap 缓存陷阱
很多人一上来就想双缓冲:先画到 QPixmap,再 drawPixmap 到窗口。这在高 DPI 或缩放界面下极易出错——QPixmap 不自动适配设备像素比(device pixel ratio),导致模糊、偏移或锯齿。
正确做法是直接在 QPainter 上绘制,Qt 会自动处理 DPR。只有当你有复杂静态背景(比如地图底图)且频繁重绘区域很小时,才考虑缓存,但要用 QImage + setDevicePixelRatio 显式设置。
立即学习“C++免费学习笔记(深入)”;
- 获取当前 DPR:
devicePixelRatioF()(Qt 5.6+)或qApp->devicePixelRatio() - 若真要缓存图像,创建
QImage后立刻调用image.setDevicePixelRatio(widget->devicePixelRatioF()) -
QPainter::renderHints建议至少开QPainter::Antialiasing和QPainter::TextAntialiasing
自定义控件嵌入布局后不显示?检查 setSizePolicy
控件编译通过、构造函数跑完、paintEvent 也进了,但加进 QVBoxLayout 或 QGridLayout 就消失——八成是尺寸策略没设。默认 QSizePolicy::Preferred 配合空的 sizeHint,会让布局器给它 0×0 的空间。
这不是 bug,是 Qt 布局系统的设计逻辑:它只相信你显式声明的尺寸意图。
- 简单控件(如圆形按钮):在构造函数里写
setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed),再实现sizeHint()返回固定QSize - 弹性控件(如自绘文本框):用
QSizePolicy::Expanding,sizeHint返回合理最小宽高,heightForWidth按需重写 - 千万别依赖父控件 resize 后手动
resize()子控件——布局器会立刻覆盖它
Qt 6 下 QPainter 的坐标系变化容易被忽略
Qt 6 默认启用高 DPI 缩放,QPainter 的逻辑坐标(你在代码里写的 x/y)和物理像素不再一一对应。你画一个宽 100 的矩形,实际可能占 200 像素(2x 缩放),但 width() 返回的仍是 100。
这本身不是问题,但如果你硬编码了像素级位置(比如“文字距左 12px”)、或用 QCursor::pos() 减去 mapToGlobal(QPoint(0,0)) 做点击判断,就可能错位。
- 所有涉及坐标的交互逻辑,统一用
mapFromGlobal()/mapToParent()转换,别手动算 - 字体大小用
pointSizeF,别用pixelSize(后者在高 DPI 下行为不可靠) - 调试时临时加一句
qDebug() ,比猜快得多
真正难的不是画出形状,而是让这个控件在不同缩放、不同父容器、不同主题色下,始终知道自己该占多大、点在哪、画多细。这些细节藏在 sizeHint、devicePixelRatioF 和坐标转换里,而不是 paintEvent 的第一行。
















