目录
1. Qt消息机制和事件
1.1 事件
1.2 event()
1.3 事件过滤器
1.4 总结
2 绘图和绘图设备
2.1 QPainter
2.2 绘图设备
2.2.1 QPixmap、QBitmap、QImage
2.2.2 QPicture
3. 文件系统
3.1 基本文件操作
3.2 二进制文件读写
3.3 文本文件读写
事件(event)是由系统或者 Qt 本身在不同的时刻发出的。当用户按下鼠标、敲下键盘,或者是窗口需要重新绘制的时候,都会发出一个相应的事件。一些事件在对用户操作做出响应时发出,如键盘事件等;另一些事件则是由系统自动发出,如计时器事件。
在前面我们也曾经简单提到,Qt 程序需要在main()函数创建一个QApplication对象,然后调用它的exec()函数。这个函数就是开始 Qt 的事件循环。在执行exec()函数之后,程序将进入事件循环来监听应用程序的事件。当事件发生时,Qt 将创建一个事件对象。Qt 中所有事件类都继承于QEvent。在事件对象创建完毕后,Qt 将这个事件对象传递给QObject的event()函数。event()函数并不直接处理事件,而是按照事件对象的类型分派给特定的事件处理函数(event handler),关于这一点,会在后边详细说明。在所有组件的父类QWidget中,定义了很多事件处理的回调函数,如
keyPressEvent()
keyReleaseEvent()
mouseDoubleClickEvent()
mouseMoveEvent()
mousePressEvent()
mouseReleaseEvent() 等。
这些函数都是 protected virtual 的,也就是说,我们可以在子类中重新实现这些函数。下面来看一个例子:
class EventLabel : public QLabel
{
protected:
void mouseMoveEvent(QMouseEvent *event);
void mousePressEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
};
void EventLabel::mouseMoveEvent(QMouseEvent *event)
{
this->setText(QString("
Move: (%1, %2)
").arg(QString::number(event->x()),
QString::number(event->y())));
}
void EventLabel::mousePressEvent(QMouseEvent *event)
{
this->setText(QString("
Press:(%1, %2)
").arg(QString::number(event->x()),
QString::number(event->y())));
}
void EventLabel::mouseReleaseEvent(QMouseEvent *event)
{
QString msg;
msg.sprintf("
", Release: (%d, %d)
event->x(), event->y());
this->setText(msg);
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
EventLabel *label = new EventLabel;
label->setWindowTitle("MouseEvent Demo");
label->resize(300, 200);
label->show();
return a.exec();
}
1. EventLabel继承了QLabel,覆盖了mousePressEvent()、mouseMoveEvent()和MouseReleaseEvent()三个函数。我们并没有添加什么功能,只是在鼠标按下(press)、鼠标移动(move)和鼠标释放(release)的时候,把当前鼠标的坐标值显示在这个Label上面。由于QLabel是支持 HTML 代码的,因此我们直接使用了 HTML 代码来格式化文字。
2.QString的arg()函数可以自动替换掉QString中出现的占位符。其占位符以 % 开始,后面是占位符的位置,例如 %1,%2 这种。QString("[%1, %2]").arg(x).arg(y);语句将会使用x替换 %1,y替换 %2,因此,生成的QString为[x, y]。
3.在mouseReleaseEvent()函数中,我们使用了另外一种QString的构造方法。我们使用类似 C 风格的格式化函数sprintf()来构造QString。运行上面的代码,当我们点击了一下鼠标之后,label 上将显示鼠标当前坐标值。
为什么要点击鼠标之后才能在mouseMoveEvent()函数中显示鼠标坐标值?
这是因为QWidget中有一个mouseTracking属性,该属性用于设置是否追踪鼠标。只有鼠标被追踪时,mouseMoveEvent()才会发出。如果mouseTracking是 false(默认即是),组件在至少一次鼠标点击之后,才能够被追踪,也就是能够发出mouseMoveEvent()事件。如果mouseTracking为 true,则mouseMoveEvent()直接可以被发出:
知道了这一点,我们就可以在main()函数中添加如下代码:
label->setMouseTracking(true);
在运行程序就没有这个问题了。
事件对象创建完毕后,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(e);
if (keyEvent->key() == Qt::Key_Tab) {
qDebug() << "You press tab.";
return true;
}
}
return QWidget::event(e);
}
CustomWidget是一个普通的QWidget子类。我们重写了它的event()函数,这个函数有一个QEvent对象作为参数,也就是需要转发的事件对象。函数返回值是 bool 类型。
我们可以通过使用QEvent::type()函数可以检查事件的实际类型,其返回值是QEvent::Type类型的枚举。我们处理过自己感兴趣的事件之后,可以直接返回 true,表示我们已经对此事件进行了处理;对于其它我们不关心的事件,则需要调用父类的event()函数继续转发,否则这个组件就只能处理我们定义的事件了。为了测试这一种情况,我们可以尝试下面的代码:
bool CustomTextEdit::event(QEvent *e)
{
if (e->type() == QEvent::KeyPress)
{
QKeyEvent *keyEvent = static_cast(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()中一定会有如下的代码:
事实也的确如此。timerEvent()和mouseMoveEvent()这样的函数,就是我们前面章节所说的事件处理器 event handler。也就是说,event()函数中实际是通过事件处理器来响应一个具体的事件。这相当于event()函数将具体事件的处理“委托”给具体的事件处理器。而这些事件处理器是 protected virtual 的,因此,我们重写了某一个事件处理器,即可让 Qt 调用我们自己实现的版本。
由此可以见,event()是一个集中处理不同类型的事件的地方。如果你不想重写一大堆事件处理器,就可以重写这个event()函数,通过QEvent::type()判断不同的事件。鉴于重写event()函数需要十分小心注意父类的同名函数的调用,一不留神就可能出现问题,所以一般还是建议只重写事件处理器(当然,也必须记得是不是应该调用父类的同名处理器)。这其实暗示了event()函数的另外一个作用:屏蔽掉某些不需要的事件处理器。正如我们前面的CustomTextEdit例子看到的那样,我们创建了一个只能响应 tab 键的组件。这种作用是重写事件处理器所不能实现的。
有时候,对象需要查看、甚至要拦截发送到另外对象的事件。例如,对话框可能想要拦截按键事件,不让别的组件接收到;或者要修改回车键的默认处理。通过前面的章节,我们已经知道,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(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);
}
}
MainWindow是我们定义的一个类。我们重写了它的eventFilter()函数。为了过滤特定组件上的事件,首先需要判断这个对象是不是我们感兴趣的组件,然后判断这个事件的类型。在上面的代码中,我们不想让textEdit组件处理键盘按下的事件。所以,首先我们找到这个组件,如果这个事件是键盘事件,则直接返回 true,也就是过滤掉了这个事件,其他事件还是要继续处理,所以返回 false。对于其它的组件,我们并不保证是不是还有过滤器,于是最保险的办法是调用父类的函数
eventFilter()函数相当于创建了过滤器,然后我们需要安装这个过滤器。安装过滤器需要调用QObject::installEventFilter()函数。函数的原型如下:
void QObject::installEventFilter ( QObject * filterObj )
这个函数接受一个QObject *类型的参数。记得刚刚我们说的,eventFilter()函数是QObject的一个成员函数,因此,任意QObject都可以作为事件过滤器(问题在于,如果你没有重写eventFilter()函数,这个事件过滤器是没有任何作用的,因为默认什么都不会过滤)。已经存在的过滤器则可以通过QObject::removeEventFilter()函数移除。我们可以向一个对象上面安装多个事件处理器,只要调用多次installEventFilter()函数。如果一个对象存在多个事件过滤器,那么,最后一个安装的会第一个执行,也就是后进先执行的顺序。还记得我们前面的那个例子吗?我们使用event()函数处理了 Tab 键:
bool CustomWidget::event(QEvent *e)
{
if (e->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast(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(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添加事件过滤器。这种全局的事件过滤器将会在所有其它特性对象的事件过滤器之前调用。尽管很强大,但这种行为会严重降低整个应用程序的事件分发效率。因此,除非是不得不使用的情况,否则的话我们不应该这么做。
注意,
事件过滤器和被安装过滤器的组件必须在同一线程,否则,过滤器将不起作用。另外,如果在安装过滤器之后,这两个组件到了不同的线程,那么,只有等到二者重新回到同一线程的时候过滤器才会有效。
Qt 的事件是整个 Qt 框架的核心机制之一,也比较复杂。说它复杂,更多是因为它涉及到的函数众多,而处理方法也很多,有时候让人难以选择。现在我们简单总结一下 Qt 中的事件机制。Qt 中有很多种事件:鼠标事件、键盘事件、大小改变的事件、位置移动的事件等等。如何处理这些事件,实际有两种选择:
所有事件对应一个事件处理函数,在这个事件处理函数中用一个很大的分支语句进行选择,其代表作就是 win32 API 的WndProc()函数:
LRESULT CALLBACK WndProc(HWND hWnd,
UINT message,
WPARAM wParam,
LPARAM lParam)
在这个函数中,我们需要使用switch语句,选择message参数的类型进行处理,典型代码是:
switch(message)
{
case WM_PAINT:
// ...
break;
case WM_DESTROY:
// ...
break;
...
}
每一种事件对应一个事件处理函数。Qt 就是使用的这么一种机制:
mouseEvent()
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是单例的)。
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 像素宽轮廓线、蓝色填充的椭圆。
绘图设备是指继承QPainterDevice的子类。Qt提供了很多这样的类,例如QPixmap、QBitmap、QImage和 QPicture。其中
QPicture则可以记录和重现QPainter的各条命令。
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()等方法直接存取指定的像素。
5.QImage与QPixmap之间的转换:
QImage转QPixmap
使用QPixmap的静态成员函数: fromImage()
QPixmap fromImage(const QImage & image,
Qt::ImageConversionFlags flags = Qt::AutoColor)
6.QPixmap转QImage:
使用QPixmap类的成员函数: toImage()
QImage toImage() const
最后一个需要说明的是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();
}
文件操作是应用程序必不可少的部分。Qt 作为一个通用开发库,提供了跨平台的文件操作能力。Qt 通过QIODevice提供了对 I/O 设备的抽象,这些设备具有读写字节块的能力。下面是 I/O 设备的类图(Qt5):
QFlie:访问本地文件或者嵌入资源;
QTcpSocket:TCP协议网络数据传输;
QUdpSocket:传输 UDP 报文;
文件系统分类:
顺序访问设备:
是指它们的数据只能访问一遍:从头走到尾,从第一个字节开始访问,直到最后一个字节,中途不能返回去读取上一个字节,这其中,QProcess、QTcpSocket、QUdpSoctet和QSslSocket是顺序访问设备。
随机访问设备:
可以访问任意位置任意次数,还可以使用QIODevice::seek()函数来重新定位文件访问位置指针,QFile、QTemporaryFile和QBuffer是随机访问设备。
文件操作是应用程序必不可少的部分。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 循环,将每一行读到的内容输出。
可以使用QFileInfo获取有关该文件的信息。QFileInfo有很多类型的函数,我们只举出一些例子。比如:
- isDir()检查该文件是否是目录;
- isExecutable() 检查该文件是否是可执行文件等。
- baseName() 可以直接获得文件名;
- completeBaseName() 获取完整的文件名
- suffix() 则直接获取文件后缀名。
- 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"
QString ext = fi.completeSuffix(); // ext = "tar.gz"
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;
在这段代码中,我们首先打开一个名为 file.dat 的文件(注意,我们为简单起见,并没有检查文件打开是否成功,这在正式程序中是不允许的)。然后,我们将刚刚创建的file对象的指针传递给一个QDataStream实例out。类似于std::cout标准输出流,QDataStream也重载了输出重定向<<运算符。后面的代码就很简单了:将“the answer is”和数字 42 输出到数据流。由于我们的 out 对象建立在file之上,因此相当于将问题和答案写入file。
需要指出一点:最好使用 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;
上一节我们介绍了有关二进制文件的读写。二进制文件比较小巧,却不是人可读的格式。而文本文件是一种人可读的文件。为了操作这种文件,我们需要使用QTextStream类。QTextStream和QDataStream的使用类似,只不过它是操作纯文本文件的。QTextStream会自动将 Unicode 编码同操作系统的编码进行转换,这一操作对开发人员是透明的。它也会将换行符进行转换,同样不需要自己处理。QTextStream使用 16 位的QChar作为基础的数据存储单位,同样,它也支持 C++ 标准类型,如 int 等。实际上,这是将这种标准类型与字符串进行了相互转换。QTextStream同QDataStream的使用基本一致,例如下面的代码将把“The answer is 42”写入到 file.txt 文件中。
QFile data("file.txt");
if (data.open(QFile::WriteOnly | QIODevice::Truncate))
{
QTextStream out(&data);
out << "The answer is " << 42;
}
这里,我们在open()函数中增加了QIODevice::Truncate打开方式。我们可以从下表中看到这些打开方式的区别:
枚举值 描述
QIODevice::NotOpen 未打开
QIODevice::ReadOnly 以只读方式打开
QIODevice::WriteOnly 以只写方式打开
QIODevice::ReadWrite 以读写方式打开
QIODevice::Append 以追加的方式打开,
QIODevice::Truncate 以重写的方式打开,在写入新的数据时会将原有
数据全部清除,游标设置在文件开头。
QIODevice::Text 在读取时,将行结束符转换成 \n;在写入时,将行结束符转换成本地格式,例如 Win32 平台上是 \r\n。
QIODevice::Unbuffered 忽略缓存
我们在这里使用了QFile::WriteOnly | QIODevice::Truncate,也就是以只写并且覆盖已有内容的形式操作文件。注意,QIODevice::Truncate会直接将文件内容清空。虽然QTextStream的写入内容与QDataStream一致,但是读取时却会有些困难。
QFile data("file.txt");
if (data.open(QFile::ReadOnly))
{
QTextStream in(&data);
QString str;
int ans = 0;
in >> str >> ans;
}
在使用QDataStream的时候,这样的代码很方便,但是使用了QTextStream时却有所不同:读出的时候,str 里面将是 The answer is 42,ans 是 0。这是因为当使用QDataStream写入的时候,实际上会在要写入的内容前面,额外添加一个这段内容的长度值。而以文本形式写入数据,是没有数据之间的分隔的。因此,使用文本文件时,很少会将其分割开来读取,而是使用诸如使用:
QTextStream::readLine() 读取一行
QTextStream::readAll()读取所有文本
这种函数之后再对获得的QString对象进行处理。默认情况下,QTextStream的编码格式是 Unicode,如果我们需要使用另外的编码,可以使用:
stream.setCodec("UTF-8");
这样的函数进行设置。
附录:
QFile:
Widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = 0);
~Widget();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
Widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include
#include
#include
#include
#include
#include
#include
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
//点击按钮,弹出文件的对话框,选择文件。
connect(ui->pushButton,&QPushButton::clicked,[=](){
QString filePath= QFileDialog::getOpenFileName(this,"打开文件","C:\\Users\\Administrator\\Desktop");
qDebug()<lineEdit->setText(filePath);
//文本编码格式类
QTextCodec *codec=QTextCodec::codecForName("utf8");
QFile file(filePath);
//指定打开的方式
file.open(QIODevice::ReadOnly);
QByteArray array;
array=file.readAll();
// array=file.readLine("长恨歌");
//默认支持的格式是utf-8
// ui->textEdit->setText((array));
ui->textEdit->setText(codec->toUnicode(array));
file.close();
// //写文件==================================================
//==============================================================
// file.open(QIODevice::Append); //使用追加的方式进行写文件
// file.write("aaaaa");
// file.close();
QFileInfo info(filePath);
qDebug()<<"文件的后缀名:"<
QPainterDevice:
widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = 0);
~Widget();
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include
#include
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
//QPixmap 做绘图设备,对平台有不同的显示优化
// QPixmap pix(300,300);
// pix.fill(Qt::white);
// QPainter painter(&pix);
// painter.setPen(QPen(Qt::red));
// painter.drawEllipse(QPoint(150,150),100,100);
// pix.save("C:\\Users\\Administrator\\Desktop\\pix.png");
//QImage 做绘图设备。 不同平台下显示效果一样,可以对像素
// QImage img(300,300,QImage::Format_RGB32);
// img.fill(Qt::cyan);
// QPainter painter(&img);
// painter.setPen(Qt::green);
// painter.drawEllipse(QPoint(40,50),20,20);
// img.save("C:\\Users\\Administrator\\Desktop\\img.png");
// QPicture 重现和记录绘图指令的
QPicture pic;
QPainter painter;
painter.begin(&pic);
painter.setPen(QPen(Qt::blue));
painter.drawEllipse(QPoint(40,50),20,20);
painter.end();
pic.save("C:\\Users\\Administrator\\Desktop\\pic.log");
}
void Widget:: paintEvent(QPaintEvent *){
QImage img;
img.load(":/new/prefix1/Image/Luffy.png");
QPainter painter(this);
//修改像素
// for(int i=100;i<150;i++){
// for(int j=100;j<150;j++){
// QRgb value =qRgb(255,0,0);
// img.setPixel(i,j,value);
// }
// }
// painter.drawImage(0,0,img);
QPicture pic;
pic.load("C:\\Users\\Administrator\\Desktop\\pic.log");
painter.drawPicture(0,0,pic);
}
Widget::~Widget()
{
delete ui;
}
QEvent
mylabel.h
#ifndef MYLABEL_H
#define MYLABEL_H
#include
#include
class MyLabel : public QLabel
{
Q_OBJECT
public:
explicit MyLabel(QWidget *parent = 0);
// void enterEvent(QEvent *);
// void leaveEvent(QEvent *);
//鼠标移动事件
void mouseMoveEvent(QMouseEvent *);
void mousePressEvent(QMouseEvent *);
void mouseReleaseEvent(QMouseEvent *);
//事件的分发器
bool event(QEvent *e) ;
signals:
public slots:
};
#endif // MYLABEL_H
mylabel.cpp
#include "mylabel.h"
#include
#include
MyLabel::MyLabel(QWidget *parent) : QLabel(parent)
{
//设置鼠标的追踪
setMouseTracking(true);
}
//鼠标进入事件
//void MyLabel:: enterEvent(QEvent *){
// qDebug()<<"鼠标进入了";
//}
//鼠标离开事件
//void MyLabel:: leaveEvent(QEvent *){
// qDebug()<<"鼠标离开了";
//}
//=================================================
//====================================================
void MyLabel:: mouseMoveEvent(QMouseEvent *ev){
if(ev->buttons()& Qt::LeftButton){
QString str=QString("鼠标 移动了 x=%1 y=%2").arg(ev->x()).arg((ev->y()));
qDebug()<button()==Qt::LeftButton){
QString str=QString("鼠标按下了 x=%1 y=%2").arg(ev->x()).arg((ev->y()));
qDebug()<<"鼠标按下了";
}
}
void MyLabel:: mouseReleaseEvent(QMouseEvent *ev){
if(ev->button()==Qt::LeftButton){
QString str=QString("鼠标释放了 x=%1 y=%2").arg(ev->x()).arg((ev->y()));
// qDebug()<<"鼠标释放了";
}
}
//事件分发器的用途主要是分发事件的。
bool MyLabel:: event(QEvent *e) {
if(e->type()==QEvent::MouseButtonPress){
//如果鼠标按下,拦截事件,不向下分发。
QMouseEvent *ev=static_cast(e);
QString str=QString("event 鼠标释放了 x=%1 y=%2").arg(ev->x()).arg((ev->y()));
qDebug()<
widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
Q_OBJECT
public:
explicit Widget(QWidget *parent = 0);
~Widget();
//添加定时器的事件
void timerEvent(QTimerEvent *);
int id1;
int id2;
//重写过滤器事件
bool eventFilter(QObject *,QEvent *);
private:
Ui::Widget *ui;
};
#endif // WIDGET_H
#include "widget.h"
#include "ui_widget.h"
#include //定时器的一个类
#include
#include
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
//开启定时器
id1= startTimer(1000);//毫秒
id2= startTimer(2000);
QTimer *timer1=new QTimer(this);
timer1->start(500);
//每隔0.5s 会抛出一个信号出来。
connect(timer1,&QTimer::timeout,[=](){
static int num3=1;
ui->label_4->setText(QString::number(num3++));
});
//点击停止的按钮, 停止计时器
connect(ui->pushButton,&QPushButton::clicked,[=](){
timer1->stop();
});
//给label 添加事件过滤器,做更高级的拦截操作
//1.给控件安装过滤器
ui->label->installEventFilter(this);
//重写过滤器事件
}
bool Widget:: eventFilter(QObject *obj,QEvent *e){
if(obj==ui->label){
//如果鼠标按下,拦截事件,不向下分发。
QMouseEvent *ev=static_cast(e);
QString str=QString("事件过滤器 x=%1 y=%2").arg(ev->x()).arg((ev->y()));
qDebug()<timerId()==id1){
//每隔1s 钟,让label_2 数字++
static int num=1;
ui->label_2->setText(QString::number(num++));
}
//每隔2s 让label_3 ++
if(ev->timerId()==id2){
static int num2=1;
ui->label_3->setText(QString::number(num2++));
}
}
提纲:
- 自定义控件封装
- 右键项目- 添加新文件
- Qt - Qt设计师界面类
- 在ui文件中设计自定义控件
- 在主窗口中 拖拽 Widget做提升的操作
- 在自定义控件中 封装对外接口
- SpinBox和 slider控件交互
- getData获取显示数字
- setData设置数字
- Qt鼠标常用事件
- 鼠标进入
- EnterEvent
- 鼠标离开
- LeaveEvent
- 鼠标按下
- MousePressEvent
- 鼠标释放
- MouseReleaseEvent
- 鼠标移动
- MouseMoveEvent
- 获取 x y 坐标 ev->x() ev->y()
- 判断如果是左键 按下
- ev->button() 移动 ev->buttons() &
- 设置鼠标追踪状态
- setMouseTracking(true);
- 定时器
- 定时器事件 void timerEvent( QTimerEvent * e)
- 启动定时器 id1 = startTimer(毫秒)
- 判断具体定时器标准 e->timerId() == id1
- 定时器2
- 通过定时器类实现 QTimer类
- 创建定时器对象
- QTimer * timer1 = new QTimer(this);
- 开启定时器
- timer1->start(x毫秒)
- 每隔x毫秒 会抛出一个信号timeout出来
- 监听信号处理逻辑
- 暂停定时器 timer1->stop();
- 事件分发器
- 用途:用于分发事件
- 原型 bool event(QEvent * e);
- 返回值如果是 true代表用户自己处理事件,不在向下分发
- 事件过滤器
- 步骤1 给控件安装过滤器
- ui->label->installEventFilter(this);
- 步骤2 重写过滤器事件
- bool Widget::eventFilter(QObject * obj , QEvent * e)
- QPainter绘图
- 绘图事件 void paintEvent(QPaintEvent *);
- 创建画家 QPainter painter( this ) this代表绘图设备
- 画线、圆、矩形、文字
- 设置画笔 QPen painter.setPen() 设置笔宽度、风格
- 设置画刷 QBrush painter.setBrush() 设置画刷风格
- 绘图高级设置
- 设置抗锯齿
- painter.setRenderHint(QPainter::Antialiasing);
- 移动画家
- painter.translate(100,0);
- 保存画家状态
- painter.save();
- 取出画家状态
- painter.restore();
- 利用画家画资源图片
- painter.drawPixmap(posX,0,QPixmap(":/Image/Luffy.png"));
- 手动调用绘图事件
- update()
- 绘图设备
- QPixmap
- 对不同的平台显示做了优化
- QImage
- 可以对像素级进行访问
- QPicture
- 记录和重新绘图指令
- 文件读写
- 读文件
- QFile file(文件路径)
- file.open 指定打开方式 QIODevice::ReadOnly
- 利用 file.readAll一次性全部读取
- 利用 file.readLine 按行读取
- file.close 关闭文件对象
- 读取gbk格式
- QTextCodec * codec = QTextCodec::codecForName("gbk");
- codec->toUnicode(array)
- 写文件
- QFile file(路径)
- file.open(QIODevice::Append); //QIODevice::Append追加的方式写文件
- file.write("啊啊啊啊!");
- file.close();
- 文件信息类
- QFileInfo info(路径)
- info获取文件信息
- 后缀名: suffix 文件大小 size 文件名 filename 文件路径 filePath
- 创建日期 created 修改日期 lastModify
- QDateTime 需要用 toString转为自己制定的输出格式