Mostrando entradas con la etiqueta QT. Mostrar todas las entradas
Mostrando entradas con la etiqueta QT. Mostrar todas las entradas

lunes, 16 de junio de 2014

Lógica de aplicación en QtQuick con JavaScript.

QtQuick es una biblioteca del framework Qt para el desarrollo de aplicaciones con QML. En este artículo veremos cómo implementar la lógica de una aplicación con interfaz gráfica desarrollada en QML mediante un fichero JavaScript, para esto emplearemos un ejemplo desarrollado con la versión 5.2 de Qt. Lo primero que haremos será crear un proyecto de QtQuick. En la pantalla de bienvenida de Qt Creator seleccionamos “New Project”.

clip_image001

Luego en el asistente de nuevo proyecto escogemos la plantilla “Applications” y de las opciones escogemos “Qt Quick Application

clip_image003 clip_image004

Al seleccionar esta plantilla aparece el asistente para llenar los datos del proyecto. Introducimos el nombre, la ubicación, la selección de componentes de Qt Quick (dejamos por defecto Qt Quick 2), el kit de compilación (por defecto) y por último seleccionamos “Finish”. Una vez creado el proyecto se abre el “main.qml” que debe verse así:

clip_image006

Ahora vamos a crear el fichero JavaScript, para esto damos clic derecho en la raíz del árbol del proyecto y seleccionamos “Add new…”. Al abrir el asistente escogemos como plantilla “Qt” y en las opciones seleccionamos “JS File”:

clip_image008 clip_image010

Al abrir el asistente para crear el fichero ponemos el nombre, por ejemplo “logic”, y en la ubicacion seleccionamos la carpeta donde se encuentran los QML del proyecto; esto facilita la localizacion del fichero a la hora de importarlo. Dejamos las opciones por defecto y damos clic en “Finish”. Ya está creado el fichero, ahora vamos a diseñar una inerfaz sencilla para calcular el cuadrado de un número entrado por el usuario. Para esto sustituimos el código del main.qml por esto:

Rectangle {
width: 360
height: 200

Column{
anchors.centerIn: parent
spacing: 10

Rectangle{
anchors.horizontalCenter: parent.horizontalCenter
color: "lightgrey"; width: 40; height: 20

TextInput{
id: inputNum
anchors.fill: parent; color: "black"
font.pointSize: 15.0
}
}

Text { id: result; text: qsTr("El cuadrado es: ")
anchors.horizontalCenter: parent.horizontalCenter }

Text {
text: qsTr("CALCULAR")
anchors.horizontalCenter: parent.horizontalCenter

MouseArea {
anchors.fill: parent
onClicked: {

}
}
}
}
}

Se utiliza un elemento TextInput para obtener la entrada del usuario, un elemento Text para mostrar el resultado y otro para ejecutar el método que implementaremos más adelante. Es importante señalar aquí la propiedad “id” del TextInput y del Text que mostrará el resultado. Esta propiedad es el identificador mediante el cual se puede acceder a todas las propiedades del objeto desde el fichero JavaScript o desde los métodos de los otros elementos en el mismo QML; por eso es necesario que sea único para cada objeto que se declare en el fichero. Ahora, vamos a escribir la directiva que permite importar el fichero JavaScript desde el código QML, en la parte superior del código agregar:

import "logic.js" as Logic

Esto significa que vamos a utilizar los métodos implementados en el fichero “logic.js” a través del id “Logic”. La manera específica de hacerlo lo veremos más adelante. Ahora, abrimos el fichero JavaScript e implementamos el método para calcular el cuadrado del número entrado por el usuario:

function square(num) {
var r = num * num
result.text = "EL cuadrado es: " + r
}

Es un método sencillo, pero lo importante aquí es notar que podemos cambiar la propiedad “text” del elemento Text del fichero QML a través del identificador definido. Lo mismo podemos hacer con cualquiera de sus otras propiedades.

Ya podemos usar el método implementado, para esto vamos al método onClicked del elemento MouseArea y escribimos lo siguiente:

Logic.square(inputNum.text)

Aquí notar dos cosas: primero, que podemos acceder a los métodos del fichero JavaScript a través del id definido en la directiva import como si fuera un objeto y segundo, que desde aquí también podemos acceder a las propiedades de los elementos QML mediante su identificador. En este caso el texto del TextInput.

De esta manera se ha implementado un método sencillo, pero en una aplicación más compleja donde se necesiten más métodos la manera de llamarlos es la misma. Una vez terminado todo solo falta probarlo, para esto compilamos la aplicación y comprobamos que todo funcione bien.

Hasta aquí el artículo de hoy, en resumen hemos visto cómo utilizar los métodos de un fichero JavaScript en el código QML y como acceder a las propiedades de los elementos QML a través de su identificador.

{ Leer Más }


martes, 20 de mayo de 2014

Como colocar Widgets dentro de un QHeaderView usando el framework Qt.

clip_image002

Hace poco tuve la necesidad de lograr colocar QComboBoxes en las secciones dentro de un QHeaderView, en un inicio pensé que esto sería bastante sencillo, ya que podría utilizar el método setViewportMargins () y poner los widgets en la parte superior. Pero por desgracia esto no es posible hacerlo porque los itemviews establecen los márgenes de ventana gráfica y si se establece fuera de este puede causar problemas por lo que no se recomienda este enfoque.

Por lo tanto, la manera de conseguir que funcione es crear los widgets y colocarlos en el QHeaderView directamente, esto significa que tenemos que ajustar de forma manual cuando se cambia el tamaño de las secciones, se mueve o cuando el propio itemview se desplaza [1]. Así que para hacer esto tenemos que empezar por hacer una clase que herede de QHeaderView. En este caso me estoy centrando exclusivamente en una cabecera horizontal, ya que esto hace que sea más sencillo centrarse en lugar de tratar de darse cuenta de las secciones de dirección se están moviendo.

MyHorizontalHeader(QWidget *parent = 0) : QHeaderView(Qt::Horizontal, parent)
{
connect(this, SIGNAL(sectionResized(int, int, int)), this,
SLOT(handleSectionResized(int)));
connect(this, SIGNAL(sectionMoved(int, int, int)), this,
SLOT(handleSectionMoved(int, int, int)));
setMovable(true);
}

Hemos añadido dos slots aquí, uno es para el manejo de una sección cuando se cambia el tamaño y otro para cuando se mueve una sección. Vamos a llegar a ello dentro de poco, pero primero vamos a cubrir la inicialización de los widgets. No podemos hacer esto en el constructor (aunque podríamos hacerlo si sabemos más sobre el itemview que estará asociado, pero para este caso yo quiero ser lo más genérico posible), ya que no sabemos cuántas secciones habrá . Así que el mejor lugar para hacer esto es el método re-implementado showEvent().

void showEvent(QShowEvent *e)
{
for (int i=0;i<count();i++) {
if (!boxes[i]) {
QComboBox *box = new QComboBox(this);
boxes[i] = box;
}
boxes[i]->setGeometry(sectionViewportPosition(i), 0,
sectionSize(i) - 5, height());
boxes[i]->show();
}
QHeaderView::showEvent(e);
}

La matriz de las cajas es sólo un QMap <int, QComboBox *> con el entero que se refiere al índice lógico de la sección de la lista desplegable que está conectada. Con el fin de conseguir la posición correcta en la ventana gráfica para la sección utilizamos sectionViewportPosition () y sectionSize () se usa para obtener el tamaño de la sección. La razón del -5 existe solamente para que yo pueda ver un poco de la parte inferior para permitir el movimiento de las secciones. Si sólo desea ver el divisor para cambiar el tamaño de las secciones puede cambiar a -2.

Lo que queda entonces es reaccionar al cambio de tamaño de las secciones y el movimiento de las secciones con los siguientes slots:

void handleSectionResized(int i)
{
for (int j=visualIndex(i);j<count();j++) {
int logical = logicalIndex(j);
boxes[logical]->setGeometry(sectionViewportPosition(logical), 0,
sectionSize(logical) - 5, height());
}
}
void handleSectionMoved(int logical, int oldVisualIndex, int newVisualIndex)
{
for (int i=qMin(oldVisualIndex, newVisualIndex);i<count();i++){
int logical = logicalIndex(i);
boxes[logical]->setGeometry(sectionViewportPosition(logical), 0,
sectionSize(logical) - 5, height());
}
}

El código es fundamentalmente el mismo en ambos slots, pero el bucle está ajustado para limitar la cantidad de widgets tiene que ser tocado, cuando una sección cambia de tamaño sólo vamos a partir del índice visual de la redimensionada y llegaremos al resto de las secciones después de esa. Cuando se mueve una sección que tenemos que ir desde el primer índice visual accionado hasta el final del headerview, como las otras secciones tendrán que ser actualizados para adaptarse.
Hasta aquí todo bien, lo único que nos queda por hacer ahora es manejar el caso en que el itemview se desplaza ya que tenemos que cambiar la posición de los widgets de nuevo. Como el QHeaderView no ha sido notificado de ninguna manera que esto ocurre ya que está en la ventana que tenemos que conectar este a partir del propio itemview cuando se produce un desplazamiento. Para hacer esto necesitamos re-implementar scrollContentsBy ():

void scrollContentsBy(int dx, int dy)
{
QTableWidget::scrollContentsBy(dx, dy);
if (dx != 0)
horizHeader->fixComboPositions();
}

Lo que no se muestra aquí es el constructor que crea una instancia de la clase MyHorizontalHeader y establece con setHorizontalHeader (). Así que cada vez que el contenido se desplaza en cualquier dirección horizontal llamamos al método fixComboPositions() que hace:

void fixComboPositions()
{
for (int i=0;i<count();i++)
boxes[i]->setGeometry(sectionViewportPosition(i), 0,
sectionSize(i) - 5, height());
}

En efecto es el mismo que hace el método showEvent (), ya que no sabemos cuánto se ha hecho visible u oculta la forma más fácil es ir sobre todos los widgets.
Y ahí lo tienen, un enfoque bastante sencillo de conseguir widgets en un QHeaderView, explicado paso a paso, por lo que debería ser fácilmente adaptable para sus propias necesidades. Es todo por hoy esperamos que esta entrada le sea de utilidad.

{ Leer Más }


martes, 6 de mayo de 2014

QtSerialPort, un módulo de la biblioteca Qt que permite la comunicación a través del puerto serie.

clip_image002

El módulo QtSerialPort es una extensión (módulo add-on) para la biblioteca QT5, proporcionando una interfaz única para el hardware y los puertos serie virtuales [1]. Proporciona las funcionalidades básicas, que incluye la configuración, operaciones entrada / salida (I / O), obtener y establecer las señales de control de la interfaz RS-232.

Las interfaces seriales, debido a su sencillez y fiabilidad, siguen siendo populares en algunos sectores como el desarrollo de sistemas embebidos, robótica, etc.

Con el módulo QtSerialPort, los desarrolladores pueden reducir significativamente el tiempo necesario para implementar aplicaciones Qt que requieren acceso a una interfaz serial.

Historia
QtSerialPort se originó a partir de la biblioteca de terceros QSerialDevice [gitorious.org] (rama 2.0), que se mudó recientemente a un repositorio en https://codereview.qt-project.org/. Esto se hizo para permitir un desarrollo más abierto, y para reunir y coordinar a una comunidad que está interesada en el desarrollo de este módulo.

Funcionalidad
Actualmente, la API de módulo contiene dos clases: QSerialPort y QSerialPortInfo:

QSerialPort es la clase base del módulo y proporciona un conjunto de métodos y propiedades básicas para acceder a los recursos de los puertos serie.
Soporta los siguientes sistemas operativos:

Sistema Operativo

Estado de Sorporte

Nota

Windows NT/2K/XP/Vista/7

SI

Soporte completo

Windows CE

SI

Probado solo en emuladores de las plataformas 5 y 6

Gnu/Linux

SI

Soporte completo

MacOSX

SI

Soporte completo

Otros Unix

SI

Todos los compatibles con POSIX

Symbian1

SI

Parcialmente, probado solo en el emulador

QSerialPortInfo es una clase de ayuda. Proporciona información sobre los puertos serie disponibles en el sistema.

Soporta los siguientes sistemas operativos:

Sistema operativo

Estado de soporte

Nota

Windows NT/2K/XP/Vista/7

SI

Soporte completo (using SetupAPI)

Windows CE

SI

Probado solo en emuladores de las plataformas 5 y 6

Gnu/Linux

SI

Soporte completo (using libudev or simple search in /dev)

MacOSX

SI

Soporte completo

Otros Unix

SI

Todos los compatibles con POSIX (only simple search in /dev)

Symbian2

SI

Parcialmente, probado solo en el emulador

Ver el código fuente

Puesto en marcha recientemente un espejo público del proyecto de repositorio en Gitorious [qt.gitorious.org]

Ahora todo el mundo puede ver libremente y rápidamente los últimos cambios a través de un navegador web.

Obtener el código fuente

Para obtener la instantánea actual del código fuente como un archivo, haga clic en este enlace [qt.gitorious.org].

Para aquellos que quieren usar Git puede ejecutar el siguiente comando:

  1. git clone git://gitorious.org/qt/qtserialport.git

Construcción e Instalación

Construir e instalar desde la línea de comandos

Antes de la construcción usted necesita:

· Instalar Perl 3

· Asegurarse de que las variables de entorno están establecidas correctamente:

- Correctamente especificado la ruta de acceso al Qt4/Qt5 instalado

- Correctamente especificado la ruta para usar el compilador

- Correctamente especificado la ruta de acceso al Perl 3

· Crear un directorio de construcción que esté en el mismo nivel que el directorio con el código fuente

- /

- / SerialPort-src

- / SerialPort-build

Los siguientes son los procedimientos recomendados para la construcción de la biblioteca QtSerialPort en Qt4/Qt5 desde la línea de comandos.

  • cd serialport-build
  • qmake .. / serialport-src/qtserialport.pro
  • make [o 'nmake' para el compilador MSVC o '-make mingw32' para el compilador MinGW]
  • make install [o 'nmake install' para el compilador MSVC o 'mingw32-make install' para el compilador MinGW]

Nota: en los sistemas * nix es posible que se requieran privilegios de superusuario:

sudo make install

Construir e instalar desde la QtCreator

Antes de la construcción de lo que necesita:

  • Instalar el Perl 3 y estar convencido de que ha especificado correctamente la ruta al Perl en un ambiente global.
  • Estar convencido de que las cadenas de herramientas que desee ( kits) del QtCreator se ha configurado correctamente

Los procedimientos recomendados para la construcción de la biblioteca QtSerialPort para Qt4/Qt5 del QtCreator:

  • Descargar y desempaquetar el código fuente de QtSerialPort
  • Ejecutar QtCreator y abra el archivo de proyecto "qtserialport.pro"
  • Llegar a " Projects->(Your Kit)->Build->Build Steps "
  • Añadir un nuevo make "Build Step" y escribir en los " argumentos" (“Make arguments) el objetivo a compilar.
  • Haga clic en el menú "Rebuild Project qtserialport".

Como resultado, la biblioteca QtSerialPort será compilada e instalado automáticamente en la instancia de Qt deseada (depende del Kit seleccionado). El uso QtCreator - la manera más sencilla y rápida de instalación manual de la biblioteca.

Nota: en los sistemas *nix este método puede fallar si Qt fue instalado desde los repositorios en los directorios del sistema. Los privilegios de superusuario podrían ser necesarios para ejecutar "make install".

3 Perl sólo se requiere en el caso de QT5, consulte aquí [qt- project.org]. Al usar Qt4 omita este punto.

//**

Uso

Para utilizar la biblioteca adicione serialport al fichero *.pro de tu proyecto.

Qt4
  1. CONFIG += serialport
Qt5
  1. QT += serialport

Incluya los archivos de cabecera de QtSerialPort donde sea necesario:

  1. ...
  2. #include <QtSerialPort/QSerialPort>
  3. #include <QtSerialPort/QSerialPortInfo>
  4. ...
Ejemplo simple

Debajo se muestra un ejemplo sencillo del main.cpp:

#include <QtCore/QCoreApplication>
#include <QtCore/QDebug>
#include <QtSerialPort/QSerialPort>
#include <QtSerialPort/QSerialPortInfo>
QT_USE_NAMESPACE
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
// Example use QSerialPortInfo
foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) {
qDebug() << "Name : " << info.portName();
qDebug() << "Description : " << info.description();
qDebug() << "Manufacturer: " << info.manufacturer();
// Example use QSerialPort
QSerialPort serial;
serial.setPort(info);
if (serial.open(QIODevice::ReadWrite))
serial.close();
}
return a.exec();
}

Nota: CONFIG += serial port / QT += serialport debe ser la primera o la segunda linea en tu fichero .pro.

Es todo por hoy esperamos que esta entrada le hay resultado de utilidad.

{ Leer Más }


lunes, 14 de abril de 2014

Ejemplo Mandelbrot, programación concurrente con el Framework Qt.

clip_image002

El ejemplo Mandelbrot, demuestra la programación concurrente usando el Framework Qt, específicamente explica cómo usar un hilo trabajador para realizar operaciones computacionales complejas sin bloquear el hilo principal del ciclo de eventos [1].

clip_image004

El cómputo pesado en el presente ejemplo es el conjunto Mandelbrot, probablemente el fractal más famoso del mundo.

En la vida real, el enfoque aquí descrito es aplicable a una amplia gama de problemas, incluyendo la red de E / S síncrona y acceso a base de datos, donde la interfaz de usuario debe ser capaz de responder mientras una operación pesada está teniendo lugar.

La aplicación Mandelbrot admite zoom y el scroll con el ratón o el teclado. Para evitar la congelación de bucle de eventos del hilo principal (y, en consecuencia, la interfaz de usuario de la aplicación), ponemos todo el cálculo fractal en un subproceso de trabajo independiente. El hilo emite una señal cuando se termina de hacer la representación del fractal.

Durante el tiempo en que el subproceso de trabajo esta recalculando el fractal para reflejar la nueva posición factor de zoom, el hilo principal, simplemente escala el pixmap previamente representado para proporcionar una respuesta inmediata. El resultado no se ve tan bueno como lo que el subproceso de trabajo con el tiempo termina dando, pero al menos hace que la aplicación sea más sensible. En la secuencia de imágenes que a continuación mostramos se ve: la imagen original, la imagen escalada, y la imagen representada nuevamente.

clip_image006clip_image008 clip_image009

Del mismo modo, cuando el usuario se desplaza, el mapa de pixeles anterior se desplaza inmediatamente, revelando las áreas no pintadas más allá del borde del mapa de píxeles, mientras que la imagen se representa por el subproceso de trabajo.

clip_image011 clip_image013 clip_image015

La aplicación consta de dos clases:

· RenderThread es una subclase QThread que representa (render) el conjunto de Mandelbrot.

· MandelbrotWidget es una subclase QWidget que muestra el conjunto de Mandelbrot en la pantalla y permite al usuario las operaciones de ampliar (zoom) y desplazar (scroll).

Definición de la clase RenderThread

Vamos a empezar con la definición de la clase RenderThread:

class RenderThread : public QThread
{
Q_OBJECT

public:
RenderThread(QObject *parent = 0);
~RenderThread();

void render(double centerX, double centerY, double scaleFactor, QSize resultSize);

signals:
void renderedImage(const QImage &image, double scaleFactor);

protected:
void run();

private:
uint rgbFromWaveLength(double wave);

QMutex mutex;
QWaitCondition condition;
double centerX;
double centerY;
double scaleFactor;
QSize resultSize;
bool restart;
bool abort;

enum { ColormapSize = 512 };
uint colormap[ColormapSize];
};

La clase hereda de QThread de esa forma adquiere la capacidad de ejecutarse en un subproceso independiente. Además del constructor y destructor, render () es la única función pública. Cada vez que el hilo realiza la representación de una imagen, emite la señal renderedImage.

La función protegida run () se reimplementada de QThread. Se llama automáticamente cuando se inicia el hilo.

En la sección privada, tenemos un QMutex, un QWaitCondition, y algunos otros miembros de datos. La exclusión mutua protege al otro miembro de datos.

Implementación de la clase RenderThread

RenderThread::RenderThread(QObject *parent)
: QThread(parent)
{
restart = false;
abort = false;

for (int i = 0; i < ColormapSize; ++i)
colormap[i] = rgbFromWaveLength(380.0 + (i * 400.0 / ColormapSize));
}

En el constructor, inicializamos las operaciones de reiniciar y abortar las variables en false. Estas variables controlan el flujo de la función run (). También se inicia arreglo de mapa de colores, que contiene una serie de colores RGB


 

RenderThread::~RenderThread()
{
mutex.lock();
abort = true;
condition.wakeOne();
mutex.unlock();

wait();
}

El destructor se puede llamar en cualquier momento mientras el hilo está activo. Fijamos abortar en true para decirle a run () que debe detener la ejecución tan pronto como sea posible. También hacemos una llamada a QWaitCondition::wakeOne() para despertar el hilo si está durmiendo. (Como veremos cuando examinemos run (), el hilo es puesto a dormir cuando no tiene nada que hacer.)

Lo importante a notar aquí es que run () se ejecuta en su propio hilo (el subproceso de trabajo), mientras que el constructor y el destructor RenderThread (así como la función render ()) son llamados por el subproceso que creó el subproceso de trabajo. Por lo tanto, necesitamos un mutex para proteger los accesos a las variables de abortar y condición, que pueden ser leídas en cualquier momento el método run ().

Al final del destructor, que llamamos QThread::wait() para esperar a que el método run () ha salido antes de invocar el destructor de la clase base.

void RenderThread::render(double centerX, double centerY, double scaleFactor,
QSize resultSize)
{
QMutexLocker locker(&mutex);

this->centerX = centerX;
this->centerY = centerY;
this->scaleFactor = scaleFactor;
this->resultSize = resultSize;

if (!isRunning()) {
start(LowPriority);
} else {
restart = true;
condition.wakeOne();
}
}

La función render () es llamada por el MandelbrotWidget cada vez que necesita generar una nueva imagen del conjunto de Mandelbrot. Los parámetros CenterX, CenterY y scaleFactor especifican la parte del fractal para representar; resultSize especifica el tamaño de la QImage resultante.

La función almacena los parámetros de las variables miembro. Si el hilo no se está ejecutando, se inicia; de otro modo, se establece el reinicio a true (diciendo al run () que debe detener cualquier cálculo sin terminar y empezar de nuevo con los nuevos parámetros) y se despierta el hilo, que podría estar durmiendo.

void RenderThread::run()
{
forever {
mutex.lock();
QSize resultSize = this->resultSize;
double scaleFactor = this->scaleFactor;
double centerX = this->centerX;
double centerY = this->centerY;
mutex.unlock();

La función run() es muy grande, por lo cual se divide en dos partes. El cuerpo de la función es un bucle infinito que se inicia mediante el almacenamiento de los parámetros de representación en variables locales. Como de costumbre, protegemos los accesos a las variables miembro utilizando el mutex de la clase. El almacenamiento de las variables miembro en variables locales nos permite minimizar la cantidad que de código que necesita ser protegido por un mutex. Esto asegura que el hilo principal nunca tendrá que bloquear durante demasiado tiempo cuando se necesita acceder a las variables miembros de RenderThread (por ejemplo, en el render ()). La palabra clave forever, es como foreach, una seudo-palabra clave de Qt.

int halfWidth = resultSize.width() / 2;
int halfHeight = resultSize.height() / 2;
QImage image(resultSize, QImage::Format_RGB32);

const int NumPasses = 8;
int pass = 0;
while (pass < NumPasses) {
const int MaxIterations = (1 << (2 * pass + 6)) + 32;
const int Limit = 4;
bool allBlack = true;

for (int y = -halfHeight; y < halfHeight; ++y) {
if (restart)
break;
if (abort)
return;

uint *scanLine =
reinterpret_cast<uint *>(image.scanLine(y + halfHeight));
double ay = centerY + (y * scaleFactor);

for (int x = -halfWidth; x < halfWidth; ++x) {
double ax = centerX + (x * scaleFactor);
double a1 = ax;
double b1 = ay;
int numIterations = 0;

do {
++numIterations;
double a2 = (a1 * a1) - (b1 * b1) + ax;
double b2 = (2 * a1 * b1) + ay;
if ((a2 * a2) + (b2 * b2) > Limit)
break;

++numIterations;
a1 = (a2 * a2) - (b2 * b2) + ax;
b1 = (2 * a2 * b2) + ay;
if ((a1 * a1) + (b1 * b1) > Limit)
break;
} while (numIterations < MaxIterations);

if (numIterations < MaxIterations) {
*scanLine++ = colormap[numIterations % ColormapSize];
allBlack = false;
} else {
*scanLine++ = qRgb(0, 0, 0);
}
}
}

if (allBlack && pass == 0) {
pass = 4;
} else {
if (!restart)
emit renderedImage(image, scaleFactor);
++pass;
}
}

Luego viene el núcleo del algoritmo. En lugar de tratar de crear una imagen perfecta del conjunto de Mandelbrot, hacemos varias pasadas y generamos más y más precisas (y computacionalmente costosas) aproximaciones del fractal.

Si descubrimos dentro del bucle que la variable restart se ha establecido en true (por render ()), rompemos el bucle de inmediato, por lo que el control vuelve rápidamente a la parte superior del bucle externo (el bucle forever) y buscamos a los nuevos parámetros de renderizado. Del mismo modo, si descubrimos que la variable abort se ha establecido en true (por el destructor RenderThread), salimos de la función de inmediato, terminando el hilo.

El algoritmo principal está más allá del alcance de este tutorial.

        mutex.lock();
if (!restart)
condition.wait(&mutex);
restart = false;
mutex.unlock();
}
}

