Más contenido relacionado Más de Cedemir Pereira (9) Cap71. Processamento de Eventos
● Reimplementando Manipuladores de Eventos
● Instalando Filtros de Eventos
● Manter Respondendo durante Processamento Intensivo
Eventos são gerados pelo gerenciador de janelas ou pelo próprio Qt em resposta a várias
ocorrências. Quando o usuário pressiona uma tecla ou o botão do mouse, um evento de tecla
(KeyPress) ou um evento do mouse (MouseDown) é gerado. Quando uma janela é exibida pela
primeira vez, um evento paint é gerado para dizer a janela recém visível que ela precisa ser
desenhada. A maioria dos eventos são gerados em resposta às ações de usuários, mas alguns,
como os eventos de timer, são gerados de forma independente pelo sistema.
Quando se programa com Qt, raramente é preciso pensar sobre os eventos, porque os widgets
do Qt emitem sinais quando algo de significativo acontece. Eventos tornamse úteis quando
escrevemos nossos próprios widgets personalizados ou quando queremos modificar o
comportamento dos widgets já existentes do Qt.
Os eventos não devem ser confundidos com sinais. Como regra, os sinais são úteis ao utilizar um
widget, enquanto que os eventos são úteis na implementação de um widget. Por exemplo, quando
estamos usando QPushButton, estamos mais interessados no sinal clicked() do que nos eventos
do mouse em baixo nível ou nos eventos do teclado que causaram o sinal emitido. Mas se
estamos implementando uma classe como QPushButton, precisamos escrever um código para
lidar com os eventos do mouse e do teclado. Assim, devemos emitir o sinal clicked(), quando
necessário.
Reimplementando Manipuladores de Eventos
No Qt, um evento é uma instância de uma subclasse QEvent. Qt trata mais de uma centena de
tipos de eventos, cada uma identificada por um valor de enumeração. Por exemplo, QEvent::type()
retorna QEvent::MouseButtonPress para eventos de pressionamento do botão do mouse.
Muitos tipos de eventos exigem mais informações do que pode ser armazenado em um simples
objeto QEvent; por exemplo: eventos de pressionamento do mouse necessitam armazenar a
informação de qual botão do mouse foi pressionado, bem como onde o ponteiro do mouse estava
posicionado quando o evento ocorreu. Esta informação adicional é armazenado em uma
subclasse derivada da QEvent denominada QMouseEvent.
Os eventos são comunicados aos objetos através da função event(), herdada de QObject. A
implementação de event() em QWidget encaminha os tipos mais comuns de eventos para os
manipuladores específicos de eventos, como mousePressEvent(), keyPressEvent() e
paintEvent().
Já vimos vários manipuladores de eventos ao implementar MainWindow, IconEditor e Plotter em
2. capítulos anteriores. Muitos outros tipos de eventos são listados na documentação de referência
de QEvent. Também é possível criar tipos de eventos personalizados e despachar eventos nós
mesmos. Aqui, vamos analisar dois tipos de eventos comuns que merecem mais explicações: os
eventos de tecla e eventos de timer.
Os eventos de tecla são manipulados por reimplementação dos eventos keyPressEvent() e
keyReleaseEvent(). O widget Plotter reimplementa keyPressEvent(). Normalmente, precisamos
apenas reimplementar keyPressEvent(), uma vez que o código da tecla liberada normalmente é
modificado por Ctrl, Shift e Alt, e estes podem ser checados no evento keyPressEvent() usando
QKeyEvent()::modifiers. Por exemplo, se estivéssemos implementando um widget CodeEditor,
seu evento keyPressEvent() implementado para distinguir entre o pressionamento da tecla Home
ou o pressionamento das teclas Ctrl + Home ficaria assim:
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);
}
}
As teclas Tab e Backtab (Shift+Tab) são casos especiais. Elas são tratadas por QWidget::event()
antes que o evento keyPressEvent() seja chamado. Essas teclas passam o foco do widget atual
para o próximo widget ou para o widget anterior da cadeia. Este comportamento é normalmente o
que desejamos para a tecla Tab, mas para um widget CodeEditor podemos preferir que a tecla Tab
faça recuar uma linha. A reimplementação de event() ficaria assim:
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);
}
Se o evento foi um pressionamento de tecla, fazemos um cast do objeto QEvent para o
QKeyEvent e verificamos qual tecla foi pressionada. Se a tecla for Tab, fazemos algum
processamento e retornamos true para informar ao Qt que nós tratamos o evento. Se retornarmos
false, o Qt irá tratar o evento, propagando para o widget pai.
Uma abordagem de alto nível para tratamento de teclas é usar um QAction. Por exemplo, se
goToBeginningOfLine() e goToBeginningOfDocument() são slots públicos do widget CodeEditor e
o CodeEditor é usado como o elemento central em uma classe MainWindow, podemos adicionar
3. os atalhos de teclado com o seguinte código:
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()));
...
}
Isto torna fácil adicionar comandos ao menu ou a barra de ferramentas, como foi visto no Capítulo
3. Se os comandos não aparecem na interface do usuário, poderíamos substituir o objeto QAction
pelo objeto QShortcut, que é usado internamente pelo QAction para suporte a teclas de atalho.
Por padrão, atalhos de tecla configurados usando QAction ou QShortcut em um widget, são
habilitados sempre que a janela que contém o widget está ativa. Isso pode ser mudado usando
QAction::setShortcutContext() ou QShortcut::setContext().
Outro tipo comum de evento é o evento timer. Enquanto a maioria dos outros eventos ocorrem
através de uma ação do usuário, o evento timer ocorre em um intervalo regular de tempo,
permitindo que o aplicativo faça algum processamento periódico. Os eventos de timer podem ser
usados para implementar um cursor piscando e outras animações, ou simplesmente para
atualizar a tela.
Para demonstrar eventos de timer, vamos implementar um widget Ticker mostrado na Figura 7.1.
Este widget mostra um banner de texto que rola um pixel para a esquerda a cada 30
milissegundos. Se o widget é maior do que o texto, o texto é repetido tantas vezes quanto
necessário para preencher toda a largura do widget.
Figura 7.1. O widget Ticker
Este é o arquivo de cabeçalho:
#ifndef TICKER_H
#define TICKER_H
#include <QWidget>
class Ticker : public QWidget
{
Q_OBJECT
Q_PROPERTY(QString text READ text WRITE setText)
5. A função sizeHint() retorna o espaço necessário para o texto como tamanho ideal do widget.
QWidget::fontMetrics() retorna um objeto QFontMetrics que pode ser consultado para obter
informações relativas à fonte do widget. Neste caso, solicitamos o tamanho necessário para um
determinado texto. (O primeiro parâmetro para QFontMetrics::size() é um flag não necessário para
uma string simples, então passamos apenas zero).
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;
}
}
A função paintEvent() desenha o texto usando QPainter::drawText(). Ele usa fontMetrics() para
verificar o espaço horizontal que o texto precisa e em seguira desenha o texto quantas vezes
forem necessárias para preencher toda a largura do widget levando em conta o offset.
void Ticker::showEvent(QShowEvent * /* event */)
{
myTimerId = startTimer(30);
}
A função showEvent() inicia o timer. A chamada para QObject::startTimer() retorna um número ID
que pode ser usado mais tarde para identificar o timer. QObject suporta vários timers
independentes, cada um com o seu respectivo intervalo de tempo. Após a chamada de
startTimer(), o Qt irá gerar um evento de timer a cada 30 milissegundos aproximadamente. A
precisão depende do sistema operacional usado.
Poderíamos ter chamado startTimer() logo no construtor de Ticker, mas poupamos alguns
recursos fazendo o Qt gerar os eventos de timer somente quando o widget está realmente visível.
void Ticker::timerEvent(QTimerEvent *event)
{
if (event>timerId() == myTimerId) {
++offset;
if (offset >= fontMetrics().width(text()))
offset = 0;
scroll(1, 0);
} else {
QWidget::timerEvent(event);
}
}
O sistema chama a função timerEvent() periodicamente. Isto incrementa a variável offset em 1
para simular o movimento, envolvendo a largura do texto. Em seguida ele rola o conteúdo do
widget 1 pixel para a esquerda, usando QWidget::scroll(). Poderíamos ter chamado update() em
vez de scroll() que já seria suficiente, porém scroll() é mais eficiente porque ele simplesmente
7. CustomerInfoDialog::CustomerInfoDialog(QWidget *parent)
: QDialog(parent)
{
...
firstNameEdit>installEventFilter(this);
lastNameEdit>installEventFilter(this);
cityEdit>installEventFilter(this);
phoneNumberEdit>installEventFilter(this);
}
Uma vez que o evento de filtro é registrado, os eventos enviados para os widgets firstNameEdit,
lastNameEdit, cityEdit, e phoneNumberEdit são primeiro enviados para a função eventFilter() de
CustomerInfoDialog antes de serem enviados para seu destino alvo.
Esta é a função eventFilter() que recebe os eventos:
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);
}
Primeiro verificamos se o widget alvo é um dos QLineEdits. Se o evento foi uma tecla
pressionada, fazemos um cast de QKeyEvent e verificamos qual tecla foi pressionada.
Se a tecla pressionada foi Space, chamamos focusNextChild() para passar o foco ao próximo
widget da cadeia de foco e retornamos true para avisar ao Qt que tratamos o evento. Se
tivéssemos retornado false, Qt iria enviar o evento para seu alvo, resultando em um espaço
espúrio sendo inserido no QLineEdit.
Se o widget alvo não é um QLineEdit, ou se o evento não é um pressionamento da tecla de
espaço, podemos passar o controle para implementação da classe base de eventFilter(). O widget
alvo também poderia ser algum widget que a classe base QDialog está monitorando. (Em Qt 4. 3,
este não é o caso de QDialog. No entanto, outras classes de widget do Qt, tais como QScrollArea,
monitoram algumas das suas widgets filhas por diversos motivos.)
Qt oferece cinco níveis em que eventos podem ser processados e filtrados:
1. Podemos reimplementar um manipulador de eventos específicos.
Reimplementação de manipuladores de eventos, tais como mousePressEvent(),
keyPressEvent(), e paintEvent() é de longe a forma mais comum de processar eventos.
Nós já vimos vários exemplos disso.
2. Podemos reimplementar QObject::event().
8. Ao reimplementar a função event(), nós podemos processar os eventos antes que eles
atinjam os manipuladores de eventos específicos. Esta abordagem é necessária para
substituir o comportamento padrão da tecla Tab como foi visto anteriormente (p. ?x?). Esta
abordagem também é necessária para manipular tipos raros de eventos, os quais não
existem manipuladores específicos de eventos (por exemplo, QEvent::HoverEnter).
Quando reimplementamos event(), temos que chamar a função event() da classe base
para tratar os casos que não tratamos explicitamente.
3. Podemos instalar um filtro de eventos em um único QObject.
Depois que o objeto foi registrado usando installEventFilter(), todos os eventos para o
objeto alvo são primeiro enviados para a função de monitoramento de objetos, eventFilter().
Se vários eventos de filtro são instalados no mesmo objeto, os filtros são ativados por sua
vez do mais recentemente instalado voltando para o primeiro instalado.
4. Podemos instalar um filtro de eventos no objeto QApplication.
Depois que um filtro de evento foi registrado para o qApp (o único objeto QApplication),
todos os eventos de cada objeto do aplicativo são enviados para a função eventFilter()
antes de serem enviados para qualquer outro filtro de eventos. Essa abordagem é mais útil
para depuração. Ela também pode ser usada para manipular eventos de mouse enviados
para widgets desabilitados, que QApplication normalmente descarta.
5. Podemos herdar QApplication e reimplementar notify().
O Qt chama QApplication::notify() para enviar um evento. Reimplementar esta função é a
única maneira de obter os eventos antes que qualquer filtro de eventos os receba. Os filtros
de eventos são normalmente mais úteis, porque podem existir vários filtros de eventos
simultâneos, enquanto que somente uma função notify().
Muitos tipos de eventos, incluindo os de mouse e os de teclado podem ser propagados. Se o
evento não for tratado no caminho para seu objeto alvo ou pelo próprio objeto alvo, todo o
processamento do evento é repetido, mas desta vez com o objeto pai sendo o novo objeto alvo.
Isso continua se repetindo passando de filho para pai até que o evento seja tratado ou que o objeto
toplevel seja alcançado.
A Figura 7.2 mostra como um evento de pressionamento de tecla é propagado de filho para pai em
uma caixa de diálogo. Quando um usuário pressiona uma tecla, o evento primeiro é enviado para o
widget com o foco, neste caso o QCheckBox inferior direito (1). Se o QCheckBox não tratar o
evento, o Qt vai enviálo para QGroupBox (2), e finalmente para o objeto QDialog (3).
Figura 7.2. Propagação de evento em uma caixa de diálogo
9. Manter Respondendo durante Processamento Intensivo
Quando chamamos Qapplication::exec(), iniciamos o event loop do Qt. O Qt emite alguns eventos
na inicialização para mostrar e desenhar os widgets. Depois disso o event loop é executado,
checando constantemente se ocorreram eventos e despachando esses eventos para os Qobjects
do aplicativo.
Enquanto um evento está sendo processado, eventos adicionais podem ser gerados e anexados a
fila de eventos do Qt. Se gastamos muito tempo processando um evento específico, a interface do
usuário ficará sem responder. Por exemplo, todos os eventos gerados pelo sistema de janelas
enquanto o aplicativo está salvando um arquivo em disco não serão processados até que o
arquivo seja salvo. Durante a gravação, o aplicativo não responderá às solicitações do sistema
janelas para se redesenhar.
Uma solução é usar múltiplos threads: um thread para a interface do usuário e outro para o
salvamento do arquivo (ou qualquer outra operação demorada). Desta forma a interface do usuário
continuará respondendo enquanto o arquivo estiver sendo salvo. Veremos como fazer isso no
Capítulo 14.
Uma solução mais simples é fazer chamadas frequentes para a função
QApplication::processEvents() no código de gravação do arquivo. Esta função fiz para o Qt
processar todos os eventos pendentes e depois retornar o controle para seu chamador. Na
verdade Qapplication::exec() é um pouco mais do que um loop while em torno de uma chamada
de função processEvents().
Aqui está um exemplo de como podemos manter a interface do usuário respondendo usando
processEvents(), baseado no código de gravação do arquivo usado em Spreadsheet (p. ?x?):
bool Spreadsheet::writeFile(const QString &fileName)
{
QFile file(fileName);
...
QApplication::setOverrideCursor(Qt::WaitCursor);
for (int row = 0; row < RowCount; ++row) {
for (int column = 0; column < ColumnCount; ++column) {
QString str = formula(row, column);
if (!str.isEmpty())
out << quint16(row) << quint16(column) << str;
}
qApp>processEvents();
}
QApplication::restoreOverrideCursor();
return true;
}
Um perigo desta abordagem é que o usuário pode fechar a janela principal enquanto o arquivo
ainda está sendo gravado, ou clicar em File|Save uma segunda vez, resultando em um
comportamento indefinido. A solução mais fácil para este problema é substituir
qApp>processEvents();
por
qApp>processEvents(QEventLoop::ExcludeUserInputEvents);
10. dizendo assim para o Qt ignorar os eventos do mouse e do teclado.
Muitas vezes queremos mostrar um QprogressDialog enquanto é executada uma operação
demorada. O QprogressDialog tem uma barra de progresso que mantém o usuário informado
sobre o progresso da operação. QprogressDialog também fornece um botão Cancel que permite
ao usuário cancelar a operação. Aqui está um código para salvar um arquivo do Spreadsheet
usando esta abordagem.
Veja o Código:
bool Spreadsheet::writeFile(const QString &fileName)
{
QFile file(fileName);
...
QProgressDialog progress(this);
progress.setLabelText(tr("Saving %1").arg(fileName));
progress.setRange(0, RowCount);
progress.setModal(true);
for (int row = 0; row < RowCount; ++row) {
progress.setValue(row);
qApp>processEvents();
if (progress.wasCanceled()) {
file.remove();
return false;
}
for (int column = 0; column < ColumnCount; ++column) {
QString str = formula(row, column);
if (!str.isEmpty())
out << quint16(row) << quint16(column) << str;
}
}
return true;
}
Criamos um QprogressDialog com NumRows indicando o número total de etapas. Então para
cada linha, chamamos setValue() para atualizar a barra de prograsso. QprogressDialog calcula
automaticamante a porcentagem, dividindo o valor atual do prograsso pelo número total de etapas.
Chamamos Qapplication::processEvents() para processar algum evento rapaint, cliques e
pressionamento de teclas (por exemplo, permitir que o usuário clique em Cancel). Se o usuário
clicar em Cancel, podemos cancelar o salvamento e remover o arquivo.
Não chamamos show() em QprogressDialog porque diálogos de progresso fazem isso por sí
mesmos. Se a operação acaba rapidamente, provavelmente pelo arquivo a salvar ser muito
pequeno, ou porque a máquina é muito rápida, QProgressDialog irá detectar isto e não será
mostrado totalmente.
Além disso, para multithreading e usando QProgressDialog, há uma maneira completamente
diferente de lidar com operações de longa duração: em vez de executar o processamento quando
o usuário solicita, nós podemos adiar o tratamento até que o aplicativo fique ocioso. Isso pode
funcionar se o processo puder ser interrompido e retomado com segurança, pois não podemos
prever por quanto tempo o aplicativo ficará ocioso.
No Qt, esta abordagem pode ser implementada usando um timer de 0milissegundos. Esses
timers esgotam o tempo sempre que não existem eventos pendentes. Aqui está um exemplo de
implementação de timerEvent() que mostra uma abordagem de processamento ocioso: