事件处理(- Event Processingn)
事件是视窗系统或者Qt 本身在各种不同的情况下产生的。当用户点击或者释放鼠标,键盘时,一个鼠标事件或者键盘事件就产生了。当窗口第一次显示时,一个绘制事件会产生告诉新可见的窗口绘制自己。很多事件是为了相应用户动作产生的,也有一些事件是由 系统独立产生的。 在用Qt 编程时,我们很少要考虑事件,当一些事件发生时,Qt 控件会发出相应的信号。只有当实现用户控件或者需要修改现有控件的行为时,我们才需要考虑事件。事件不能和信号混淆。一般来讲,在使用控件时需要处理的是信号,在实现一个控件时需要处理事件。例如,我们使用QPushButton 时,我们只要clicked()信号就可以了,而不用管鼠标点击事件。但是如果我们实现一个像 QPushButton 这样的类,我们就需要处理鼠标或者键盘事件 ,发出clicked()信号。
重写事件处理函数(Reimplementing Event Handlers)
在 Qt 中,一个事件是 QEvent 的子类的对象。Qt 能够处理上百种类型的事件,每一类型的事件由一个枚举值确定。例如,对鼠标点击事件,QEvent::type()返回的值为 QEvent::MouseButtonPress。
很多情况下,一个 QEvent 对象不能保存有关事件的所有信息,例如,鼠标点击事件需要保存是左键还是右键触发了这个信息,还要知道事件发生时鼠标指针的位置,这些额外的信息储存在 QEvent 的子类QMouseEvent 中。
Qt 的对象通过QObject::event()得到有关事件的信息。QWidget::event()提供了很多普通类型的信息,实现了很多事件处理函数,例如 mousePressEvent(),keyPressEvent(),paintEvent()等等。
在前面的章节中,我们已经在MainWindow 类,IconEditor 类,Plotter 类中看到了很多事件处理函数,在QEvent 参考文档中,还列举了很多类型的事件。
我们还可以定义自己的事件,把事件分派出去。这里,我们讨论一下两种最常用的事件:键盘事件和时间事件。
重写函数 keyPressEvent()和keyReleaseEvent()可以处理键盘事件。 Plotter 控件就重写了 keyPressEvent()函数。通常,我们只需要重写 keyPressEvent(),需要处理键盘释放事件的只有修改键(Ctrl, Shift, Alt),而这些键的信息可以通过 QKeyEvent::modifiers()得到。例如,如果我们重写了控件 CodeEditor 控件的 KeyPressEvent()函数,区分Home 键和 Ctrl+Home 键:
void CodeEditor::keyPressEvent(QKeyEvent *event)
{
switch (event->key()) { case Qt::Key_Home:
if (event->modifiers() & Qt::ControlModifier) { goToBeginningOfDocument();
} else {
goToBeginningOfLine();
}
break;
case Qt::Key_End:
...
default:
QWidget::keyPressEvent(event);
}
}
Tab 键和Backtab(Shift+Tab)键很特殊,它们是在控件调用 keyPressEvent()之前,由 QWidget::event()处理的,这两个键的作用是把输入焦点转到前一控件或者下一个控件上,在 CodeEditor 中,希望 Tab 键的作用是缩进,可以这样重写 event():
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。例如,goToBeginningOfLine()和 goToBeginningOfDocument()是CodeEditor 的两个公有槽函数, 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())); goToBeginningOfDocumentAction =
new QAction(tr("Go to Beginning of Document"), this); goToBeginningOfDocumentAction->setShortcut(tr("Ctrl+Home")); connect(goToBeginningOfDocumentAction, SIGNAL(activated()),
editor, SLOT(goToBeginningOfDocument()));
...
}
这样可以很容易把一个键盘敲击的命令加入到菜单或者工具条中。如果命令没有出现在用户界面中,可用用 QShortcut 对象代替QAction 对象,在QAction内部就是使用这个类实现键盘的绑定。
通常情况下,只要窗口中有激活的控件,控件上用 QAction 和 QShortcut 设置的键盘绑定都是可用的。绑定的键可用 QAction::setShortcutContext()或者 QShortcur::setContext()进行修改。
另一个常用的事件类型是时间事件。其他事件都是由用户的某种活动引发的,而时间事件则使程序按照一定的时间间隔执行特定的任务。时间事件一般用来使光标闪烁,或者播放动画,或者只是绘制显示界面或者控件。
为了介绍时间事件,我们将实现一个 Ticker 控件。这个控件显示一条标语,每隔 30 毫秒钟向左移动一个象素。如果控件比标语要宽,标语的文本重复的显示在控件上,填满整个控件。
#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 的四个事件处理函数,其中三个 timeEvent(), showEvent()和hideEvent()是我们以前没有见过的。
#include <QtGui> #include "ticker.h"
Ticker::Ticker(QWidget *parent)
: QWidget(parent)
{
offset = 0;
myTimerId = 0;
}
在构造函数中,设置offset 为 0,这个变量是文本要显示的x 坐标值。时间ID 总是非 0 的,这里设置myTimerId 为 0 说明我们还没有启动任何时间
void Ticker::setText(const QString &newText)
{
myText = newText; update(); updateGeometry();
}
函数 setText()设置要显示的文本。调用 update()引发绘制事件重新显示文本, updateGeometry()通知布局管理器改变控件的大小。
QSize Ticker::sizeHint() const
{return fontMetrics().size(0, text());
}
函数 sizeHint()返回的是控件在不同文本时完整显示所需的尺寸。 QWidget::fontMetrics()返回一个 QFontMetrics 对象,得到控件所用的字体的信息。在这里我们需要得到的是文本的大小。(在 QFontMetrics::size()中,第一个参数是一个标识,对字符串来讲并不需要,所有赋了 0 值)。
void Ticker::paintEvent(QPaintEvent * /* event */)
{
QPainter painter(this);
int textWidth = fontMetrics().width(text()); if (textWidth < 1)
return;
int x = -offset;
while (x < width()) {
painter.drawText(x, 0, textWidth, height(), Qt::AlignLeft | Qt::AlignVCenter, text());
x += textWidth;
}
}
函数paintEvent()使用QPainter::drawText()绘制文本。调用fontMetrics()得到文本所需要的水平空间,然后多次绘制文本,直至填满整个控件
void Ticker::showEvent(QShowEvent * /* event */)
{
myTimerId = startTimer(30);
}
showEvent()启动了一个计时器。调用QObject::startTimer()返回一个ID值,这个 ID 值可以帮助我们识别这个计时器。QObject 能够支持多个独立的不同的时间间隔的计时器。调用 startTimer()以后,Qt 大约每 30 毫秒产生一个事件,时间的准确与否取决于不同的操作系统。
我们也可以在 Ticker 的构造函数中调用 startTimer()。但是在控件可见以后再启动,能够节省一些资源。
void Ticker::timerEvent(QTimerEvent *event)
{
if (event->timerId() == myTimerId) {
++offset;
if (offset >= fontMetrics().width(text())) offset = 0;
scroll(-1, 0);
} else {
QWidget::timerEvent(event);}
}
函数 timerEvent()由系统以一定间隔进行调用的。把offset 增加 1 来模仿文字的移动,增加到标语的宽度时文字的宽度是重新设置为 0。然后调用scroll()把控件向左滚动一个象素。也可以调用 update(),但是 scroll()更加高效,它对可见的象素进行移动,只是对需要新绘制的地方调用绘制事件(在这个例子中 ,只是一个象素宽的区域)。
如果计时器不是我们需要处理的,则把它传递给基类。
void Ticker::hideEvent(QHideEvent * /* event */)
{
killTimer(myTimerId);
}
在 hideEvent()中,调用QObject::killTimer()停止计时器。
时间事件的优先级很低,如果需要多个计时器,那么跟踪每一个计时器的ID 是很费时的。这种情况下,较好的方法是为每一个计时器创建一个QTimer 对象。在每一个时间间隔内,QTimer 发出一个 timeout()信号。QTimer 还支持一次性计时器(只发出一次 timeout()信号的计时器)。