您的位置:首页 > 编程语言 > Qt开发

Qt学习笔记(三)事件处理

2013-10-15 09:21 155 查看
事件处理

1、事件

事件是由窗口或者Qt自身产生的,用以响应所发生的各类事情。当用户按下或者键盘或者鼠标按钮时,就可以产生一个键盘或者鼠标事件;当某个窗口第一次显示的时候,就会产生一个绘制事件,用来告知窗口需要重新绘制它本身,从而使窗口可见。大多数事件是作为用户动作的响应而产生的,但是也有一些例外,比如定时器事件,则是由系统产生的。

在使用Qt进行编程开发时,基本不需要考虑事件,因为在发生某些重要的事件的时候,Qt窗口部件都会发射信号。但是当我们需要编写自己的自定义窗口部件或者是我们希望改变已经存在的Qt窗口部件的行为时,事件就变得很有用。

信号和事件是有区别的。一般情况下,在使用窗口部件的时候,信号是很有用的;而在实现窗口部件的时候,事件是有用的。例如,当我们使用QPushButton时,对于他的clicked()信号往往很关注,而很少关心促成发射该信号的底层鼠标或者键盘事件。但是,当我们实现的是一个类似于QPushButton的类,就需要编写一定的处理鼠标和键盘事件的代码,而且在必要的时候,还需要发射clicked()信号。

2、重新实现事件处理器

在Qt中,事件就是QEvent子类的一个实例。Qt处理的事件类型有100多种,其中每一种都可以通过一个枚举值来进行识别。例如,QEvent::type()可以返回用于处理鼠标按键事件QEvent::MouseButtonPress.许多事件需要的信息比存储在普通QEvent对象中的信息多的多。例如鼠标单击事件,需要保存是哪一个按键、坐标等,这些信息需要使用专门QEvent子类,如QMouseEvent。

通过继承QObject,事件通过他们的event()函数来通知对象。在QWidget中的event()实现把绝大多数常用类型的事件提前传递给特定的事件处理器,比如mousePressEvent()、keyPressEvent()等。

在QEvent的参考文档中,列出了很多类型的事件,并且我们也可以自己创建一些自定义事件类型和发布一些事件。

通过实现keyPressEvent()和keyReleaseEvent()就可以处理键盘事件。通常我们只需要重新实现keyPressEvent()就可以,因为用于表明释放重要性的键只是Ctrl, Shift, Alt这些修饰键,而这些键的状态可以通过keyPressEvent()检测出来。

例如,要实现一个CodeEditor窗口部件,需要区分按下的键是Home键还是Ctrl+Home键,keyPressEvent中可以写:

void CodeEditor::keyPressEvent(QKeyEvent* event)

{

switch(event->key())

{

case Qt::Key_Home:

if(event->modifiers() & Qt::ControlModifer)

goToBeginningOfDocument();

else

goToBeginningOfLine();

case Qt::Key_End:

...

default:

QWidget::keyPressEvent(event);

}

}

Tab键和BackTab(Shift + Tab)是两种特殊情况,在窗口部件调用eyPressEvent()之前,QWidget::keyPressEvent()会先处理他们,它所包含的语义就是用于把焦点传递给焦点序列中的下一个或者上一个窗口部件。如在CodeEditor中,我们想要实现用Tab键缩进文本的功能:

bool CodeEditor::event(QEvent* event)

{

if(event->type() == QEvent::KeyPress)

{

QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);

if(keyEvent->key() == Qt::Key_Tab)

{

insertAtCurrentPosition('\t');

return true;

}

}

return QWidget::event(event);

}

上面的实现中,如果该事件是一个按键事件,那么就把这个QEvent对象强制转换成QKeyEvent对象并且检查按下的按键。如果是Tab键,就会做些处理,返回true,告诉Qt已经把该事件处理好,如果返回的是false,则Qt将会把这个事件传递给它的父窗口部件来处理。

实现键的绑定一种更为高级的方法为使用QAction。例如在CodeEditor窗口部件中有goToBeginningOfDocument和goToBeginnningOfLine()两个公共槽,并且这个CodeEditor是用作MainWindow类的中央窗口部件的,可以采用如下绑定:

MainWindow::MainWindow()

{

editor = new CodeEditor;

setCentralWidget(editor);

goToBeginningOfLineAction = new QAction(tr("Go to beginning of Line"), this);

goToBeginningOfLineAction->setShortcut(tr("Home"));//设置快捷键, 快捷键触发动作

connect(goToBeginningOfLineAction, SIGNAL(activated()), editor, SLOT(goToBeginningOfLine()));

..........

}这样就可以将这个命令添加到菜单或工具栏,如果这些命令没有出现在用户界面中,那么就可以用一个QShortcut对象替换这些QAction对象,这个类在QAction内部用于按键绑定。默认情况下,一旦把包含窗口部件的窗口激活,就可以在该窗口部件上使用QAction或QShortcut来启用所绑定的快捷键。但使用QAction::setShortcutContext()或者QShortcut::setContext()可以改变这一点。

