QGraphics体系事件经验记录

1 默认事件处理函数的事件传递

在Qt中,与事件相关的eventFilter和event都需要返回bool,默认返回false,事件会继续往后传递;与事件相关的具体事件处理函数默认接收事件,事件不继续往后传递。

2 自定义类不会主动执行基类的事件处理函数

事件过滤器、事件分发以及具体的事件处理函数都是protected virtual的,在继承QGraphicsItem自定义类时可以重写这三个函数。

class MyItem : public QGraphicsRectItem
{
    MyItem();
    ~MyItem();
    
    virtual void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
    virtual void mouseReleaseEvent(QGraphicsSceneMouseEvent *event)override;
    virtual void mouseMoveEvent(QGraphicsSceneMouseEvent *event)override;
}
MyItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    QGraphicsRectItem::mousePressEvent(event);
}
MyItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
    QGraphicsRectItem::mouseReleaseEvent(event);
}
MyItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
    QGraphicsRectItem::mouseMoveEvent(event);
}

自定义类重写了事件处理函数,因此执行时并不会执行父类的事件处理函数。由于基类函数中涉及默认的处理,常常需要在对应的事件处理函数中手动调用。

因此,若不想执行父类的某个函数,重写或者覆盖都可以;若想在父类某个函数的基础上增加某些执行操作,则在重写或者覆盖的函数中通过作用域符调用基类的对应函数即可。

2.1 在自定义类中不调用父类就不会执行父类的事件处理函数 实例

今天在实现功能时,需要实现可拉伸矩形,但是不能移动矩形,在setFlag(QGraphicsItem::ItemIsMovalbe, false)上纠缠两个小时,不停的测试,将该参数对事件传递的影响记录如下:主要任务是需要通过继承已有的矩形类来实现可拉伸但不移动的功能,该类已经能够实现矩形的拉伸以及移动,我的目的就是去掉移动操作。

首先要知道,在自定义的派生类中,即使不调用基类的事件处理函数,也是能够执行派生类的事件处理函数的,只不过功能可能少一些。

已有的矩形类中,移动是通过在派生类中调用基类的点击、移动、释放事件处理函数实现的,该操作会改变Item的Pos。

已有的矩形类中,拉伸是在派生类的点击、移动、释放事件处理函数中实现的,点击时记录鼠标的位置以及点击的标准点方位(就是PPt中图形的八个点),另外如果点击在八个点上,则要将在移动函数中就走另一条不调用基类事件移动函数的分支(否则会拖动),移动时边计算偏移量,边setRect。由于rect.setTop()、rect.setTopLeft()等函数十分方便,因此改变Item的rect即可。

MyItem::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
    QGraphicsRectItem::mousePressEvent(event);
    if(pressDir in Dirs)//伪代码
    {
    	m_ptPointF = event->pos();
    	m_bSetSize = true;
    	m_rtRect = this->rect();
    }
}
MyItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
    QGraphicsRectItem::mouseReleaseEvent(event);
}
MyItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
    if(m_bSetSize)
    {
        QPointF offset = event->pos() - m_ptPointF;
        if(pressDir is topLeftDir)
        {
            m_rtRect.setTopLeft(m_rtRect.topLeft() + offset);//由于rect在设置左侧的坐标时右侧的边是不动的,因此宽度也会变化
        }
        else
        {
        	QGraphicsRectItem::mouseMoveEvent(event);
        }
    }
}

而我需要继承此类实现不移动,只拉伸的效果。因此在我的代码中,不调用MyItem::mouseMoveEvent(QGraphicsSceneMouseEvent *event)即可,只实现拉伸的部分就行。

3 QGraphicsItem的状态对事件传递的影响

结论:

  1. QGraphicsItem的Flags状态默认都为false。
  2. 若不对选中或者移动状态进行特殊设置,事件到达QGraphicsItem后,继续向父Item或者下一层Item传递。
  3. 若继承QGraphicsItem,派生类的mousePressEvent默认接收事件,但若在内部调用基类的mousePressEvent,则事件同样会继续传递到下一层。
  4. 在QGraphicsItem::mousePressEvent前设置Item的状态都有可能影响后续事件的传递。
  5. 总结起来就是,默认不能选中不能移动的Item不能成为事件的grabber。

下图是QGraphicsItem中的鼠标点击事件源码,从中可以看出:

  • 进入第一分支要求可选中
  • 进入第二分支要求不可移动
因此当Item处于不可选中不可移动的状态下不能抓到事件,事件会继续传递到下一个Item。(对于QGraphicItem,mousePressEvent抓取事件后成为grabber,而后续的鼠标移动、释放、双击事件才会继续传递给鼠标的grabber,因此如果在mousePressEvent中发生了event->ignore(),事件会传递到下一个item,那么后续的移动、释放、双击事件都会传递给下一个Item。)

下图是Qt文档中的内容,setFlags()中的状态默认设置为disabled。

4 事件中常用的pos是什么?

  1. view中event->pos()和event->localview()返回的是view坐标系中的位置,event->pos()通过mapToScene()函数可转换为scene中的位置,view坐标系就是左上角为(0,0)的坐标系。

  2. scene中event->scenePos()就是场景中的位置,与view中event->pos()经过map后的位置相同,可以通过itemAt来获取对应位置最上层的item。

  3. item中event->pos()是在自身坐标系中的位置,event->scenePos()是在场景中的位置。
    item常用event->screenPos()来获取选择的右键菜单项,进而做出选择

    //右键菜单的使用
    void MyItem::contextMenuEvent(QGraphicsSceneContextMenuEvent *event)
    {
        QMenu menu;
        QAction *moveAction = menu.addAction("move back");
        QAction *actAction = menu.addAction("test");
        QAction *selectedAction = menu.exec(event->screenPos());//只能使用screenPos,其他的如pos或者scenePos都会报错
        if(selectedAction == moveAction) {
            setPos(0, 0);
        }
    }
    

5 Qt鼠标点击事件与itemChange选中状态

在之前的项目中,需要使用QGraphicsItem实现如下的控件:

具体的功能有点类似PPT中选中不同控件组合后的结果,其中的Rect是三角形和圆形的选中框,Rect的有效区域(shape)是两个控件的有效区域之和。此任务的重点在于其点击方式,第一次点击时选中Rect(高亮),第二次点击则选中某个子Item,后续的点击可以在两个子控件之间切换,若点击在有效区域之外,就全部失去选中。另外要求双击时要调出弹窗来设置属性。

这个过程中需要考虑的几个问题:

  1. 如何区分单纯点击事件、移动事件、双击事件?因为需要对纯点击的次数进行记录,只有两个相隔一定事件的纯点击才可以选中某个子控件,而双击事件的两次点击中第一次就是一次单纯的点击(Press--Release),而第二次点击才会激发双击事件(dblClick--Release).
  2. 如何控制事件从子控件传递到父控件以及子控件截获事件?ignore.
  3. 如何在控件处于不同的点击状态下点击有效区域外后控件失去选中?可能考虑使用itemChange中事件失去选中的状态,但是此处需要注意的是控件之间的状态切换:大矩形切换到子控件以及子控件之间的相互切换,其中涉及一个控件失去选中以及另一个控件获取选中,另外点击有效区域外部也会导致控件失去选中,那每次只要有控件失去选中时都需要通知大矩形,让大矩形检查一下当前是否有被点击的其他控件,有的话则依然高亮,没有的话就熄灭。因此需要搞明白失去选中与获取选中的函数调用先后顺序。
  4. 如何在控件处于不同的点击状态下双击后调出属性窗口?因为在整体未选中或者子控件被选中的状态下,都直接经过各自的双击事件处理函数即可,而在大矩形被选中的时候进行双击(第一次点击是纯点击),可能会导致从大矩形切换到子控件,两次点击不在同一个控件上导致无法触发双击事件。

关于这部分在单独用一篇进行记录。在这里需要关注的是,从大矩形手动切换到新点击的item上,需要在itemChange中检测并手动设置选中框是否点亮。因此就引出了点击事件与切换前后item的状态变化函数itemChange的调用顺序问题。

  1. 点击即选中,但函数调用有顺序。点击鼠标后,触发点击事件,随之会调用itemChange中的选中分支。

  2. 点击会调用当前部件的事件处理函数,与基类的事件处理函数无关。只要当前类中定义了事件处理的虚函数,其相当于覆盖了父类的事件处理函数。但为了避免失去控件的部分功能,常常会在当前类的处理函数中显式地调用基类的事件处理函数。

  3. 选中状态的切换:从一个对象切换到另一个对象时,先执行鼠标点击事件,而后调用前选中控件的itemChange,前控件的选中状态取消,而后调用当前选中控件的itemCHange,当前控件设置为选中。

    在选中状态的切换过程中,并不是一个控件取消选中而另一个控件就设置为选中:在前控件的itemChange中,前控件和当前控件的isSelected状态均为false。因此需要自己临时创建选中状态,并设定点击即选中来对其设置。

举例说明,从三角形切换到圆形,Qt中调用顺序为:

  • Cir::mousePressEvent(点击事件)
  • Tri::itemChange::ItemHasSelected状态的false分支(失去选中),在此分支执行过程中,虽然Cir被点击,但是Cir->isSelected()==false,因此在失去选中时向大矩形发送消息,让其使用子控件的isSelected()函数检测是否还有其他被选中的控件则会不成功,因此在点击事件中设置状态量,点击就被设置为true。
  • Cir::itemChange::itemHsSelected状态的true分支(获取选中)
posted @   YueLiGo  阅读(521)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示