Signals e slots do Qt
Nesta seção iremos adicionar à janela principal um botão para carregar modelos 3D. Para isso, damos um clique duplo em mainwindow.ui na árvore do projeto, arrastamos soltamos um objeto Push Button para a interface do usuário o posicionando abaixo do objeto QFrame, alteramos a propriedade text desse botão de “PushButton” para “Load Model” e alteramos o campo objectName para “loadModelButton”.
Vamos fazer com que esse botão abra uma janela de diálogo para a escolha de um arquivo OBJ que será carregado para nossa aplicação. Para isso, o framework Qt traz um mecanismo flexível de troca de mensagens através de três conceitos: signals, slots e connections:
- Um sinal (signal) é uma mensagem enviada por um objeto.
- Um slot é uma função que será chamada quando esse sinal for acionado.
- A função de conexão (
connection) especifica qual sinal está vinculado a qual slot.
O Qt já fornece sinais e slots para suas classes, que podemos usar em nosso aplicativo. Por exemplo, QPushButton tem um sinal clicked(), que será acionado quando o usuário clicar no botão.
Sinais e slots do Qt possuem as seguintes características e vantagens no seu uso:
- Um slot continua sendo uma função comum, então podemos chamá-lo no seu código normalmente.
- Um único sinal pode ser vinculado a diferentes slots.
- Um único slot pode ser chamado por diferentes sinais vinculados.
- É possível estabelecer uma conexão entre um sinal e um slot de objetos diferentes e até mesmo entre objetos que vivem dentro de threads diferentes.
Para conectar um sinal a um slot, as assinaturas de seus métodos devem corresponder. A contagem, ordem e tipo de argumentos devem ser idênticos. Observe que sinais e slots nunca retornam valores.
Toda vez que usamos o sistema de sinal/slot, é necessário incluir a macro Q_OBJECT no cabeçalho.
Para usar sinais e slots em nosso código, devemos conseguir reuni-los. Isso é feito com o método QObject::connect(), que conecta um sinal a um slot. Ele vem em muitas variantes sobrecarregadas. Neste projeto, no entanto, usaremos apenas a variante estática com quatro parâmetros. Este pode ser usado em qualquer lugar do programa, mesmo em funções independentes.
Esta é a sintaxe de uma conexão no Qt que usa essa variante:
connect(sender, SIGNAL(signalName), receiver, SLOT(slotName));
Esse método precisa saber quatro coisas: o objeto que envia o sinal (sender), o sinal ao qual o slot deve ser conectado (signalName), o objeto que receberá o sinal (receiver) e o slot que será conectado ao sinal (slotName). O sinal e o slot são apenas digitados com seus nomes e tipos de parâmetros e agrupados com SIGNAL() e SLOT(), respectiva- mente. Um erro comum nesse local é especificar valores em vez de tipos, como escrever SIGNAL(activated(3)) em vez de SIGNAL(activated(int)) . Além disso, o Qt não permite argumentos padrão.
Agora que sabemos como conectar um sinal a um slot existente, vamos ver como declarar e implementar um slot chamado loadModel em nossa classe MainWindow. Esse slot será chamado quando o usuário clicar em ui->loadModelButton.
Adicionamos o seguinte código em MainWindow.h:
public slots:
void loadModel();
O Qt usa o identificador slot para identificar slots. Como um slot é uma função, podemos ajustar a visibilidade (public, protected ou private), dependendo de nossas necessidades.
Adicionamos a seguinte implementação de slot no arquivo MainWindow.cpp:
#include <QFileDialog>
...
void MainWindow::loadModel() {
const QString fileName = QFileDialog::getOpenFileName(
this,
tr("Open 3D Model"),
QDir::homePath(),
tr("3D Model Files (*.obj *.OBJ)")
);
}
A implementação usa a função estática QFileDialog::getOpenFileName() para obter um novo nome de arquivo do usuário. Essa função exibe uma caixa de diálogo de arquivo, que permite ao usuário escolher um arquivo e retorna o nome do arquivo – ou uma string vazia, se o usuário clicar em Cancel.
O primeiro argumento para QFileDialog::getOpenFileName() é o widget pai. Uma caixa de diálogo é sempre uma janela por si só, mas se tiver um pai, ela será centralizada no topo do pai por padrão.
O segundo argumento é o título que o diálogo deve usar. O terceiro argumento indica a partir de qual diretório deve começar, no nosso caso, o diretório principal do usuário que obtemos através da função estática QDir::homePath().
O quarto argumento especifica os filtros de arquivo. Um filtro de arquivo consiste em um texto descritivo e as extensões com os tipos de arquivo.
#include "model.h"
...
if (!fileName.isEmpty()) {
QSharedPointer<Model> model =
QSharedPointer<Model>::create();
model->readOBJFile(fileName);
m_vulkanWindow->renderer()->addObject(model);
}
Então é verificado se fileName não é vazio. Caso não seja, é criado um objeto Model e invocado a função membro readOBJFile. Por fim, adicionamos objeto Model ao objeto Renderer utilizando a função addObject.
Para acessarmos o objeto renderizador de VulkanWindow a partir da classe MainWindow, precisamos adicionar a função pública renderer em vulkanwindow.h:
public:
VulkanWindow(QWindow *parentWindow = nullptr);
QVulkanWindowRenderer *createRenderer() override;
Renderer *renderer() {
return m_renderer;
}
Agora estamos prontos para conectar o sinal QPushButton::clicked no slot QMainWindow::loadModel. Para isso, basta chamar a função connect no final do construtor de MainWindow:
MainWindow::MainWindow(QWidget *parent) :
QWidget(parent),
ui(new Ui::MainWindow) {
...
connect(
ui->loadModelButton,
SIGNAL(clicked()),
this,
SLOT(loadModel())
);
}
Agora podemos executar o programa e testá-lo com um arquivo OBJ qualquer. O arquivo que usamos para este exemplo está disponível neste link: http://www.prinmath.com/csci5229/OBJ/f-16.zip. Após o download, precisamos descompactar o arquivo em um diretório de nossa preferencia e carregamos-o utilizando o botão que criamos nesta seção. Depois disso, devemos ver o mesmo resultado que o mostrado na Figura 10.

Podemos ver na Figura 10 que a geometria esta sendo renderizada rotacionada no eixo X. Isso ocorre porque, na matriz de transformação view consideramos o vetor apontando para cima como o eixo Z (0.0, 0.0, 0.1) e o correto seria utilizar o eixo Y (0.0, 1.0, 0.0). Para corrigir esse problema precismos alterar o vetor up utilizado pela função lookAt da matriz view para QVector3D(0.0, 1.0, 0.0):
QVector3D eye = QVector3D(1.0, 1.0, 1.0);
QVector3D center = QVector3D(0.0, 0.0, 0.0);
QVector3D up = QVector3D(0.0, 1.0, 0.0);
ubo.view.setToIdentity();
ubo.view.lookAt(eye, center, up);
Agora se executarmos o programa novamente e deveremos ver a geometria rotacionada corretamente.