另外一种常用的事件是定时器事件,定时器事件允许应用程序可以在一定的时间间隔后执行事件处理,定时器事件可以用来实现光标的闪烁和其他动画的播放,或者只是简单的用作显示的刷新。

Ticker窗口部件实例:显示了一串文本标语,每30毫秒向左移动一个像素,如果窗口部件比文本宽,那么文本将会被多次重复,直到能够填满整个窗口部件的宽度为止。 源码如下:ticker.h[cpp] view plaincopy#ifndef TICKER_H  
#define TICKER_H  
  
#include <QWidget>  
  
class Ticker : public QWidget  
{  
    Q_OBJECT  
    Q_PROPERTY(QString text READ text WRITE setText)  
  
public:  
    Ticker(QWidget *parent = 0);    //析构  
  
    void setText(const QString &newText);   //设置要显示的文本  
    QString text() const { return myText; }  
    QSize sizeHint() const;  
  
protected:  
    void paintEvent(QPaintEvent *event);  
    void timerEvent(QTimerEvent *event);  
    void showEvent(QShowEvent *event);  
    void hideEvent(QHideEvent *event);  
  
private:  
    QString myText;  
    int offset;  
    int myTimerId;  
};  
  
#endif  
ticker.cpp[cpp] view plaincopy#include <QtGui>  
  
#include "ticker.h"  
  
Ticker::Ticker(QWidget *parent)  
    : QWidget(parent)  
{  
    offset = 0;  
    myTimerId = 0;  
}  
  
void Ticker::setText(const QString &newText)    //设置要显示的文本  
{  
    myText = newText;  
    update();   //强制执行一个重绘操作  
    updateGeometry();   //通知对Ticker窗口部件负责的任意布局管理器,提示该窗口部件的大小发生了变化。  
}  
  
QSize Ticker::sizeHint() const  //返回文本所需的空间大小,并以此作为窗口部件的理想尺寸。  
{  
    return fontMetrics().size(0, text());   //返回一个QFontMetrics对象,可以用这个对象查询并获得与这个窗口部件字体相关的信息  
}  
  
void Ticker::paintEvent(QPaintEvent * /* event */)  //使用QPainter::drawText()绘制文本  
{  
    QPainter painter(this);  
  
    int textWidth = fontMetrics().width(text());    //使用fontMetrics()确定文本在水平方向上所需的空间  
    if (textWidth < 1)  
        return;  
    int x = -offset;  
    while (x < width()) {                //考虑offset的值,多次绘制文本,填充整个窗口部件的宽度为止  
        painter.drawText(x, 0, textWidth, height(),  
                         Qt::AlignLeft | Qt::AlignVCenter, text());  
        x += textWidth;  
    }  
}  
  
void Ticker::showEvent(QShowEvent * /* event */)    //启动定时器  
{  
    myTimerId = startTimer(30);  
}  
  
void Ticker::timerEvent(QTimerEvent *event)  
{  
    if (event->timerId() == myTimerId) {  
        ++offset;  
        if (offset >= fontMetrics().width(text()))  
            offset = 0;  
        scroll(-1, 0);  //把窗口部件的内容向左滚动一个像素  
    } else {  
        QWidget::timerEvent(event);  
    }  
}  
  
void Ticker::hideEvent(QHideEvent * /* event */)  
{  
    killTimer(myTimerId);   停止定时器  
    myTimerId = 0;  
}


QObject::startTimer()调用会返回一个ID数字,可以在以后用这个数字识别该定时器。QObject支持多个独立的定时器,每一个都可以有自己的时间间隔,在startTimer()之后,每个一段时间都会触发一个定时器事件。在 void timerEvent(QTimerEvent* event)中进行处理。系统每隔一段时间都会调用一次timerEvent()函数,通过QObject::killTimer(id)来关闭该定时器。

定时器事件是一种低级事件,而且如果需要多个定时器时,保持对所有定时器ID的追踪将会变得麻烦,在这种情况下,通常为每个定时器分别创建一个QTimer对象,QTimer会在每个时间间隔发射一个timeout()信号。QTimer也提供了一个非常方便的接口,可用于单触发定时器(只触发一次的定时器)。

安装事件过滤器:

QObject实例在看到他自己的事件之前,可以通过设置另外一个QObject实例先监视这些事件。

假定有一个由几个QLineEdit组成的TestDialog窗口部件,并且希望使用空格键将光标移动到下一个QLineEdit中,一种更为直接的解决方案是子类化QLineEdit并且重新实现keyPressEvent()函数,由他调用focusNextChild():

void MyLineEidt::keyPressEvent(QKeyEvent* event)

{

if (event->key() == Qt::Key_Space)

focusNextChild();

else

QLineEdit::keyPressEvent(event);

}

这种方法的一个缺点是如果窗体中使用了许多不同类型的窗口部件(如QComboBox, QSpinBox),我们也必须一一对他们子类化,以便他们能实现相同的行为。一个更好的方法是让TestDialog监视他的子窗口部件中键的按下事件并且在监视代码中实现所需的行为。这种方法可以通过使用事件过滤器来实现。创建一个事件过滤器需要两步:

(1)通过对目标对象调用installEventFilet()来注册监视对象

(2)在监视对象的eventFilter()函数中处理目标对象的事件

在构造函数里注册监测对象是一个好地方:

[c-sharp]
view plaincopy

CustomerInfoDialog::CustomerInfoDialog(QWidget *parent) :QDialog(parent)

{

...

firstNameEdit->installEventFilter(this);

lastNameEdit->installEventFilter(this);

cityEdit->installEventFilter(this);

phoneNumberEdit->installEventFilter(this);

}

一旦event Filter注册了, 发送到firstNameEdit, lastNameEdit, cityEdit和phoneNumberEdit的事件在被发送到原来的目的地之前, 会先发到CustomerInfoDialog的eventFilter()函数.这是接收这些事件的eventFilter()函数:

[c-sharp]
view plaincopy

bool CustomerInfoDialog::eventFilter(QObject *target, QEvent *event)

{

if (target == firstnameEdit || target == lastNameEdit

|| target == cityEdit || target == phoneNumberEdit)

{

if(event->type() == QEvent::KeyPress)

{

QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);

if (keyEvent->key() == Qt::Key_Space)

{

focusNextChild();

return true;

}

}

}

return QDialog::eventFilter(target, event);

}

首先,我们检查是否目标widget是一个QLineEdit. 如果是个key Press事件, 把它转换为QKeyEvent, 并检查哪个键值被按下.如果是space, 我们调用focusNextChild()把focus传到focus链上的下一个widget上, 返回true告诉Qt我们已经处理了这个事件.
如果我们返回false, Qt会发送这个event到它原来的目的地,导致一个假的空格被插入到QLineEdit.如果目标widget不是QLineEdit, 或者这个event不是一个space按键, 我们把控制权传回到基类的eventFilter去. 目标wdiget可以是基类-QDialog正在监测的某个widget.

Qt提供了5个级别来处理和过滤事件.

1. 我们可以重新实现特定的event handler.

重新实现像mousePressEvent(), keyPressEvent()和paintEvent()这样的event Handler是目前处理event最普通的方式.

2. 我们可以重新实现QObject::event().

通过重新实现event(),我们可以在事件到达特定的event handler之前对它们作出处理. 这个方法主要是用来覆写Tab键的缺省实现. 也可以用来处理不同发生的事件类型,对它们,就没有特定的event handler. 当重新实现event()的时候,我们必须调用基类的event()来处理我们不显式处理的情况.

3. 我们可以安装一个event filter到一个单独的QObject.

一旦一个对象用installEventFilter注册了, 发到目标对象的所有事件都会先发到监测对象的eventFilter(). 如果同一object安装了多个event filter, filter会依次被激活, 从最近安装的回到第一个.

4. 我们可以在QApplication对象上安装event filter.

一旦一个event filter被注册到qApp(唯一的QApplication对象), 程序里发到每个对象的每个事件在发到其他event filter之前,都要首先发到eventFilter(). 这个方法对debugging非常有用. 也可以用来处理发到disable的widget上的事件, QApplication通常会丢弃它们.

5. 我们可以子类QApplication并重新实现notify().

Qt调用QApplication::notify()来发出事件. 在任何event filter得到之前, 重新实现这个函数是得到所有事件的唯一方法. event filter通常更有用, 因为可以有任意数目且同时存在的event filter, 但是只有一个notify()函数.

许多事件类型,包括鼠标和按键事件, 可以被传播. 如果一个事件没有在传到目标对象的过程中被处理,或者被目标对象本身处理, 整个事件处理过程会重复, 不过, 这次, 目标对象的parent作为新的目标对象. 从parent到parent,这样继续下去,知道事件被处理了,或者到达了顶层的对象

处理密集时的响应保持

当调用QApplication::exec()时,就启动了Qt的事件循环。在开始的时候,Qt会发出一些命令来显示和绘制窗口部件。在这之后,事件循环就开始运行,它不断检查是否有事件发生,并把事件发送给应用程序的QObject。

当处理一个事件,可能会同时产生一些其他的事件并且会将其追加到Qt的事件队列中,如果在处理一个事件上耗费的时间过多(如保存文件操作),则用户界面就无法响应。在这种情况下,一种解决方法是使用多线程, 一个线程用于处理应用程序的用户界面,另一个线程则 执行文件的保存操作。一种更为简单的办法是在文件保存的代码中频繁调用 QApplication::processEvents(),这个函数告诉Qt处理所有那些还没被处理的各类事件,然后再将控制权返回调用者。实际上QApplication::exec()就是一个不停调用processEvent()函数的while循环。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: