Iniciando no QT, parte II – Sinais, Slots e Timers

February 1, 2008 · Posted in C/C++, Qt 

Neste post vou seguindo com a brincadeira de fazer pequenos tutoriais sobre o QT para iniciantes. Desta vez vou mostrar como criar nossos próprios sinais e slots e como manipular um timer.

O código fonte pode ser encontrado neste link.

O programa de exemplo será um cronômetro que vai utilizar a classe QTimer, uma das principais classes do QT, e vai mostrar o tempo utilizando um display estilo LCD de calculadora com a classe QLCDNumber.

QT e os Timers

O suporte mais básico a timers no QT é disponibilizado pela classe QObject, que fornece os métodos QObject::startTimer() e QObject::killTimer(). O primeiro método retorna um ID único de timer e o segundo finaliza o timer através desse ID.

Para que isso funcione, no entanto, o trecho de código que utiliza esse mecanismo precisa estar dentro de um “event loop”. A partir do momento que o timer for iniciado ele de tempos em tempos (timeout) faz com que a aplicação dispate um QTimerEvent, que interrompe o fluxo normal do programa até que o evento seja processado.

O tempo máximo de timeout não é delimitado, sendo possível criar timers com timeout de anos, porém, o tempo mínimo pode variar de sistema para sistema. No windows Vista o timer mínimo é de 10 ms enquanto no Linux 2.6.x isso é configurável (o default é 4 ms). O QT vai tentar entregar todos os eventos, conforme pedidos, mas caso o sistema não permita ele vai descartar os “excedentes”.

Timers também podem ser utilizados em threads, porém deve-se respeitar a condição de estar dentro de um event loop. Threads merecem um artigo à parte, como sempre…

A classe QTimer implementa timers de mais alto nível, possibilitando algumas funcionalidades. Uma delas é o QTimer::singleShot(), que dispara um evento uma única vez.

Leia a extensa e repetitiva documentação, é chato, mas acredite, vai te poupar de muita QDorDeCabeca…

Mais sobre Sinais e Slots

Sinais e slots são utilizados para comunicação entre objetos, sendo uma das peças fundamentais do QT. Nesse sistema ao invés de se implementar callbacks para tratar eventos, utiliza-se o conceito de conectar sinais a slots, tornando a programação mais intuitiva.

Os sinais e slots são métodos de classe que são tratados pelo Meta-Object Compiler (moc) antes do código fonte ser compilado. Eles têm declarações especiais que não são parte do padrão C++, por isso o moc fazum parsing e gera o código fonte compilável.

Ao executar o comando moc sobre qualquer classe que implemente sinais e slots, uma certa quantidade de código fonte é gerada. Com o uso do programa qmake, a chamada ao programa moc fica a cargo do Makefile, tornando-a transparente para o programador.

Para que uma classe possa implementar sinais e slots, ela precisa ter acesso ao Meta-Object System. Por isso ela deve herdar da classe QObject ou suas subclasses, e precisa ter a macro Q_OBJECT na sua área de declaração privada. Terminados os preparativos, sinais são declarados em uma seção “signals:” e slots em seções “[public|protected|private] slots:“.

Um slot, depois de declarado, é então definido como qualquer outro método comum, podendo ser explicitamente chamado como qualquer outro. Já os sinais, são um pouco mais delicados, e são apenas declarados e jamais definidos pelo programador. A razão? Esta mensagem no final da tentativa de compilação:

tmp/moc_mydisplay.o: In function `MyDisplay::signalPlay()':
~/stopwatch/tmp/moc_mydisplay.cpp:89: multiple definition of `MyDisplay::signalPlay()'
tmp/mydisplay.o:~/stopwatch/mydisplay.cpp:169: first defined here
collect2: ld returned 1 exit status
make: ** [stopwatch] Erro 1

Dentro do arquivo moc_mydisplay.cpp é gerado o seguinte código:

// SIGNAL 0
void MyDisplay::signalPlay()
{
    QMetaObject::activate(this, &staticMetaObject, 0, 0);
}

Sim, o moc gera a definição dos sinais com chamadas a meta-métodos do QT. Portanto, definir o corpo de um sinal é ilegal no QT e vai gerar um erro de compilação por redefinição de método. Deixe que o moc cuida da implementação do sinal pra você.

Conecte-se

Não adianta apenas definir sinais e slots. É preciso definir como eles vão interagir. A maneira de fazer isso é conectando-os através do método connect() presente em todo herdeiro de QObject. Isso fará o moc implementar o sinal de forma que uma chamada a ele resulte numa chamada ao(s) slot(s) conectado(s), com os mesmos parâmetros.

Dependendo da forma de conexão, um sinal pode até retornar o mesmo valor que o último slot chamado retornar. Note que isso não é muito seguro para sinais conectados a vários slots, pois, nada garante a ordem de chamada.

Um dado sinal pode ser conectado a um segundo sinal diretamente. Um emit() no primeiro sinal é então equivalente a um emit() no segundo sinal, visto que após a etapa de criação dos mocs, a chamada ao primeiro resulta na chamada ao segundo, que resulta na chamada de outro método conectado e assim sucessivamente.

Formas de conexão

Existem basicamente três formas de conectar sinais e slots e se não usadas adequadamente podem ser fonte de bugs terrivelmente difíceis de se descobrir. Essas formas de conexão são passadas como parâmetros extras para o método connect(). São elas:

Conexão Direta: O slot é chamado imediatamente ao sinal ter sido emitido, na thread onde o sinal foi emitido. Isso funciona como uma chamada direta ao slot.

Conexão Enfileirada: O sinal é emitido e a chamada ao slot vai para uma lista interna do QT, e o sinal retorna imediatamente, independentemente do slot ter sido chamado ou não. O evente loop vai então processando essa lista e só mais tarde o slot será invocado na thread onde o objeto do slot reside.

Conexão Automática: Este é o tipo default, utilizado quando não se especifica o tipo de conexão. É uma das “Sementes do Mal”, pois apresenta dois comportamentos distintos: se o sinal e o slot residirem em uma mesma thread, funciona como conexão direta, mas caso contrário, funciona como conexão enfileirada.

Conexão enfileirada Bloqueante: Ué!? não eram só três? Well, essa é uma das vantagens de ler esse artigo: te salva de documentação inconsitente! Essa conexão é semelhante à conexão enfileirada, exceto pelo fato de que a thread do sinal fica bloqueada até o sinal ser devidamente executado. Note que só deve se utilizada com muito cuidado, e para sinais e slots em threads diferentes. O mal uso disso poderá causar deadlocks. Você vai saber quando vir algo assim:

user@host:~/stopwatch$ ./stopwatch
Qt: Dead lock detected while activating a BlockingQueuedConnection: Sender is
QPushButton(0x807e2d8), receiver is MyDisplay(0x8076ac0)

Para sanar as dúvidas a respeito do que significa a expressão “thread onde um objeto reside” consulte a documentação do QT arespeito de threads.

Tome sempre muito cuidado com sinais, slots, timers e threads. Esses são recursos fundamentais do QT, mas o seu mal uso pode acarretar em bugs extremamente difíceis de serem localizados. Consulte toda a documentação à respeito.

Descendo à fonte

O nosso exemplo se compões de duas classes MyDisplay e StopWatch e um arquivo main comum. O main apenas cria uma QApplication e um objeto StopWatch. A classe MyDisplay cuida da parte visual do programa, consistindo em um diálogo com botões e um display no estilo LCD. A classe StopWatch vai criar um widget de display e implementar o timer. Note que essa arquitetura não é a mais elegente, sendo mais interessante fazer a classe StopWatch herdar de MyDisplay. Porém, isso estragaria a brincadeira praticamente eliminando a necessidade do uso de sinais e slots.

