一直想做一个像卡牌游戏一样的,可以拖动卡片,实现改变位置,顺序交换的效果,今天我们一起来尝试一下。
1.先绘制一个基于QWidget的控件
类名为Card
h文件
#ifndef CARD_H
#define CARD_H
#include <QWidget>
#include <QPaintEvent>
#include <QPainter>
class Card : public QWidget
{
Q_OBJECT
public:
explicit Card(QWidget *parent = nullptr);
protected:
void paintEvent(QPaintEvent *event) override;
};
#endif // CARD_H
cpp文件
#include "card.h"
Card::Card(QWidget *parent) : QWidget(parent)
{
this->setGeometry(0,0,200,400);
}
void Card::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing,true);
painter.drawRoundedRect(QRectF(5,5,190,390),10,10);
}
我们完成了一个很简单的200*400的圆角卡片
在主界面中展示看看
widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include "card.h"
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private:
Ui::Widget *ui;
Card* cd[8];
};
#endif // WIDGET_H
widget.cpp
#include "widget.h"
#include "ui_widget.h"
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
for(int i=0;i<8;i++){
cd[i] = new Card(this);
connect(cd[i],&Card::sendSelf,this,&Widget::getObject);
cd[i]->move(i%4*200,i/4*400);
}
}
Widget::~Widget()
{
delete ui;
}
运行后的效果:
2.用QMouseEvent实现控件可拖动
首先要实现控件拖动,需要有2个要素,1:要拖动的控件对象,2:控件的初始位置
card[8]是以数组形式一次性加载到界面的,鼠标点击时我们并不知道当前点击的对象。我们可以在Card类中做修改,使点击时通知主界面它是谁。
card.h
#ifndef CARD_H
#define CARD_H
#include <QWidget>
#include <QPaintEvent>
#include <QPainter>
#include <QMouseEvent>
class Card : public QWidget
{
Q_OBJECT
public:
explicit Card(QWidget *parent = nullptr);
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
signals:
void sendSelf(Card *w);
};
#endif // CARD_H
我们新增了mousePressEvent和一个信号sendSelf,信号将自己本身的地址发送出去
card.cpp
#include "card.h"
Card::Card(QWidget *parent) : QWidget(parent)
{
this->setGeometry(0,0,200,400);
}
void Card::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing,true);
painter.drawRoundedRect(QRectF(5,5,190,390),10,10);
}
void Card::mousePressEvent(QMouseEvent *event)
{
Q_UNUSED(event);
emit sendSelf(this);
}
到widget.h中设置相应的槽函数
private slots:
void getObject(Card *w);
接收到对象后,我们接下来要对其进行操作,改变位置只需要用move()函数即可,我们要实时显示出移动的过程,因此需要在mouseMoveEvent中做处理,重写mouseMoveEvent函数:
protected:
void mouseMoveEvent(QMouseEvent *event) override;
还需要声明一个Card成员变量,来临时存储getObject中获得的对象地址,声明一个开始移动时鼠标指针位置startP,一个卡片本身的位置yuanP。
private:
Ui::Widget *ui;
Card *temp;
QPoint startP;
QPoint yuanP;
实现getObject函数和mouseMoveEvent函数
widget.cpp
void Widget::getObject(Card *w)
{
temp = w;
startP = cursor().pos()-this->pos();
yuanP = temp->pos();
}
void Widget::mouseMoveEvent(QMouseEvent *event)
{
temp->move(yuanP.x()+event->x()-startP.x(),yuanP.y()+event->y()-startP.y());
}
运行后,可以成功拖动卡片了!
但是我们又发现了一个问题,当卡片重叠时,我们点击重叠部分无法确定选中的是哪张卡片,因此我们制定一个规则,最近移动的卡片总是处于最上层。我们可以在mouseReleaseEvent中做处理:
void Widget::mouseReleaseEvent(QMouseEvent *event)
{
temp->raise();
}
到这一步,我们已经实现了可自由拖拽改变位置的控件。
3.接下来实现拖拽交换卡片位置的功能
这里我制定了一个规则,拖拽某张卡片时,若此时的鼠标指针进入了另一张卡片范围,则进行交换。
整体思路是这样的:
在widget中的mouseMoveEvent中做处理:当鼠标移动时,给Card发送信号,由Card的槽函数中做判断,指针是否进入了自身范围if(this->geometry().contains(pos)),如果进入了,就向widget发送信号,避免重复发送信号,增加一个开关量,widget收到反馈信号后就关闭。为了避免拖拽的Card本身触发进入范围的判断,在widget的getObject槽函数中先关闭了当前Card的连接。
为了能看清是否交换位置了,我们给card增加了标号显示。
完成后的代码:
card.h
#ifndef CARD_H
#define CARD_H
#include <QWidget>
#include <QPaintEvent>
#include <QPainter>
#include <QMouseEvent>
class Card : public QWidget
{
Q_OBJECT
public:
explicit Card(QWidget *parent = nullptr);
QString txt;//显示卡片标号
public slots:
void getPos(QPoint p);//接收widget发送的鼠标坐标
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
private:
bool isDragging=false;//当前是否被拖拽
signals:
void sendSelf(Card *w);
void sendNeedChange(Card *w);//发送给widget表明自己需要被交换
};
#endif // CARD_H
card.cpp
#include "card.h"
#include <QDebug>
Card::Card(QWidget *parent) : QWidget(parent)
{
this->setGeometry(0,0,200,400);
}
void Card::getPos(QPoint p)
{
if(this->geometry().contains(p)){
qDebug()<<"enter"<<txt;
emit sendNeedChange(this);
}
}
void Card::paintEvent(QPaintEvent *event)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing,true);
painter.drawRoundedRect(QRectF(5,5,190,390),10,10);
painter.drawText(50,100,100,200,Qt::AlignCenter,txt);//绘制卡片标号
}
void Card::mousePressEvent(QMouseEvent *event)
{
Q_UNUSED(event);
isDragging = true;
emit sendSelf(this);
}
widget.h
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QMouseEvent>
#include "card.h"
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
private slots:
void getObject(Card *w);
void needChange(Card *w);//执行交换
protected:
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
private:
Ui::Widget *ui;
Card* cd[8];
Card *temp;
QPoint startP;
QPoint yuanP;
QRect yuanR;
bool isMoving=false;
signals:
void sendPos(QPoint p);//发送鼠标坐标
};
#endif // WIDGET_H
widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
for(int i=0;i<8;i++){
cd[i] = new Card(this);
cd[i]->txt=QString::number(i+1);//绘制卡片标号
connect(cd[i],&Card::sendSelf,this,&Widget::getObject);
connect(cd[i],&Card::sendNeedChange,this,&Widget::needChange);
connect(this,&Widget::sendPos,cd[i],&Card::getPos);
cd[i]->move(i%4*200,i/4*400);
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::getObject(Card *w)
{
temp = w;
disconnect(this,&Widget::sendPos,w,&Card::getPos);//暂时断开正在拖拽的card连接,避免触发与自己的交换
startP = cursor().pos()-this->pos();
yuanP = temp->pos();
yuanR = temp->geometry();
isMoving=true;
}
void Widget::needChange(Card *w)
{
targetR = w->geometry();//记录被交换对象的位置
w->setGeometry(yuanR);//被交换的card移动到被拖拽的card的原位置
isMoving=false;
}
void Widget::mouseMoveEvent(QMouseEvent *event)
{
temp->move(yuanP.x()+event->x()-startP.x(),yuanP.y()+event->y()-startP.y());
if(isMoving){
emit sendPos(event->pos());
}
}
void Widget::mouseReleaseEvent(QMouseEvent *event)
{
connect(this,&Widget::sendPos,temp,&Card::getPos);//交换完成恢复连接
temp->raise();
qDebug()<<targetR;
temp->setGeometry(targetR);//松开后被拖拽card自动调整到被交换card的原位置
}
到这一步就已经基本实现我们最初想要的功能了,接下来可以再移动过程中加点动画效果,把widget也封装起来,可以自定义设置卡片数量,卡片尺寸等
完整代码链接:
Qt自定义可拖拽控件
03.01更新:增加的动画效果
利用QPropertyAnimation实现卡片的动画移动效果
card.h
#ifndef CARD_H
#define CARD_H
#include <QWidget>
#include <QPaintEvent>
#include <QPainter>
#include <QMouseEvent>
#include <QPropertyAnimation>
class Card : public QWidget
{
Q_OBJECT
public:
explicit Card(QWidget *parent = nullptr);
QString txt;
void moveTo(QRect r);//新增移动接口,r为目标QRect
public slots:
void getPos(QPoint p);
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
private slots:
void reset();//动画结束后触发
private:
bool isDragging=true;//拖拽标志,防止重复触发动画
signals:
void sendSelf(Card *w);
void sendNeedChange(Card *w);
};
#endif // CARD_H
card.cpp
void Card::moveTo(QRect r)
{
QPropertyAnimation *animation = new QPropertyAnimation(this,"geometry");
connect(animation,&QPropertyAnimation::finished,this,&Card::reset);
animation->setDuration(300);
animation->setStartValue(this->geometry());
animation->setEndValue(r);
animation->start();
}
void Card::getPos(QPoint p)
{
if(isDragging && this->geometry().contains(p)){
isDragging=false;
qDebug()<<"enter"<<txt;
emit sendNeedChange(this);
}
}
void Card::reset()
{
isDragging = true;
}