Una vez que hemos terminado con todas las iteraciones, llamamos QWaitCondition::wait() para poner el hilo a dormir mediante el llamado, a menos que la variable restart tenga valor true. No tiene sentido mantener un subproceso de trabajo en bucle indefinidamente mientras no hay nada que hacer.

uint RenderThread::rgbFromWaveLength(double wave)
{
double r = 0.0;
double g = 0.0;
double b = 0.0;

if (wave >= 380.0 && wave <= 440.0) {
r = -1.0 * (wave - 440.0) / (440.0 - 380.0);
b = 1.0;
} else if (wave >= 440.0 && wave <= 490.0) {
g = (wave - 440.0) / (490.0 - 440.0);
b = 1.0;
} else if (wave >= 490.0 && wave <= 510.0) {
g = 1.0;
b = -1.0 * (wave - 510.0) / (510.0 - 490.0);
} else if (wave >= 510.0 && wave <= 580.0) {
r = (wave - 510.0) / (580.0 - 510.0);
g = 1.0;
} else if (wave >= 580.0 && wave <= 645.0) {
r = 1.0;
g = -1.0 * (wave - 645.0) / (645.0 - 580.0);
} else if (wave >= 645.0 && wave <= 780.0) {
r = 1.0;
}

double s = 1.0;
if (wave > 700.0)
s = 0.3 + 0.7 * (780.0 - wave) / (780.0 - 700.0);
else if (wave < 420.0)
s = 0.3 + 0.7 * (wave - 380.0) / (420.0 - 380.0);

r = pow(r * s, 0.8);
g = pow(g * s, 0.8);
b = pow(b * s, 0.8);
return qRgb(int(r * 255), int(g * 255), int(b * 255));
}

La función rgbFromWaveLength () es una función auxiliar que convierte una longitud de onda a un valor RGB compatible con QImages 32 bits. Se llama desde el constructor para inicializar el arreglo de mapa de colores con colores agradables.

 

Definición de la clase MandelbrotWidget


La clase MandelbrotWidget utiliza RenderThread para dibujar el conjunto de Mandelbrot en la pantalla. Aquí está la definición de la clase:

class MandelbrotWidget : public QWidget
{
Q_OBJECT

public:
MandelbrotWidget(QWidget *parent = 0);

protected:
void paintEvent(QPaintEvent *event);
void resizeEvent(QResizeEvent *event);
void keyPressEvent(QKeyEvent *event);
#ifndef QT_NO_WHEELEVENT
void wheelEvent(QWheelEvent *event);
#endif
void mousePressEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);

private slots:
void updatePixmap(const QImage &image, double scaleFactor);
void zoom(double zoomFactor);

private:
void scroll(int deltaX, int deltaY);

RenderThread thread;
QPixmap pixmap;
QPoint pixmapOffset;
QPoint lastDragPos;
double centerX;
double centerY;
double pixmapScale;
double curScale;
};

El widget reimplementa muchos controladores de eventos de QWidget. Además, cuenta con un método slot updatePixmap () que realiza la conexión a la señal renderedImage () del subproceso de trabajo para actualizar la pantalla cada vez que lleguen nuevos datos desde el hilo.

Entre las variables privadas, tenemos hilo de tipo RenderThread y mapa de píxeles, que contiene la última imagen renderizada.

Implementación de la clase MandelbrotWidget

const double DefaultCenterX = -0.637011f;
const double DefaultCenterY = -0.0395159f;
const double DefaultScale = 0.00403897f;

const double ZoomInFactor = 0.8f;
const double ZoomOutFactor = 1 / ZoomInFactor;
const int ScrollStep = 20;

La aplicación se inicia con algunas constantes que vamos a necesitar más adelante.

MandelbrotWidget::MandelbrotWidget(QWidget *parent)
: QWidget(parent)
{
centerX = DefaultCenterX;
centerY = DefaultCenterY;
pixmapScale = DefaultScale;
curScale = DefaultScale;

connect(&thread, SIGNAL(renderedImage(QImage,double)), this, SLOT(updatePixmap(QImage,double)));

setWindowTitle(tr("Mandelbrot"));
#ifndef QT_NO_CURSOR
setCursor(Qt::CrossCursor);
#endif
resize(550, 400);

}

La parte interesante del constructor es la llamada a los métodos qRegisterMetaType() y QObject::connect(). Vamos a empezar con la llamada al connect().

Aunque se parece a una conexión de señal-ranura estándar entre dos QObjects, ya que la señal se emite en un subproceso distinto al subproceso donde vive el receptor, la conexión es efectivamente una conexión en cola (queued connection). Estas conexiones son asíncronas (es decir, no-bloqueantes), significa que el slot será llamado en algún momento después de la declaración emit. Lo que es más, la ranura se invocará en el hilo en el que el receptor vive. Aquí, la señal se emite en el subproceso de trabajo, y la ranura se ejecuta en el hilo GUI cuando el control vuelve al bucle de eventos.

Con conexiones en cola, Qt debe guardar una copia de los argumentos que se han pasado a la señal para que pueda pasarlos a la ranura más adelante. Qt sabe tomar de copia de muchos tipos de C + + y Qt, pero QImage no es uno de ellos. Por tanto, debemos llamar a la función de plantilla qRegisterMetaType() antes de que podamos utilizar QImage como parámetro de conexiones de cola.

void MandelbrotWidget::paintEvent(QPaintEvent * /* event */)
{
QPainter painter(this);
painter.fillRect(rect(), Qt::black);

if (pixmap.isNull()) {
painter.setPen(Qt::white);
painter.drawText(rect(), Qt::AlignCenter, tr("Rendering initial image, please wait..."));
return;
}

En paintEvent(), se comienza por llenar el fondo con negro. Si no tenemos nada aún para pintar (mapa de pixels es nulo), se muestra un mensaje en el widget que pide al usuario que ser paciente y termina la función de inmediato.

    if (curScale == pixmapScale) {
painter.drawPixmap(pixmapOffset, pixmap);
} else {
double scaleFactor = pixmapScale / curScale;
int newWidth = int(pixmap.width() * scaleFactor);
int newHeight = int(pixmap.height() * scaleFactor);
int newX = pixmapOffset.x() + (pixmap.width() - newWidth) / 2;
int newY = pixmapOffset.y() + (pixmap.height() - newHeight) / 2;

painter.save();
painter.translate(newX, newY);
painter.scale(scaleFactor, scaleFactor);
QRectF exposed = painter.matrix().inverted().mapRect(rect()).adjusted(-1, -1, 1, 1);
painter.drawPixmap(exposed, pixmap, exposed);
painter.restore();
}

Si el mapa de píxeles tiene el factor de escala correcto, podemos extraer el mapa de píxeles directamente sobre el widget. De lo contrario, podemos aumentar la escala y traducimos el sistema de coordenadas (coordinate system) antes de sacar el mapa de pixels. Mediante el mapeo inverso rectángulo del widget usando la matriz de pintado a escala, también nos aseguramos de que sólo las áreas expuestas del mapa de píxeles se dibujan. Las llamadas a QPainter::save() y QPainter::restore() aseguran de que todo el proceso de pintado que se realiza después utiliza el sistema de coordenadas estándar.

QString text = tr("Use mouse wheel or the '+' and '-' keys to zoom. "
"Press and hold left mouse button to scroll.");
QFontMetrics metrics = painter.fontMetrics();
int textWidth = metrics.width(text);

painter.setPen(Qt::NoPen);
painter.setBrush(QColor(0, 0, 0, 127));
painter.drawRect((width() - textWidth) / 2 - 5, 0, textWidth + 10, metrics.lineSpacing() + 5);
painter.setPen(Qt::white);
painter.drawText((width() - textWidth) / 2, metrics.leading() + metrics.ascent(), text);
}

Al final del controlador de eventos de pintura, trazamos una cadena de texto y un rectángulo semitransparente en la parte superior del fractal.

void MandelbrotWidget::resizeEvent(QResizeEvent * /* event */)
{
thread.render(centerX, centerY, curScale, size());
}

Cada vez que el usuario cambia el tamaño del widget, llamamos al método render () para comenzar a generar una nueva imagen, con los mismo parámetros: centerX, CenterY y curScale pero con el nuevo tamaño de widget.

Nótese que confiamos en que resizeEvent () se llama automáticamente por Qt cuando el widget se muestra la primera vez para generar la imagen del primer momento.

void MandelbrotWidget::keyPressEvent(QKeyEvent *event)
{
switch (event->key()) {
case Qt::Key_Plus:
zoom(ZoomInFactor);
break;
case Qt::Key_Minus:
zoom(ZoomOutFactor);
break;
case Qt::Key_Left:
scroll(-ScrollStep, 0);
break;
case Qt::Key_Right:
scroll(+ScrollStep, 0);
break;
case Qt::Key_Down:
scroll(0, -ScrollStep);
break;
case Qt::Key_Up:
scroll(0, +ScrollStep);
break;
default:
QWidget::keyPressEvent(event);
}
}

El manejador del evento de presionar una tecla proporciona algunas asociaciones de teclas para el beneficio de los usuarios que no disponen de un ratón. Las funciones zoom () y scroll () serán cubiertos más adelante.

void MandelbrotWidget::wheelEvent(QWheelEvent *event)
{
int numDegrees = event->delta() / 8;
double numSteps = numDegrees / 15.0f;
zoom(pow(ZoomInFactor, numSteps));
}

El controlador de eventos de la rueda es reimplementada para hacer el control nivel de zoom con la rueda del ratón. QWheelEvent::delta() devuelve el ángulo del movimiento de la rueda del ratón, en octavos de un grado. Para la mayoría de los ratones, un paso de la rueda corresponde a 15 grados. Averiguamos cuántos pasos ratón tenemos y determinamos el factor de zoom en consecuencia. Por ejemplo, si tenemos dos pasos de rueda en la dirección positiva (es decir, 30 grados), el factor de zoom se vuelve ZoomInFactor a la segunda potencia, es decir, 0,8 * 0,8 = 0,64.

void MandelbrotWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
lastDragPos = event->pos();
}

Cuando el usuario pulsa el botón izquierdo del ratón, almacenamos la posición del puntero del ratón en lastDragPos.

void MandelbrotWidget::mouseMoveEvent(QMouseEvent *event)
{
if (event->buttons() & Qt::LeftButton) {
pixmapOffset += event->pos() - lastDragPos;
lastDragPos = event->pos();
update();
}
}

Cuando el usuario mueve el puntero del ratón mientras se mantiene pulsado el botón izquierdo del ratón, ajustamos pixmapOffset para pintar el mapa de pixels en una posición desplazada y se llama al QWidget::update() para forzar un repintado.

void MandelbrotWidget::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
pixmapOffset += event->pos() - lastDragPos;
lastDragPos = QPoint();

int deltaX = (width() - pixmap.width()) / 2 - pixmapOffset.x();
int deltaY = (height() - pixmap.height()) / 2 - pixmapOffset.y();
scroll(deltaX, deltaY);
}
}

Al soltar el botón izquierdo del ratón, actualizamos pixmapOffset tal como lo hicimos en un movimiento del ratón y reiniciamos lastDragPos a un valor predeterminado. A continuación, hacemos un llamado de scroll () para representar una nueva imagen para la nueva posición. (El ajuste de pixmapOffset no es suficiente, ya que las áreas reveladas al arrastrar el mapa de píxeles se dibujan en negro.)

void MandelbrotWidget::updatePixmap(const QImage &image, double scaleFactor)
{
if (!lastDragPos.isNull())
return;

pixmap = QPixmap::fromImage(image);
pixmapOffset = QPoint();
lastDragPos = QPoint();
pixmapScale = scaleFactor;
update();
}

El slot updatePixmap () se invoca cuando el subproceso de trabajo ha terminado de representar una imagen. Empezamos comprobando si un lastre está en vigor y no hacemos nada en ese caso. En el caso normal, guardamos la imagen en mapa de pixels e inicializar algunos de los otros miembros. Al final, llamamos QWidget::update() para actualizar la pantalla.

En este punto, uno podría preguntarse por qué usamos un QImage para el parámetro y un QPixmap para el miembro de datos. ¿Por qué no se adhieren a un tipo? La razón es que QImage es la única clase que admite la manipulación de píxeles directa, lo que necesitamos en el subproceso de trabajo. Por otro lado, antes de que una imagen se pueda dibujar en la pantalla, debe ser convertida en un mapa de pixels. Es mejor hacer la conversión de una vez por todas aquí, y no en paintEvent ().

void MandelbrotWidget::zoom(double zoomFactor)
{
curScale *= zoomFactor;
update();
thread.render(centerX, centerY, curScale, size());
}

En el zoom (), se recalcula curScale. Entonces llamamos QWidget::update() para dibujar un mapa de pixels a escala, y le pedimos al subproceso de trabajo para hacer una nueva imagen correspondiente al nuevo valor curScale.

void MandelbrotWidget::scroll(int deltaX, int deltaY)
{
centerX += deltaX * curScale;
centerY += deltaY * curScale;
update();
thread.render(centerX, centerY, curScale, size());
}

scroll() es similar a zoom(), excepto que los parámetros afectados son centerX y CenterY.

 

La función main()


La naturaleza multiproceso de la aplicación no tiene impacto en su función main (), que es tan simple como siempre:

int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MandelbrotWidget widget;
widget.show();
return app.exec();
}

El código fuente de esta aplicación pueden descargarlo desde aquí.

Esto es todo por hoy, esperamos que este artículo le haya sido útil para aprender a usar un hilo trabajador para realizar operaciones computacionales complejas sin bloquear el hilo principal del ciclo de eventos. En próximas entradas de nuestro blog estaremos profundizando sobre otros temas de programación concurrente usando el framework Qt, que permite grandes potencialidades para el desarrollo de aplicaciones con excelente rendimiento.




{ Leer Más }


viernes, 28 de marzo de 2014

Principios básicos de la programación concurrente usando el Framework Qt.

clip_image002

El presente artículo tiene el objetivo de mostrar los principios básicos de la programación multi-hilo usando el Framework Qt.

Los hilos permiten hacer que los sistemas ejecuten tareas en paralelo, al igual que procesos. Entonces, ¿cómo se diferencian los hilos de los procesos? Mientras que usted está haciendo los cálculos en una hoja de cálculo, puede haber también un reproductor multimedia que se ejecuta en el mismo escritorio tocando su canción favorita. Aquí hay un ejemplo de dos procesos trabajando en paralelo: uno que ejecuta el programa de hoja de cálculo, una ejecución de un reproductor de música. La multitarea es un término muy conocido para esto. Una mirada más cercana al reproductor de música revela que allí son cosas que suceden en paralelo dentro de un único proceso. Mientras reproductor de música realiza el envío de la música para el controlador de audio, la interfaz de usuario con todas sus campanas y silbatos se actualiza constantemente. Para esto son los hilos - la concurrencia dentro de un único proceso [1].

Entonces, ¿cómo se implementa la concurrencia? Trabajo paralelo en las CPU de un solo núcleo, es una ilusión que es algo similar a la ilusión de imágenes en movimiento en el cine. Para los procesos, la ilusión se produce mediante la interrupción de trabajo del procesador en un proceso después de un tiempo muy corto. A continuación, el procesador se mueve al siguiente proceso. Para cambiar entre procesos, el contador de programa actual se guarda y contador de programa del próximo procesador se carga. Esto no es suficiente, ya que el mismo hay que hacer con los registros y cierta arquitectura y datos específicos del sistema operativo.

Así como una CPU puede alimentar dos o más procesos, también es posible dejar que la CPU ejecute en dos segmentos de código diferentes de un único proceso. Cuando se inicia un proceso, siempre se ejecuta un segmento de código y por lo tanto, se dice que el proceso ejecuta un hilo. Sin embargo, el programa puede optar por iniciar un segundo hilo. Entonces, dos secuencias de códigos diferentes se procesan simultáneamente dentro de un solo proceso. La concurrencia se logra en las CPU de un solo núcleo guardando repetidamente contadores de programa y luego los registros cargan contadores y registros del programa del próximo hilo. No se requiere la cooperación del programa para desplazarse entre los hilos activos. Un hilo puede estar en cualquier estado cuando se produce el cambio al siguiente contexto.

La tendencia actual en el diseño de la CPU es tener varios núcleos. Una aplicación de un único subproceso típico puede hacer uso de un solo núcleo. Sin embargo, un programa con múltiples hilos se puede asignar a varios núcleos, lo que hace que las cosas sucedan de una manera verdaderamente concurrente. Como resultado, la distribución del trabajo a más de un hilo puede hacer que un programa funcione mucho más rápido en las CPU multinúcleo debido a que núcleos adicionales pueden ser utilizados.

Hilo GUI e Hilo Trabajador