O uso de sinais e slots fica mais claro quando queremos que objetos se comuniquem com o seu exterior. Em geral, as classes não conhecem o que há fora delas. Passar ponteiros de objetos externos para dentro delas, fere o encapsulamento e torna o código menos genérico. Em nosso exemplo, o uso mais interessante para sinais e slots é para o objeto myDisplay (interno a StopWatch) comunicar eventos de/para o objeto stopwatch (externo a Mydisplay), sem que para isso se perca em generalidade ou encapsulamento.

Com a ajuda do QT Designer (num outro artigo eu falarei dele), criei o diálogo, botões e LCD. Depois simplifiquei o código para se adequar aos nossos propósitos. Como myDisplay herda de QDialog e este herda indiretamente de QObject, para usarmos sinais e slots bastou acrescentar a macro Q_OBJECT na seção privada da declaração da classe. Após isso, acrescentei entre outros auxiliares, os métodos:

public slots:
    void slotDisplayValue( long );
 
private slots:
    void slotPlay();
    void slotStop();
 
signals:
    void signalPlay();
    void signalPause();
    void signalStop();
    void signalReset();

Os sinais comunicarão ao exterior os eventos relacionados aos cliques nos botões. Já os slots privados farão um pré-processamento interno, enquanto o público receberá o valor de ticks vindo do timer externo. Dessa forma a classe envia informações ao exterior e recebe informações de fora, sem conhecer o que está do outro lado. A boa e velha interface por contrato.

public slots:
    void slotPlay();
    void slotPause();
    void slotStop();
    void slotReset();
 
private slots:
    void slotTick();
 
signals:
    void signalTicks( long );

Análoga e inversamente a classe StopWatch implementa os slots que receberão os sinais da classe MyDisplay, e declara o sinal que enviará informações para o seu slot público.

Ela também implementa um QTimer que vai disparar a cada 10 milissegundos (para que os nosso amigos lerdinhos do Vista possam acompanhar!), incrementando um contador. Esse contador é enviado de volta ao display que o formata e exibe.

O cronômetro pode ser parado, pausado e reiniciado a qualquer momento, clicando nos botões correspondentes.

Os Finalmentes

O exemplo em si não tem muito código, mas ilustra o uso mais básico de timers e a construção de sinais e slots customizados. Uma atenção toda especial, no entanto, deve ser dada aos detalhes que envolvem o uso desses recursos, pois eles podem se tornar armadilhas letais.

Links importantes

Sinais e Slots
QTimer
Threads no QT
Documentação on-line do QT

Comments

6 Responses to “Iniciando no QT, parte II – Sinais, Slots e Timers”

  1. Walison on February 6th, 2008 22:19

    Muito bom o texto, ja estou acessando o blog periodicamente so para saber mais sobre QT4 e é claro que estou pesquisando e lendo a documentacao.

    Att

  2. Blabos on February 7th, 2008 08:59

    Agradeço sinceramente pela audiência e espero poder ajudar cada vez mais.
    Meu muito obrigado e um forte abraço.

  3. Rodrigo on July 5th, 2009 16:10

    Cara, você é fera, estou adorando seus tutorias, abraços e muito obrigado!!!

  4. Lucas on February 3rd, 2010 08:32

    Muito boa suas explicaçoes, muito bem detalhada tambem!
    Voce ja fez algum programa para parte grafica como exemplo de funçoes matematicas?
    Porque nao estou conseguindo!
    Agradeço pela atençao

  5. blabos on February 7th, 2010 02:03

    Oi Lucas,

    Eu não entendi muito bem a sua dúvida.

    Você está procurando por soluções gráficas relacionadas a funções matemáticas? Será que o QWT te ajuda?

    Qualquer coisa entre em contado detalhando mais o que você precisa.

    Abraços

  6. Lucas on February 11th, 2010 14:32

    Sim, estou procurando por soluções gráficas relacionadas a funções matemáticas como seno e cosseno por exemplo.

    Eu estou com problema com o QWT acho que o problema é de instalação mas nao tenho certeza, queria saber que versão você usa de QWT e da QT?

    Se ja você já fez algo parecido com isso que estou querendo fazer e puder me dar uma ajuda serei grato.

    Agradeço pela atenção

    Saudações

Leave a Reply




Powered by WP Hashcash