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.
