在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毫秒钟向左移动一个象素。如果控件比标语要宽,标语的文本重复的显示在控件上,填满整个控件。
Figure 7.1. The Ticker widget
头文件如下:
#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()信号的计时器)。