QT入门

programmer 程式師 程序员
回复
擎天殿
test ranks
帖子: 132
注册时间: 2025-02-21T13:46:27+00:00

Re: QT入门

帖子 擎天殿 »

9.3 事件分发函数 event()

事件对象创建完毕后,Qt 将这个事件对象传递给QObject的event()函数。event()函数并不直接处理事件,而是将这些事件对象按照它们不同的类型,分发给不同的事件处理器(event handler)。

如上所述,event()函数主要用于事件的分发。所以,如果你希望在事件分发之前做一些操作,就可以重写这个event()函数了。例如,我们希望在一个QWidget组件中监听 tab 键的按下,那么就可以继承QWidget,并重写它的event()函数,来达到这个目的:

代码: 全选

    bool CustomWidget::event(QEvent *e)
    {
        if (e->type() == QEvent::KeyPress) {
            QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
            if (keyEvent->key() == Qt::Key_Tab) {
                qDebug() << "You press tab.";
                return true;
            }
        }
        return QWidget::event(e);
    }
CustomWidget是一个普通的QWidget子类。我们重写了它的event()函数,这个函数有一个QEvent对象作为参数,也就是需要转发的事件对象。函数返回值是 bool 类型。

1.如果传入的事件已被识别并且处理,则需要返回 true,否则返回 false。如果返回值是 true,那么 Qt 会认为这个事件已经处理完毕,不会再将这个事件发送给其它对象,而是会继续处理事件队列中的下一事件。

2.在event()函数中,调用事件对象的accept()和ignore()函数是没有作用的,不会影响到事件的传播。

我们可以通过使用QEvent::type()函数可以检查事件的实际类型,其返回值是QEvent::Type类型的枚举。我们处理过自己感兴趣的事件之后,可以直接返回 true,表示我们已经对此事件进行了处理;对于其它我们不关心的事件,则需要调用父类的event()函数继续转发,否则这个组件就只能处理我们定义的事件了。为了测试这一种情况,我们可以尝试下面的代码:

代码: 全选

    bool CustomTextEdit::event(QEvent *e)
    {
       if (e->type() == QEvent::KeyPress) 
    {
            QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
           if (keyEvent->key() == Qt::Key_Tab) 
    {
                qDebug() << "You press tab.";
                return true;
           }
        }
        return false;
    }
CustomTextEdit是QTextEdit的一个子类。我们重写了其event()函数,却没有调用父类的同名函数。这样,我们的组件就只能处理 Tab 键,再也无法输入任何文本,也不能响应其它事件,比如鼠标点击之后也不会有光标出现。这是因为我们只处理的KeyPress类型的事件,并且如果不是KeyPress事件,则直接返回 false,鼠标事件根本不会被转发,也就没有了鼠标事件。

通过查看QObject::event()的实现,我们可以理解,event()函数同前面的章节中我们所说的事件处理器有什么联系:

代码: 全选

    //!!! Qt5
    bool QObject::event(QEvent *e)
    {
        switch (e->type()) {
        case QEvent::Timer:
            timerEvent((QTimerEvent*)e);
            break;
     
        case QEvent::ChildAdded:
        case QEvent::ChildPolished:
        case QEvent::ChildRemoved:
            childEvent((QChildEvent*)e);
            break;
        // ...
        default:
            if (e->type() >= QEvent::User) {
                customEvent(e);
                break;
            }
            return false;
        }
        return true;
    }
这是 Qt 5 中QObject::event()函数的源代码(Qt 4 的版本也是类似的)。我们可以看到,同前面我们所说的一样,Qt 也是使用QEvent::type()判断事件类型,然后调用了特定的事件处理器。比如,如果event->type()返回值是QEvent::Timer,则调用timerEvent()函数。可以想象,QWidget::event()中一定会有如下的代码:

代码: 全选

    switch (event->type()) {
            case QEvent::MouseMove:
                mouseMoveEvent((QMouseEvent*)event);
                break;
            // ...
        }
事实也的确如此。timerEvent()和mouseMoveEvent()这样的函数,就是我们前面章节所说的事件处理器 event handler。也就是说,event()函数中实际是通过事件处理器来响应一个具体的事件。这相当于event()函数将具体事件的处理“委托”给具体的事件处理器。而这些事件处理器是 protected virtual 的,因此,我们重写了某一个事件处理器,即可让 Qt 调用我们自己实现的版本。

