Content Table

单例的简单实现

用简单直观的方式来实现一个单例的类 ConfigUtil,这里不使用宏,模版等技术,先了解实现一个单例类的理论知识,然后在此基础之上进行思考,优化,最终让我们的实现真正的达到实用的目的,而不只是功能上可用,但是质量却很不好。

实现单例时,需要注意以下几点:
  • C++ 的书里经常强调:一个类,至少要提供构造函数拷贝构造函数析构函数赋值运算操作符,尤其是有成员变量是指针类型,保存指针的数组或集合时更是需要注意(实现深拷贝)
  • 要限制创建和删除 ConfigUtil 的对象
    • 构造函数定义为 private 的,是为了防止其他地方使用 new 创建 ConfigUtil 的对象
    • 析构函数定义为 private 的,是为了防止其他地方使用 delete 删除 ConfigUtil 的对象
    • 拷贝构造函数定义为 private 的,是为了防止通过拷贝构造函数创建新的 ConfigUtil 对象
    • 赋值运算操作符定义为 private 的,是为了防止通过赋值操作创建新的 ConfigUtil 对象
  • 通过 ConfigUtil::getInstance() 获取 ConfigUtil 的对象
  • 当程序结束的时候调用 ConfigUtil::release() 删除它的对象,否则会造成内存泄漏。虽然程序结束了,内存会被系统回收,但是理论上还是要保证谁分配的内存谁回收

单例

单例的意图是为了保证一个类只能创建一个对象(栈对象或者堆对象都可以),并提供访问它的唯一全局访问点。只需要一个对象的场景,比如数据库连接池、线程池、系统日志的输出、读写配置等。

为了保证一个类只有一个对象,那么就不能随便地创建和删除这个类的对象。如果它的构造函数是 public 的,那么就可以随意地创建它的对象了,所以它构造函数不能是 public 的。如果它的析构函数是 public 的,那么也可以调用 delete 删除它的对象。所以对于要使用单例的类,它的构造函数和析构函数我们都定义为 private 的,同时要防止通过拷贝构造函数和赋值操作创建对象。

先思考一下,什么情况下能访问一个类的 private 成员变量和成员函数?单例的实现需要用到这个知识点。

Qt Tips

无边框对话框不在任务栏显示图标

1
2
QDialog *dlg = new QDialog(NULL, Qt::Dialog | Qt::Popup | Qt::FramelessWindowHint);
dlg->show();

Qt::Popup 是个小技巧,只有先点击对话框后才能点击对话框后面的 widget

实时动态曲线

在群里经常有朋友问:不停的从下位机,传感器接收到数据,怎么实时的把这些数据的曲线画出来?就像 Windows 的任务管理器 CPU 监控的动态曲线那样,曲线从左向右移动。

先分析一下这个问题:
  • 接收数据:与设备有关,不同的设备接收数据的方式不一样,有的用串口,有的用 TCP,UPD 等,不过这不是本章的重点,我们会用生成随机数模拟从设备接收到数据。
  • 随着程序运行的时间越来越长,接收到的数据从开始的几个到几百个,几千个,几万甚至几十上百万个,难道要把所有的数据都要显示出来?不需要,只要把最后接收到的例如 100 个数据显示出来就可以了。
  • 曲线怎么才能动起来?以只显示 100 个最新数据为例,存放在链表里,假设链表已经存满 100 个数据,当接收到一个新的数据时,把它放到链表尾部并删除链表的第一个数据,这样就保证了链表存储的都是最新的 100 个数据,前一次的 100 个数据里下标为 1 到 99 的数据和后一次数据里下标为 0 到 98 的数据是一样的,用他们绘制出来的 2 个曲线,后一次数据的曲线就像前一次数据的曲线向左移动了一点一样,这个过程不停的发生,曲线看上去就动起来了。

绘制平滑曲线

得到曲线上的点,画出曲线,这是一个很常见的需求。画曲线嘛,当然难不住我们,用 QPainter::drawLine() 把曲线上的点连起来不就好了?So easy,轻轻松松搞定,开开心心的交任务去了。

正在聚精会神炒股的老板一瞅,气不打一处来:“你这画的是什么鬼,这个线直来直去的,太不专业了”,抬头指着屏幕上的炒股软件,瞅着迷离的眼神:“看看人家的这个曲线,就像少女的皮肤般那么的柔顺、平滑”,口气马上一百八十度大转弯:“在看看你的,像八十岁老头的那样全是褶皱!” 擦完脸上的口水,赶快想办法去吧。