Como se mencionó, cada programa tiene un hilo cuando se inicia. Este hilo se llama el "hilo conductor" (también conocido como el "hilo GUI" en aplicaciones Qt). La interfaz gráfica de usuario Qt se debe ejecutar en este hilo. Todos los widgets y varias clases relacionadas, por ejemplo QPixmap, no trabajan en los hilos secundarios. Un subproceso secundario que comúnmente se conoce como un "subproceso de trabajo" ya que se utiliza para descargar el trabajo de procesamiento del hilo principal.

El acceso simultáneo a los datos

Cada hilo tiene su propia pila, lo que significa que cada hilo tiene su propio historial de llamadas y variables locales. A diferencia de los procesos, los hilos comparten el mismo espacio de direcciones. El siguiente diagrama muestra cómo se encuentran los bloques de construcción de los hilos en la memoria. Contador y los registros de hilos inactivos Programa suelen mantenerse en el espacio del núcleo. Hay una copia compartida del código y una pila separada para cada hilo.

clip_image004

Si dos hilos tienen un puntero al mismo objeto, es posible que los dos hilos tengan acceso a dicho objeto al mismo tiempo y esto potencialmente puede destruir la integridad del objeto. Es fácil imaginar las muchas cosas que pueden salir mal cuando dos métodos del mismo objeto se ejecutan simultáneamente.

A veces es necesario para acceder a un objeto a partir de diferentes hilos, por ejemplo, cuando los objetos que viven en diferentes hilos necesitan comunicarse. Desde hilos utilizan el mismo espacio de direcciones, es más fácil y más rápido para hilos para intercambiar datos de lo que es para los procesos. Los datos no tiene que ser serializado y copiado. Pasar punteros es posible, pero debe haber una coordinación estricta de lo que toques de hilo que se oponen. La ejecución simultánea de las operaciones en un objeto debe ser prevenida. Hay varias maneras de lograr esto y algunas de ellos se describen a continuación.

Entonces, ¿Cómo se pueden programar estas aplicaciones de manera segura? Todos los objetos creados en un hilo se pueden utilizar de manera segura dentro de ese hilo, siempre que otros hilos no tengan referencias a los mismos y los objetos no tienen acoplamiento implícito con otros hilos. Tal acoplamiento implícito puede suceder cuando los datos son compartidos entre instancias como miembros estáticos, singletons o datos globales. Debe familiarizarse con el concepto de clase hilo y funciones seguras y de reentrada.

Uso de hilos

Básicamente, existen dos escenarios fundamentales para el uso de los hilos:

  • Hacer un procesamiento más rápido, haciendo uso de los procesadores multinúcleo.
  • Mantener el hilo GUI u otros hilos críticos liberados al descargar el procesamiento de larga duración o el bloqueo de llamadas a otros hilos.


¿Qué Tecnología Qt se debe usar para el manejo de hilos?

A veces quieres hacer algo más que la ejecución de un método en el contexto de otro hilo. Es posible que desee tener un objeto que vive en otro hilo que proporciona un servicio al hilo GUI. Tal vez usted quiere otro hilo que siga con vida para siempre para sondear los puertos de hardware y enviar una señal al hilo GUI cuando algo notable ha sucedido. Qt proporciona diferentes soluciones para el desarrollo de aplicaciones con subprocesos. La solución correcta depende de la finalidad del nuevo hilo, así como el tiempo de vida que se requiere de ese hilo:

Vida útil del hilo

Tarea de Desarrollo

Solución

Una llamada

Ejecutar un método dentro de otro hilo y dejar el hilo cuando termine el método.

Qt proporciona diferentes soluciones:

Escribir una función y ejecutarla con QtConcurrent::run()

Derivar una clase de QRunnable y ejecutarla en el hilo global con QThreadPool::globalInstance()->start()

Derivar una clase de QThread, reimplementar el método QThread::run() y utilizar QThread::start() para ejecutarlo.

Una llamada

Operaciones se han de realizar en todos los hilos de un contenedor. El procesamiento deberá ser realizado utilizando todos los núcleos disponibles. Un ejemplo común es la producción de imágenes en miniatura de una lista de imágenes.

QtConcurrent proporciona la función de mapa () para aplicar las operaciones en cada elemento contenedor, filtro () para la selección de elementos de recipiente, y la opción de especificar una función de reducir para combinar los elementos restantes.

Una llamada

Una operación de larga duración tiene que ser puesto en otro hilo. Durante el curso de procesamiento, información de estado se debe enviar al hilo de la interfaz gráfica de usuario.

Utilice QThread, reimplementar run() y también las signal/slot sean necesarias. Conecte las señales a las ranuras (slots) del hilo GUI utilizando conexiones de señal / ranura (signal/slot) en cola.

Permanente

Tener un objeto de estar en otro hilo y dejar que se realicen diferentes tareas bajo petición. Esto significa que la comunicación hacia y desde el subproceso de trabajo que se requiere.

Derivar una clase de QObject e implementar las ranuras y las señales necesarias, mover el objeto a un hilo con un bucle de eventos de correr y comunicarse con el objeto a través de conexiones de señal / ranura en cola.

Permanente

Tener un objeto que vive en otro hilo, dejar al objeto realizar tareas repetitivas, como el sondeo de puertos y permitir la comunicación con el hilo GUI.

Igual que el anterior, pero también utilizar un temporizador en el subproceso de trabajo para implementar la encuesta. Sin embargo, la mejor solución para la encuesta es evitarla por completo. A veces, usar QSocketNotifier es una alternativa.

En próximas entradas estaremos profundizando sobre otros temas de programación concurrente usando el framework Qt.

{ Leer Más }


viernes, 28 de febrero de 2014

Desarrollo de videojuegos con Qt Quick + QML

 

image

Qt es un framework que consta de herramientas destinadas a la creación rápida y eficiente de aplicaciones e interfaces de usuario para escritorio, sistemas embebidos y plataformas móviles. Con Qt es posible reutilizar el código de forma eficiente para ser desplegado en diferentes plataformas con un mismo código base. La biblioteca de clases en C++ y las diferentes herramientas permiten al programador crear aplicaciones en una plataforma y compilarla y distribuirlas en otras.

Entre los diferentes módulos de Qt se encuentra Qt Quick el cual es una biblioteca estándar para el desarrollo de aplicaciones con QML. Mientras el módulo QML brinda el motor y la infraestructura del lenguaje, Quick brinda todos los tipos básicos necesarios para la creación de interfaces gráficas. Posee un canvas para mostrar el contenido y diferentes tipos de datos para la creación y animación de componentes visuales, procesamiento de la entrada de teclado y ratón, creación de modelos y vistas de datos y la creación retardada de objetos. El módulo Quick brinda una API QML la cual provee los tipos de datos QML para la creación de interfaces gráficas y una API C++ para extender las aplicaciones QML con código C++.

Qt Quick provee todo lo necesario para crear aplicaciones potentes con una interfaz gráfica dinámica y fluida. Las animaciones y los efectos de transición entre estados son elementos de vital importancia para este módulo. Los efectos visuales pueden ser complementados con componentes especiales como sistemas de partículas y shaders. A continuación se muestran algunas características de Qt Quick que lo convierten en una buena elección para el desarrollo de videojuegos sencillos:

Alto rendimiento con grafo de escena: Qt Quick (versión 2.0) requiere OpenGL 3.0 (u OpenGL 2.x con la extensión framebuffer_object) para renderizar la ventana diseñada en QML. El módulo define un grafo de escena que es luego renderizado. Mediante el uso de la GPU se logra mucho mayor rendimiento que en la versión anterior.

Entradas de usuarios: Qt Quick brinda los siguientes elementos para procesar entradas:

  • Táctil: Qt Quick fue diseñado teniendo en mente las pantallas táctiles y por esto cuenta con eventos táctiles soportados por diferentes elementos visuales.
  • Ratón: el módulo cuenta con el elemento MouseArea el cual recibe automáticamente eventos de ratón (como clicks y eventos de rueda).
  • Teclado: cualquier elemento visual recibe entradas de teclados a través del tipo Key.
  • Gestos de movimientos en dispositivos: Qt Quick por si mismo no ofrece soporte de nativo para gestos físicos de dispositivos pero el módulo Qt Sensors provee tipos QML los cuales soportan dichos gestos.

Estados, transiciones y animaciones: un aspecto llamativo de los juegos son las transiciones entre estados y las animaciones que brindan un atractivo visual al usuario, en Qt Quick estos son conceptos de máxima importancia.

· Estados: el estado de un elemento visual es el conjunto de informaciones que describe cómo y dónde se muestran las diferentes partes que conforman el elemento y todos los datos asociados al estado. Qt Quick provee el elemento State que contiene propiedades que definen su semántica y se puede utilizar para desencadenar comportamiento o animaciones.

· Transiciones: cuando un elemento cambia de un estado a otro su apariencia cambia, una transición es lo que separa dos estados. Qt Quick provee el elemento Transition el cual define lo que ocurre cuando se pasa de un estado a otro.

· Animaciones: cuando se pasa de un estado a otro se puede utilizar una animación para suavizar la transición. Cambios abruptos de la interfaz resultan en una mala experiencia visual y deben ser evitados utilizando animaciones.

· Animación de propiedades: las animaciones no solo están relacionadas con estados y transiciones entre estados. A veces es útil animar el cambio de ciertas propiedades de los elementos, por ejemplo la opacidad. Qt Quick provee el elemento PropertyAnimation que se puede utilizar para este fin.

· Animadores: el tipo Animator es un tipo especial de animación el cual sobrepasa el objeto QML y opera directamente en las primitivas del grafo de escena. Entre los animadores se encuentran: XAnimator, YAnimator, ScaleAnimator, RotationAnimator, OpacityAnimator y UniformAnimator.

Datos, Modelos, Vistas y Almacenamiento de datos: la mayoría de los juegos tienen datos guardados que necesitan mostrar al usuario, estos datos pueden venir de la red, ficheros locales o bases de datos.

· Modelos y vistas: a veces es útil mostrar un mismo dato de diferentes maneras, de esta manera surge la idea de tener un modelo que contiene los datos y las vistas las cuales muestran los datos. El paradigma Modelo/Vista está implementado en Qt Quick el cual puede ser utilizado en el juego.

· Almacenamiento y acceso a datos: las bases de datos son utilizados comúnmente para almacenar información en los juegos, como por ejemplo los records de puntos o de tiempo alcanzados por los jugadores en los diferentes niveles. El módulo Qt Quick local storage provee acceso simplificado a bases de datos relacionales.

Partículas y efectos gráficos: los juegos con interfaces gráficas llamativas resultan más interesantes. El módulo Qt Quick provee herramientas para lograr una aplicación visualmente atractiva pero manteniendo la experiencia de usuario.

  • Transformación visual: los elementos visuales pueden ser transformados, por ejemplo escalados o rotados.
  • Efectos de shader: los efectos de shader permiten utilizar al máximo la capacidad de procesamiento de la tarjeta grafica mediante vextex o fragment shaders.
  • Partículas: el sistema de partículas permite crear explosiones, fuegos artificiales, humo, niebla, efectos de viento incluso gravedad y turbulencia.
  • Sprites: un sprite es un tipo de imagen animada constituida por frames. Qt Quick provee un tipo de dato visual para visualizar sprites los cuales pueden ser utilizados en juegos.
  • Opacidad: los objetos pueden ser opacos o traslucidos, por ejemplo se puede hacer un objeto opaco y otro transparente para centrar la atención del usuario en el opaco.

Tipos de datos convenientes: en un juego muchas veces el programador necesita reaccionar a eventos y desencadenar varias respuestas. QML tiene soporte para estos conceptos a través de enlaces, señales, gestores de señales e inicialización dinámica de objetos.

  • Inicialización dinámica de objetos: QML provee varias maneras de crear y gestionar objetos. Se pueden crear dinámicamente mediante Javascript o utilizando los tipos de datos Loader, Repeater, ListView, GridView y PathView.
  • Conexión dinámica de señales: QML soporta la conexión dinámica de señales a través del método connect().
  • Eventos basados en timers: otro caso común es desencadenar una funcionalidad un periodo de tiempo después de que un evento ocurra. Qt Quick provee el tipo Timer, el cual posee soporte para eventos simples o recurrentes.

Lógica del juego en Javascript: la lógica del juego se puede programar mediante un fichero Javascript. Este es un lenguaje mucho más sencillo que C++ pero que tiene todas las herramientas para desarrollar un juego de calidad. Para llamar a las funciones desde la interfaz solo basta con importar el fichero, y para acceder a los elementos de la interfaz desde el fichero Javascript simplemente hay que utilizar el id de cada elemento. Además el código puede ser depurado mediante Qt Creator solo hay que poner un breakpoint en la función y ejecutar la aplicación con F5. En el modo de depuración se puede ir observando el valor de las variables y así encontrar errores que de otra manera serían difíciles de detectar.

Todas las características de Qt Quick mencionadas anteriormente demuestran que es un módulo muy potente para la creación de juegos. Con relativamente poco código haciendo uso del lenguaje declarativo QML se puede lograr un juego atractivo y tan complejo como se quiera. En la ayuda de Qt se pueden encontrar varios ejemplos de todo lo que se puede lograr con este módulo. Es recomendable utilizarlo en juegos 2D y aplicaciones para pantallas táctiles para sacarle el máximo provecho a todas las características que posee.

{ Leer Más }


martes, 3 de septiembre de 2013

Programación multi-hilos usando QWaitCondition y QMutex.

clip_image002

En el artículo anterior mostramos los principales elementos a tener en cuenta para realizar programación concurrente usando el Framework Qt, específicamente empleamos la clase QSemaphore para resolver el problema del productor-consumidor. En este nuevo artículo explicaremos otra alternativa que también es muy eficiente para programar aplicaciones multi-hilos, se trata de la combinación de QWaitCondition y QMutex.

El objetivo del artículo es mostrar cómo utilizar QWaitCondition y QMutex para controlar el acceso a un búfer circular que comparten un hilo productor y otro hilo consumidor [1].

El productor escribe datos en el búfer hasta que se llega al final del búfer, en cuyo punto se reinicia desde el principio, sobrescribiendo los datos existentes. El hilo consumidor lee los datos a medida que se produce y lo escribe en la salida de error estándar.

Las condiciones de espera (Wait Condition) hacen que sea posible tener un mayor nivel de concurrencia que el que lograríamos si usáramos solamente mutex. Si los accesos al búfer simplemente fueron vigilados por un QMutex, el hilo consumidor no podría acceder al búfer al mismo tiempo que el hilo productor. Sin embargo, no hay nada malo en tener dos hilos que trabajan en diferentes partes del búfer al mismo tiempo.

El ejemplo consta de dos clases: Productor (Producer) y Consumidor (Consumer). Ambos heredan de QThread. El búfer circular utilizado para la comunicación entre estas dos clases y las herramientas de sincronización que lo protegen son variables globales.

Variables globales

Vamos a empezar por la revisión del búfer circular y las herramientas de sincronización asociadas:

const int DataSize = 100000;

const int BufferSize = 8192;
char buffer[BufferSize];

QWaitCondition bufferNotEmpty;
QWaitCondition bufferNotFull;
QMutex mutex;
int numUsedBytes = 0;

DataSize es la cantidad de datos que el productor va a generar. Para mantener el ejemplo tan simple como sea posible, hacemos una constante. BufferSize es el tamaño del búfer circular. Es menos de DataSize, lo que significa que en algún momento el productor alcanzará el final del búfer y reiniciará desde el principio.

Para sincronizar el productor y el consumidor, necesitamos dos condiciones de espera y un mutex. La condición bufferNotEmpty se señala cuando el productor ha generado algunos datos, diciéndole al consumidor que puede empezar a leerlo. La condición bufferNotFull se señala cuando el consumidor haya leído algunos datos, dice la productora que pueda generar mucho más. La variable numUsedBytes indica el número de bytes en el búfer que contiene los datos.

Juntos, las condiciones de espera, la exclusión mutua, y el contador numUsedBytes deben garantizar que el productor nunca está escribiendo bytes en una ubicación mayor que bufferSize por delante del consumidor, y que el consumidor nunca lee los datos que el productor no ha generado todavía.

Clase Productor

Repasemos el código de la clase Producer:

class Producer : public QThread
{
public:
Producer(QObject *parent = NULL) : QThread(parent)
{
}

void run()
{
qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));

for (int i = 0; i < DataSize; ++i) {
mutex.lock();
if (numUsedBytes == BufferSize)
bufferNotFull.wait(&mutex);
mutex.unlock();

buffer[i % BufferSize] = "ACGT"[(int)qrand() % 4];

mutex.lock();
++numUsedBytes;
bufferNotEmpty.wakeAll();
mutex.unlock();
}
}
};

El productor genera DataSize bytes de datos. Antes de escribir un byte en el búfer circular, debe comprobar primero si el búfer está lleno (es decir, si numUsedBytes es igual a BufferSize). Si el búfer está lleno, el hilo espera en la condición bufferNotFull.

Al final, el productor incrementa la variable numUsedBytes y envía la señal indicando que la condición bufferNotEmpty es verdadera, ya que numUsedBytes es necesariamente mayor que 0.

