베지어 곡선과 QPainter를 사용하여 C++에서 둥근 모서리 모양을 얻는 방법: 단계별 가이드

게시 됨: 2022-03-11

소개

그래픽 디자인의 현재 추세는 모든 종류의 모양에 둥근 모서리를 많이 사용하는 것입니다. 우리는 많은 웹 페이지, 모바일 장치 및 데스크톱 응용 프로그램에서 이 사실을 관찰할 수 있습니다. 가장 주목할만한 예는 클릭 시 일부 작업을 트리거하는 데 사용되는 애플리케이션 푸시 버튼입니다. 모서리에 90도 각도의 엄격한 직사각형 모양 대신 둥근 모서리로 그려지는 경우가 많습니다. 둥근 모서리는 사용자 인터페이스를 더 부드럽고 멋지게 만듭니다. 나는 이것에 대해 완전히 확신하지 못하지만, 내 디자이너 친구는 나에게 그렇게 말한다.

둥근 모서리는 사용자 인터페이스를 더 부드럽고 멋지게 만듭니다.

둥근 모서리는 사용자 인터페이스를 더 부드럽고 멋지게 만듭니다.
트위터

사용자 인터페이스의 시각적 요소는 디자이너가 만들고 프로그래머는 올바른 위치에 배치하기만 하면 됩니다. 그러나 둥근 모서리가 있는 모양을 즉석에서 생성해야 하고 미리 로드할 수 없는 경우에는 어떻게 될까요? 일부 프로그래밍 라이브러리는 모서리가 둥근 미리 정의된 모양을 만드는 데 제한된 기능을 제공하지만 일반적으로 더 복잡한 경우에는 사용할 수 없습니다. 예를 들어, Qt 프레임워크에는 위젯, 픽스맵 및 이미지를 포함하여 QPaintDevice 에서 파생된 모든 클래스를 그리는 데 사용되는 QPainter 클래스가 있습니다. 이름에서 알 수 있듯이 둥근 모서리가 있는 직사각형을 그리는 drawRoundedRect 라는 메서드가 있습니다. 그러나 좀 더 복잡한 모양이 필요하면 직접 구현해야 합니다. 직선 세그먼트 그룹으로 둘러싸인 평면 모양인 다각형으로 어떻게 그렇게 할 수 있습니까? 종이에 연필로 다각형을 그린 경우 첫 번째 아이디어는 지우개를 사용하여 각 모서리에 있는 선의 작은 부분을 삭제한 다음 나머지 세그먼트 끝을 원호로 연결하는 것입니다. 전체 프로세스는 아래 그림에서 설명할 수 있습니다.

둥근 모서리를 수동으로 만드는 방법

QPainter 클래스에는 원형 호를 그릴 수 있는 drawArc 라는 오버로드된 메서드가 있습니다. 모두 호 중심과 크기, 시작 각도 및 호 길이를 정의하는 매개변수가 필요합니다. 회전하지 않은 직사각형에 대해 이러한 매개변수의 필요한 값을 결정하는 것은 쉽지만 더 복잡한 다각형을 다룰 때는 완전히 다른 문제입니다. 또한 모든 다각형 정점에 대해 이 계산을 반복해야 합니다. 이 계산은 길고 지루한 작업이며 인간은 그 과정에서 모든 종류의 계산 오류가 발생하기 쉽습니다. 그러나 컴퓨터가 인간을 위해 작동하도록 하는 것은 소프트웨어 개발자의 몫이지 그 반대는 아닙니다. 그래서 여기에서는 복잡한 다각형을 모서리가 둥근 모양으로 바꿀 수 있는 간단한 클래스를 개발하는 방법을 보여 드리겠습니다. 이 클래스의 사용자는 폴리곤 정점만 추가하면 되며 나머지는 클래스에서 수행합니다. 이 작업에 사용하는 필수 수학 도구는 베지어 곡선입니다.

베지어 곡선

베지어 곡선 이론을 설명하는 수학 서적과 인터넷 리소스가 많이 있으므로 관련 속성에 대해 간략하게 설명하겠습니다.

정의에 따르면 베지어 곡선은 2차원 표면의 두 점 사이의 곡선이며, 그 궤적은 하나 이상의 제어점에 의해 제어됩니다. 엄밀히 말하면 추가 제어점이 없는 두 점 사이의 곡선도 베지어 곡선입니다. 그러나 결과적으로 두 점 사이에 직선이 생성되므로 특별히 흥미롭지도 유용하지도 않습니다.