用画家的思维绘制图形

曾经遇到一个这样的需求:使用 代码 实现下面样式的按钮

观察按钮的样式,发现有多种渐变、有高光、有阴影、有不规则形状、有半透明效果,看到这么复杂的效果图,我的第一反应是用 Photoshop 做出效果图,使用 QSS 设置为按钮的背景图就可以了。但是,客户就是上帝,上帝说要用代码,那就用代码。有没有什么办法能直接绘制出这样的图形来呢?翻遍了 Qt 的帮助文档,没有,Qt 只提供了一些绘制简单图形的函数,如点、线、图片、填充、渐变、融合等,没有直接实现这种复杂需求的函数,那么,怎么用 QPainter 绘制出这样的图形呢?

Pixmap

Pixmap 的绘制有下面四种方式(每种方式都有几个重载的函数,没有全部列举出来):

  1. 在指定位置绘制 pixmap,pixmap 不会被缩放

    1
    2
    3
    /* pixmap 的左上角和 widget 上 x, y 处重合 */
    void QPainter::drawPixmap(int x, int y, const QPixmap & pixmap)
    void QPainter::drawPixmap(const QPointF &point, const QPixmap &pixmap)
  2. 在指定的矩形内绘制 pixmap,pixmap 被缩放填充到此矩形内

    1
    2
    3
    /* target 是 widget 上要绘制 pixmap 的矩形区域 */
    void QPainter::drawPixmap(int x, int y, int width, int height, const QPixmap &pixmap)
    void QPainter::drawPixmap(const QRect &target, const QPixmap &pixmap)
  3. 绘制 pixmap 的一部分,可以称其为 sub-pixmap

    1
    2
    3
    4
    5
    /* source 是 sub-pixmap 的 rectangle */
    void QPainter::drawPixmap(const QPoint &point, const QPixmap &pixmap, const QRect &source)
    void QPainter::drawPixmap(const QRect &target, const QPixmap &pixmap, const QRect &source)
    void QPainter::drawPixmap(int x, int y, const QPixmap &pixmap,
    int sx, int sy, int sw, int sh)
  4. 平铺绘制 pixmap,水平和垂直方向都会同时使用平铺的方式

    1
    2
    3
    4
    5
    6
    void QPainter::drawTiledPixmap(const QRect &rectangle,
    const QPixmap &pixmap,
    const QPoint &position = QPoint())
    void QPainter::drawTiledPixmap(int x, int y, int width, int height,
    const QPixmap & pixmap,
    int sx = 0, int sy = 0)

    drawTiledPixmap() 比我们自己计算 pixmap 的长宽,然后重复的绘制实现平铺的效率高一些:Calling drawTiledPixmap() is similar to calling drawPixmap() several times to fill (tile) an area with a pixmap, but is potentially much more efficient depending on the underlying window system.

使用上面这张图来演示 drawPixmap() 的各种用法,左上角绘制原始大小的 pixmap,右上角缩放绘制 pixmap 到指定的矩形内 QRect(225, 20, 250, 159),中间绘制 sub-pixmap,底部则使用平铺的方式绘制,最后结果如下图(文字是标记上去帮助理解的):

1
2
3
4
5
6
7
8
9
10
11
12
void PixmapWidget::paintEvent(QPaintEvent *) {
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
QPixmap pixmap(":/img/Butterfly.png"); // 从资源文件读取 pixmap

painter.drawPixmap(20, 20, pixmap); // 按原始尺寸绘制 pixmap
painter.drawPixmap(225, 20, 250, 159, pixmap); // 缩放绘制 pixmap
painter.drawPixmap(20, 133, pixmap, 128, 0, 57, 46); // 绘制 pixmap 的一部分

painter.translate(0, 199);
painter.drawTiledPixmap(0, 0, width(), height(), pixmap);
}

QPainter 的状态保存与恢复

实现这样的一个程序,把 QPainter 的坐标原点从左上角移动到 (100, 100),然后画出坐标轴,接下来顺时针旋转坐标轴 45 度,设置画笔,画刷,字体,画一个矩形和字符串,最后恢复 QPainter 到最开始的状态,即还原画笔,画刷,字体,逆时针旋转坐标轴 45 度,移动 QPainter 的坐标原点到左上角,再画一个矩形和字符串,就像下图这样: