베지어 곡선과 QPainter를 사용하여 C++에서 둥근 모서리 모양을 얻는 방법: 단계별 가이드
게시 됨: 2022-03-11소개
그래픽 디자인의 현재 추세는 모든 종류의 모양에 둥근 모서리를 많이 사용하는 것입니다. 우리는 많은 웹 페이지, 모바일 장치 및 데스크톱 응용 프로그램에서 이 사실을 관찰할 수 있습니다. 가장 주목할만한 예는 클릭 시 일부 작업을 트리거하는 데 사용되는 애플리케이션 푸시 버튼입니다. 모서리에 90도 각도의 엄격한 직사각형 모양 대신 둥근 모서리로 그려지는 경우가 많습니다. 둥근 모서리는 사용자 인터페이스를 더 부드럽고 멋지게 만듭니다. 나는 이것에 대해 완전히 확신하지 못하지만, 내 디자이너 친구는 나에게 그렇게 말한다.
사용자 인터페이스의 시각적 요소는 디자이너가 만들고 프로그래머는 올바른 위치에 배치하기만 하면 됩니다. 그러나 둥근 모서리가 있는 모양을 즉석에서 생성해야 하고 미리 로드할 수 없는 경우에는 어떻게 될까요? 일부 프로그래밍 라이브러리는 모서리가 둥근 미리 정의된 모양을 만드는 데 제한된 기능을 제공하지만 일반적으로 더 복잡한 경우에는 사용할 수 없습니다. 예를 들어, Qt 프레임워크에는 위젯, 픽스맵 및 이미지를 포함하여 QPaintDevice 에서 파생된 모든 클래스를 그리는 데 사용되는 QPainter 클래스가 있습니다. 이름에서 알 수 있듯이 둥근 모서리가 있는 직사각형을 그리는 drawRoundedRect 라는 메서드가 있습니다. 그러나 좀 더 복잡한 모양이 필요하면 직접 구현해야 합니다. 직선 세그먼트 그룹으로 둘러싸인 평면 모양인 다각형으로 어떻게 그렇게 할 수 있습니까? 종이에 연필로 다각형을 그린 경우 첫 번째 아이디어는 지우개를 사용하여 각 모서리에 있는 선의 작은 부분을 삭제한 다음 나머지 세그먼트 끝을 원호로 연결하는 것입니다. 전체 프로세스는 아래 그림에서 설명할 수 있습니다.
QPainter 클래스에는 원형 호를 그릴 수 있는 drawArc 라는 오버로드된 메서드가 있습니다. 모두 호 중심과 크기, 시작 각도 및 호 길이를 정의하는 매개변수가 필요합니다. 회전하지 않은 직사각형에 대해 이러한 매개변수의 필요한 값을 결정하는 것은 쉽지만 더 복잡한 다각형을 다룰 때는 완전히 다른 문제입니다. 또한 모든 다각형 정점에 대해 이 계산을 반복해야 합니다. 이 계산은 길고 지루한 작업이며 인간은 그 과정에서 모든 종류의 계산 오류가 발생하기 쉽습니다. 그러나 컴퓨터가 인간을 위해 작동하도록 하는 것은 소프트웨어 개발자의 몫이지 그 반대는 아닙니다. 그래서 여기에서는 복잡한 다각형을 모서리가 둥근 모양으로 바꿀 수 있는 간단한 클래스를 개발하는 방법을 보여 드리겠습니다. 이 클래스의 사용자는 폴리곤 정점만 추가하면 되며 나머지는 클래스에서 수행합니다. 이 작업에 사용하는 필수 수학 도구는 베지어 곡선입니다.
베지어 곡선
베지어 곡선 이론을 설명하는 수학 서적과 인터넷 리소스가 많이 있으므로 관련 속성에 대해 간략하게 설명하겠습니다.
정의에 따르면 베지어 곡선은 2차원 표면의 두 점 사이의 곡선이며, 그 궤적은 하나 이상의 제어점에 의해 제어됩니다. 엄밀히 말하면 추가 제어점이 없는 두 점 사이의 곡선도 베지어 곡선입니다. 그러나 결과적으로 두 점 사이에 직선이 생성되므로 특별히 흥미롭지도 유용하지도 않습니다.
2차 베지어 곡선
2차 베지어 곡선에는 하나의 제어점이 있습니다. 이론에 따르면 제어점 P 1 이 있는 점 P 0 과 P 2 사이의 2차 베지어 곡선은 다음과 같이 정의됩니다.
B(t) = (1 - t) 2 P 0 + 2t(1 - t)P 1 + t 2 P 2 , 여기서 0 ≤ t ≤ 1 (1)
따라서 t 가 0 과 같을 때 B(t) 는 P0 를 산출하고, t 가 1 과 같으면 B(t) 는 P2 를 산출하지만 다른 모든 경우에 B(t) 의 값도 다음에 따라 달라집니다. P 1 . 표현식 2t(1 - t) 는 t = 0.5 에서 최대값을 갖기 때문에 B(t) 에 대한 P 1 의 영향이 가장 큽니다. 우리는 P 1 을 중력의 가상 소스로 생각할 수 있습니다. 이 소스는 함수 궤적을 자신을 향해 끌어당깁니다. 아래 그림은 시작점, 끝점 및 제어점이 있는 2차 베지어 곡선의 몇 가지 예를 보여줍니다.
그렇다면 베지어 곡선을 사용하여 문제를 해결하는 방법은 무엇입니까? 아래 그림은 설명을 제공합니다.
다각형 꼭짓점과 그 주변에서 연결된 선분의 짧은 부분을 삭제하는 것을 상상하면 한 선분 끝은 P 0 , 다른 선분 끝은 P 2 , 삭제된 꼭짓점은 P 1 로 생각할 수 있습니다. 우리는 이 점 세트에 2차 베지어 곡선을 적용하고 짜잔, 원하는 둥근 모서리가 있습니다.
QPainter를 사용한 C++/Qt 구현
QPainter 클래스에는 2차 베지어 곡선을 그리는 방법이 없습니다. 방정식 (1)에 따라 처음부터 그것을 구현하는 것은 매우 쉽지만 Qt 라이브러리는 더 나은 솔루션을 제공합니다. 2D 드로잉을 위한 또 다른 강력한 클래스가 있습니다: QPainterPath . QPainterPath 클래스는 QPainter 객체와 함께 나중에 추가하고 사용할 수 있는 선과 곡선의 모음입니다. 현재 컬렉션에 베지어 곡선을 추가하는 몇 가지 오버로드된 메서드가 있습니다. 특히, quadTo 메서드는 2차 베지어 곡선을 추가합니다. 곡선은 현재 QPainterPath 지점( P 0 )에서 시작하는 반면 P 1 및 P 2 는 매개변수로 quadTo 에 전달되어야 합니다.
QPainter 의 drawPath 메서드는 활성 펜과 브러시를 사용하여 매개변수로 제공되어야 하는 QPainterPath 개체에서 선과 곡선 모음을 그리는 데 사용됩니다.
그럼 클래스 선언을 보자:
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; }; 나는 정점과 다른 것들을 추가하는 것을 스스로 구현할 필요가 없도록 QPolygon 을 서브클래싱하기로 결정했습니다. 반경을 적절한 초기 값으로 설정하는 생성자 외에 이 클래스에는 두 가지 다른 공용 메서드가 있습니다.
-
SetRadius메소드는 반경을 주어진 값으로 설정합니다. 반경은 각 꼭짓점 근처의 직선 길이(픽셀 단위)로, 둥근 모서리에 대해 삭제(또는 더 정확하게는 그려지지 않음)됩니다. -
GetPath는 모든 계산이 이루어지는 곳입니다.RoundedPolygon에 추가된 폴리곤 포인트에서 생성된QPainterPath객체를 반환합니다.
private 부분의 메서드는 GetPath 에서 사용하는 보조 메서드일 뿐입니다.
구현을 살펴보고 개인 메서드부터 시작하겠습니다.
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); }여기에서 설명할 것이 많지는 않지만 이 방법은 주어진 두 점 사이의 유클리드 거리를 반환합니다.
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; } GetLineStart 메서드는 시계 방향으로 다각형에 점이 추가되면 마지막 그림에서 점 P 2 의 위치를 계산합니다. 보다 정확하게는 i 번째 정점에서 (i+1) 번째 정점 방향으로 m_uiRadius 픽셀 떨어진 지점을 반환합니다. ( (i+1)%count() (i+1) 번째 꼭짓점에 접근할 때 우리는 폴리곤에서 마지막 꼭짓점과 첫 번째 꼭짓점 사이에 선분도 있다는 것을 기억해야 합니다. (i+1)%count() . 이것은 또한 메서드가 범위를 벗어나는 것을 방지하고 대신 첫 번째 지점에 액세스합니다. 변수 fRat 는 반지름과 i 번째 선분 길이 사이의 비율을 유지합니다. fRat 가 0.5 보다 큰 값을 갖지 않도록 하는 검사도 있습니다. fRat 의 값이 0.5 보다 크면 두 개의 연속적인 둥근 모서리가 겹쳐져 시각적 결과가 좋지 않습니다.

점 P 1 에서 P 2 까지 직선으로 이동할 때 거리의 30%를 완성하면 공식 0.7 • P 1 + 0.3 • P 2 를 사용하여 위치를 결정할 수 있습니다. 일반적으로 전체 거리의 일부를 달성하고 α = 1 이 전체 거리를 나타내는 경우 현재 위치는 (1 - α) • P1 + α • P2 입니다.
이것이 GetLineStart 메소드가 i 번째 정점에서 (i+1) 번째 방향으로 m_uiRadius 픽셀 떨어진 지점의 위치를 결정하는 방법입니다.
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; } 이 방법은 GetLineStart 와 매우 유사합니다. i 번째가 아닌 (i+1) 번째 꼭짓점에 대한 점 P 0 의 위치를 계산합니다. 다시 말해, 0 과 n-1 사이의 모든 i 에 대해 GetLineEnd(i) GetLineStart(i) 까지 선을 그리면 n 은 다각형의 꼭짓점 수이며, 꼭짓점이 지워진 다각형을 얻을 수 있습니다. 주변 주변.
이제 기본 클래스 메서드는 다음과 같습니다.
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; } 이 방법에서는 QPainterPath 개체를 빌드합니다. 폴리곤에 3개 이상의 꼭짓점이 없으면 더 이상 2D 모양을 처리하지 않으며 이 경우 메서드는 경고를 표시하고 빈 경로를 반환합니다. 충분한 점이 사용 가능하면 다각형의 모든 직선 세그먼트(선 세그먼트의 수는 물론 꼭짓점의 수와 같음)를 반복하여 반올림된 사이의 각 직선 세그먼트의 시작과 끝을 계산합니다. 모서리. 이 두 점 사이에 직선을 그리고 현재 정점의 위치를 제어점으로 사용하여 이전 선분의 끝과 현재의 시작 사이에 2차 베지어 곡선을 둡니다. 루프 후에는 루프에서 베지어 곡선보다 하나의 직선을 더 그렸기 때문에 마지막 선분과 첫 번째 선분 사이에 베지어 곡선으로 경로를 닫아야 합니다.
RoundedPolygon 클래스 사용 및 결과
이제 이 클래스를 실제로 사용하는 방법을 볼 시간입니다.
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"); 이 소스 코드는 매우 간단합니다. 두 개의 QPixmaps 와 QPainters 를 초기화한 후 RoundedPolygon 개체를 만들고 점으로 채웁니다. Painter P1 은 일반 다각형을 그리는 반면 P2 는 다각형에서 생성된 둥근 모서리가 있는 QPainterPath 를 그립니다. 결과 픽스맵은 모두 파일에 저장되며 결과는 다음과 같습니다.
결론
우리는 특히 Qt와 같은 우수한 프로그래밍 프레임워크를 사용하는 경우 다각형에서 모서리가 둥근 모양을 생성하는 것이 그렇게 어렵지 않다는 것을 보았습니다. 이 프로세스는 이 블로그에서 개념 증명으로 설명한 클래스에서 자동화할 수 있습니다. 그러나 다음과 같이 여전히 개선의 여지가 많습니다.
- 모든 정점이 아닌 선택한 정점에서만 둥근 모서리를 만듭니다.
- 다른 꼭짓점에서 다른 반지름으로 둥근 모서리를 만듭니다.
- 모서리가 둥근 폴리라인을 생성하는 방법을 구현하십시오(Qt 용어의 폴리라인은 마지막 정점과 첫 번째 정점 사이의 선분을 누락했기 때문에 닫힌 모양이 아니라는 점을 제외하고는 다각형과 같습니다).
-
RoundedPolygon을 사용하여 비트맵을 생성합니다. 비트맵을 배경 위젯 마스크로 활용하여 미친 모양의 위젯을 생성할 수 있습니다. -
RoundedPolygon클래스는 실행 속도에 최적화되어 있지 않습니다. 개념의 이해를 돕기 위해 그대로 두었습니다. 최적화에는 다각형에 새 정점을 추가할 때 많은 중간 값을 계산하는 것이 포함될 수 있습니다. 또한GetPath가 생성된QPainterPath에 대한 참조를 반환하려고 할 때 객체가 최신 상태임을 나타내는 플래그를 설정할 수 있습니다.GetPath에 대한 다음 호출은 아무 것도 다시 계산하지 않고 동일한QPainterPath객체만 반환하게 됩니다. 그러나 개발자는 이 플래그가 모든 다각형 꼭짓점과 모든 새 꼭짓점에서 변경될 때마다 지워졌는지 확인해야 하므로 최적화된 클래스는 파생되지 않고 처음부터 개발하는 것이 더 낫다고 생각합니다.QPolygon에서 . 좋은 소식은 이것이 들리는 것처럼 어렵지 않다는 것입니다.
전체적으로 RoundedPolygon 클래스는 사전에 픽스맵이나 모양을 준비하지 않고도 GUI에 디자이너 터치를 추가하고 싶을 때 언제든지 도구로 사용할 수 있습니다.
