是否还记得在开始的时候我们说过:
- 给我一个 QPainter,我也能实现整个操作系统的图形界面
- 操作系统的界面本质也是**画(Hua)**出来的
- 图形界面的本质都是一样的,就是一张静态的画
- 点击按钮,看到按钮动了
这里我们就用 QPainter 绘图来模拟实现一个系统的界面原型,为了简单说明问题,只绘制了 Button 和 CheckBox,其他的控件同理。当然 Button 是能够点击的,点击 CheckBox 也能够切换选中状态,没有点击到 Button 和 CheckBox 的时候它们不会接收到鼠标事件,点击一个控件也不会影响另一个控件,效果如下图:
Widget 是所有控件的所有控件的父类,实现控件共有的逻辑,Button 和 CheckBox 是具体的控件,不同控件的行为和样式都不一样,都需要在自己的类中实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| #ifndef WIDGET_H #define WIDGET_H #include <QRect>
class QPainter;
class Widget { public: Widget(const QRect &boundingRect);
virtual void paint(QPainter *painter) = 0;
virtual void mouseMove(); virtual void mouseEnter(); virtual void mouseLeave(); virtual void mousePressed(); virtual void mouseReleased();
bool hover; bool pressed; QRect boundingRect; };
#endif
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include "Widget.h"
Widget::Widget(const QRect &boundingRect) : hover(false), pressed(false), boundingRect(boundingRect) { }
void Widget::mouseMove() {
}
void Widget::mouseEnter() { hover = true; }
void Widget::mouseLeave() { hover = false; }
void Widget::mousePressed() { pressed = true; }
void Widget::mouseReleased() { pressed = false; }
|
下面的代码只做出了鼠标移动到 Button 上,鼠标点击时的不同高亮效果,点击 Button 时没有发射 clicked() 事件。
如果需要实现点击时发射 clicked() 信号也容易,鼠标事件的时候把鼠标的坐标发送给 Button,在 mouseReleased() 事件发生时如果鼠标仍然在 Button 上,发射 clicked() 信号即可,和这个信号关联的槽函数就能被调用了,实现点击的事件处理。传递坐标的时候最好把 parent 的坐标映射为 child 的坐标,即 parent 的坐标减去 Button 在 parent 中左上角的坐标即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #ifndef BUTTON_H #define BUTTON_H
#include "Widget.h"
#include <QRect> #include <QColor> #include <QPainter>
class Button : public Widget { public: Button(const QString &text, const QRect &boundingRect = QRect(0, 0, 100, 25), const QColor &normalBackgroundColor = QColor(200, 200, 200), const QColor &hoverBackgroundColor = QColor(200, 0, 200), const QColor &pressedBackgroundColor = QColor(0, 200, 200));
void paint(QPainter *painter) Q_DECL_OVERRIDE;
QString text; QColor normalBackgroundColor; QColor hoverBackgroundColor; QColor pressedBackgroundColor; };
#endif
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include "Button.h"
Button::Button(const QString &text, const QRect &boundingRect, const QColor &normalBackgroundColor, const QColor &hoverBackgroundColor, const QColor &pressedBackgroundColor) : Widget(boundingRect), text(text), normalBackgroundColor(normalBackgroundColor), hoverBackgroundColor(hoverBackgroundColor), pressedBackgroundColor(pressedBackgroundColor) { }
void Button::paint(QPainter *painter) { QColor backgroundColor;
if (pressed) { backgroundColor = pressedBackgroundColor; } else if (hover) { backgroundColor = hoverBackgroundColor; } else { backgroundColor = normalBackgroundColor; }
painter->setBrush(QBrush(backgroundColor)); painter->drawRoundedRect(boundingRect, 2, 2); painter->drawText(boundingRect, Qt::AlignCenter, text); }
|
CheckBox
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #ifndef CHECKBOX_H #define CHECKBOX_H #include "Widget.h" #include <QString> #include <QRect>
class QPainter;
class CheckBox : public Widget { public: CheckBox(const QString &text, bool checked = true, const QRect &boundingRect = QRect(0, 0, 100, 25)); void paint(QPainter *painter) Q_DECL_OVERRIDE; void mousePressed() Q_DECL_OVERRIDE;
bool checked; QString text; };
#endif
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #include "CheckBox.h" #include <QPainter>
CheckBox::CheckBox(const QString &text, bool checked, const QRect &boundingRect) : Widget(boundingRect), checked(checked), text(text) { }
void CheckBox::paint(QPainter *painter) { painter->translate(boundingRect.x(), boundingRect.y());
int w = boundingRect.width(); int h = boundingRect.height();
painter->setPen(QPen(Qt::darkGray, 2)); painter->drawRect(0, 0, h, h);
if (checked) { painter->setPen(QPen(Qt::darkGray, 3, Qt::SolidLine, Qt::RoundCap)); painter->drawLine(h*0.2, h*0.5, h*0.4, h*0.75); painter->drawLine(h*0.4, h*0.75, h*0.9, h*0.3); }
painter->setPen(Qt::black); painter->drawText(h+10, 0, w-h-10, h, Qt::AlignLeft|Qt::AlignVCenter, text); }
void CheckBox::mousePressed() { checked = !checked; }
|
OSUi
OSUi 就假设是操作系统的界面吧,各种控件都是放置在它的上面,当 OSUi 接收到鼠标事件后,会查找此时鼠标在哪个控件上,然后就把鼠标事件传递给对应的控件,然后控件对此鼠标事件作出自己特有的响应。
因为所有的控件都继承自 Widget,所以用一个 List 保存了所有控件的指针,paintEvent() 更新界面时调 Widget::paint() 把所有的控件都重新绘制一次,需要注意的是每个 Widget::paint() 调用的时候,都需要保存一下 QPainter 的状态,绘制完后恢复,避免不同的控件使用 QPainter 后影响到其他控件的绘制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #ifndef OSUI_H #define OSUI_H
#include <QWidget> #include <QList>
class Widget;
class OSUi : public QWidget { Q_OBJECT
public: explicit OSUi(QWidget *parent = 0); ~OSUi();
protected: void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE; void mouseMoveEvent(QMouseEvent *event) Q_DECL_OVERRIDE; void mousePressEvent(QMouseEvent *event) Q_DECL_OVERRIDE; void mouseReleaseEvent(QMouseEvent *event) Q_DECL_OVERRIDE;
private: QList<Widget*> widgets; };
#endif
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| #include "OSUi.h" #include "Button.h" #include "CheckBox.h"
#include <QRect> #include <QPainter> #include <QMouseEvent>
OSUi::OSUi(QWidget *parent) : QWidget(parent) { setMouseTracking(true);
widgets << new Button("按钮一", QRect(20, 20, 100, 25)) << new Button("按钮二", QRect(20, 70, 100, 25), QColor(200, 200, 200), QColor(200, 200, 0), QColor(0, 200, 0)) << new CheckBox("生杀大权在你手里", true, QRect(150, 50, 150, 25)); }
OSUi::~OSUi() { qDeleteAll(widgets); }
void OSUi::paintEvent(QPaintEvent *) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true);
foreach (Widget *w, widgets) { painter.save(); w->paint(&painter); painter.restore(); } }
void OSUi::mouseMoveEvent(QMouseEvent *event) { foreach (Widget *w, widgets) { if (w->boundingRect.contains(event->pos())) { w->mouseMove();
if (!w->hover) { w->mouseEnter(); } } else { if (w->hover) { w->mouseLeave(); } } }
update(); }
void OSUi::mousePressEvent(QMouseEvent *event) { foreach (Widget *w, widgets) { if (w->boundingRect.contains(event->pos())) { w->mousePressed(); } }
update(); }
void OSUi::mouseReleaseEvent(QMouseEvent *) { foreach (Widget *w, widgets) { if (w->pressed) { w->mouseReleased(); } }
update(); }
|
main
1 2 3 4 5 6 7 8 9
| #include <QApplication> #include "AppWidget.h"
int main(int argc, char *argv[]) { QApplication a(argc, argv); AppWidget w; w.show(); return a.exec(); }
|
思考
- 上面的程序只绘制了 Button 和 CheckBox,只是最最简单的控件,绘制复杂控件例如 Table 应该要怎么做呢?当需要的控件都一个一个地加入到这个系统里,并能够进行相应的事件响应,我们就创造了一个系统的界面。
- 更新的时候把所有的控件都绘制了一遍,如果控件很多时效率是不是很低?如果能够只绘制状态改变了的控件是不是就更好了?要实现好这样的算法应该不容易,会涉及到重叠绘制。
- 鼠标事件发生时选择控件也是遍历了所有的控件,Qt Graphics/View 框架使用 binary space partition 算法来选择控件,大幅的提升了效率,所以它能够高效的处理上百万个图元。
- 如果控件放在了界面上不可见的地方,是不是就不需要绘制出来了呢?
- 怎么实现像 QPushButton 的 clicked 信号,响应点击事件?
- 鼠标事件处理的时候没有把鼠标的坐标信息传给 Widget。
- 如果多个控件重叠到一起,鼠标移动到它们上时,它们都应该做出响应还是只有最上面那个作出响应?可以参考 QGraphicsItem 的 zValue。
还有太多太多的问题,这里就不一一列举了,说了这么多,只是希望大家能够理解系统界面的本质画出来的,不要感觉很神秘,大道至简。
当然道理很简单,但要做好绝不是简单的事,就像原子弹的原理很简单一样:把两块或几块较小的铀的同位素铀-235放进弹头里,周围用黄色炸药包围,只要先引爆炸药,强迫几块较小的铀燃料合并成一块大的,使铀达到可与中子进行链式核反应的程度,就能够引起核爆炸。世界上只有几个有限的国家能够制造原子弹,不是其他国家不知道原理,而是制造工艺跟不上,这就和我们知道了系统界面的原理一个样,但是做不出一个好用的来,原因是各种处理的算法跟不上。