Guardamos todos los accesos a la variable numUsedBytes con un mutex. Además, la función QWaitCondition::wait() acepta un mutex como argumento. Este mutex está desbloqueado antes el hilo se pone a dormir y bloqueado cuando el hilo se despierta. Por otra parte, la transición desde el estado de bloqueo al estado de espera es atómica, para evitar que se produzcan las condiciones de carrera.

Clase del Consumidor

Volvamos a la clase de los consumidores:

class Consumer : public QThread
{
Q_OBJECT
public:
Consumer(QObject *parent = NULL) : QThread(parent)
{
}

void run()
{
for (int i = 0; i < DataSize; ++i) {
mutex.lock();
if (numUsedBytes == 0)
bufferNotEmpty.wait(&mutex);
mutex.unlock();

fprintf(stderr, "%c", buffer[i % BufferSize]);

mutex.lock();
--numUsedBytes;
bufferNotFull.wakeAll();
mutex.unlock();
}
fprintf(stderr, "\n");
}

signals:
void stringConsumed(const QString &text);
};

El código es muy similar a la del productor. Antes de leer el byte, comprobamos si el búfer está vacío (numUsedBytes es 0) y esperamos en la condición bufferNotEmpty si está vacío. Después de que hemos leído el byte, hacemos el decremento de la variable numUsedBytes (en lugar de incrementarlo), y señalamos la condición bufferNotFull (en lugar de la condición bufferNotEmpty).

La función main ()

En la función main (), creamos los dos hilos y llamamos a la función QThread::wait() para asegurar de que ambos contextos de ejecución tienen tiempo para terminar antes de la salida:

int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
Producer producer;
Consumer consumer;
producer.start();
consumer.start();
producer.wait();
consumer.wait();
return 0;
}

Entonces, ¿qué sucede cuando ejecutamos el programa? Inicialmente, el hilo productor es el único que puede hacer cualquier cosa, el consumidor está bloqueado en espera de que la condición bufferNotEmpty sea señalizada (numUsedBytes es 0). Una vez que el productor ha puesto un byte en el buffer, numUsedBytes es BufferSize - 1 y la condición bufferNotEmpty se marca. En ese momento, pueden ocurrir dos cosas: o bien el hilo consumidor se hace cargo y lee el byte, o el productor genera un segundo byte.

El modelo productor-consumidor que se presenta en este ejemplo permite escribir aplicaciones multiprocesos altamente concurrentes. En un equipo con varios procesadores, el programa es potencialmente hasta el doble de rápido que el programa equivalente basado en exclusión mutua, ya que los dos hilos pueden estar activos al mismo tiempo en diferentes partes del búfer.

Tenga en cuenta que estos beneficios no siempre son efectivos. Los procesos de bloqueo y desbloqueo de un QMutex tiene un costo. En la práctica, probablemente valdría la pena dividir el búfer en trozos, pues es más eficiente operar en trozos en lugar de bytes individuales. El tamaño del búfer es también un parámetro que se debe seleccionar cuidadosamente, basado en la experimentación.

El código fuente de esta aplicación pueden descargarlo desde aquí.




{ Leer Más }


viernes, 30 de agosto de 2013

Programación concurrente usando Semáforos con el Framework Qt.

clip_image002

El presente artículo tiene el objetivo de mostrar las bondades de la programación multi-hilo usando el Framework Qt.

Se utilizara la tecnología de Semáforos a través de la clase QSemaphore para controlar el acceso a un búfer circular que comparten un hilo productor y un hilo consumidor.

El productor escribe datos en el búfer hasta que se llega al final del buffer, en cuyo punto se reinicia desde el principio, sobrescribiendo los datos existentes. El hilo consumidor lee los datos a medida que se produce y lo escribe en la salida de error estándar.

Los semáforos hacen posible tener un mayor nivel de concurrencia que los mutex. Si los accesos a la memoria intermedia fueran vigilados por un QMutex, el hilo consumidor no podría acceder a la memoria intermedia al mismo tiempo que el hilo productor. Sin embargo, no hay nada malo en tener dos hilos que trabajan en diferentes partes de la memoria intermedia en el mismo tiempo.

El ejemplo consta de dos clases: productor y consumidor. Ambos heredan de QThread. La memoria intermedia circular utilizado para la comunicación entre estas dos clases y los semáforos que lo protegen son variables globales.

Una alternativa al uso QSemaphore para resolver el problema del productor-consumidor es utilizar QWaitCondition y QMutex.

Variables globales

Vamos a empezar por la revisión del buffer circular y los semáforos asociados:

const int DataSize = 100000;
const int BufferSize = 8192;

char buffer[BufferSize];
QSemaphore freeBytes(BufferSize);
QSemaphore usedBytes;

DataSize es la cantidad que de los datos que el productor va a generar. Para mantener el ejemplo tan simple como sea posible, será una constante. BufferSize es el tamaño del buffer circular. Es menos de DataSize, lo que significa que en algún momento el productor alcanzará el final del buffer y reiniciar desde el principio.

Para sincronizar el productor y el consumidor, necesitamos dos semáforos. El semáforo freeBytes controla el área "libre" del búfer (el área que el productor no ha llenado con los datos todavía o que el consumidor ya ha leído). El semáforo usedBytes controla el área "usado" de la memoria intermedia (el área que el productor ha llenado pero que el consumidor todavía no ha leído).

Juntos, los semáforos garantizan que el productor nunca este más de bufferSize por delante del consumidor, y que el consumidor nunca lee los datos que el productor no ha generado todavía.

El semáforo freeBytes se inicializa con BufferSize, porque al principio todo el buffer está vacío. El semáforo usedBytes se inicializa a 0 (el valor por defecto si no se especifica ninguno).

Clase Productor

Repasemos el código de la clase Producer:

class Producer : public QThread
{
public:
void run()
{
qsrand(QTime(0,0,0).secsTo(QTime::currentTime()));
for (int i = 0; i < DataSize; ++i) {
freeBytes.acquire();
buffer[i % BufferSize] = "ACGT"[(int)qrand() % 4];
usedBytes.release();
}
}
};

El productor genera DataSize bytes de datos. Antes de que escriba un byte en el buffer circular, se debe adquirir un byte "libre" con el semáforo freeBytes. La llamada al QSemaphore :: adquirir () puede bloquear si el consumidor no ha mantenido el ritmo con el productor.

Al final, el productor libera un byte utilizando el semáforo usedBytes. El byte "libre" con éxito se ha transformado en un byte "usado", listo para ser leído por el consumidor.

Clase del Consumidor

Pasemos ahora a la clase de los consumidores:

class Consumer : public QThread
{
Q_OBJECT
public:
void run()
{
for (int i = 0; i < DataSize; ++i) {
usedBytes.acquire();
fprintf(stderr, "%c", buffer[i % BufferSize]);
freeBytes.release();
}
fprintf(stderr, "\n");
}

signals:
void stringConsumed(const QString &text);

protected:
bool finish;
};

El código es muy similar a la del productor, excepto que esta vez que adquirimos un byte "usado" y liberar un byte "libre", en lugar de lo contrario.

La función main ()

En main (), creamos los dos hilos y llamamos QThread :: wait () para asegurarse de que ambos contextos tienen tiempo para terminar antes de la salida:

int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
Producer producer;
Consumer consumer;
producer.start();
consumer.start();
producer.wait();
consumer.wait();
return 0;
}

Entonces, ¿qué sucede cuando ejecutamos el programa? Inicialmente, el hilo productor es el único que puede hacer cualquier cosa, el consumidor está bloqueado esperando el semáforo usedBytes se libere (a su disposición inicial () count es 0). Una vez que el productor ha puesto un byte en el buffer, freeBytes.available () es BufferSize - 1 y usedBytes.available () es 1. En ese momento, pueden ocurrir dos cosas: o bien el hilo consumidor se hace cargo y dice que el byte, o si el cliente llega a producir un segundo byte.

El modelo productor-consumidor se presenta en este ejemplo permite escribir aplicaciones multiproceso altamente concurrentes. En un equipo con varios procesadores, el programa es potencialmente hasta el doble de rápido que el programa de exclusión mutua basado equivalente, ya que los dos hilos pueden estar activos al mismo tiempo en diferentes partes del buffer.

Tenga en cuenta que estos beneficios no siempre son efectivos. La adquisición y liberación de un QSemaphore tiene un costo. En la práctica, probablemente valdría la pena dividir la memoria intermedia en trozos y para operar en trozos en lugar de bytes individuales. El tamaño del búfer es también un parámetro que se debe seleccionar cuidadosamente, basado en la experimentación.

El código fuente de esta aplicación pueden descargarlo desde aquí.




{ Leer Más }


IconIconIcon