由此可以见,event()是一个集中处理不同类型的事件的地方。如果你不想重写一大堆事件处理器,就可以重写这个event()函数,通过QEvent::type()判断不同的事件。鉴于重写event()函数需要十分小心注意父类的同名函数的调用,一不留神就可能出现问题,所以一般还是建议只重写事件处理器(当然,也必须记得是不是应该调用父类的同名处理器)。这其实暗示了event()函数的另外一个作用:屏蔽掉某些不需要的事件处理器。正如我们前面的CustomTextEdit例子看到的那样,我们创建了一个只能响应 tab 键的组件。这种作用是重写事件处理器所不能实现的。
擎天殿
test ranks
帖子: 132
注册时间: 2025-02-21T13:46:27+00:00

Re: QT入门

帖子 擎天殿 »

9.4 事件过滤器

有时候,对象需要查看、甚至要拦截发送到另外对象的事件。例如,对话框可能想要拦截按键事件,不让别的组件接收到;或者要修改回车键的默认处理。

通过前面的章节,我们已经知道,Qt 创建了QEvent事件对象之后,会调用QObject的event()函数处理事件的分发。显然,我们可以在event()函数中实现拦截的操作。由于event()函数是 protected 的,因此,需要继承已有类。如果组件很多,就需要重写很多个event()函数。这当然相当麻烦,更不用说重写event()函数还得小心一堆问题。好在 Qt 提供了另外一种机制来达到这一目的:事件过滤器。

QObject有一个eventFilter()函数,用于建立事件过滤器。函数原型如下:

代码: 全选

    virtual bool QObject::eventFilter ( QObject * watched, QEvent * event );
这个函数正如其名字显示的那样,是一个“事件过滤器”。所谓事件过滤器,可以理解成一种过滤代码。事件过滤器会检查接收到的事件。如果这个事件是我们感兴趣的类型,就进行我们自己的处理;如果不是,就继续转发。这个函数返回一个 bool 类型,如果你想将参数 event 过滤出来,比如,不想让它继续转发,就返回 true,否则返回 false。事件过滤器的调用时间是目标对象(也就是参数里面的watched对象)接收到事件对象之前。也就是说,如果你在事件过滤器中停止了某个事件,那么,watched对象以及以后所有的事件过滤器根本不会知道这么一个事件。

我们来看一段简单的代码:

代码: 全选

    class MainWindow : public QMainWindow
    {
    public:
        MainWindow();
    protected:
        bool eventFilter(QObject *obj, QEvent *event);
    private:
        QTextEdit *textEdit;
    };
     
    MainWindow::MainWindow()
    {
        textEdit = new QTextEdit;
        setCentralWidget(textEdit); 
        textEdit->installEventFilter(this);
    }
     
    bool MainWindow::eventFilter(QObject *obj, QEvent *event)
    {
        if (obj == textEdit) {
            if (event->type() == QEvent::KeyPress) {
                QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
                qDebug() << "Ate key press" << keyEvent->key();
                return true;
            } else {
                return false;
            }
        } else {
            // pass the event on to the parent class
            return QMainWindow::eventFilter(obj, event);
        }
    }
1.MainWindow是我们定义的一个类。我们重写了它的eventFilter()函数。为了过滤特定组件上的事件,首先需要判断这个对象是不是我们感兴趣的组件,然后判断这个事件的类型。在上面的代码中,我们不想让textEdit组件处理键盘按下的事件。所以,首先我们找到这个组件,如果这个事件是键盘事件,则直接返回 true,也就是过滤掉了这个事件,其他事件还是要继续处理,所以返回 false。对于其它的组件,我们并不保证是不是还有过滤器,于是最保险的办法是调用父类的函数。

2.eventFilter()函数相当于创建了过滤器,然后我们需要安装这个过滤器。安装过滤器需要调用QObject::installEventFilter()函数。函数的原型如下:

代码: 全选

void QObject::installEventFilter ( QObject * filterObj );
这个函数接受一个QObject *类型的参数。记得刚刚我们说的,eventFilter()函数是QObject的一个成员函数,因此,任意QObject都可以作为事件过滤器(问题在于,如果你没有重写eventFilter()函数,这个事件过滤器是没有任何作用的,因为默认什么都不会过滤)。已经存在的过滤器则可以通过QObject::removeEventFilter()函数移除。

3.我们可以向一个对象上面安装多个事件处理器,只要调用多次installEventFilter()函数。如果一个对象存在多个事件过滤器,那么,最后一个安装的会第一个执行,也就是后进先执行的顺序。

还记得我们前面的那个例子吗?我们使用event()函数处理了 Tab 键:

代码: 全选

    bool CustomWidget::event(QEvent *e)
    {
        if (e->type() == QEvent::KeyPress) {
            QKeyEvent *keyEvent = static_cast<QKeyEvent *>(e);
            if (keyEvent->key() == Qt::Key_Tab) {
                qDebug() << "You press tab.";
                return true;
            }
        }
        return QWidget::event(e);
    }
现在,我们可以给出一个使用事件过滤器的版本:

代码: 全选

    bool FilterObject::eventFilter(QObject *object, QEvent *event)
    {
        if (object == target && event->type() == QEvent::KeyPress) 
        {
            QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
            if (keyEvent->key() == Qt::Key_Tab) {
                qDebug() << "You press tab.";
                return true;
            } else {
                return false;
            }
        }
        return false;
    }
事件过滤器的强大之处在于,我们可以为整个应用程序添加一个事件过滤器。记得,installEventFilter()函数是QObject的函数,QApplication或者QCoreApplication对象都是QObject的子类,因此,我们可以向QApplication或者QCoreApplication添加事件过滤器。这种全局的事件过滤器将会在所有其它特性对象的事件过滤器之前调用。尽管很强大,但这种行为会严重降低整个应用程序的事件分发效率。因此,除非是不得不使用的情况,否则的话我们不应该这么做。

注意:

事件过滤器和被安装过滤器的组件必须在同一线程创建,否则,过滤器将不起作用。另外,如果在安装过滤器之后,这两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。
擎天殿
test ranks
帖子: 132
注册时间: 2025-02-21T13:46:27+00:00

Re: QT入门

帖子 擎天殿 »

9.5 消息事件机制和信号和槽机制的关系

来看下官方文档对信号和槽以及事件机制的原文和翻译:
In Qt, we have an alternative to the callback technique: We use signals and slots. A signal is emitted when a particular event occurs. Qt's widgets have many predefined signals, but we can always subclass widgets to add our own signals to them. A slot is a function that is called in response to a particular signal. Qt's widgets have many pre-defined slots, but it is common practice to subclass widgets and add your own slots so that you can handle the signals that you are interested in.
在Qt中,我们有一个替代回调技术:我们使用信号和槽。当某个特定事件发生时发出一个信号。Qt的小部件有许多预定义的信号,但我们总是可以将小部件添加到它们中。槽是响应特定信号的函数。Qt的小部件有许多预定义的槽,但是子类化小部件并添加自己的槽是常见的做法,这样您就可以处理您感兴趣的信号。
In Qt, events are objects, derived from the abstract QEvent class, that represent things that have happened either within an application or as a result of outside activity that the application needs to know about. Events can be received and handled by any instance of a QObject subclass, but they are especially relevant to widgets.
在Qt中,事件是对象,从抽象的QEvent类派生出来,它表示应用程序中发生的事件或者应用程序需要知道的外部活动的结果。事件可以由QObject子类的任何实例接收和处理,但它们与小部件特别相关。

简单总结可以有以下几点

1.两者都是Qt中的通讯机制

2.信号和槽被用于程序内对象之间的通信,也是一种程序回调机制(回调的意思是让框架调用,某个函数叫回调函数意思就是这个函数不是我们手动调用的,而是把函数指针告诉框架,然后框架在适当时机调用)。

3.事件不仅有程序内部产生的,也有程序外部、系统产生的,如:鼠标、键盘、定时器事件

4.信号和槽都是函数,当某个信号发生了就调用相关联的槽函数。

5.事件是对象,代表着一条消息。其他对象接收到这个事件要对这个事件做相应的处理,也可以不做任何处理。

6.消息事件比信号槽更底层,一般Qt中的标准信号会是某些消息事件处理的结果,比如:鼠标移动到按钮上,按钮一直收到鼠标移动的事件,可是按钮没有做任何处理,但是只要当鼠标点击按钮,按钮就会收到一个鼠标点击的事件,他会处理这个事件并且主动去发送一个被点击的信号clicked,这时候相关联的槽函数就会被调用。

在前面我们也曾经简单提到,Qt 程序需要在main()函数创建一个QApplication对象,然后调用它的exec()函数。这个函数就是开始 Qt 的消息事件循环,不断地处理收到的消息事件。当事件发生时,Qt框架将创建一个事件对象。Qt中所有事件类都继承于QEvent。在事件对象创建完毕后,Qt 将这个事件对象传递给QObject的event()函数。event()函数并不直接处理事件,而是按照事件对象的类型分派给特定的事件处理函数(event handler),关于这一点,会在后边详细说明。
擎天殿
test ranks
帖子: 132
注册时间: 2025-02-21T13:46:27+00:00

Re: QT入门

帖子 擎天殿 »

9.6 总结

Qt 的事件是整个 Qt 框架的核心机制之一,也比较复杂。说它复杂,更多是因为它涉及到的函数众多,而处理方法也很多,有时候让人难以选择。现在我们简单总结一下 Qt 中的事件机制。

Qt 中有很多种事件:鼠标事件、键盘事件、大小改变的事件、位置移动的事件等等。如何处理这些事件,实际有两种选择:

1.所有事件对应一个事件处理函数,在这个事件处理函数中用一个很大的分支

2.每一种事件对应一个事件处理函数。Qt 就是使用的这么一种机制:

1)mouseEvent()

2)keyPressEvent()

。。。

Qt 具有这么多种事件处理函数,肯定有一个地方对其进行分发,否则,Qt 怎么知道哪一种事件调用哪一个事件处理函数呢?这个分发的函数,就是event()。显然,当QMouseEvent产生之后,event()函数将其分发给mouseEvent()事件处理器进行处理。

event()函数会有两个问题:

1.event()函数是一个 protected 的函数,这意味着我们要想重写event(),必须继承一个已有的类。试想,我的程序根本不想要鼠标事件,程序中所有组件都不允许处理鼠标事件,是不是我得继承所有组件,一一重写其event()函数?protected 函数带来的另外一个问题是,如果我基于第三方库进行开发,而对方没有提供源代码,只有一个链接库,其它都是封装好的。我怎么去继承这种库中的组件呢?

2.event()函数的确有一定的控制,不过有时候我的需求更严格一些:我希望那些组件根本看不到这种事件。event()函数虽然可以拦截,但其实也是接收到了QMouseEvent对象。我连让它收都收不到。这样做的好处是,模拟一种系统根本没有那个事件的效果,所以其它组件根本不会收到这个事件,也就无需修改自己的事件处理函数。这种需求怎么办呢?

这两个问题是event()函数无法处理的。于是,Qt 提供了另外一种解决方案:事件过滤器。事件过滤器给我们一种能力,让我们能够完全移除某种事件。事件过滤器可以安装到任意QObject类型上面,并且可以安装多个。如果要实现全局的事件过滤器,则可以安装到QApplication或者QCoreApplication上面。这里需要注意的是,如果使用installEventFilter()函数给一个对象安装事件过滤器,那么该事件过滤器只对该对象有效,只有这个对象的事件需要先传递给事件过滤器的eventFilter()函数进行过滤,其它对象不受影响。如果给QApplication对象安装事件过滤器,那么该过滤器对程序中的每一个对象都有效,任何对象的事件都是先传给eventFilter()函数。

事件过滤器可以解决刚刚我们提出的event()函数的两点不足:

1.首先,事件过滤器不是 protected 的,因此我们可以向任何QObject子类安装事件过滤器;

2.其次,事件过滤器在目标对象接收到事件之前进行处理,如果我们将事件过滤掉,目标对象根本不会见到这个事件。

事实上,还有一种方法,我们没有介绍。Qt 事件的调用最终都会追溯到QCoreApplication::notify()函数,因此,最大的控制权实际上是重写QCoreApplication::notify()。这个函数的声明是:

代码: 全选

    virtual bool QCoreApplication::notify ( QObject * receiver,
                                            QEvent * event );
该函数会将event发送给receiver,也就是调用receiver->event(event),其返回值就是来自receiver的事件处理器。注意,这个函数为任意线程的任意对象的任意事件调用,因此,它不存在事件过滤器的线程的问题。不过我们并不推荐这么做,因为notify()函数只有一个,而事件过滤器要灵活得多。

现在我们可以总结一下 Qt 的事件处理,实际上是有五个层次:

1.重写paintEvent()、mousePressEvent()等事件处理函数。这是最普通、最简单的形式,同时功能也最简单。

2.重写event()函数。event()函数是所有对象的事件入口,QObject和QWidget中的实现,默认是把事件传递给特定的事件处理函数。

3.在特定对象上面安装事件过滤器。该过滤器仅过滤该对象接收到的事件。

4.在QCoreApplication::instance()上面安装事件过滤器。该过滤器将过滤所有对象的所有事件,因此和notify()函数一样强大,但是它更灵活,因为可以安装多个过滤器。全局的事件过滤器可以看到 disabled 组件上面发出的鼠标事件。

5.重写QCoreApplication::notify()函数。这是最强大的,和全局事件过滤器一样提供完全控制,并且不受线程的限制。但是全局范围内只能有一个被使用(因为QCoreApplication是单例的)。
擎天殿
test ranks
帖子: 132
注册时间: 2025-02-21T13:46:27+00:00

Re: QT入门

帖子 擎天殿 »

十、绘图事件和绘图设备
10.1QPainter

Qt 的绘图系统允许使用相同的 API 在屏幕和其它打印设备上进行绘制。整个绘图系统基于QPainter,QPainterDevice和QPaintEngine三个类。

QPainter用来执行绘制的操作;QPaintDevice是一个二维空间的抽象,这个二维空间允许QPainter在其上面进行绘制,也就是QPainter工作的空间;QPaintEngine提供了画家(QPainter)在不同的设备上进行绘制的统一的接口。QPaintEngine类应用于QPainter和QPaintDevice之间,通常对开发人员是透明的。除非你需要自定义一个设备,否则你是不需要关心QPaintEngine这个类的。我们可以把QPainter理解成画笔;把QPaintDevice理解成使用画笔的地方,比如纸张、屏幕等;而对于纸张、屏幕而言,肯定要使用不同的画笔绘制,为了统一使用一种画笔,我们设计了QPaintEngine类,这个类让不同的纸张、屏幕都能使用一种画笔。

下图给出了这三个类之间的层次结构:

图片

上面的示意图告诉我们,Qt 的绘图系统实际上是,使用QPainter在QPainterDevice上进行绘制,它们之间使用QPaintEngine进行通讯(也就是翻译QPainter的指令)。

下面我们通过一个实例来介绍QPainter的使用:

代码: 全选

    class PaintedWidget : public QWidget
    {
        Q_OBJECT
    public:
        PaintedWidget(QWidget *parent = 0);
    protected:
        void paintEvent(QPaintEvent *);
    }
注意我们重写了QWidget的paintEvent()函数。接下来就是PaintedWidget的源代码:

代码: 全选

    PaintedWidget::PaintedWidget(QWidget *parent) :
        QWidget(parent)
    {
        resize(800, 600);
        setWindowTitle(tr("Paint Demo"));
    }
     
    void PaintedWidget::paintEvent(QPaintEvent *)
    {
        QPainter painter(this);
        painter.drawLine(80, 100, 650, 500);
        painter.setPen(Qt::red);
        painter.drawRect(10, 10, 100, 400);
        painter.setPen(QPen(Qt::green, 5));
        painter.setBrush(Qt::blue);
        painter.drawEllipse(50, 150, 400, 200);
    }
在构造函数中,我们仅仅设置了窗口的大小和标题。而paintEvent()函数则是绘制的代码。首先,我们在栈上创建了一个QPainter对象,也就是说,每次运行paintEvent()函数的时候,都会重建这个QPainter对象。注意,这一点可能会引发某些细节问题:由于我们每次重建QPainter,因此第一次运行时所设置的画笔颜色、状态等,第二次再进入这个函数时就会全部丢失。有时候我们希望保存画笔状态,就必须自己保存数据,否则的话则需要将QPainter作为类的成员变量

QPainter接收一个QPaintDevice指针作为参数。QPaintDevice有很多子类,比如QImage,以及QWidget。注意回忆一下,QPaintDevice可以理解成要在哪里去绘制,而现在我们希望画在这个组件,因此传入的是 this 指针。

QPainter有很多以 draw 开头的函数,用于各种图形的绘制,比如这里的drawLine(),drawRect()以及drawEllipse()等。当绘制轮廓线时,使用QPainter的pen()属性。比如,我们调用了painter.setPen(Qt::red)将 pen 设置为红色,则下面绘制的矩形具有红色的轮廓线。接下来,我们将 pen 修改为绿色,5 像素宽(painter.setPen(QPen(Qt::green, 5))),又设置了画刷为蓝色。这时候再调用 draw 函数,则是具有绿色 5 像素宽轮廓线、蓝色填充的椭圆。
擎天殿
test ranks
帖子: 132
注册时间: 2025-02-21T13:46:27+00:00

Re: QT入门

帖子 擎天殿 »

10.2 绘图设备

绘图设备是指继承QPainterDevice的子类。Qt一共提供了四个这样的类,分别是QPixmap、QBitmap、QImage和 QPicture。其中,

1.QPixmap专门为图像在屏幕上的显示做了优化

2.QBitmap是QPixmap的一个子类,它的色深限定为1,可以使用 QPixmap的isQBitmap()函数来确定这个QPixmap是不是一个QBitmap。

3.QImage专门为图像的像素级访问做了优化。

4.QPicture则可以记录和重现QPainter的各条命令。

10.2.1 QPixmap 、QBitmap、QImage

QPixmap继承了QPaintDevice,因此,你可以使用QPainter直接在上面绘制图形。QPixmap也可以接受一个字符串作为一个文件的路径来显示这个文件,比如你想在程序之中打开png、jpeg之类的文件,就可以使用 QPixmap。使用QPainter的drawPixmap()函数可以把这个文件绘制到一个QLabel、QPushButton或者其他的设备上面。QPixmap是针对屏幕进行特殊优化的,因此,它与实际的底层显示设备息息相关。注意,这里说的显示设备并不是硬件,而是操作系统提供的原生的绘图引擎。所以,在不同的操作系统平台下,QPixmap的显示可能会有所差别。

QBitmap继承自QPixmap,因此具有QPixmap的所有特性,提供单色图像。QBitmap的色深始终为1. 色深这个概念来自计算机图形学,是指用于表现颜色的二进制的位数。我们知道,计算机里面的数据都是使用二进制表示的。为了表示一种颜色,我们也会使用二进制。比如我们要表示8种颜色,需要用3个二进制位,这时我们就说色深是3. 因此,所谓色深为1,也就是使用1个二进制位表示颜色。1个位只有两种状态:0和1,因此它所表示的颜色就有两种,黑和白。所以说,QBitmap实际上是只有黑白两色的图像数据。

由于QBitmap色深小,因此只占用很少的存储空间,所以适合做光标文件和笔刷。

下面我们来看同一个图像文件在QPixmap和QBitmap下的不同表现:

代码: 全选

    void PaintWidget::paintEvent(QPaintEvent *)
    {
        QPixmap pixmap(":/Image/butterfly.png");
        QPixmap pixmap1(":/Image/butterfly1.png");
        QBitmap bitmap(":/Image/butterfly.png");
        QBitmap bitmap1(":/Image/butterfly1.png");
     
        QPainter painter(this);
        painter.drawPixmap(0, 0, pixmap);
        painter.drawPixmap(200, 0, pixmap1);
        painter.drawPixmap(0, 130, bitmap);
        painter.drawPixmap(200, 130, bitmap1);
    }
图片

这里我们给出了两张png图片。butterfly1.png是没有透明色的纯白背景,而butterfly.png是具有透明色的背景。我们分别使用QPixmap和QBitmap来加载它们。注意看它们的区别:白色的背景在QBitmap中消失了,而透明色在QBitmap中转换成了黑色;其他颜色则是使用点的疏密程度来体现(像以前黑白报纸图片打印原理一样)的。

QPixmap使用底层平台的绘制系统进行绘制,无法提供像素级别的操作,而QImage则是使用独立于硬件的绘制系统,实际上是自己绘制自己,因此提供了像素级别的操作,并且能够在不同系统之上提供一个一致的显示形式。

我们声明了一个QImage对象,大小是300 x 300,颜色模式是RGB32,即使用32位数值表示一个颜色的RGB值,也就是说每种颜色使用8位。然后我们对每个像素进行颜色赋值,从而构成了这个图像。我们可以把QImage想象成一个RGB颜色的二维数组,记录了每一像素的颜色。

代码: 全选

    void PaintWidget::paintEvent(QPaintEvent *)
    {
        QPainter painter(this);
        QImage image(300, 300, QImage::Format_RGB32);
        QRgb value;
     
        //将图片背景填充为白色
        image.fill(Qt::white);
     
        //改变指定区域的像素点的值
        for(int i=50; i<100; ++i)
        {
            for(int j=50; j<100; ++j)
            {
                value = qRgb(255, 0, 0); // 红色
                image.setPixel(i, j, value);
            }
        }
     
        //将图片绘制到窗口中
        painter.drawImage(QPoint(0, 0), image);
    }
图片

QImage与QPixmap的区别

1.QPixmap主要是用于绘图,针对屏幕显示而最佳化设计,QImage主要是为图像I/O、图片访问和像素修改而设计的

2.QPixmap依赖于所在的平台的绘图引擎,故例如反锯齿等一些效果在不同的平台上可能会有不同的显示效果,QImage使用Qt自身的绘图引擎,可在不同平台上具有相同的显示效果

3.由于QImage是独立于硬件的,也是一种QPaintDevice,因此我们可以在另一个线程中对其进行绘制,而不需要在GUI线程中处理,使用这一方式可以很大幅度提高UI响应速度。

4.QImage可通过setPixpel()和pixel()等方法直接存取指定的像素。

QImage与QPixmap之间的转换:

5.QImage转QPixmap

使用QPixmap的静态成员函数: fromImage()

代码: 全选

    QPixmap	fromImage(const QImage & image,                  
    Qt::ImageConversionFlags flags = Qt::AutoColor)
6.QPixmap转QImage:

使用QPixmap类的成员函数: toImage()

代码: 全选

QImage toImage() const;
擎天殿
test ranks
帖子: 132
注册时间: 2025-02-21T13:46:27+00:00

Re: QT入门

帖子 擎天殿 »

10.2.2 QPicture

最后一个需要说明的是QPicture。这是一个可以记录和重现QPainter命令的绘图设备。 QPicture将QPainter的命令序列化到一个IO设备,保存为一个平台独立的文件格式。这种格式有时候会是“元文件(meta- files)”。Qt的这种格式是二进制的,不同于某些本地的元文件,Qt的pictures文件没有内容上的限制,只要是能够被QPainter绘制的元素,不论是字体还是pixmap,或者是变换,都可以保存进一个picture中。

QPicture是平台无关的,因此它可以使用在多种设备之上,比如svg、pdf、ps、打印机或者屏幕。回忆下我们这里所说的QPaintDevice,实际上是说可以有QPainter绘制的对象。QPicture使用系统的分辨率,并且可以调整 QPainter来消除不同设备之间的显示差异。

如果我们要记录下QPainter的命令,首先要使用QPainter::begin()函数,将QPicture实例作为参数传递进去,以便告诉系统开始记录,记录完毕后使用QPainter::end()命令终止。代码示例如下:

代码: 全选

    void PaintWidget::paintEvent(QPaintEvent *)
    {
        QPicture pic;
        QPainter painter;
    	 //将图像绘制到QPicture中,并保存到文件
        painter.begin(&pic);
        painter.drawEllipse(20, 20, 100, 50);
        painter.fillRect(20, 100, 100, 100, Qt::red);
        painter.end();
        pic.save("D:\\drawing.pic");
     
    	 //将保存的绘图动作重新绘制到设备上
        pic.load("D:\\drawing.pic");
        painter.begin(this);
        painter.drawPicture(200, 200, pic);
        painter.end();
    }
擎天殿
test ranks
帖子: 132
注册时间: 2025-02-21T13:46:27+00:00

Re: QT入门

帖子 擎天殿 »

十一、文件操作

文件操作是应用程序必不可少的部分。Qt 作为一个通用开发库,提供了跨平台的文件操作能力。Qt 通过QIODevice提供了对 I/O 设备的抽象,这些设备具有读写字节块的能力。下面是 I/O 设备的类图(Qt5):

图片

1.QIODevice:所有 I/O 设备类的父类,提供了字节块读写的通用操作以及基本接口;

2.QFileDevice:Qt5新增加的类,提供了有关文件操作的通用实现。

3.QFlie:访问本地文件或者嵌入资源;

4.QTemporaryFile:创建和访问本地文件系统的临时文件;

5.QBuffer:读写QbyteArray, 内存文件;

6.QProcess:运行外部程序,处理进程间通讯;

7.QAbstractSocket:所有套接字类的父类;

8.QTcpSocket:TCP协议网络数据传输;

9.QUdpSocket:传输 UDP 报文;

10.QSslSocket:使用 SSL/TLS 传输数据;

文件系统分类:

1.顺序访问设备

是指它们的数据只能访问一遍:从头走到尾,从第一个字节开始访问,直到最后一个字节,中途不能返回去读取上一个字节,这其中,QProcess、QTcpSocket、QUdpSoctet和QSslSocket是顺序访问设备。

2.随机访问设备:

可以访问任意位置任意次数,还可以使用QIODevice::seek()函数来重新定位文件访问位置指针,QFile、QTemporaryFile和QBuffer是随机访问设备。
擎天殿
test ranks
帖子: 132
注册时间: 2025-02-21T13:46:27+00:00

Re: QT入门

帖子 擎天殿 »

11.1 基本文件操作

文件操作是应用程序必不可少的部分。Qt 作为一个通用开发库,提供了跨平台的文件操作能力。在所有的 I/O 设备中,文件 I/O 是最重要的部分之一。因为我们大多数的程序依旧需要首先访问本地文件(当然,在云计算大行其道的将来,这一观点可能改变)。QFile提供了从文件中读取和写入数据的能力。

我们通常会将文件路径作为参数传给QFile的构造函数。不过也可以在创建好对象最后,使用setFileName()来修改。QFile需要使用 / 作为文件分隔符,不过,它会自动将其转换成操作系统所需要的形式。例如 C:/windows 这样的路径在 Windows 平台下同样是可以的。

QFile主要提供了有关文件的各种操作,比如打开文件、关闭文件、刷新文件等。我们可以使用QDataStream或QTextStream类来读写文件,也可以使用QIODevice类提供的read()、readLine()、readAll()以及write()这样的函数。值得注意的是,有关文件本身的信息,比如文件名、文件所在目录的名字等,则是通过QFileInfo获取,而不是自己分析文件路径字符串。

下面我们使用一段代码来看看QFile的有关操作:

代码: 全选

    int main(int argc, char *argv[])
    {
        QApplication app(argc, argv);
        
        QFile file("in.txt");
        if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            qDebug() << "Open file failed.";
            return -1;
        } else {
            while (!file.atEnd()) {
                qDebug() << file.readLine();
            }
        }
        
        QFileInfo info(file);
        qDebug() << info.isDir();
        qDebug() << info.isExecutable();
        qDebug() << info.baseName();
        qDebug() << info.completeBaseName();
        qDebug() << info.suffix();
        qDebug() << info.completeSuffix();
        
        return app.exec();
    }
