Este artículo explora cómo combinar Qt y VTK (Visualization Toolkit) para desarrollar interfaces gráficas de usuario (GUI), mediante un caso concreto que detalla cómo incrustar un renderizador de VTK en una aplicación Qt y crear elementos gráficos interactivos, como botones, para lograr una experiencia de usuario más enriquecedora. También se analiza en profundidad cómo gestionar los eventos de clic de los botones y cómo convertir datos de imagen de Qt al formato de VTK.
Introducción
En el desarrollo de software moderno, el diseño e implementación de interfaces gráficas de usuario (GUI) es una parte fundamental. Qt, como framework de aplicaciones multipaltaforma popular, ofrece un amplio conjunto de herramientas y funcionalidades para el desarrollo de GUI. Por su parte, VTK es un potente kit de herramientas de visualización utilizado para crear gráficos 3D complejos y visualización de datos. Este artículo presenta cómo integrar Qt y VTK, aprovechando las ventajas de cada uno, para desarrollar interfaces gráficas de usuario ricas en funcionalidades e interactividad.
Integración de Qt con VTK
Qt y VTK pueden integrarse de manera fluida, lo que permite implementar potentes interfaces gráficas y funciones de visualización de datos. En el código de ejemplo de este artículo, primero creamos una aplicación Qt y utilizamos la clase QVTKOpenGLNativeWidget para crear una ventana de renderizado de VTK. De esta forma, es posible incrustar el renderizador de VTK dentro de la aplicación Qt, logrando la combinación de interfaz gráfica y visualización de datos.
int main(int argc, char** argv)
{
QApplication app(argc, argv);
QVTKOpenGLNativeWidget vtkWidget;
// Crear el renderizador
vtkSmartPointer<vtkRenderer> renderer = vtkSmartPointer<vtkRenderer>::New();
auto renderWindow = vtkWidget.renderWindow();
renderWindow->AddRenderer(renderer);
...
vtkWidget.show();
...
}
Creación de un botón interactivo
En nuestro ejemplo, utilizamos la clase vtkButtonWidget para crear un botón interactivo. Este botón puede responder a los eventos de clic del usuario y ejecutar la acción correspondiente. Para mostrar una imagen en el botón, empleamos la clase vtkTexturedButtonRepresentation2D y establecemos la textura del botón con los datos de imagen. De esta manera, podemos crear botones interactivos dentro de la interfaz gráfica, ofreciendo al usuario una experiencia de operación más amigable.
// Crear la representación 2D del botón
vtkSmartPointer<vtkTexturedButtonRepresentation2D> buttonRep2D = vtkSmartPointer<vtkTexturedButtonRepresentation2D>::New();
buttonRep2D->SetPlaceFactor(1);
buttonRep2D->SetNumberOfStates(2);
vtkSmartPointer<vtkPNGReader> pngReaderOff = vtkSmartPointer<vtkPNGReader>::New();
QString tempPathOff = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/poweroff.png";
QFile::copy(":/resources/poweroff.png", tempPathOff);
pngReaderOff->SetFileName(tempPathOff.toStdString().c_str());
pngReaderOff->Update();
buttonRep2D->SetButtonTexture(0, pngReaderOff->GetOutput());
QImage imgPowerOn = QImage(":/resources/poweron.png");
buttonRep2D->SetButtonTexture(1, QImageToVtkImageData(imgPowerOn));
// Crear el widget de botón y asignarle la representación
vtkSmartPointer<vtkButtonWidget> buttonWidget = vtkSmartPointer<vtkButtonWidget>::New();
buttonWidget->SetInteractor(vtkWidget.interactor());
buttonWidget->SetRepresentation(buttonRep2D);
//buttonWidget->SetEnabled(1);
buttonWidget->On();
// Crear el objeto de comando para gestionar el evento de clic del botón
vtkSmartPointer<ButtonCallback> callback = vtkSmartPointer<ButtonCallback>::New();
buttonWidget->AddObserver(vtkCommand::StateChangedEvent, callback);
Gestión de eventos de clic del botón
Para gestionar los eventos de clic del botón, creamos una clase personalizada derivada de vtkCommand y sobrescribimos su método Execute. Dentro de este método, verificamos el tipo de evento y realizamos la operación correspondiente según sea necesario. De este modo, podemos lograr un manejo flexible de los eventos de clic del botón y así implementar funcionalidades interacitvas más complejas.
// Clase de comando para gestionar el evento de clic del botón
class ButtonCallback : public vtkCommand {
public:
static ButtonCallback* New()
{
auto instance = new ButtonCallback();
instance->InitializeObjectBase();
return instance;
}
virtual void Execute(vtkObject* caller, unsigned long eventId, void* callData) override
{
// Verificar el tipo de evento
if (eventId == vtkCommand::StateChangedEvent)
{
// Procesar el evento de cambio de estado del botón
vtkButtonWidget* buttonWidget = reinterpret_cast<vtkButtonWidget*>(caller);
qDebug() << vtkCommand::GetStringFromEventId(eventId);
vtkButtonRepresentation* representation = reinterpret_cast<vtkButtonRepresentation*>(buttonWidget->GetRepresentation());
qDebug() << "vtkButtonWidget State: " << representation->GetState();
}
}
};
Por supuesto, también es posible añadir elementos de interfaz de Qt; este artículo se centra en presentar vtkWidget.
Conversión de datos de imagen
Al transferir datos de imagen entre Qt y VTK, es necesario realizar una conversión de formato. En nuestro ejemplo, demostramos cómo convertir un objeto QImage de Qt a un objeto vtkImageData de VTK. De esta manera, podemos transferir eficientemente datos de imagen entre la aplicación Qt y el renderizador de VTK, logrando funcionalidades de visualización de datos más enriquecedoras.
/// @brief Convierte QImage a vtkImageData
/// @param src
/// @return
vtkSmartPointer<vtkImageData> QImageToVtkImageData(const QImage& src)
{
vtkSmartPointer<vtkImageData> dst = vtkSmartPointer<vtkImageData>::New();
int w = src.width();
int h = src.height();
// Establecer las dimensiones
dst->SetDimensions(w, h, 1);
// Establecer el tipo de dato y el número de componentes
vtkNew<vtkInformation> info;
dst->SetScalarType(VTK_UNSIGNED_CHAR, info);
int channels = src.pixelFormat().channelCount();
dst->SetNumberOfScalarComponents(channels, info);
// Asignar memoria
dst->AllocateScalars(info);
// Obtener el puntero inicial a los datos de la imagen
unsigned char* dstPtr = static_cast<unsigned char*>(dst->GetScalarPointer());
// Recorrer los píxeles de la imagen y copiarlos a vtkImageData
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
// Obtener el valor del píxel
QRgb pixel = src.pixel(x, y);
// El orden de las filas en QImage es de arriba hacia abajo,
// mientras que en vtkImageData es de abajo hacia arriba
switch (channels)
{
case 1:
dstPtr[(h - y - 1) * w + x] = qGray(pixel);
break;
case 3:
dstPtr[((h - y - 1) * w + x) * 3 + 0] = qRed(pixel);
dstPtr[((h - y - 1) * w + x) * 3 + 1] = qGreen(pixel);
dstPtr[((h - y - 1) * w + x) * 3 + 2] = qBlue(pixel);
break;
case 4:
dstPtr[((h - y - 1) * w + x) * 4 + 0] = qRed(pixel);
dstPtr[((h - y - 1) * w + x) * 4 + 1] = qGreen(pixel);
dstPtr[((h - y - 1) * w + x) * 4 + 2] = qBlue(pixel);
dstPtr[((h - y - 1) * w + x) * 4 + 3] = qAlpha(pixel);
break;
}
}
}
return dst;
}
Código cmopleto
#include <QApplication>
#include <QVTKOpenGLNativeWidget.h>
#include <QFile>
#include <QStandardPaths>
#include <QImage>
#include <QString>
#include <QColorSpace>
#include <QDebug>
#include <vtkActor.h>
#include <vtkProperty.h>
#include <vtkSmartPointer.h>
#include <vtkRenderer.h>
#include <vtkRenderWindow.h>
#include <vtkButtonWidget.h>
#include <vtkTexturedButtonRepresentation.h>
#include <vtkTexturedButtonRepresentation2D.h>
#include <vtkImageData.h>
#include <vtkPNGReader.h>
#include <vtkRenderWindowInteractor.h>
#include <vtkCommand.h>
#include <vtkNew.h>
#include <vtkInformation.h>
// Clase de comando para gestionar el evento de clic del botón
class ButtonCallback : public vtkCommand {
public:
static ButtonCallback* New()
{
auto instance = new ButtonCallback();
instance->InitializeObjectBase();
return instance;
}
virtual void Execute(vtkObject* caller, unsigned long eventId, void* callData) override
{
// Verificar el tipo de evento
if (eventId == vtkCommand::StateChangedEvent)
{
// Procesar el evento de cambio de estado del botón
vtkButtonWidget* buttonWidget = reinterpret_cast<vtkButtonWidget*>(caller);
qDebug() << vtkCommand::GetStringFromEventId(eventId);
vtkButtonRepresentation* representation = reinterpret_cast<vtkButtonRepresentation*>(buttonWidget->GetRepresentation());
qDebug() << "vtkButtonWidget State: " << representation->GetState();
}
}
};
/// @brief Convierte QImage a vtkImageData
/// @param src
/// @return
vtkSmartPointer<vtkImageData> QImageToVtkImageData(const QImage& src)
{
vtkSmartPointer<vtkImageData> dst = vtkSmartPointer<vtkImageData>::New();
int w = src.width();
int h = src.height();
// Establecer las dimensiones
dst->SetDimensions(w, h, 1);
// Establecer el tipo de dato y el número de componentes
vtkNew<vtkInformation> info;
dst->SetScalarType(VTK_UNSIGNED_CHAR, info);
int channels = src.pixelFormat().channelCount();
dst->SetNumberOfScalarComponents(channels, info);
// Asignar memoria
dst->AllocateScalars(info);
// Obtener el puntero inicial a los datos de la imagen
unsigned char* dstPtr = static_cast<unsigned char*>(dst->GetScalarPointer());
// Recorrer los píxeles de la imagen y copiarlos a vtkImageData
for (int y = 0; y < h; y++)
{
for (int x = 0; x < w; x++)
{
// Obtener el valor del píxel
QRgb pixel = src.pixel(x, y);
// El orden de las filas en QImage es de arriba hacia abajo,
// mientras que en vtkImageData es de abajo hacia arriba
switch (channels)
{
case 1:
dstPtr[(h - y - 1) * w + x] = qGray(pixel);
break;
case 3:
dstPtr[((h - y - 1) * w + x) * 3 + 0] = qRed(pixel);
dstPtr[((h - y - 1) * w + x) * 3 + 1] = qGreen(pixel);
dstPtr[((h - y - 1) * w + x) * 3 + 2] = qBlue(pixel);
break;
case 4:
dstPtr[((h - y - 1) * w + x) * 4 + 0] = qRed(pixel);
dstPtr[((h - y - 1) * w + x) * 4 + 1] = qGreen(pixel);
dstPtr[((h - y - 1) * w + x) * 4 + 2] = qBlue(pixel);
dstPtr[((h - y - 1) * w + x) * 4 + 3] = qAlpha(pixel);
break;
}
}
}
return dst;
}
int main(int argc, char** argv)
{
QApplication app(argc, argv);
QVTKOpenGLNativeWidget vtkWidget;
// Crear el renderizador
vtkSmartPointer<vtkRenderer> renderer = vtkSmartPointer<vtkRenderer>::New();
auto renderWindow = vtkWidget.renderWindow();
renderWindow->AddRenderer(renderer);
// Crear la representación 2D del botón
vtkSmartPointer<vtkTexturedButtonRepresentation2D> buttonRep2D = vtkSmartPointer<vtkTexturedButtonRepresentation2D>::New();
buttonRep2D->SetPlaceFactor(1);
buttonRep2D->SetNumberOfStates(2);
vtkSmartPointer<vtkPNGReader> pngReaderOff = vtkSmartPointer<vtkPNGReader>::New();
QString tempPathOff = QStandardPaths::writableLocation(QStandardPaths::TempLocation) + "/poweroff.png";
QFile::copy(":/resources/poweroff.png", tempPathOff);
pngReaderOff->SetFileName(tempPathOff.toStdString().c_str());
pngReaderOff->Update();
buttonRep2D->SetButtonTexture(0, pngReaderOff->GetOutput());
QImage imgPowerOn = QImage(":/resources/poweron.png");
buttonRep2D->SetButtonTexture(1, QImageToVtkImageData(imgPowerOn));
// Crear el widget de botón y asignarle la representación
vtkSmartPointer<vtkButtonWidget> buttonWidget = vtkSmartPointer<vtkButtonWidget>::New();
buttonWidget->SetInteractor(vtkWidget.interactor());
buttonWidget->SetRepresentation(buttonRep2D);
//buttonWidget->SetEnabled(1);
buttonWidget->On();
// Crear el objeto de comando para gestionar el evento de clic del botón
vtkSmartPointer<ButtonCallback> callback = vtkSmartPointer<ButtonCallback>::New();
buttonWidget->AddObserver(vtkCommand::StateChangedEvent, callback);
vtkWidget.show();
int code = app.exec();
QFile::remove(tempPathOff);
//QFile::remove(tempPathOn);
return code;
}
A través de este artículo, el lector puede aprender a combinar Qt y VTK para desarrollar interfaces gráficas de usuario ricas en funcionalidades e interactividad. Hemos detallado cómo incrustar un renderizador de VTK en una aplicación Qt y crear botones interactivos. Asimismo, hemos analizado en profundidad cómo gestionar los eventos de clic de los botones y cómo realizar la conversión de formato de datos de imagen. Esperamos que este artículo sea de utilidad y permita a los lectores aprovechar de manera más flexible Qt y VTK en el desarrollo de interfaces gráficas y visualización de datos.