o tendência atual em design gráfico é usar muitos cantos arredondados em todos os tipos de formatos. Podemos observar esse fato em muitas páginas da web, dispositivos móveis e aplicativos de desktop. Os exemplos mais notáveis são os botões de ação do aplicativo, que são usados para acionar alguma ação quando clicados. Em vez da forma estritamente retangular com ângulos de 90 graus nos cantos, eles geralmente são desenhados com cantos arredondados. Os cantos arredondados tornam a interface do usuário mais suave e agradável. Não estou totalmente convencido disso, mas meu amigo designer me diz isso.
arquivos de cabeçalho c ++Os cantos arredondados tornam a interface do usuário mais suave e agradável. Tweet
Os elementos visuais das interfaces de usuário são criados por designers, e o programador só precisa colocá-los nos lugares certos. Mas o que acontece, quando temos que gerar uma forma com cantos arredondados instantaneamente e não podemos pré-carregá-la? Algumas bibliotecas de programação oferecem recursos limitados para a criação de formas predefinidas com cantos arredondados, mas geralmente, elas não podem ser usadas em casos mais complicados. Por exemplo, Framework Qt tem uma aula QPainter
, que é usado para desenhar em todas as classes derivadas de QPaintDevice
, incluindo widgets, pixmaps e imagens. Possui um método denominado drawRoundedRect
, que, assim como o nome sugere, desenha um retângulo com cantos arredondados. Mas se precisarmos de uma forma um pouco mais complexa, temos que implementá-la nós mesmos. Como poderíamos fazer isso com um polígono, uma forma plana limitada por um grupo de segmentos de linha reta? Se tivermos um polígono desenhado com um lápis em um pedaço de papel, minha primeira ideia seria usar uma borracha e deletar uma pequena parte das linhas em cada canto e então conectar as pontas restantes do segmento com um arco circular. Todo o processo pode ser ilustrado na figura abaixo.
Classe QPainter
possui alguns métodos sobrecarregados chamados drawArc
, que podem desenhar arcos circulares. Todos eles requerem parâmetros, que definem o centro e o tamanho do arco, o ângulo inicial e o comprimento do arco. Embora seja fácil determinar os valores necessários desses parâmetros para um retângulo não girado, é uma questão totalmente diferente quando lidamos com polígonos mais complexos. Além disso, teríamos que repetir esse cálculo para cada vértice do polígono. Esse cálculo é uma tarefa longa e cansativa, e os humanos estão sujeitos a todos os tipos de erros de cálculo no processo. No entanto, é trabalho dos desenvolvedores de software fazer com que os computadores funcionem para os seres humanos, e não vice-versa. Então, aqui vou mostrar como desenvolver uma classe simples, que pode transformar um polígono complexo em uma forma com cantos arredondados. Os usuários desta classe terão apenas que anexar vértices poligonais e a classe fará o resto. A ferramenta matemática essencial que utilizo para esta tarefa é a Curva de Bezier .
Existem muitos livros de matemática e recursos da Internet que descrevem a teoria das curvas de Bezier, portanto, irei delinear brevemente as propriedades relevantes.
Por definição, a curva de Bézier é uma curva entre dois pontos em uma superfície bidimensional, cuja trajetória é governada por um ou mais pontos de controle. A rigor, uma curva entre dois pontos sem pontos de controle adicionais também é uma curva de Bézier. No entanto, como isso resulta em uma linha reta entre os dois pontos, não é particularmente interessante, nem útil.
As curvas quadráticas de Bezier têm um ponto de controle. A teoria diz que uma curva de Bézier quadrática entre pontos P0 e P2 com ponto de controle P1 é definido como segue:
B (t) = (1 - t)2P0+ 2t (1 - t) P1+ t2P2, onde 0 ≤ t ≤ 1 (1)
Então quando t é igual a 0 , B (t) vai render P0 , quando t é igual a 1 , B (t) vai render P2 , mas em todos os outros casos, o valor de B (t) também dependerá de P1 . Desde a expressão 2t (1 - t) tem um valor máximo em t = 0,5 , é aí que a influência de P1 em B (t) será o maior. Podemos pensar em P1 como de uma fonte imaginária de gravidade, que puxa a trajetória da função em sua direção. A figura abaixo mostra alguns exemplos de curvas quadráticas de Bezier com seus pontos inicial, final e de controle.
scorp vs c corp vs llc
Então, como resolvemos nosso problema usando as curvas de Bézier? A figura abaixo oferece uma explicação.
Se imaginarmos deletar um vértice do polígono e uma pequena parte dos segmentos de linha conectados em seus arredores, podemos pensar em uma extremidade de segmento de linha a partir de P0 , o outro segmento de linha termina a partir de P2 e o vértice excluído a partir de P1 . Aplicamos uma curva de Bézier quadrática a este conjunto de pontos e voila, lá está o canto arredondado desejado.
Classe QPainter
não tem como desenhar curvas quadráticas de Bézier. Embora seja muito fácil implementá-lo do zero seguindo a equação (1), a biblioteca Qt oferece uma solução melhor. Há outra classe poderosa para desenho 2D: QPainterPath
. Classe QPainterPath
é uma coleção de linhas e curvas que podem ser adicionadas e usadas posteriormente com QPainter
objeto. Existem alguns métodos sobrecarregados que adicionam curvas de Bézier a uma coleção atual. Em particular, os métodos quadTo
irá adicionar curvas de Bézier quadráticas. A curva começará no atual QPainterPath
ponto ( P0 ), enquanto P1 e P2 deve ser passado para quadTo
como parâmetros.
QPainter
Método de drawPath
é usado para desenhar uma coleção de linhas e curvas de QPainterPath
objeto, que deve ser dado como parâmetro, com caneta e pincel ativos.
ajuste de desempenho do sql server 2014
Então, vamos ver a declaração da classe:
class RoundedPolygon : public QPolygon { public: RoundedPolygon() { SetRadius(10); } void SetRadius(unsigned int iRadius) { m_iRadius = iRadius; } const QPainterPath& GetPath(); private: QPointF GetLineStart(int i) const; QPointF GetLineEnd(int i) const; float GetDistance(QPoint pt1, QPoint pt2) const; private: QPainterPath m_path; unsigned int m_iRadius; };
Decidi criar uma subclasse QPolygon
para que eu não precise implementar a adição de vértices e outras coisas sozinho. Além do construtor, que apenas define o raio para algum valor inicial sensível, esta classe tem dois outros métodos públicos:
SetRadius
método define o raio para um determinado valor. Raio é o comprimento de uma linha reta (em pixels) próxima a cada vértice, que será excluída (ou, mais precisamente, não desenhada) para o canto arredondado.GetPath
é onde todos os cálculos ocorrem. Ele retornará o QPainterPath
objeto gerado a partir dos pontos do polígono adicionados a RoundedPolygon
.Os métodos da parte privada são apenas métodos auxiliares usados por GetPath
.
tela @media e (largura máxima
Vamos ver a implementação e começarei com os métodos privados:
float RoundedPolygon::GetDistance(QPoint pt1, QPoint pt2) const { float fD = (pt1.x() - pt2.x())*(pt1.x() - pt2.x()) + (pt1.y() - pt2.y()) * (pt1.y() - pt2.y()); return sqrtf(fD); }
Não há muito o que explicar aqui, o método retorna a distância euclidiana entre os dois pontos dados.
QPointF RoundedPolygon::GetLineStart(int i) const { QPointF pt; QPoint pt1 = at(i); QPoint pt2 = at((i+1) % count()); float fRat = m_uiRadius / GetDistance(pt1, pt2); if (fRat > 0.5f) fRat = 0.5f; pt.setX((1.0f-fRat)*pt1.x() + fRat*pt2.x()); pt.setY((1.0f-fRat)*pt1.y() + fRat*pt2.y()); return pt; }
Método GetLineStart
calcula a localização do ponto P2 da última figura, se os pontos forem adicionados ao polígono no sentido horário. Mais precisamente, ele retornará um ponto, que é m_uiRadius
pixels de i
-ésimo vértice na direção do (i+1)
-ésimo vértice. Ao acessar o (i+1)
-ésimo vértice, devemos lembrar que no polígono também existe um segmento de reta entre o último e o primeiro vértice, o que o torna uma forma fechada, daí a expressão (i+1)%count()
. Isso também evita que o método saia do alcance e acesse o primeiro ponto. Variável fRat
mantém a proporção entre o raio e o i
-ésimo comprimento do segmento de linha. Também há uma verificação que impede fRat
de ter um valor acima de 0.5
. Se fRat
tivesse um valor acima de 0.5
, então os dois cantos arredondados consecutivos se sobreporiam, o que causaria um resultado visual ruim.
Ao viajar de um ponto P1 para P2 em uma linha reta e completando 30 por cento da distância, podemos determinar nossa localização usando a fórmula 0,7 • P1+ 0,3 • P2 . Em geral, se atingirmos uma fração da distância total, e α = 1 denota a distância total, a localização atual é em (1 - α) • P1 + α • P2 .
É assim que o GetLineStart
método determina a localização do ponto que é m_uiRadius
pixels de i
-ésimo vértice na direção de (i+1)
-ésimo.
QPointF RoundedPolygon::GetLineEnd(int i) const { QPointF pt; QPoint pt1 = at(i); QPoint pt2 = at((i+1) % count()); float fRat = m_uiRadius / GetDistance(pt1, pt2); if (fRat > 0.5f) fRat = 0.5f; pt.setX(fRat*pt1.x() + (1.0f - fRat)*pt2.x()); pt.setY(fRat*pt1.y() + (1.0f - fRat)*pt2.y()); return pt; }
Este método é muito semelhante a GetLineStart
. Calcula a localização do ponto P0 para o (i+1)
-ésimo vértice, não i
-ésimo. Em outras palavras, se desenharmos uma linha de GetLineStart(i)
para GetLineEnd(i)
para cada i
entre 0
e n-1
, onde n
é o número de vértices no polígono, obteríamos o polígono com vértices apagados e seus arredores próximos.
E agora, o método principal da classe:
é uma empresa considerada uma sociedade unipessoal
const QPainterPath& RoundedPolygon::GetPath() { m_path = QPainterPath(); if (count() <3) { qWarning() << 'Polygon should have at least 3 points!'; return m_path; } QPointF pt1; QPointF pt2; for (int i = 0; i < count(); i++) { pt1 = GetLineStart(i); if (i == 0) m_path.moveTo(pt1); else m_path.quadTo(at(i), pt1); pt2 = GetLineEnd(i); m_path.lineTo(pt2); } // close the last corner pt1 = GetLineStart(0); m_path.quadTo(at(0), pt1); return m_path; }
Neste método, construímos o QPainterPath
objeto. Se o polígono não possui pelo menos três vértices, não estamos mais lidando com uma forma 2D e, neste caso, o método emite um aviso e retorna o caminho vazio. Quando pontos suficientes estão disponíveis, fazemos um loop sobre todos os segmentos de linha reta do polígono (o número de segmentos de linha é, obviamente, igual ao número de vértices), calculando o início e o fim de cada segmento de linha reta entre os cantos. Colocamos uma linha reta entre esses dois pontos e uma curva Bézier quadrática entre o final do segmento de linha anterior e o início da corrente, usando a localização do vértice atual como ponto de controle. Após o loop, temos que fechar o caminho com uma curva de Bézier entre o último e o primeiro segmento de linha, porque no loop desenhamos uma linha reta a mais do que as curvas de Bezier.
RoundedPolygon
uso e resultadosAgora é hora de ver como usar essa aula na prática.
QPixmap pix1(300, 200); QPixmap pix2(300, 200); pix1.fill(Qt::white); pix2.fill(Qt::white); QPainter P1(&pix1); QPainter P2(&pix2); P1.setRenderHints(QPainter::Antialiasing); P2.setRenderHints(QPainter::Antialiasing); P1.setPen(QPen(Qt::blue, 2)); P1.setBrush(Qt::red); P2.setPen(QPen(Qt::blue, 2)); P2.setBrush(Qt::red); RoundedPolygon poly; poly << QPoint(147, 187) << QPoint(95, 187) << QPoint(100, 175) << QPoint(145, 165) << QPoint(140, 95) << QPoint(5, 85) << QPoint(5, 70) << QPoint(140, 70) << QPoint(135, 45) << QPoint(138, 25) << QPoint(145, 5) << QPoint(155, 5) << QPoint(162, 25) << QPoint(165, 45) << QPoint(160, 70) << QPoint(295, 70) << QPoint(295, 85) << QPoint(160, 95) << QPoint(155, 165) << QPoint(200, 175) << QPoint(205, 187) << QPoint(153, 187) << QPoint(150, 199); P1.drawPolygon(poly); P2.drawPath(poly.GetPath()); pix1.save('1.png'); pix2.save('2.png');
Este pedaço de código-fonte é bastante simples. Após inicializar dois QPixmaps
e seu QPainters
, criamos um RoundedPolygon
objeto e preencha-o com pontos. Pintor P1
desenha o polígono regular, enquanto P2
desenha o QPainterPath
com cantos arredondados, gerados a partir do polígono. Ambos os pixmaps resultantes são salvos em seus arquivos e os resultados são os seguintes:
Vimos que gerar uma forma com cantos arredondados a partir de um polígono não é tão difícil afinal, especialmente se usarmos uma boa estrutura de programação como o Qt. Esse processo pode ser automatizado pela classe que descrevi neste blog como uma prova de conceito. No entanto, ainda há muito espaço para melhorias, como:
RoundedPolygon
para gerar bitmaps, que podem ser utilizados como máscara de widget de fundo para produzir widgets de formas malucas.RoundedPolygon
a classe não é otimizada para velocidade de execução; Deixei como está para facilitar a compreensão do conceito. A otimização pode incluir o cálculo de muitos valores intermediários ao anexar um novo vértice ao polígono. Além disso, quando GetPath
está prestes a retornar uma referência ao QPainterPath
gerado, ele pode definir um sinalizador, indicando que o objeto está atualizado. A próxima chamada para GetPath
resultaria em apenas retornar o mesmo QPainterPath
objeto, sem recalcular nada. O desenvolvedor teria, no entanto, que certificar-se de que este sinalizador seja limpo em cada mudança em qualquer um dos vértices do polígono, bem como em cada novo vértice, o que me faz pensar que a classe otimizada seria melhor desenvolvida do zero e não derivada de QPolygon
. A boa notícia é que isso não é tão difícil quanto parece.Ao todo, o RoundedPolygon
classe, como está, pode ser usada como uma ferramenta sempre que quisermos adicionar um toque de designer para nossa GUI em tempo real, sem preparar pixmaps ou formas com antecedência.