Qt proporciona al programador funciones muy ricas, pero el conjunto de widgets es limitado. Si ninguno de los disponibles es adecuado, debe dibujar algo propio. La forma más simple, usar imágenes ya hechas, tiene serios inconvenientes: la necesidad de almacenar imágenes en un archivo o recursos, problemas de escalabilidad y portabilidad de formatos de imagen. A continuación se describe el uso de los principios de los gráficos vectoriales sin utilizar las imágenes vectoriales reales.
Preámbulo
Todo comenzó con el hecho de que una vez se necesitaba una indicación de signos de un bit. Algunas aplicaciones reciben algunos datos en algún puerto, el paquete debe desarmarse y mostrarse en la pantalla. Sería bueno al mismo tiempo imitar de alguna manera el tablero familiar. Para mostrar datos digitales, Qt ofrece "fuera de la caja" la clase QLCDNumber, similar a los familiares indicadores de siete segmentos, pero algo no es visible en las lámparas individuales.
Usar indicadores (son casillas de verificación) e interruptores (son botones de radio) para estos fines es malo, y aquí hay una lista de razones:
- Esto está mal semánticamente. Botones: son botones y están destinados a la entrada del usuario y no para mostrarle nada.
- Esto implica lo segundo: el usuario se esfuerza por tocar esos botones. Si al mismo tiempo la actualización de la información no es particularmente rápida, la indicación mentirá, y el usuario informará un mal funcionamiento del programa, riendo vilmente.
- Si bloquea el botón para presionar (setEnabled (false)), se vuelve gris feo. Recuerdo que en Delphi, en la región de la versión 6, había una finta con orejas: se podía poner una bandera en el panel y desactivar la disponibilidad del panel, no la bandera, entonces la bandera no era gris ni activa. Este truco no funciona aquí.
- Los botones tienen foco de entrada. En consecuencia, si hay elementos de entrada en la ventana, y el usuario los recorre usando la tecla Tab, tendrá que caminar a lo largo de los elementos de salida, lo cual es inconveniente y feo.
- Al final, estos botones se ven sin estética, especialmente al lado del segmento de siete.
Conclusión: debes dibujar una bombilla tú mismo.
Harina de elección
Primero busqué soluciones ya hechas. En ese tiempo lejano, cuando usaba Delphi, podía encontrar una cantidad gigantesca de componentes terminados, tanto de compañías serias como de fabricantes aficionados. Qt tiene muchos problemas con esto. QWT tiene algunos elementos, pero no eso. No vi el amateurismo en absoluto. Probablemente, si cavas correctamente en Github, puedes encontrar algo, pero probablemente lo haré más rápido.
Lo primero que se sugirió del casero fue usar dos archivos de imagen con imágenes de la luz encendida y apagada. Malo:
- Es necesario encontrar buenas imágenes (o dibujar, pero no soy artista);
- La cuestión de principio es: atar no es bueno, incluso imágenes, incluso tumbarse bajo los pies;
- Deben almacenarse en algún lugar. Los archivos son muy malos: borrados accidentalmente y no hay botones. Los recursos son mejores, pero tampoco siento que pueda sobrevivir;
- Sin escalabilidad;
- La personalización (colores, por ejemplo) se logra solo agregando archivos. Es decir, recursos intensivos e inflexibles.
La segunda cosa que se desprende de la primera es usar imágenes vectoriales en lugar de imágenes. Además, Qt puede renderizar SVG. Ya es un poco más fácil buscar la imagen en sí: hay muchas lecciones sobre los gráficos vectoriales en la red, puede encontrar algo más o menos adecuado y adaptarlo a sus necesidades. Pero la cuestión sigue siendo el almacenamiento y la personalización, y la representación no es gratuita para los recursos. Peniques, por supuesto, pero aún así ...
Y el tercero se deduce del segundo: ¡puede usar los principios de los gráficos vectoriales para imágenes de auto-dibujo! Un archivo de imagen vectorial en forma de texto indica qué y cómo dibujar. Puedo especificar el mismo código usando tutoriales vectoriales. Afortunadamente, el objeto QPainter tiene las herramientas necesarias: un bolígrafo, un pincel, un degradado y primitivas de dibujo, incluso un relleno de textura. Sí, las herramientas están lejos de todo: no hay máscaras, modos de fusión, pero absolutamente no se requiere fotorrealismo.
Busqué algunos ejemplos en la red. Tomó la primera lección que surgió: "Dibujamos un botón en el editor de gráficos Inkscape" del sitio "Es fácil de dibujar". El botón de esta lección se parece mucho más a una bombilla que a un botón, lo cual me conviene perfectamente. Estoy haciendo un borrador: en lugar de Inkscape, un proyecto en Qt.
Prueba de plumas
Estoy creando un nuevo proyecto. Elijo el nombre del proyecto rgbled (porque quiero hacer algo como un LED RGB) y la ruta a él. Elijo la clase base QWidget y el nombre RgbLed, me niego a crear un archivo de formulario. El proyecto por defecto después del lanzamiento hace una ventana vacía, todavía no es interesante.
Preparación para el dibujo
Hay un espacio en blanco. Ahora necesita obtener los miembros privados de la clase, que determinarán la geometría de la imagen. Una ventaja esencial de los gráficos vectoriales es su escalabilidad, por lo que debe haber un mínimo de números constantes, y solo establecen las proporciones. Las dimensiones se volverán a calcular en el evento resizeEvent (), que deberá redefinirse.
En el tutorial de dibujo utilizado, las dimensiones se especifican en píxeles a medida que avanza. Necesito determinar de antemano lo que usaré y cómo contarlo.
Una imagen dibujada consta de los siguientes elementos:
- anillo exterior (inclinado hacia afuera, parte del borde convexo)
- anillo interior (inclinado hacia adentro)
- Carcasa de lámpara LED, "vidrio"
- sombra en el borde del cristal
- punto culminante
- llamarada inferior
Los círculos concéntricos, es decir, todo excepto el resplandor, están determinados por la posición del centro y el radio. El deslumbramiento está determinado por el centro, el ancho y la altura, y la posición de los centros de deslumbramiento X coincide con la posición del centro X de toda la imagen.
Para calcular los elementos de la geometría, debe determinar cuál es mayor: el ancho o la altura, porque la bombilla es redonda y debe caber en un cuadrado con un lado igual a la menor de las dos dimensiones. Entonces, agrego los miembros privados correspondientes al archivo de encabezado.
codigoprivate: int height; int width; int minDim; int half; int centerX; int centerY; QRect drawingRect; int outerBorderWidth; int innerBorderWidth; int outerBorderRadius; int innerBorderRadius; int topReflexY; int bottomReflexY; int topReflexWidth; int topReflexHeight; int bottomReflexWidth; int bottomReflexHeight;
Luego redefiní la función protegida que se llama cuando se cambia el tamaño del widget.
codigo protected: void resizeEvent(QResizeEvent *event); void RgbLed::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); this->height = this->size().height(); this->width = this->size().width(); this->minDim = (height > width) ? width : height; this->half = minDim / 2; this->centerX = width / 2; this->centerY = height / 2; this->outerBorderWidth = minDim / 10; this->innerBorderWidth = minDim / 14; this->outerBorderRadius = half - outerBorderWidth; this->innerBorderRadius = half - (outerBorderWidth + innerBorderWidth); this->topReflexY = centerY - (half - outerBorderWidth - innerBorderWidth) / 2; this->bottomReflexY = centerY + (half - outerBorderWidth - innerBorderWidth) / 2; this->topReflexHeight = half / 5; this->topReflexWidth = half / 3; this->bottomReflexHeight = half / 5; this->bottomReflexWidth = half / 3; drawingRect.setTop((height - minDim) / 2); drawingRect.setLeft((width - minDim) / 2); drawingRect.setHeight(minDim); drawingRect.setWidth(minDim); }
Aquí, se calcula el lado del cuadrado en el que está inscrita la bombilla, el centro de este cuadrado, el radio del borde que ocupa el área máxima posible, el ancho del borde, cuya parte externa debe ser 1/10 del diámetro y el interior 1/14. Luego, se calcula la posición del resplandor, que se encuentra en el medio de los radios superior e inferior, el ancho y la altura se seleccionan por ojo.
Además, en los campos protegidos, agregaré inmediatamente un conjunto de colores para usar.
codigo QColor ledColor; QColor lightColor; QColor shadowColor; QColor ringShadowDarkColor; QColor ringShadowMedColor; QColor ringShadowLightColor; QColor topReflexUpColor; QColor topReflexDownColor; QColor bottomReflexCenterColor; QColor bottomReflexSideColor;
Por los nombres, es aproximadamente claro que estos son los colores de la bombilla, la parte clara de la sombra, la parte oscura de la sombra, los tres colores de la sombra anular alrededor de la bombilla y los colores de los gradientes de deslumbramiento.
Los colores deben inicializarse, por lo que complementaré la pieza de trabajo del diseñador.
codigo RgbLed::RgbLed(QWidget *parent) : QWidget(parent), ledColor(Qt::green), lightColor(QColor(0xE0, 0xE0, 0xE0)), shadowColor(QColor(0x70, 0x70, 0x70)), ringShadowDarkColor(QColor(0x50, 0x50, 0x50, 0xFF)), ringShadowMedColor(QColor(0x50, 0x50, 0x50, 0x20)), ringShadowLightColor(QColor(0xEE, 0xEE, 0xEE, 0x00)), topReflexUpColor(QColor(0xFF, 0xFF, 0xFF, 0xA0)), topReflexDownColor(QColor(0xFF, 0xFF, 0xFF, 0x00)), bottomReflexCenterColor(QColor(0xFF, 0xFF, 0xFF, 0x00)), bottomReflexSideColor(QColor(0xFF, 0xFF, 0xFF, 0x70)) { }
Además, no olvide insertar en el archivo de encabezado las inclusiones de clases que se necesitarán al dibujar.
codigo #include <QPainter> #include <QPen> #include <QBrush> #include <QColor> #include <QGradient>
Este código se compila correctamente, pero nada ha cambiado en la ventana del widget. Es hora de empezar a dibujar.
Dibujo
Entro en una función cerrada
void drawLed(const QColor &color);
y redefinir la función protegida
void paintEvent(QPaintEvent *event);
El evento redibujado causará el dibujo real, al cual se pasa el color del "vidrio" como parámetro.
codigo void RgbLed::paintEvent(QPaintEvent *event) { QWidget::paintEvent(event); this->drawLed(ledColor); }
Hasta ahora Y comenzamos a completar gradualmente la función de dibujo.
codigo void RgbLed::drawLed(const QColor &color) { QPainter p(this); QPen pen; pen.setStyle(Qt::NoPen); p.setPen(pen); }
Primero, se crea un objeto de artista, que se dedicará al dibujo. Luego se crea un lápiz que es necesario para que no haya lápiz: en esta imagen, el trazo del contorno no solo no es necesario, sino que tampoco es necesario.
Luego, el primer círculo se dibuja de manera aproximada de acuerdo con la lección sobre gráficos vectoriales: un círculo grande, lleno de un degradado radial. El gradiente tiene un punto de anclaje claro en la parte superior, pero no en el borde mismo, y uno oscuro en la parte inferior, pero tampoco en el borde. Se crea un pincel basado en el degradado, con este pincel el pintor pinta un círculo (es decir, una elipse inscrita en un cuadrado). Resulta que tal código
codigo QRadialGradient outerRingGradient(QPoint(centerX, centerY - outerBorderRadius - (outerBorderWidth / 2)), minDim - (outerBorderWidth / 2))
El entorno enfatiza el parámetro de color de la función drawLed porque no se usa. Déjelo tolerar, todavía no lo necesitan, pero pronto lo necesitará. El proyecto lanzado produce el siguiente resultado:
Agregue otro lote de código.
codigo QRadialGradient innerRingGradient(QPoint(centerX, centerY + innerBorderRadius + (innerBorderWidth / 2)), minDim - (innerBorderWidth / 2))
Casi el mismo círculo, solo que más pequeño y al revés. Obtenemos la siguiente imagen:
Entonces finalmente necesitas el color del vidrio:
codigo QColor dark(color.darker(120)); QRadialGradient glassGradient(QPoint(centerX, centerY), innerBorderRadius); glassGradient.setColorAt(0, color); glassGradient.setColorAt(1, dark); QBrush glassBrush(glassGradient); p.setBrush(glassBrush); p.drawEllipse(QPoint(centerX, centerY), innerBorderRadius, innerBorderRadius);
Aquí, usando la función más oscura del color transmitido, se obtiene el mismo color, pero más oscuro, para organizar el gradiente. Coeficiente 120 seleccionado por ojo. Aquí está el resultado:
Agregue una sombra anular alrededor del vidrio. Esto se hace en la lección sobre gráficos vectoriales, y esto debería agregar volumen y realismo:
codigo QRadialGradient shadowGradient(QPoint(centerX, centerY), innerBorderRadius); shadowGradient.setColorAt(0, ringShadowLightColor); shadowGradient.setColorAt(0.85, ringShadowMedColor); shadowGradient.setColorAt(1, ringShadowDarkColor); QBrush shadowBrush(shadowGradient); p.setBrush(shadowBrush); p.drawEllipse(QPoint(centerX, centerY), innerBorderRadius, innerBorderRadius);
Hay un gradiente de tres pasos, de modo que la sombra es más gruesa hasta el borde y se vuelve pálida hacia el centro. Resulta así:
Agregue reflejos, ambos a la vez. El resaltado superior, a diferencia del inferior (y todos los demás elementos), se convierte en un degradado lineal. Mi artista es regular, tomo mi palabra para el autor de la lección. Quizás haya algo de verdad en esto, no experimentaré con diferentes tipos de gradientes.
codigo QLinearGradient topTeflexGradient(QPoint(centerX, (innerBorderWidth + outerBorderWidth)), QPoint(centerX, centerY))
De hecho, eso es todo, una bombilla prefabricada, como en KDPV.
La visibilidad del resplandor y el abultamiento del vidrio se ve afectada por el color, o más bien, por lo oscuro que es. Puede tener sentido agregar un ajuste para el brillo del resplandor y el coeficiente de atenuación en la función más oscura dependiendo de la oscuridad, pero creo que esto es perfeccionismo.
A continuación se muestra un ejemplo de uso en una ventana del programa.
Mimos
Por diversión, puedes jugar con flores. Por ejemplo, anular un evento de clic de mouse protegido
void mousePressEvent(QMouseEvent *event);
de esta manera:
codigo void RgbLed::mousePressEvent(QMouseEvent *event) { static int count = 0; if (event->button() == Qt::LeftButton) { switch (count) { case 0: ledColor = Qt::red; count++; break; case 1: ledColor = Qt::green; count++; break; case 2: ledColor = Qt::blue; count++; break; case 3: ledColor = Qt::gray; count++; break; default: ledColor = QColor(220, 30, 200); count = 0; break; } this->repaint(); } QWidget::mousePressEvent(event); }
sin olvidar agregar eventos del mouse al encabezado:
#include <QMouseEvent>
Ahora, al hacer clic con el mouse en el componente, cambiará el color de la bombilla: rojo, verde, azul, gris y algo de luz aleatoria de la lámpara.
Epílogo
En cuanto al dibujo, eso es todo. Y el widget debería agregar funcionalidad. En mi caso, se agregó un campo booleano "use state", otro campo booleano que define el estado "On" u "Off" y los colores predeterminados para estos estados, así como getters y setters abiertos para todo esto. la función paintEvent () para seleccionar el color pasado a drawLed () como parámetro. Como resultado, puede desactivar el uso de estados y configurar la bombilla a cualquier color, o activar los estados y encender o apagar la bombilla de acuerdo con los eventos. Es especialmente conveniente hacer que el configurador de estado sea una ranura abierta y unidos ya sea con la señal que debe ser monitoreada.
El uso de mousePressEvent demuestra que un widget se puede convertir no solo en un indicador, sino también en un botón, haciendo que se presione, suelte, doble, tuerza, coloree y lo que desee para los eventos de apuntar, hacer clic y soltar.
Pero esto ya no es fundamental. El objetivo era mostrar dónde puede tomar modelos a seguir al dibujar sus propios widgets y cómo este dibujo es fácil de implementar sin usar imágenes ráster o vectoriales, en recursos o archivos.