마우스 충돌 예제
그래픽 보기에서 항목에 애니메이션을 적용하는 방법을 보여줍니다.
충돌하는 마우스 예제는 그래픽 보기 프레임워크를 사용하여 애니메이션 항목을 구현하고 항목 간의 충돌을 감지하는 방법을 보여줍니다.
그래픽 보기는 QGraphicsItem 클래스에서 파생된 수많은 사용자 지정 2D 그래픽 항목을 관리하고 상호 작용하기 위한 QGraphicsScene 클래스와 확대/축소 및 회전을 지원하여 항목을 시각화하기 위한 QGraphicsView 위젯을 제공합니다.
이 예제는 아이템 클래스와 메인 함수로 구성되며, Mouse
클래스는 QGraphicsItem 을 확장하는 개별 마우스를 나타내고 main()
함수는 메인 애플리케이션 창을 제공합니다.
먼저 Mouse
클래스를 검토하여 항목에 애니메이션을 적용하고 항목 충돌을 감지하는 방법을 살펴본 다음 main()
함수를 검토하여 항목을 장면에 배치하는 방법과 해당 뷰를 구현하는 방법을 살펴보겠습니다.
마우스 클래스 정의
mouse
클래스는 QGraphicsItem 에서 상속합니다. QGraphicsItem 클래스는 그래픽 보기 프레임워크의 모든 그래픽 항목에 대한 기본 클래스이며, 사용자 지정 항목을 작성하기 위한 가벼운 기반을 제공합니다.
class Mouse : public QGraphicsItem { public: Mouse(); QRectF boundingRect() const override; QPainterPath shape() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; protected: void advance(int step) override; private: qreal angle = 0; qreal speed = 0; qreal mouseEyeDirection = 0; QColor color; };
사용자 지정 그래픽 항목을 작성할 때는 QGraphicsItem 의 두 가지 순수 가상 공용 함수인 boundingRect()를 구현해야 하며, 이는 항목이 칠한 영역의 추정치를 반환하고 paint()는 실제 페인팅을 구현합니다. 또한 shape() 및 advance()을 다시 구현합니다. shape ()를 다시 구현하여 마우스 항목의 정확한 모양을 반환합니다. 기본 구현은 단순히 항목의 경계 직사각형을 반환합니다. 애니메이션을 처리하기 위해 advance()를 다시 구현하여 한 번의 업데이트에서 모든 작업이 이루어지도록 합니다.
마우스 클래스 정의
마우스 아이템을 구성할 때 먼저 클래스에서 직접 초기화되지 않은 모든 아이템의 개인 변수가 제대로 초기화되었는지 확인합니다:
Mouse::Mouse() : color(QRandomGenerator::global()->bounded(256), QRandomGenerator::global()->bounded(256), QRandomGenerator::global()->bounded(256)) { setRotation(QRandomGenerator::global()->bounded(360 * 16)); }
마우스 색상의 다양한 구성 요소를 계산하기 위해 QRandomGenerator 을 사용합니다.
그런 다음 QGraphicsItem 에서 상속된 setRotation() 함수를 호출합니다. 항목은 자체 로컬 좌표계에 있습니다. 좌표는 일반적으로 (0, 0)을 중심으로 하며 모든 변환의 중심이 되기도 합니다. 항목의 setRotation() 함수를 호출하면 마우스가 움직이기 시작할 방향을 변경합니다.
QGraphicsScene 에서 장면을 한 프레임씩 진행하기로 결정하면 각 항목에서 QGraphicsItem::advance()을 호출합니다. 이렇게 하면 advance() 함수를 다시 구현하여 마우스에 애니메이션을 적용할 수 있습니다.
void Mouse::advance(int step) { if (!step) return; QLineF lineToCenter(QPointF(0, 0), mapFromScene(0, 0)); if (lineToCenter.length() > 150) { qreal angleToCenter = std::atan2(lineToCenter.dy(), lineToCenter.dx()); angleToCenter = normalizeAngle((Pi - angleToCenter) + Pi / 2); if (angleToCenter < Pi && angleToCenter > Pi / 4) { // Rotate left angle += (angle < -Pi / 2) ? 0.25 : -0.25; } else if (angleToCenter >= Pi && angleToCenter < (Pi + Pi / 2 + Pi / 4)) { // Rotate right angle += (angle < Pi / 2) ? 0.25 : -0.25; } } else if (::sin(angle) < 0) { angle += 0.25; } else if (::sin(angle) > 0) { angle -= 0.25; }
먼저, 단계가 0
인 경우 전진을 수행하지 않습니다. 왜냐하면 advance()가 두 번 호출되기 때문입니다. 한 번은 step == 0
으로 항목이 곧 전진할 것임을 나타내고, 그 다음에는 step == 1
으로 실제 전진하기 때문입니다. 또한 마우스가 반경 150픽셀의 원 안에 머물도록 합니다.
QGraphicsItem 에서 제공하는 mapFromScene() 함수를 참고하세요. 이 함수는 장면 좌표로 지정된 위치를 항목의 좌표계에 매핑합니다.
const QList<QGraphicsItem *> dangerMice = scene()->items(QPolygonF() << mapToScene(0, 0) << mapToScene(-30, -50) << mapToScene(30, -50)); for (const QGraphicsItem *item : dangerMice) { if (item == this) continue; QLineF lineToMouse(QPointF(0, 0), mapFromItem(item, 0, 0)); qreal angleToMouse = std::atan2(lineToMouse.dy(), lineToMouse.dx()); angleToMouse = normalizeAngle((Pi - angleToMouse) + Pi / 2); if (angleToMouse >= 0 && angleToMouse < Pi / 2) { // Rotate right angle += 0.5; } else if (angleToMouse <= TwoPi && angleToMouse > (TwoPi - Pi / 2)) { // Rotate left angle -= 0.5; } } if (dangerMice.size() > 1 && QRandomGenerator::global()->bounded(10) == 0) { if (QRandomGenerator::global()->bounded(1)) angle += QRandomGenerator::global()->bounded(1 / 500.0); else angle -= QRandomGenerator::global()->bounded(1 / 500.0); }
그런 다음 다른 마우스와 충돌하지 않도록 합니다.
speed += (-50 + QRandomGenerator::global()->bounded(100)) / 100.0; qreal dx = ::sin(angle) * 10; mouseEyeDirection = (qAbs(dx / 5) < 1) ? 0 : dx / 5; setRotation(rotation() + dx); setPos(mapToParent(0, -(3 + sin(speed) * 3))); }
마지막으로 마우스의 속도와 눈 방향(마우스를 칠할 때 사용)을 계산하여 새 위치를 설정합니다.
항목의 위치는 부모 좌표에서 원점(로컬 좌표(0, 0))을 나타냅니다. QGraphicsItem::setPos () 함수는 항목의 위치를 부모 좌표계에서 지정된 위치로 설정합니다. 부모가 없는 항목의 경우 지정된 위치는 장면 좌표로 해석됩니다. QGraphicsItem 또한 mapToParent() 함수는 항목 좌표로 지정된 위치를 부모 좌표계에 매핑하는 함수를 제공합니다. 항목에 부모가 없는 경우 위치는 대신 장면의 좌표계에 매핑됩니다.
이제 QGraphicsItem 에서 상속된 순수 가상 함수에 대한 구현을 제공할 차례입니다. 먼저 boundingRect() 함수를 살펴보겠습니다:
QRectF Mouse::boundingRect() const { qreal adjust = 0.5; return QRectF(-18 - adjust, -22 - adjust, 36 + adjust, 60 + adjust); }
boundingRect() 함수는 항목의 바깥쪽 경계를 직사각형으로 정의합니다. 그래픽 보기 프레임워크는 경계 사각형을 사용하여 항목에 다시 그리기가 필요한지 여부를 결정하므로 모든 그리기는 이 사각형 내부에서 수행해야 합니다.
void Mouse::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) { // Body painter->setBrush(color); painter->drawEllipse(-10, -20, 20, 40); // Eyes painter->setBrush(Qt::white); painter->drawEllipse(-10, -17, 8, 8); painter->drawEllipse(2, -17, 8, 8); // Nose painter->setBrush(Qt::black); painter->drawEllipse(QRectF(-2, -22, 4, 4)); // Pupils painter->drawEllipse(QRectF(-8.0 + mouseEyeDirection, -17, 4, 4)); painter->drawEllipse(QRectF(4.0 + mouseEyeDirection, -17, 4, 4)); // Ears painter->setBrush(scene()->collidingItems(this).isEmpty() ? Qt::darkYellow : Qt::red); painter->drawEllipse(-17, -12, 16, 16); painter->drawEllipse(1, -12, 16, 16); // Tail QPainterPath path(QPointF(0, 20)); path.cubicTo(-5, 22, -5, 22, 0, 25); path.cubicTo(5, 27, 5, 32, 0, 30); path.cubicTo(-5, 32, -5, 42, 0, 35); painter->setBrush(Qt::NoBrush); painter->drawPath(path); }
그래픽 보기 프레임워크는 paint() 함수를 호출하여 항목의 내용을 칠합니다. 이 함수는 항목을 로컬 좌표로 칠합니다.
마우스 항목이 다른 마우스 항목과 충돌할 때마다 귀가 빨간색으로 채워지고 그렇지 않으면 진한 노란색으로 채워지는 귀 페인팅에 주목하세요. QGraphicsScene::collidingItems () 함수를 사용하여 충돌하는 마우스가 있는지 확인합니다. 실제 충돌 감지는 도형-도형 교차점을 사용하는 그래픽 보기 프레임워크에서 처리합니다. 우리가 해야 할 일은 QGraphicsItem::shape() 함수가 항목에 대한 정확한 모양을 반환하는지 확인하는 것입니다:
QPainterPath Mouse::shape() const { QPainterPath path; path.addRect(-10, -20, 20, 40); return path; }
도형이 복잡하면 임의의 도형-도형 교집합의 복잡성이 엄청나게 커지기 때문에 이 작업은 눈에 띄게 시간이 많이 걸릴 수 있습니다. 다른 방법은 collidesWithItem() 함수를 다시 구현하여 사용자 지정 항목 및 도형 충돌 알고리즘을 제공하는 것입니다.
이것으로 Mouse
클래스 구현이 완료되었으며 이제 사용할 준비가 되었습니다. 이제 main()
함수를 살펴보고 마우스용 장면과 장면의 내용을 표시하는 뷰를 구현하는 방법을 살펴보겠습니다.
Main() 함수
main()
함수는 기본 애플리케이션 창을 제공할 뿐만 아니라 항목, 해당 장면 및 해당 뷰를 생성합니다.
int main(int argc, char **argv) { QApplication app(argc, argv);
먼저 애플리케이션 객체를 생성하고 장면을 만듭니다:
QGraphicsScene scene; scene.setSceneRect(-300, -300, 600, 600);
QGraphicsScene 클래스는 QGraphicsItems의 컨테이너 역할을 합니다. 또한 항목의 위치를 효율적으로 결정하고 장면의 임의 영역 내에 표시할 항목을 결정할 수 있는 기능을 제공합니다.
장면을 만들 때 장면의 범위를 정의하는 사각형인 장면의 직사각형을 설정하는 것이 좋습니다. 주로 QGraphicsView 에서 뷰의 기본 스크롤 가능 영역을 결정하고 QGraphicsScene 에서 항목 인덱싱을 관리하는 데 사용됩니다. 명시적으로 설정하지 않으면 장면의 기본 사각형은 장면이 생성된 이후 장면에 있는 모든 항목의 가장 큰 경계 사각형이 됩니다. 즉, 씬에서 항목이 추가되거나 이동하면 사각형이 커지지만 절대 줄어들지 않습니다.
scene.setItemIndexMethod(QGraphicsScene::NoIndex);
아이템 인덱스 기능은 아이템 검색 속도를 높이기 위해 사용됩니다. NoIndex 는 씬의 모든 아이템이 검색되므로 아이템 위치가 선형적으로 복잡하다는 것을 의미합니다. 그러나 아이템 추가, 이동 및 제거는 일정한 시간에 이루어집니다. 이 접근 방식은 많은 항목이 지속적으로 추가, 이동 또는 제거되는 동적 장면에 이상적입니다. 대안으로 이진 검색을 사용하여 대수적 복잡성에 가까운 항목 위치 알고리즘을 구현하는 BspTreeIndex 을 사용할 수 있습니다.
for (int i = 0; i < MouseCount; ++i) { Mouse *mouse = new Mouse; mouse->setPos(::sin((i * 6.28) / MouseCount) * 200, ::cos((i * 6.28) / MouseCount) * 200); scene.addItem(mouse); }
그런 다음 장면에 마우스를 추가합니다.
QGraphicsView view(&scene); view.setRenderHint(QPainter::Antialiasing); view.setBackgroundBrush(QPixmap(":/images/cheese.jpg"));
장면을 볼 수 있으려면 QGraphicsView 위젯도 만들어야 합니다. QGraphicsView 클래스는 스크롤 가능한 뷰포트에서 장면의 내용을 시각화합니다. 또한 앤티앨리어싱을 사용하여 콘텐츠가 렌더링되도록 하고 뷰의 배경 브러시를 설정하여 치즈 배경을 만듭니다.
배경에 사용된 이미지는 Qt의 리소스 시스템을 사용하여 애플리케이션의 실행 파일에 바이너리 파일로 저장됩니다. QPixmap 생성자는 디스크의 실제 파일을 참조하는 파일 이름과 애플리케이션에 내장된 리소스를 참조하는 파일 이름을 모두 허용합니다.
view.setCacheMode(QGraphicsView::CacheBackground); view.setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate); view.setDragMode(QGraphicsView::ScrollHandDrag);
그런 다음 캐시 모드를 설정합니다. QGraphicsView 는 미리 렌더링된 콘텐츠를 픽셀맵에 캐시한 다음 뷰포트에 그릴 수 있습니다. 이러한 캐싱의 목적은 텍스처, 그라데이션 및 알파 블렌딩 배경과 같이 렌더링 속도가 느린 영역의 전체 렌더링 시간을 단축하는 것입니다. CacheMode 속성은 뷰의 어느 부분이 캐시되는지를 저장하며 CacheBackground 플래그는 뷰의 배경 캐싱을 사용하도록 설정합니다.
dragMode 속성을 설정하면 사용자가 장면 배경을 클릭하고 마우스를 드래그할 때 어떤 일이 일어날지 정의할 수 있습니다. ScrollHandDrag 플래그를 사용하면 커서가 가리키는 손으로 바뀌고 마우스를 드래그하면 스크롤 막대가 스크롤됩니다.
view.setWindowTitle(QT_TRANSLATE_NOOP(QGraphicsView, "Colliding Mice")); view.resize(400, 300); view.show(); QTimer timer; QObject::connect(&timer, &QTimer::timeout, &scene, &QGraphicsScene::advance); timer.start(1000 / 33); return app.exec(); }
마지막으로 QApplication::exec() 함수를 사용하여 메인 이벤트 루프에 들어가기 전에 애플리케이션 창의 제목과 크기를 설정합니다.
마지막으로 QTimer 를 생성하고 시간 초과() 신호를 장면의 advance() 슬롯에 연결합니다. 타이머가 실행될 때마다 장면이 한 프레임씩 진행됩니다.
그런 다음 타이머에 1000/33밀리초마다 실행되도록 지시합니다. 이렇게 하면 초당 30프레임의 프레임 속도를 얻을 수 있으며, 이는 대부분의 애니메이션에 충분히 빠른 속도입니다. 단일 타이머 연결로 애니메이션을 실행하여 장면을 진행하면 모든 마우스가 한 지점에서 움직이고, 더 중요한 것은 모든 마우스가 움직인 후 화면에 한 번만 업데이트가 전송된다는 점입니다.
© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.