经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » C++ » 查看文章
高仿富途牛牛-组件化(二)-磁力吸附
来源:cnblogs  作者:朝十晚八  时间:2019/6/18 8:59:47  对本文有异议

一、概述

上一篇文章高仿富途牛牛-组件化(一)-支持页签拖拽、增删、小工具我们讲述了组件化的一些基础东西,并有了一个基本的雏形,使用过富途牛牛的同学应该对其中的gif图比较熟悉了。虽然效果糙了一点儿,但是该有的基础功能是已经有了。

  • 工具栏页签拖拽
  • 工具栏之间页签拖拽
  • 小工具
  • 多页签架构
  • 小窗口

上述几个功能在上一篇文章中都已经有了,今天我们来讲述下第二个关键功能--磁力吸附和一些其他小功能

二、效果展示

磁力吸附,顾名思义就是说窗口移动时,快要接近另一个窗口边缘时,会有一种磁性,把正在拖拽的窗口直接吸过去,效果图如下图所示。






三、磁力吸附

高仿富途牛牛-组件化(一)-支持页签拖拽、增删、小工具文章最后,我列出了工程中所有的类,并做了每个类的功能说明。

本篇文章的工程代码在上一版本的基础上进行了一些优化,代码的结构也更加的清晰,阅读起来更容易,主要是增加了磁力吸附和一些同步功能。

下面来思考下磁力吸附这个功能。

首先我们来考虑下磁力吸附,什么是磁力吸附,明白我们自己的需求是什么样子的?

磁力表现出来可能像下面这样:

  1. 不同子窗口之间希望进行磁力吸附,也就是说窗口移动时,可以被吸附到邻近的窗口边框上
  2. 不同页签之间不需要关联
  3. 鼠标不能移动到subPanel之外

别名:被拖拽窗口(A)、吸附窗口(B)、事件处理(C)

有了清晰的需求之后,我们下面就来考虑怎么实现我们的需求,既然要做到小窗口之间进行吸附,想一想,这个事件处理不管写到A窗口还是B窗口都不是那么合适。那么可想而知,除过被拖拽的窗口A和将要的吸附窗口B之外,必然需要引入一个第三者C,进行事件处理,他不一定是一个窗口,主要是要能代理A和B的事件,并且进行各种处理即可。

有了第三者C之后,接下来我们在第三者C中去处理A的移动事件,循环去判断是否和其中某个窗口满足了吸附条件。一旦满足吸附条件,我们就触发吸附后操作

处理吸附事件时,可能像下面这样

假设我们有10个窗口,分别是A1、A2、A3、A4...A9、A10等

  1. 当我们拖拽A1窗口时,其他窗口都是吸附窗口(B)
  2. 当我们拖拽A2窗口时,A1和其他窗口都是吸附窗口(B)
  3. 同理,当我们拖拽其他An窗口时,除过An的窗口都是吸附窗口(B)

当要引入第三者窗口时,我们可能需要思考如下几个问题

  • 怎么样引入第三者事件处理类呢?
  • 他是怎么初始化的?
  • 他的作用范围?

思考如上3个问题,怎么去解决他们!我第一时间就想到了Qt中提供的QButtonGroup类,这个类的作用是用于管理其中的按钮,在他里边包含的按钮不允许有两个同时选中。 是不是很相似,也是管理一堆相同的控件,但是他们中,其中一个控件的操作会对其他所有的控件产生相同的效果。

也就是说:我们可以新增一个SmallGroup类,专门负责处理移动的窗口和其他窗口之间的事件

这个类可能就像这边这样!他提供了新增一个小窗口和移除一个小窗口的接口,添加进来的小窗口我们都可以进行磁力吸附管理。

  1. class SmallGroup : public QObject
  2. {
  3. public:
  4. SmallGroup(QObject * object = nullptr);
  5. ~SmallGroup(){}
  6. public:
  7. void AddSmall(SmallWidget *);
  8. void RemoveSmall(SmallWidget *);
  9. void MagneticEnable(bool);
  10. void LimitCursor(bool);//限制鼠标移动范围
  11. void MoveStart(SmallWidget *, const QPoint &);//开始移动
  12. void MovingDistance(SmallWidget *, const QPoint &);//距离开始移动时的偏差距离
  13. protected:
  14. virtual bool eventFilter(QObject *, QEvent *) override;
  15. private:
  16. QPoint MagneticPos(SmallWidget *, const QRect &);
  17. private:
  18. bool m_bMagnetic;
  19. QPoint m_startPos;
  20. QVector<SmallWidget *> m_smallVec;
  21. SmallWidget * m_pMoveWidget;
  22. };

这个类的思路不难,只是里边有一些比较繁杂的实现,这里我主要说3点

  1. 限制鼠标区域
  2. 修正窗口可以移动的区域
  3. 获取最邻近的可被吸附的窗口

1、限制鼠标区域

限制鼠标可移动区域的接口上边已经列出来来了,根据参数动态的去限制鼠标移动区域,或者不限制

  1. LimitCursor(bool)

当进行拖拽小窗口时,我们需要限制鼠标不能移除subPanel,如果不理解subPanel是什么东西,需要仔细去阅读下上一篇文章高仿富途牛牛-组件化(一)-支持页签拖拽、增删、小工具

限制鼠标移动区域的代码如下所示,主要是使用了ClipCursor这个win32接口,代码比较简单,这里就不做详细说明了。

  1. void SmallGroup::LimitCursor(bool limit)
  2. {
  3. #ifdef Q_OS_WIN
  4. if (limit)
  5. {
  6. if (QWidget * subPanel = dynamic_cast<QWidget *>(parent()))
  7. {
  8. QRect q_rect = subPanel->geometry();
  9. QPoint g_pos = subPanel->mapToGlobal(QPoint(0, 0));
  10. CRect w_rect;
  11. w_rect.left = g_pos.x();
  12. w_rect.top = g_pos.y();
  13. w_rect.right = g_pos.x() + q_rect.width();
  14. w_rect.bottom = g_pos.y() + q_rect.height();
  15. ClipCursor(&w_rect);
  16. }
  17. }
  18. else
  19. {
  20. ClipCursor(nullptr);
  21. }
  22. #endif
  23. }

2、修正窗口可以移动的区域

看到这个标题是不是有点儿蒙圈,其实这个也很简单,这里主要说明的是,我们移动小窗口时,小窗口不能移出subPanel,也就是说当subPanel显示时,其中的小窗口都可以全部显示出来,或者被其他小窗口遮挡。

当然了,这个也是需要根据需求来定的,我最开始做的就是4个边都不能出subPanel,但是后来发现,富途牛牛的代码是只有顶部不能出去。因此代码里我注释了3个if修正操作,大家可以根据自家的需求进行修改。

  1. QRect CorrentRect(const QRect & rect, const QRect & subPanel)
  2. {
  3. QRect correntRect = rect;
  4. //if (correntRect.left() < subPanel.left())
  5. //{
  6. // correntRect.moveLeft(subPanel.left());
  7. //}
  8. if (correntRect.top() < subPanel.top())
  9. {
  10. correntRect.moveTop(subPanel.top());
  11. }
  12. //if (correntRect.right() > subPanel.right())
  13. //{
  14. // correntRect.moveRight(subPanel.right());
  15. //}
  16. //if (correntRect.bottom() > subPanel.bottom())
  17. //{
  18. // correntRect.moveBottom(subPanel.bottom());
  19. //}
  20. return correntRect;
  21. }

3、获取最邻近的可被吸附的窗口

磁力吸附最复杂的地方可能就是这个功能了,当我们移动一个窗口时,我们需要判断各种情况,然后去修正我们的位置。

划重点1:磁力吸附是说当我们靠近某个小窗口边框时,我们拖拽的窗口可以被吸附过去,但是需要特别注意,我们实际移动的距离根本没有到达那么多,因此,当我们鼠标稍微往远移动一下,窗口应该像被弹开一样。

划重点2:要实现重点1,那么我们在移动窗口时,就需要有一定的技巧,需要记录小窗口开始移动的位置,和当前移动的距离。根据移动后的距离判断是否可以被吸附,如果被吸附了,那么我们直接把窗口移动多一点(或者少一点)距离,达到吸附的位置,但是实际上这个时候,我们鼠标移动的距离并不等于我们实际移动的距离,这样是为了当我们鼠标在次偏移时,我们可以继续去判断是否满足吸附条件,如果不满足则按实际的移动距离。这样就达到了被弹开的视觉效果

上边的描述可能理解起来会比较费劲,这里我在用公式说明下,理解不了就多看几遍吧

startMovePos:开始移动时,鼠标按下的位置
offsetPos:鼠标当前位置距离开始移动时的位置之间的距离
truthPos:按照鼠标位移,将要移动到的位置。
movePos:窗口将要被移动到的位置。磁力吸附后,会在truthPos上有所偏差

如上四个变量所示,当我们移动窗口时,可能会产生以下几个情况

  1. 没有磁力吸附,直接移动到truthPos
  2. 有磁力吸附,移动到被吸附的窗口边框跟前(会产生一个便宜值value,被吸过去了)
  3. 上一次有磁力吸附,本次不满足处理吸附,直接移动到truthPos,产生弹开的感觉。因为之前被吸附了,有一个偏移值value。

磁力吸附需要处理4个方向的事件,这里我们只讲下左侧吸附,其他情况类似,这里不做介绍

如下代码所示,就是处理吸附位置时的主流程,代码里我只保留了处理做边框吸附的,其他边框代码已删,逻辑都差不多。

  1. QPoint SmallGroup::MagneticPos(SmallWidget * widget, const QRect & rect)
  2. {
  3. QPoint pos(rect.topLeft());
  4. if (QWidget * subPanel = dynamic_cast<QWidget *>(parent()))
  5. {
  6. QRect panelRect = subPanel->rect();
  7. QRect correntRect = CorrentRect(rect, panelRect);
  8. if (m_bMagnetic == false)
  9. {
  10. return correntRect.topLeft();
  11. }
  12. //修改位置后的ps 更准确
  13. pos = correntRect.topLeft();
  14. QVector<SmallWidget *> smallWidgets = m_smallVec;
  15. smallWidgets.removeOne(widget);
  16. int distance = 0;
  17. //左边框与subPanel左测比较
  18. if (CanMagneticPanel(ME_LEFT, rect.left(), panelRect, distance))
  19. {
  20. pos.setX(panelRect.left());
  21. }
  22. else
  23. {
  24. //左边框与其他窗口右边框比较
  25. if (CanMagneticSmall(ME_LEFT, rect.left(), smallWidgets, distance))
  26. {
  27. pos.setX(distance);
  28. }
  29. }
  30. ...
  31. }
  32. }

左侧吸附具体分两个情况

  1. 移动窗口A和subPanel之间的吸附
  2. 移动窗口A的左边框和被吸附窗口B的右边框之间的吸附

a、A窗口和subPanel面板之间的吸附

吸附规则时:A窗口左边框吸附subPanel面板的左边框,同理其他边框都是一样

  1. bool CanMagneticPanel(MagneticEdge edge, int s, const QRect & subPanel, int & distance)
  2. {
  3. int value;
  4. switch (edge)
  5. {
  6. case ME_LEFT:
  7. value = subPanel.left();
  8. break;
  9. case ME_TOP:
  10. value = subPanel.top();
  11. break;
  12. case ME_RIGHT:
  13. value = subPanel.right();
  14. break;
  15. case ME_BOTTOM:
  16. value = subPanel.bottom();
  17. break;
  18. default:
  19. break;
  20. }
  21. distance = qFabs(s - value);
  22. if (distance <= MagneticDistance)
  23. {
  24. return true;
  25. }
  26. return false;
  27. }

b、A窗口的左边框和被吸附窗口B的右边框之间的吸附

循环判断其他可被吸附的窗口,找到一个距离最近可悲吸附的窗口,然后进行位置修正。当函数返回为真时,distance就是最后要被修复的位置。

值得注意的是,如果有多个满足吸附的窗口边框,我们需要找到一个距离最近的窗口进行修复,也就是说呗吸附的窗口边框和我们正在拖拽的窗口边框距离最近。

不同于和subPanel之间的吸附规则,子窗口之间的吸附规则是,A窗口的左边框会吸附B窗口的右边框;A窗口的顶边框会吸附B窗口的低边框,规则是不是很清晰了,刚好是反的。左对右、顶对低、右对左和低对顶

  1. bool CanMagneticSmall(MagneticEdge edge, int moving, const QVector<SmallWidget *> & allWidget, int & distance)
  2. {
  3. distance = 10000;
  4. bool result = false;
  5. int minDistance = 10000;
  6. //根据edge的值 动态去获取窗口的边
  7. //例如:edge为ME_LEFT时 需要获取其他窗口的ME_RIGHT 去对比
  8. for each (SmallWidget * widget in allWidget)
  9. {
  10. int otherValue = -1;
  11. switch (edge)
  12. {
  13. case ME_LEFT:
  14. otherValue = widget->geometry().right() + 2;
  15. break;
  16. case ME_TOP:
  17. otherValue = widget->geometry().bottom() + 2;
  18. break;
  19. case ME_RIGHT:
  20. otherValue = widget->geometry().left() - 1;
  21. break;
  22. case ME_BOTTOM:
  23. otherValue = widget->geometry().top() - 1;
  24. break;
  25. default:
  26. break;
  27. }
  28. if (otherValue != -1)
  29. {
  30. int tmp = qFabs(moving - otherValue);
  31. if (minDistance > tmp)
  32. {
  33. minDistance = tmp;
  34. if (minDistance <= MagneticDistance)
  35. {
  36. result = true;
  37. distance = otherValue;
  38. }
  39. }
  40. }
  41. }
  42. return result;
  43. }

四、其他

工具箱窗口和工具栏工具按钮联动,按理说这个功能属于比较常见的功能,但是这里我也想拿出来跟大家分享下,这里我主要是借助了QAction这个类,把工具栏种的按钮QToolButton和工具箱窗口进行了绑定,这样不需要过多的信号餐同步,我们就可以很简单的实现功能联动

以前的时候我都是使用信号槽进行同步的,后来才发现这个比较取巧的办法,不是多么高端,主要是可以让代码更清晰。当有越来越多的复杂业务时,QAction的联动同步优势就出来了。

下面是QToolButton和工具箱同步状态的代码

  1. //工具箱,关闭时,同步工具栏按钮状态
  2. void ToolBoxDialog::BindAction(QAction * act)
  3. {
  4. connect(m_pToolBoxAct, &QAction::triggered, act, &QAction::setChecked, Qt::UniqueConnection);
  5. }
  6. connect(m_pTitle, &ToolBoxTitle::CloseWindow, this, [this](){
  7. m_pToolBoxAct->triggered(false);
  8. setVisible(false);
  9. });
  10. //点击工具栏按钮时,打开工具箱
  11. void TemplateLayout::ShowToolBox(bool visible)
  12. {
  13. if (m_pToolBox == nullptr)
  14. {
  15. m_pToolBox = new ToolBoxDialog(this);
  16. m_pToolBox->BindAction(m_pToolBar->GetToolBoxButton());
  17. connect(m_pToolBox, &ToolBoxDialog::SubWindowClicked, m_pPanel, &ContentPanel::CreateSubWindow);
  18. }
  19. if (visible)
  20. {
  21. m_pToolBox->show();
  22. }
  23. else
  24. {
  25. m_pToolBox->hide();
  26. }
  27. }

五、相关文章

高仿富途牛牛-组件化(一)-支持页签拖拽、增删、小工具

以上的内容,基本上就是本篇文章的内容所有内容啦!磁力吸附功能基本完成,希望可以帮到大家。




转载声明:本站文章无特别说明,皆为原创,版权所有,转载请注明:朝十晚八 or Twowords


原文链接:http://www.cnblogs.com/swarmbees/p/11042704.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号