1.我们首先使用 QFile 创建了一个文件对象

这个文件名字是 in.txt。如果你不知道应该把它放在哪里,可以使用QDir::currentPath()来获得应用程序执行时的当前路径。只要将这个文件放在与当前路径一致的目录下即可。

2.使用open()函数打开这个文件,打开形式是只读方式,文本格式。

这个类似于fopen()的 r 这样的参数。open()函数返回一个 bool 类型,如果打开失败,我们在控制台输出一段提示然后程序退出。否则,我们利用 while 循环,将每一行读到的内容输出。

3.可以使用QFileInfo获取有关该文件的信息。

QFileInfo有很多类型的函数,我们只举出一些例子。比如:

1)isDir() 检查该文件是否是目录(在操作系统这种,文件夹是一种特殊的文件,我们可以通过QFile来打开一个文件夹或者路径,这个函数返回值就是判断他是不是一个文件夹或路径);

2)isExecutable() 检查该文件是否可以被执行。

3)baseName() 可以直接获得文件名;

4)completeBaseName() 获取完整的文件名

5)suffix() 则直接获取文件后缀名。

6)completeSuffix() 获取完整的文件后缀

我们可以由下面的示例看到,baseName()和completeBaseName(),以及suffix()和completeSuffix()的区别:

代码: 全选

    QFileInfo fi("/tmp/archive.tar.gz");
    QString base  = fi.baseName();  // base = "archive"
    QString base  = fi.completeBaseName();  // base = "archive.tar"
    QString ext   = fi.suffix();  // ext = "gz"
    QString ext   = fi.completeSuffix();  // ext = "tar.gz"
擎天殿
test ranks
帖子: 132
注册时间: 2025-02-21T13:46:27+00:00

Re: QT入门

帖子 擎天殿 »

11.2 二进制文件读写

QDataStream提供了基于QIODevice的二进制数据的序列化。数据流是一种二进制流,这种流完全不依赖于底层操作系统、CPU 或者字节顺序(大端或小端)。例如,在安装了 Windows 平台的 PC 上面写入的一个数据流,可以不经过任何处理,直接拿到运行了 Solaris 的 SPARC 机器上读取。由于数据流就是二进制流,因此我们也可以直接读写没有编码的二进制数据,例如图像、视频、音频等。

QDataStream既能够存取 C++ 基本类型,如 int、char、short 等,也可以存取复杂的数据类型,例如自定义的类。实际上,QDataStream对于类的存储,是将复杂的类分割为很多基本单元实现的。

结合QIODevice,QDataStream可以很方便地对文件、网络套接字等进行读写操作。我们从代码开始看起:

代码: 全选

    QFile file("file.dat");
    file.open(QIODevice::WriteOnly);
    QDataStream out(&file);
    out << QString("the answer is");
    out << (qint32)42;
1.在这段代码中,我们首先打开一个名为 file.dat 的文件(注意,我们为简单起见,并没有检查文件打开是否成功,这在正式程序中是不允许的)。然后,我们将刚刚创建的file对象的指针传递给一个QDataStream实例out。类似于std::cout标准输出流,QDataStream也重载了输出重定向<<运算符。后面的代码就很简单了:将“the answer is”和数字 42 输出到数据流。由于我们的 out 对象建立在file之上,因此相当于将问题和答案写入file。

2.需要指出一点:最好使用 Qt 整型来进行读写,比如程序中的qint32。这保证了在任意平台和任意编译器都能够有相同的行为。

如果你直接运行这段代码,你会得到一个空白的 file.dat,并没有写入任何数据。这是因为我们的file没有正常关闭。为性能起见,数据只有在文件关闭时才会真正写入。因此,我们必须在最后添加一行代码:

代码: 全选

file.close(); // 如果不想关闭文件,可以使用 file.flush();
接下来我们将存储到文件中的答案取出来

代码: 全选

    QFile file("file.dat");
    file.open(QIODevice::ReadOnly);
    QDataStream in(&file);
    QString str;
    qint32 a;
    in >> str >> a;
唯一需要注意的是,你必须按照写入的顺序,将数据读取出来。顺序颠倒的话,程序行为是不确定的,严重时会直接造成程序崩溃。

那么,既然QIODevice提供了read()、readLine()之类的函数,为什么还要有QDataStream呢?QDataStream同QIODevice有什么区别?区别在于,QDataStream提供流的形式,性能上一般比直接调用原始 API 更好一些。我们通过下面一段代码看看什么是流的形式:

代码: 全选

    QFile file("file.dat");
    file.open(QIODevice::ReadWrite);
         
    QDataStream stream(&file);
    QString str = "the answer is 42";
        
    stream << str;
回复

回到 “programmer 程式師 程序员”