Qt官方示例-拖放機器人
- 2020 年 2 月 24 日
- 筆記
❝拖放機器人示例演示如何在QGraphicsItem子類中實現拖放,以及如何使用Qt的Animation Framework動畫化項目。❞

Graphics View提供了QGraphicsScene類,用於管理從QGraphicsItem類派生的大量訂製2D圖形項目,並與之交互;還提供了QGraphicsView小部件,用於可視化項目,並支援縮放和旋轉。
該示例是由一個Robot類,一個ColorItem類和一個主要功能組成:Robot該類描述了一個由多個RobotPart派生肢體組成的簡單機器人,包括RobotHead和RobotLimb,ColorItem類提供了可拖動的彩色橢圓。
我們將首先看Robot類,以了解如何組裝不同的部分,以便可以使用QPropertyAnimation分別旋轉和動畫化各個部分,然後我們將看ColorItem類,以演示如何在項目之間實現拖放。最後,我們將看main()函數,以了解如何將所有部分放在一起以形成最終應用程式。
Robot類定義
該機器人包括三個主要的類:RobotHead,RobotTorso和RobotLimb,它被用於上下臂和腿。他們都繼承於RobotPart類,該類又繼承了QGraphicsObject。所述的Robot類本身不具有視覺外觀和僅充當用於機器人根節點。
讓我們從RobotPart類聲明開始。
class RobotPart : public QGraphicsObject { public: RobotPart(QGraphicsItem *parent = nullptr); protected: void dragEnterEvent(QGraphicsSceneDragDropEvent *event) override; void dragLeaveEvent(QGraphicsSceneDragDropEvent *event) override; void dropEvent(QGraphicsSceneDragDropEvent *event) override; QColor color = Qt::lightGray; bool dragOver = false; }; 該基類繼承QGraphicsObject。QGraphicsObject通過繼承QObject提供訊號和槽,它還可使用Q_PROPERTY聲明QGraphicsItem的屬性,這使該屬性可用於QPropertyAnimation。
RobotPart還實現了三個最重要的事件處理程式以接收放置事件:dragEnterEvent(),dragLeaveEvent()和dropEvent()。
顏色與變數一起存儲為成員變數,dragOver稍後將使用該變數在視覺上指示肢體可以接受拖動到的顏色。
RobotPart::RobotPart(QGraphicsItem *parent) : QGraphicsObject(parent), color(Qt::lightGray) { setAcceptDrops(true); } RobotPart的構造函數初始化dragOver成員並將顏色設置為Qt::lightGray。在構造函數主體中,我們通過調用setAcceptDrops(true)來支援接受放置事件。
void RobotPart::dragEnterEvent(QGraphicsSceneDragDropEvent *event) { if (event->mimeData()->hasColor()) { event->setAccepted(true); dragOver = true; update(); } else { event->setAccepted(false); } } 對於dragEnterEvent()事件,當拖放元素拖入機器人部分的區域後將自動調用。
事件處理器實現確定該項目作為一個整體是否可以接受與傳入拖動對象關聯的mime數據。RobotPart提供接受顏色的所有部件的基本行為。因此,如果傳入的拖動對象包含一種顏色,則表示事件被接受,我們將其設置dragOver為true並調用update(),以幫助向用戶提供積極的視覺回饋;否則,事件將被忽略,從而使事件傳播到父元素。
void RobotPart::dragLeaveEvent(QGraphicsSceneDragDropEvent *event) { Q_UNUSED(event); dragOver = false; update(); } 對於dragLeaveEvent(),當拖放元素從機器人部分的面積拖走後將自動調用。我們的實現只需將dragOver重置為false並調用update()即可幫助提供視覺回饋,說明拖動已離開了此項。
void RobotPart::dropEvent(QGraphicsSceneDragDropEvent *event) { dragOver = false; if (event->mimeData()->hasColor()) color = qvariant_cast<QColor>(event->mimeData()->colorData()); update(); } 對於dropEvent(),當拖放元素被拖放到一個項目上時,dropEvent()將被自動調用。(當滑鼠在拖動項目時釋放滑鼠按鈕時)。 我們將其重置dragOver為false,分配機器人部件的新顏色,然後調用update()。
RobotHead,RobotTorso以及RobotLimb的定義和實現幾乎相同。我們這裡只詳細介紹RobotHead。
class RobotHead : public RobotPart { public: RobotHead(QGraphicsItem *parent = nullptr); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; protected: void dragEnterEvent(QGraphicsSceneDragDropEvent *event) override; void dropEvent(QGraphicsSceneDragDropEvent *event) override; private: QPixmap pixmap; }; RobotHead類繼承於RobotPart,並提供必要的實現boundingRect()和paint() 。它還重新實現dragEnterEvent()和dropEvent()以提供影像放置的特殊處理。 該類還包含一個私有的pixmap成員,我們可以用來實現對接受影像放置的支援。
RobotHead::RobotHead(QGraphicsItem *parent) : RobotPart(parent) { } RobotHead有一個非常簡單的構造函數,可以簡單地轉發到RobotPart的構造函數。
QRectF RobotHead::boundingRect() const { return QRectF(-15, -50, 30, 50); } boundingRect()重新實現機器人的頭部的範圍。因為我們希望旋轉中心為項目的底部中心,所以我們選擇了一個以(-15,-50)開始並延伸到30個單位寬和50個單位高的邊界矩形。旋轉頭部時,"頸部"將保持靜止,同時頭部的頂部從一側向另一側傾斜。
void RobotHead::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(option); Q_UNUSED(widget); if (pixmap.isNull()) { painter->setBrush(dragOver ? color.lighter(130) : color); painter->drawRoundedRect(-10, -30, 20, 30, 25, 25, Qt::RelativeSize); painter->setBrush(Qt::white); painter->drawEllipse(-7, -3 - 20, 7, 7); painter->drawEllipse(0, -3 - 20, 7, 7); painter->setBrush(Qt::black); painter->drawEllipse(-5, -1 - 20, 2, 2); painter->drawEllipse(2, -1 - 20, 2, 2); painter->setPen(QPen(Qt::black, 2)); painter->setBrush(Qt::NoBrush); painter->drawArc(-6, -2 - 20, 12, 15, 190 * 16, 160 * 16); } else { painter->scale(.2272, .2824); painter->drawPixmap(QPointF(-15 * 4.4, -50 * 3.54), pixmap); } } 在paint()中,我們繪製實際的頭部。該實現分為兩個部分:如果將影像放置在頭部上,則繪製影像,否則將繪製帶有簡單矢量圖形的圓形矩形機器人頭部。 出於性能方面的考慮,取決於所繪製內容的複雜性,將頭部繪製為影像通常比使用一系列矢量操作更快。
void RobotHead::dragEnterEvent(QGraphicsSceneDragDropEvent *event) { if (event->mimeData()->hasImage()) { event->setAccepted(true); dragOver = true; update(); } else { RobotPart::dragEnterEvent(event); } } 機器人頭部可以接受影像源。為了支援此操作,對其dragEnterEvent()的重新實現將檢查拖動對象是否包含影像數據,如果包含,則接受該事件。否則,我們將退回到基本RobotPart實現。
void RobotHead::dropEvent(QGraphicsSceneDragDropEvent *event) { if (event->mimeData()->hasImage()) { dragOver = false; pixmap = qvariant_cast<QPixmap>(event->mimeData()->imageData()); update(); } else { RobotPart::dropEvent(event); } } 為了影像支援,我們還必須實現dropEvent()。我們檢查拖動對象是否包含影像數據,如果包含,則將其存儲為成員pixmap並調用update()。此像素圖用於paint()實現中。
RobotTorso和RobotLimb與相似RobotHead,因此讓我們直接跳到Robot類。
class Robot : public RobotPart { public: Robot(QGraphicsItem *parent = nullptr); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget = nullptr) override; }; Robot類也繼承於RobotPart,並像其他部分也實現了boundingRect()和paint()方法。但是,它提供了一個相當特殊的實現:
QRectF Robot::boundingRect() const { return QRectF(); } void Robot::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(painter); Q_UNUSED(option); Q_UNUSED(widget); } 由於Robot類僅用作機器人其餘部分的基礎節點,因此它沒有視覺表示。因此,其boundingRect()實現可以返回空的QRectF,而其paint()函數則不執行任何操作。
Robot::Robot(QGraphicsItem *parent) : RobotPart(parent) { setFlag(ItemHasNoContents); QGraphicsObject *torsoItem = new RobotTorso(this); QGraphicsObject *headItem = new RobotHead(torsoItem); QGraphicsObject *upperLeftArmItem = new RobotLimb(torsoItem); QGraphicsObject *lowerLeftArmItem = new RobotLimb(upperLeftArmItem); QGraphicsObject *upperRightArmItem = new RobotLimb(torsoItem); QGraphicsObject *lowerRightArmItem = new RobotLimb(upperRightArmItem); QGraphicsObject *upperRightLegItem = new RobotLimb(torsoItem); QGraphicsObject *lowerRightLegItem = new RobotLimb(upperRightLegItem); QGraphicsObject *upperLeftLegItem = new RobotLimb(torsoItem); QGraphicsObject *lowerLeftLegItem = new RobotLimb(upperLeftLegItem); 構造函數首先設置標誌ItemHasNoContents,這對於沒有視覺外觀的項是次要的優化。然後,我們構造所有機器人零件(頭部,軀幹以及上/下臂和下肢)。堆疊順序非常重要,我們使用父子層次結構來確保元素旋轉和正確移動。我們首先構造軀幹,因為這是根元素。然後,我們構造頭部並將軀幹傳遞給HeadItem的構造函數。這將使頭部成為軀幹的"孩子";如果旋轉軀幹,頭部將跟隨。相同的模式適用於其餘四肢。
headItem - > setPos(0 , - 18); upperLeftArmItem - > setPos(- 15 , - 10); lowerLeftArmItem - > setPos(30 , 0); upperRightArmItem - > setPos(15 , - 10); lowerRightArmItem - > setPos(30 , 0); upperRightLegItem - > setPos(10 , 32); lowerRightLegItem - > setPos(30 , 0); upperLeftLegItem - > setPos(- 10 , 32); lowerLeftLegItem - > setPos(30 , 0);
每個機器人零件都經過仔細定位。例如,左上臂精確地移動到軀幹的左上區域,右上臂移動到右上區域。
QParallelAnimationGroup *animation = new QParallelAnimationGroup(this); QPropertyAnimation *headAnimation = new QPropertyAnimation(headItem, "rotation"); headAnimation->setStartValue(20); headAnimation->setEndValue(-20); QPropertyAnimation *headScaleAnimation = new QPropertyAnimation(headItem, "scale"); headScaleAnimation->setEndValue(1.1); animation->addAnimation(headAnimation); animation->addAnimation(headScaleAnimation);
下一節將創建所有動畫對象。此程式碼段顯示了兩個在頭部的縮放和旋轉上運行的動畫。這兩個QPropertyAnimation實例僅設置對象,屬性以及各自的開始和結束值。 所有動畫均由一個頂級並行動畫組控制。比例和旋轉動畫已添加到該組中。其餘動畫以類似方式定義。
for (int i = 0; i < animation->animationCount(); ++i) { QPropertyAnimation *anim = qobject_cast<QPropertyAnimation *>(animation->animationAt(i)); anim->setEasingCurve(QEasingCurve::SineCurve); anim->setDuration(2000); } animation->setLoopCount(-1); animation->start(); 最後,我們為每個動畫設置緩動曲線和持續時間,確保頂級動畫組永遠循環播放,並啟動頂級動畫。
ColorItem類定義
ColorItem類表示可被按下以拖動顏色到機器人零件的圓形項。
class ColorItem : public QGraphicsItem { public: ColorItem(); QRectF boundingRect() const override; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override; protected: void mousePressEvent(QGraphicsSceneMouseEvent *event) override; void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; private: QColor color; }; 這個ColorItem類很簡單。它不使用動畫,不需要屬性,也不需要訊號和插槽,因此為了節省資源,最自然的是它繼承了QGraphicsItem(與QGraphicsObject相對)。 它聲明了必需的boundingRect()和paint()函數,並重新實現了mousePressEvent(),mouseMoveEvent()和mouseReleaseEvent()。它包含一個私人顏色成員。
讓我們看一下它的實現。
ColorItem::ColorItem() : color(QRandomGenerator::global()->bounded(256), QRandomGenerator::global()->bounded(256), QRandomGenerator::global()->bounded(256)) { setToolTip(QString("QColor(%1, %2, %3)n%4") .arg(color.red()).arg(color.green()).arg(color.blue()) .arg("Click and drag this color onto the robot!")); setCursor(Qt::OpenHandCursor); setAcceptedMouseButtons(Qt::LeftButton); } ColorItem的構造函數通過使用QRandomGenerator向其顏色成員分配不透明的隨機顏色。為了提高可用性,它分配了一個工具提示,向用戶提供有用的提示,並且還設置了合適的游標。這樣可以確保當滑鼠指針懸停在項目上時,游標將有機會進入Qt::OpenHandCursor狀態。 最後,我們調用setAcceptedMouseButtons()以確保該項目只能處理Qt::LeftButton。這可以大大簡化滑鼠事件處理程式,因為我們始終可以假定僅按下並釋放了滑鼠左鍵。
QRectF ColorItem::boundingRect() const { return QRectF(-15.5, -15.5, 34, 34); } 項的邊界矩形是固定的30x30單位,以該項的原點(0,0)為中心,並在各個方向上調整0.5單位,以允許可伸縮的筆繪製其輪廓。為了獲得最終的視覺效果,邊界還向右下方補償了幾個單位,從而為簡單的陰影提供了空間。
void ColorItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { Q_UNUSED(option); Q_UNUSED(widget); painter->setPen(Qt::NoPen); painter->setBrush(Qt::darkGray); painter->drawEllipse(-12, -12, 30, 30); painter->setPen(QPen(Qt::black, 1)); painter->setBrush(QBrush(color)); painter->drawEllipse(-15, -15, 30, 30); } paint()執行繪製用1個單位像素的黑色輪廓,一個普通的顏色填充,和深灰色陰影效果的橢圓。
void ColorItem::mousePressEvent(QGraphicsSceneMouseEvent *) { setCursor(Qt::ClosedHandCursor); } 當你按下該項目的區域內的滑鼠按鍵時自動調用mousePressEvent()。我們的實現只是將游標設置為Qt::ClosedHandCursor。
void ColorItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *) { setCursor(Qt::OpenHandCursor); } 當你釋放已經按了一個項目的區域內後自動調用mouseReleaseEvent()。我們的實現將游標設置回Qt::OpenHandCursor。滑鼠按下和釋放事件處理程式共同為用戶提供有用的視覺回饋:將滑鼠指針移到上時CircleItem,游標將變為張開的手。按下該項目將顯示一個閉合的手形游標。釋放將再次恢復為打開的手形游標。
void ColorItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event) { if (QLineF(event->screenPos(), event->buttonDownScreenPos(Qt::LeftButton)) .length() < QApplication::startDragDistance()) { return; } QDrag *drag = new QDrag(event->widget()); QMimeData *mime = new QMimeData; drag->setMimeData(mime); 在的區域內按下滑鼠按鈕後四處移動滑鼠時,將調用mouseMoveEvent()處理函數ColorItem。此實現提供了最重要的邏輯CircleItem啟動和管理拖動的程式碼。 該實現首先檢查滑鼠是否已被拖動足夠遠以消除滑鼠抖動雜訊。我們僅想在滑鼠被拖動的距離大於應用程式開始拖動的距離時開始拖動。 我們創建一個QDrag對象,將事件窗口小部件(即QGraphicsView)傳遞給其構造函數。Qt將確保在正確的時間刪除該對象。我們還創建了一個QMimeData實例,該實例可以包含我們的顏色或影像數據,並將其分配給拖動對象。
static int n = 0; if (n++ > 2 && QRandomGenerator::global()->bounded(3) == 0) { QImage image(":/images/head.png"); mime->setImageData(image); drag->setPixmap(QPixmap::fromImage(image).scaled(30, 40)); drag->setHotSpot(QPoint(15, 30)); 該程式碼段具有某種隨機結果:有時,會將特殊影像分配給拖動對象的mime數據。像素圖也被輔助為拖動對象的像素圖。這將確保您可以在滑鼠游標下看到被拖動為像素圖的影像。
} else { mime->setColorData(color); mime->setText(QString("#%1%2%3") .arg(color.red(), 2, 16, QLatin1Char('0')) .arg(color.green(), 2, 16, QLatin1Char('0')) .arg(color.blue(), 2, 16, QLatin1Char('0'))); QPixmap pixmap(34, 34); pixmap.fill(Qt::white); QPainter painter(&pixmap); painter.translate(15, 15); painter.setRenderHint(QPainter::Antialiasing); paint(&painter, nullptr, nullptr); painter.end(); pixmap.setMask(pixmap.createHeuristicMask()); drag->setPixmap(pixmap); drag->setHotSpot(QPoint(15, 20)); } 否則,這是最常見的結果,將簡單的顏色分配給拖動對象的mime數據。我們將此渲染ColorItem為新的像素圖,以向用戶提供顏色正在"拖拉"的視覺回饋。
drag->exec(); setCursor(Qt::OpenHandCursor); }
最後,我們執QDrag::exec()將重新進入事件循環,並且只有在拖動被刪除或取消的情況下才退出。無論如何,我們都將游標重置為Qt::OpenHandCursor。
main函數
現在Robot和ColorItem類已經完成,我們可以將這些模組放到main函數中。
int main(int argc, char **argv) { QApplication app(argc, argv); 我們首先構建QApplication,然後初始化隨機數生成器。這樣可以確保每次啟動應用程式時顏色項具有不同的顏色。
QGraphicsScene scene(-200, -200, 400, 400); for (int i = 0; i < 10; ++i) { ColorItem *item = new ColorItem; item->setPos(::sin((i * 6.28) / 10.0) * 150, ::cos((i * 6.28) / 10.0) * 150); scene.addItem(item); } Robot *robot = new Robot; robot->setTransform(QTransform::fromScale(1.2, 1.2), true); robot->setPos(0, -20); scene.addItem(robot); 我們構造一個固定大小的場景,並創建ColorItem一個排列成一個圓圈的10個實例。每個項目都添加到場景中。 在此圓的中心,我們創建一個Robot實例。縮放機器人並將其向上移動幾個單元。然後將其添加到場景中。
GraphicsView view(&scene); view.setRenderHint(QPainter::Antialiasing); view.setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate); view.setBackgroundBrush(QColor(230, 200, 167)); view.setWindowTitle("Drag and Drop Robot"); view.show(); return app.exec(); } 我們創建一個QGraphicsView窗口,並為其分配場景。為了提高視覺品質,我們啟用了抗鋸齒功能。我們還選擇使用邊界矩形更新來簡化可視更新處理。該視圖具有固定的沙色背景和窗口標題。
最後,我們顯示視圖。控制項進入事件循環後,動畫立即開始。

