在工作中经常要根据 UI 提供的稿子做自绘控件,而且在新项目中,我自己基于 Qt 做了一套项目自用控件库,还会涉及到换肤,所以对 Qt 的控件绘制,着重的研究了一下。看过代码之后,觉得 Qt 项目本身,确实是做 UI 发家,绘制流程清晰,可以说是做界面的范本了。将流程梳理记录一下。
分类
对于自绘控件的分类,我将其分为两大类:
- 基于 Qt 控件类派生
- 基于 QWidget 派生
除非是行为跟 Qt 本身的控件相差太多,或者是缺少需要的交互逻辑,否则的话尽可能从 Qt 现有的控件类派生,这是一条基本的原则。因为绘制上来说重写 paintEvent
方法,基本就可以达到想要的目的,而控件类本身会提供一些现成的控件逻辑,这样会大大减少开发难度。
绘制流程
其实提起绘制流程来说,无非就是重写 paintEvent
方法。但是如果要做一整套 UI 库,没有结构,都在 paintEvent
里边写死,在后期加换肤,或者是在代码整洁度上都会大打折扣。 Qt 本身的控件绘制,就可以给我们很大的启示。以绘制事件方法代码最简单的 QPushbutton 为例,足见一斑:
void QPushButton::paintEvent(QPaintEvent *)
{
QStylePainter p(this);
QStyleOptionButton option;
initStyleOption(&option);
p.drawControl(QStyle::CE_PushButton, option);
}
可以简单理解为两个流程:
- 装配绘制必要上下文
- 绘制
Qt 考虑到对于按钮样式,开发者有着自己的需求,而对于逻辑状态可能开发者可以用现成的。所以 initStyleOption
是一个保护方法,如果开发者没有对按钮状态的特殊要求,用这个方法,就可以把图标,按钮的点击状态什么的放进这个 option
中。在绘制的时候直接拿来用。
而下边的绘制,可以看到这里是用了 QStylePainter
而不是常规的 QPainter
。不过 QStylePainter
和 QPainter
差别不大,前者只是在里边保存了当前 QWidget::style()
的指针。所以这段代码改成:
QPainter p(this);
QStyleOptionButton option;
initStyleOption(&option);
style()->drawControl(QStyle::CE_PushButton, option, &p, this);
也是一样的。如果你翻看其他控件,比方说 QMenu
就是下边这种用法,可能是开发人员的不同习惯,但是他们逻辑是一致的。
Qt 的绘制精髓就在于此。对于绘制, Qt 控件是交给 style 来管理的。打开源码路径 qtbasesrcwidgetsstyles 这里边保存了 Qt 绘制的基本样式。为什么说是基本样式呢,因为还有一部分 qtbasesrcpluginsstyles 在这个目录下,这所有的加一起就是全部样式了。话说回来, Qt 正是因为将绘制逻辑都保存在了 style 中,所以 Qt 才可以在不同平台都表现的像一个原生控件一样,“千平台千面”。
走到这里,就不得不一探 QStyle
的究竟了。QStyle
是一个抽象类,头文件因为包含了太多枚举,所以特别长,就不粘了。里边关于绘制的几个方法是纯虚的,在绘制方法中,通过传进去的枚举类型,来找到对应控件的绘制逻辑。除了绘制方法,还有一些是计算绘制区域大小的方法,以及 hitTest 获取子控件的方法。这就是整个 Qt 绘制的大本营了。而对于 Qt 控件的实际绘制逻辑,其实也对做自绘控件库也十分有帮助,不过既然是浅谈,也就不再罗嗦。