Qt: desenho baseado em gráficos vetoriais

O Qt fornece ao programador recursos muito ricos, mas o conjunto de widgets é limitado. Se nenhuma das opções disponíveis for adequada, você deverá desenhar algo próprio. A maneira mais simples - usar imagens prontas - tem sérias desvantagens: a necessidade de armazenar imagens em um arquivo ou recursos, problemas com escalabilidade e portabilidade dos formatos de imagem. A seguir, descreve o uso dos princípios dos gráficos vetoriais sem usar as imagens vetoriais reais.


Preâmbulo


Tudo começou com o fato de que uma vez que uma indicação de sinais de um bit era necessária. Alguns aplicativos recebem alguns dados em alguma porta, o pacote deve ser desmontado e exibido na tela. Seria bom, ao mesmo tempo, imitar o painel familiar. Para exibir dados digitais, o Qt oferece "out of the box" a classe QLCDNumber, semelhante aos indicadores familiares de sete segmentos, mas algo não é visível em lâmpadas simples.


O uso de sinalizadores (caixas de seleção) e comutadores (botões de opção) para esses fins é ruim e aqui está uma lista de razões:


  • Isso está errado semanticamente. Botões - são botões e destinam-se à entrada do usuário, e não para mostrar nada a ele.
  • Isso implica o segundo: o usuário se esforça para cutucar esses botões. Se ao mesmo tempo a atualização de informações não for particularmente rápida, a indicação permanecerá e o usuário relatará um mau funcionamento do programa, rindo muito.
  • Se você travar o botão para pressionar (setEnabled (false)), ele ficará cinza feio. Lembro-me de que no Delphi, na região da versão 6, havia uma finta de orelhas: você podia colocar uma bandeira no painel e desativar a disponibilidade do painel, não a bandeira, então a bandeira não estava nem cinza nem ativa. Este truque não funciona aqui.
  • Os botões têm foco de entrada. Assim, se houver elementos de entrada na janela e o usuário caminhar com eles usando a tecla Tab, ele precisará caminhar pelos elementos de saída, o que é inconveniente e feio.
  • No final, esses botões parecem apenas esteticamente, principalmente ao lado dos sete segmentos.

Conclusão: você mesmo precisa desenhar uma lâmpada.


Farinha de escolha


Primeiro, procurei soluções prontas. Naquele tempo distante, quando usei o Delphi, era possível encontrar apenas uma quantidade gigantesca de componentes acabados, tanto de empresas sérias quanto de fabricantes amadores. O Qt tem muitos problemas com isso. O QWT tem alguns elementos, mas não isso. Eu não via amadorismo. Provavelmente, se você digitar corretamente no Github, poderá encontrar algo, mas eu provavelmente o farei mais rápido.


A primeira coisa que sugeriu a partir do caseiro foi usar dois arquivos de imagem com imagens da luz acesa e apagada. Ruim:


  • É necessário encontrar boas fotos (ou desenhar, mas não sou artista);
  • A questão do princípio é: amarrar não é bom, nem imagens, nem mesmo debaixo dos seus pés;
  • Eles devem ser armazenados em algum lugar. Os arquivos são muito ruins: apagados acidentalmente - e não há botões. Os recursos são melhores, mas também não sinto que consiga sobreviver;
  • Sem escalabilidade;
  • A personalização (cores, por exemplo) é alcançada apenas adicionando arquivos. Ou seja, recursos intensivos e inflexíveis.

A segunda coisa que se segue da primeira é usar imagens vetoriais em vez de imagens. Além disso, o Qt pode renderizar SVG. Aqui já é um pouco mais fácil com a pesquisa da própria imagem: há muitas lições sobre os gráficos vetoriais na rede, você pode encontrar algo mais ou menos adequado e adaptá-lo às suas necessidades. Mas permanece a questão de armazenamento e personalização, e a renderização não é livre de recursos. Moedas de um centavo, é claro, mas ainda assim ...


E o terceiro segue a partir do segundo: você pode usar os princípios de gráficos vetoriais para imagens de auto desenho! Um arquivo de imagem vetorial em forma de texto indica o que e como desenhar. Eu posso especificar o mesmo código usando tutoriais de vetor. Felizmente, o objeto QPainter possui as ferramentas necessárias: uma caneta, um pincel, um gradiente e primitivas de desenho, até um preenchimento de textura. Sim, as ferramentas estão longe de tudo: não há máscaras, modos de mesclagem, mas absolutamente nenhum fotorrealismo é necessário.


Procurei alguns exemplos na rede. Ele aprendeu a primeira lição que surgiu: “Desenhamos um botão no editor de gráficos do Inkscape” no site “É fácil desenhar”. O botão desta lição é muito mais parecido com uma lâmpada do que com um botão, o que combina perfeitamente comigo. Estou fazendo um rascunho: em vez do Inkscape, um projeto no Qt.


Teste de penas


Estou criando um novo projeto. Eu escolhi o nome do projeto rgbled (porque quero fazer algo como um LED RGB) e o caminho para ele. Eu escolho a classe base QWidget e o nome RgbLed, me recuso a criar um arquivo de formulário. O projeto por padrão após o lançamento cria uma janela vazia, ainda é desinteressante.


Preparação para o desenho


Há um espaço em branco. Agora você precisa obter os membros particulares da classe, que determinarão a geometria da imagem. Uma vantagem essencial dos gráficos vetoriais é sua escalabilidade, portanto deve haver um número mínimo de números constantes e eles apenas definem as proporções. As dimensões serão recalculadas no evento resizeEvent (), que precisará ser redefinido.


No tutorial de desenho usado, as dimensões são especificadas em pixels à medida que você avança. Preciso determinar com antecedência o que vou usar e como recontar.


Uma imagem desenhada consiste nos seguintes elementos:


  • anel externo (inclinado para fora, parte do aro convexo)
  • anel interno (inclinado para dentro)
  • Carcaça da lâmpada LED, “vidro”
  • sombra na borda do copo
  • destaque principal
  • reflexo inferior

Círculos concêntricos, isto é, tudo, exceto o brilho, é determinado pela posição do centro e do raio. O brilho é determinado pelo centro, largura e altura, e a posição dos X centros de brilho coincide com a posição do centro X de toda a imagem.


Para calcular os elementos da geometria, é necessário determinar qual é maior - a largura ou a altura, porque a lâmpada é redonda e deve caber em um quadrado com um lado igual à menor das duas dimensões. Então, adiciono os membros privados correspondentes ao arquivo de cabeçalho.


código
private: 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; 

Em seguida, redefino a função protegida que é chamada quando o widget é redimensionado.


código
 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); } 

Aqui, o lado do quadrado no qual a lâmpada está inscrita é calculado, o centro desse quadrado, o raio do aro ocupando a área máxima possível, a largura do aro, cuja parte externa deve ser 1/10 do diâmetro e o 1/14 interno. Em seguida, é calculada a posição do brilho, localizada no meio dos raios superior e inferior, e a largura e a altura são selecionadas a olho nu.


Além disso, nos campos protegidos, adicionarei imediatamente um conjunto de cores a serem usadas.


código
  QColor ledColor; QColor lightColor; QColor shadowColor; QColor ringShadowDarkColor; QColor ringShadowMedColor; QColor ringShadowLightColor; QColor topReflexUpColor; QColor topReflexDownColor; QColor bottomReflexCenterColor; QColor bottomReflexSideColor; 

Pelos nomes, é aproximadamente claro que essas são as cores da lâmpada, a parte clara da sombra, a parte escura da sombra, as três cores da sombra anular ao redor da lâmpada e as cores dos gradientes de brilho.


As cores devem ser inicializadas, para complementar a peça de trabalho do designer.


código
 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)) { } 

Além disso, não esqueça de inserir no arquivo de cabeçalho as inclusões de classes que serão necessárias ao desenhar.


código
 #include <QPainter> #include <QPen> #include <QBrush> #include <QColor> #include <QGradient> 

Esse código é compilado com êxito, mas nada mudou na janela do widget. É hora de começar a desenhar.


Desenhando


Entro em uma função fechada


  void drawLed(const QColor &color); 

e redefinir a função protegida


  void paintEvent(QPaintEvent *event); 

O evento redesenhar causará o desenho real, para o qual a cor do "vidro" é passada como parâmetro.


código
 void RgbLed::paintEvent(QPaintEvent *event) { QWidget::paintEvent(event); this->drawLed(ledColor); } 

Até agora. E começamos a preencher gradualmente a função de desenho.


código
 void RgbLed::drawLed(const QColor &color) { QPainter p(this); QPen pen; pen.setStyle(Qt::NoPen); p.setPen(pen); } 

Primeiro, é criado um objeto de artista, que estará envolvido no desenho. Em seguida, é criado um lápis que é necessário para que não haja lápis: nesta imagem, o traçado do contorno não é apenas necessário, mas não necessário.


Em seguida, o primeiro círculo é desenhado de acordo com a lição sobre gráficos vetoriais: um círculo grande, preenchido com um gradiente radial. O gradiente tem um ponto de ancoragem leve na parte superior, mas não na borda, e um escuro na parte inferior, mas também não na borda. Um pincel é criado com base no gradiente, com esse pintor pintando um círculo (ou seja, uma elipse inscrita em um quadrado). Acontece que esse código


código
  QRadialGradient outerRingGradient(QPoint(centerX, centerY - outerBorderRadius - (outerBorderWidth / 2)), minDim - (outerBorderWidth / 2)); outerRingGradient.setColorAt(0, lightColor); outerRingGradient.setColorAt(1, shadowColor); QBrush outerRingBrush(outerRingGradient); p.setBrush(outerRingBrush); p.drawEllipse(this->drawingRect); qDebug() << "draw"; 

O ambiente enfatiza o parâmetro de cor da função drawLed porque não é usada. Deixe-o tolerar, ele ainda não é necessário, mas em breve ele precisará. O projeto lançado produz o seguinte resultado:


desenho


Adicione outro lote de código.


código
  QRadialGradient innerRingGradient(QPoint(centerX, centerY + innerBorderRadius + (innerBorderWidth / 2)), minDim - (innerBorderWidth / 2)); innerRingGradient.setColorAt(0, lightColor); innerRingGradient.setColorAt(1, shadowColor); QBrush innerRingBrush(innerRingGradient); p.setBrush(innerRingBrush); p.drawEllipse(QPoint(centerX, centerY), outerBorderRadius, outerBorderRadius); 

Quase o mesmo círculo, apenas menor em tamanho e de cabeça para baixo. Temos a seguinte imagem:


desenho


Então, finalmente, você precisa da cor do vidro:


código
  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); 

Aqui, usando a função mais escura da cor transmitida, a mesma cor é obtida, mas mais escura, para organizar o gradiente. Coeficiente 120 selecionado a olho. Aqui está o resultado:


desenho


Adicione uma sombra anular ao redor do vidro. Isso é feito na lição sobre gráficos vetoriais, e isso deve adicionar volume e realismo:


código
  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); 

Há um gradiente de três etapas, de modo que a sombra fica mais grossa até a borda e fica pálida em direção ao centro. Acontece assim:


desenho


Adicione destaques, ao mesmo tempo. O destaque superior, diferente do inferior (e de todos os outros elementos), é transformado em um gradiente linear. O meu artista é mais ou menos assim, aceito minha palavra como autor da lição. Talvez haja alguma verdade nisso, não experimentarei diferentes tipos de gradientes.


código
  QLinearGradient topTeflexGradient(QPoint(centerX, (innerBorderWidth + outerBorderWidth)), QPoint(centerX, centerY)); topTeflexGradient.setColorAt(0, topReflexUpColor); topTeflexGradient.setColorAt(1, topReflexDownColor); QBrush topReflexbrush(topTeflexGradient); p.setBrush(topReflexbrush); p.drawEllipse(QPoint(centerX, topReflexY), topReflexWidth, topReflexHeight); QRadialGradient bottomReflexGradient(QPoint(centerX, bottomReflexY + (bottomReflexHeight / 2)), bottomReflexWidth); bottomReflexGradient.setColorAt(0, bottomReflexSideColor); bottomReflexGradient.setColorAt(1, bottomReflexCenterColor); QBrush bottomReflexBrush(bottomReflexGradient); p.setBrush(bottomReflexBrush); p.drawEllipse(QPoint(centerX, bottomReflexY), bottomReflexWidth, bottomReflexHeight); 

Na verdade, isso é tudo, uma lâmpada pronta, como no KDPV.


desenho


A visibilidade do brilho e da protuberância do vidro é afetada pela cor, ou melhor, pela escuridão. Pode fazer sentido adicionar um ajuste para o brilho do brilho e o coeficiente de escurecimento na função mais escura, dependendo da escuridão, mas acho que é perfeccionismo.


Abaixo está um exemplo de uso em uma janela de programa.


desenho


Mimos


Por diversão, você pode brincar com flores. Por exemplo, substituindo um evento de clique do mouse protegido


  void mousePressEvent(QMouseEvent *event); 

desta maneira:


código
 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); } 

não esquecendo de adicionar eventos do mouse ao cabeçalho:


 #include <QMouseEvent> 

Agora, um clique do mouse no componente mudará a cor da lâmpada: vermelha, verde, azul, cinza e alguma luz aleatória da lâmpada.


Epílogo


Quanto ao desenho, é tudo. E o widget deve adicionar funcionalidade. No meu caso, foi adicionado um campo booleano “use state”, outro campo booleano que define o estado “On” ou “Off” e as cores padrão desses estados, além de getters e setters abertos para tudo isso. a função paintEvent () para selecionar a cor passada para drawLed () como parâmetro. Como resultado, você pode desativar o uso de estados e definir a lâmpada para qualquer cor ou ativar os estados e ativar ou desativar a lâmpada de acordo com os eventos. É especialmente conveniente tornar o configurador de estados um slot aberto e unido seja com o sinal que deve ser monitorado.


O uso do mousePressEvent demonstra que um widget pode ser feito não apenas como indicador, mas também como um botão, pressionando-o, soltando-o, dobrando-o, torcendo-o, colorido e o que você quiser para os eventos de apontar, clicar e liberar.


Mas isso não é mais fundamental. O objetivo era mostrar onde é possível obter exemplos de modelos ao desenhar seus próprios widgets e como esse desenho é fácil de implementar sem o uso de imagens raster ou vetoriais, em recursos ou arquivos.

Source: https://habr.com/ru/post/pt425547/


All Articles