2차 베지어 곡선

2차 베지어 곡선에는 하나의 제어점이 있습니다. 이론에 따르면 제어점 P 1 이 있는 점 P 0P 2 사이의 2차 베지어 곡선은 다음과 같이 정의됩니다.

B(t) = (1 - t) 2 P 0 + 2t(1 - t)P 1 + t 2 P 2 , 여기서 0 ≤ t ≤ 1 (1)

따라서 t0 과 같을 때 B(t)P0 를 산출하고, t1 과 같으면 B(t)P2 를 산출하지만 다른 모든 경우에 B(t) 의 값도 다음에 따라 달라집니다. P 1 . 표현식 2t(1 - t)t = 0.5 에서 최대값을 갖기 때문에 B(t) 에 대한 P 1 의 영향이 가장 큽니다. 우리는 P 1 을 중력의 가상 소스로 생각할 수 있습니다. 이 소스는 함수 궤적을 자신을 향해 끌어당깁니다. 아래 그림은 시작점, 끝점 및 제어점이 있는 2차 베지어 곡선의 몇 가지 예를 보여줍니다.

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 1P 2 는 매개변수로 quadTo 에 전달되어야 합니다.

QPainterdrawPath 메서드는 활성 펜과 브러시를 사용하여 매개변수로 제공되어야 하는 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 번째 선분 길이 사이의 비율을 유지합니다. fRat0.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 의 위치를 ​​계산합니다. 다시 말해, 0n-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");

이 소스 코드는 매우 간단합니다. 두 개의 QPixmapsQPainters 를 초기화한 후 RoundedPolygon 개체를 만들고 점으로 채웁니다. Painter P1 은 일반 다각형을 그리는 반면 P2 는 다각형에서 생성된 둥근 모서리가 있는 QPainterPath 를 그립니다. 결과 픽스맵은 모두 파일에 저장되며 결과는 다음과 같습니다.

QPainter를 사용한 둥근 모서리

결론

우리는 특히 Qt와 같은 우수한 프로그래밍 프레임워크를 사용하는 경우 다각형에서 모서리가 둥근 모양을 생성하는 것이 그렇게 어렵지 않다는 것을 보았습니다. 이 프로세스는 이 블로그에서 개념 증명으로 설명한 클래스에서 자동화할 수 있습니다. 그러나 다음과 같이 여전히 개선의 여지가 많습니다.

  • 모든 정점이 아닌 선택한 정점에서만 둥근 모서리를 만듭니다.
  • 다른 꼭짓점에서 다른 반지름으로 둥근 모서리를 만듭니다.
  • 모서리가 둥근 폴리라인을 생성하는 방법을 구현하십시오(Qt 용어의 폴리라인은 마지막 정점과 첫 번째 정점 사이의 선분을 누락했기 때문에 닫힌 모양이 아니라는 점을 제외하고는 다각형과 같습니다).
  • RoundedPolygon 을 사용하여 비트맵을 생성합니다. 비트맵을 배경 위젯 마스크로 활용하여 미친 모양의 위젯을 생성할 수 있습니다.
  • RoundedPolygon 클래스는 실행 속도에 최적화되어 있지 않습니다. 개념의 이해를 돕기 위해 그대로 두었습니다. 최적화에는 다각형에 새 정점을 추가할 때 많은 중간 값을 계산하는 것이 포함될 수 있습니다. 또한 GetPath 가 생성된 QPainterPath 에 대한 참조를 반환하려고 할 때 객체가 최신 상태임을 나타내는 플래그를 설정할 수 있습니다. GetPath 에 대한 다음 호출은 아무 것도 다시 계산하지 않고 동일한 QPainterPath 객체만 반환하게 됩니다. 그러나 개발자는 이 플래그가 모든 다각형 꼭짓점과 모든 새 꼭짓점에서 변경될 때마다 지워졌는지 확인해야 하므로 최적화된 클래스는 파생되지 않고 처음부터 개발하는 것이 더 낫다고 생각합니다. QPolygon 에서 . 좋은 소식은 이것이 들리는 것처럼 어렵지 않다는 것입니다.

전체적으로 RoundedPolygon 클래스는 사전에 픽스맵이나 모양을 준비하지 않고도 GUI에 디자이너 터치를 추가하고 싶을 때 언제든지 도구로 사용할 수 있습니다.

관련 항목: C 및 C++ 언어 학습 방법: 궁극적인 목록