Link

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.

Figura 10 – Modelo 3D com textura com eixos Y e Z trocados

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.

Figura 11 – Modelo 3D com textura com orientação corrigida


Anterior Próximo