文章目录

  • 一、概述
  • 二、效果展示
  • 三、磁力吸附
    • 1、限制鼠标区域
    • 2、修正窗口可以移动的区域
    • 3、获取最邻近的可被吸附的窗口
      • a、A窗口和subPanel面板之间的吸附
      • b、A窗口的左边框和被吸附窗口B的右边框之间的吸附
  • 四、其他
  • 五、相关文章

一、概述

上一篇文章高仿富途牛牛-组件化(一)-支持页签拖拽、增删、小工具我们讲述了组件化的一些基础东西,并有了一个基本的雏形,使用过富途牛牛的同学应该对其中的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类,专门负责处理移动的窗口和其他窗口之间的事件

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

class SmallGroup : public QObject
{
public:SmallGroup(QObject * object = nullptr);~SmallGroup(){}public:void AddSmall(SmallWidget *);void RemoveSmall(SmallWidget *);void MagneticEnable(bool);void LimitCursor(bool);//限制鼠标移动范围void MoveStart(SmallWidget *, const QPoint &);//开始移动 void MovingDistance(SmallWidget *, const QPoint &);//距离开始移动时的偏差距离protected:virtual bool eventFilter(QObject *, QEvent *) override;private:QPoint MagneticPos(SmallWidget *, const QRect &);private:bool m_bMagnetic;QPoint m_startPos;QVector<SmallWidget *> m_smallVec;SmallWidget * m_pMoveWidget;
};

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

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

1、限制鼠标区域

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

LimitCursor(bool)

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

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

void SmallGroup::LimitCursor(bool limit)
{
#ifdef Q_OS_WINif (limit){if (QWidget * subPanel = dynamic_cast<QWidget *>(parent())){QRect q_rect = subPanel->geometry();QPoint g_pos = subPanel->mapToGlobal(QPoint(0, 0));CRect w_rect;w_rect.left = g_pos.x();w_rect.top = g_pos.y();w_rect.right = g_pos.x() + q_rect.width();w_rect.bottom = g_pos.y() + q_rect.height();ClipCursor(&w_rect);}}else{ClipCursor(nullptr);}
#endif
}

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

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

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

QRect CorrentRect(const QRect & rect, const QRect & subPanel)
{QRect correntRect = rect;//if (correntRect.left() < subPanel.left())//{//  correntRect.moveLeft(subPanel.left());//}if (correntRect.top() < subPanel.top()){correntRect.moveTop(subPanel.top());}//if (correntRect.right() > subPanel.right())//{//  correntRect.moveRight(subPanel.right());//}//if (correntRect.bottom() > subPanel.bottom())//{//  correntRect.moveBottom(subPanel.bottom());//}return correntRect;
}

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

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

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

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

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

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

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

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

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

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

QPoint SmallGroup::MagneticPos(SmallWidget * widget, const QRect & rect)
{QPoint pos(rect.topLeft());if (QWidget * subPanel = dynamic_cast<QWidget *>(parent())){QRect panelRect = subPanel->rect();QRect correntRect = CorrentRect(rect, panelRect);if (m_bMagnetic == false){return correntRect.topLeft();}//修改位置后的ps  更准确pos = correntRect.topLeft();QVector<SmallWidget *> smallWidgets = m_smallVec;smallWidgets.removeOne(widget);int distance = 0;//左边框与subPanel左测比较if (CanMagneticPanel(ME_LEFT, rect.left(), panelRect, distance)){pos.setX(panelRect.left());}else {//左边框与其他窗口右边框比较if (CanMagneticSmall(ME_LEFT, rect.left(), smallWidgets, distance)){pos.setX(distance);}}...}
}

左侧吸附具体分两个情况

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

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

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

bool CanMagneticPanel(MagneticEdge edge, int s, const QRect & subPanel, int & distance)
{int value;switch (edge){case ME_LEFT:value = subPanel.left();break;case ME_TOP:value = subPanel.top();break;case ME_RIGHT:value = subPanel.right();break;case ME_BOTTOM:value = subPanel.bottom();break;default:break;}distance = qFabs(s - value);if (distance <= MagneticDistance){return true;}return false;
}

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

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

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

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

bool CanMagneticSmall(MagneticEdge edge, int moving, const QVector<SmallWidget *> & allWidget, int & distance)
{distance = 10000;bool result = false;int minDistance = 10000;//根据edge的值  动态去获取窗口的边//例如:edge为ME_LEFT时 需要获取其他窗口的ME_RIGHT  去对比for each (SmallWidget  * widget in allWidget){int otherValue = -1; switch (edge){case ME_LEFT:otherValue = widget->geometry().right() + 2;break;case ME_TOP:otherValue = widget->geometry().bottom() + 2;break;case ME_RIGHT:otherValue = widget->geometry().left() - 1;break;case ME_BOTTOM:otherValue = widget->geometry().top() - 1;break;default:break;}if (otherValue != -1){int tmp = qFabs(moving - otherValue);if (minDistance > tmp){minDistance = tmp;if (minDistance <= MagneticDistance){result = true;distance = otherValue;}}}}return result;
}

四、其他

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

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

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

//工具箱,关闭时,同步工具栏按钮状态
void ToolBoxDialog::BindAction(QAction * act)
{connect(m_pToolBoxAct, &QAction::triggered, act, &QAction::setChecked, Qt::UniqueConnection);
}connect(m_pTitle, &ToolBoxTitle::CloseWindow, this, [this](){m_pToolBoxAct->triggered(false);setVisible(false);});//点击工具栏按钮时,打开工具箱
void TemplateLayout::ShowToolBox(bool visible)
{if (m_pToolBox == nullptr){m_pToolBox = new ToolBoxDialog(this);m_pToolBox->BindAction(m_pToolBar->GetToolBoxButton());connect(m_pToolBox, &ToolBoxDialog::SubWindowClicked, m_pPanel, &ContentPanel::CreateSubWindow);}if (visible){m_pToolBox->show();}else{m_pToolBox->hide();}
}

五、相关文章

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

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

如果您觉得文章不错,不妨给个打赏,写作不易,感谢各位的支持。您的支持是我最大的动力,谢谢!!!

很重要–转载声明

  1. 本站文章无特别说明,皆为原创,版权所有,转载时请用链接的方式,给出原文出处。同时写上原作者:朝十晚八 or Twowords

  2. 如要转载,请原文转载,如在转载时修改本文,请事先告知,谢绝在转载时通过修改本文达到有利于转载者的目的。


高仿富途牛牛-组件化(二)-磁力吸附相关推荐

  1. 高仿富途牛牛-组件化(五)-如何去管理炒鸡多的小窗口

    目录 一.概述 二.效果展示 三.功能类 四.设计上的考虑 1.功能拆分 2.关键类 五.相关文章 一.概述 代码写的久了,什么功能都想搞点儿模式.不知道是不是只有我一个人这么想的,做功能时不在是只为 ...

  2. 高仿富途牛牛-组件化(三)-界面美化

    文章目录 一.概述 二.效果展示 三.工具箱 1.布局 a.标题栏 b.客户区 2.功能详解 四.组件模板工具栏 五.其他界面美化 六.使用qss文件 七.相关文章 一.概述 今天是组件化的第三篇文章 ...

  3. 高仿富途牛牛-组件化-如何去管理超级多的小窗口

    一.概述 代码写的久了,什么功能都想搞点儿模式.不知道是不是只有我一个人这么想的,做功能时不在是只为了完成功能,而是在完成功能的同时会去考虑可扩展性怎么样?哪块儿的代码可以复用?又或者哪里需要留更多的 ...

  4. Qt (高仿Visio)流程图组件开发(二) 基本图元绘制 图元间连线绘制

    文章目录 本系列目录 前言 一.如何绘制图元 二.两图元之间如何连线 三.如何实现线跟随图元移动 四.线的位置判断 总结 本系列目录 Qt (高仿Visio)流程图组件开发(一) 效果展示及基本开发框 ...

  5. Qt (高仿Visio)流程图组件开发(三) 图元基类如何定义,流程图多种图元类型实现

    文章目录 本系列目录 前言 一.图元基类的定义 1.图元信息基类结构体 2.图元位置 3.父子对象关系 二.自定义图元实现 1.自定义图元基类(FlowchartGraphicsItem)与Qt原生图 ...

  6. Android之高仿墨迹天气桌面组件(AppWidgetProvider) .

    Android之高仿墨迹天气桌面组件(AppWidgetProvider) . 点击:382 发布时间:2012-10-03 更多0 相信墨迹天气,大家都见过,他在时间显示和天气界面上,很吸引人,今天 ...

  7. Qt (高仿Visio)流程图组件开发(九) 流程图 代码展示

    文章目录 本系列目录 本系列目录 Qt (高仿Visio)流程图组件开发(一) 效果展示及基本开发框架构思 Qt (高仿Visio)流程图组件开发(二) 基本图元绘制 图元间连线绘制 Qt (高仿Vi ...

  8. Kotlin高仿微信-第35篇-支付-二维码收款(二维码)

      Kotlin高仿微信-项目实践58篇详细讲解了各个功能点,包括:注册.登录.主页.单聊(文本.表情.语音.图片.小视频.视频通话.语音通话.红包.转账).群聊.个人信息.朋友圈.支付服务.扫一扫. ...

  9. Flutter高仿微信-第22篇-支付-二维码收款(二维码)

    Flutter高仿微信系列共59篇,从Flutter客户端.Kotlin客户端.Web服务器.数据库表结构.Xmpp即时通讯服务器.视频通话服务器.腾讯云服务器全面讲解. 详情请查看 效果图: 实现代 ...

  10. 纯html+css+js高仿苹果手机时钟小组件

    话不多数说,这是一款用html+css+js开发的高仿苹果手机时钟组件, 作者将苹果的小组件移植到浏览器插件中,可以在BdTab插件安装该组件 HTML <div style="wid ...

最新文章

  1. android获取软件大小,android获取屏幕大小包括状态栏和软件导航栏的大小
  2. .net 页面传参方式总结
  3. 电脑硬盘为什么叫计算机,电脑分区为何从C盘开始?
  4. YOLOv5瓷砖表面瑕疵质检
  5. 精选| 2019年2月R新包推荐
  6. 数据结构和算法——八种常用的排序算法------基数排序的优化(队列实现)
  7. 微信公众号扫码登录(一)—— 获取微信公众号二维码
  8. jpg格式如何转eps路径_jpg/png格式图片转eps格式的方法总结
  9. FANUC机器人示教器介绍
  10. 学习笔记:强化学习之A3C代码详解
  11. 网页页面缩小放大的快捷键
  12. HTML表格循环中合并单元格,table循环实现表格相同列合并
  13. 安然公司特殊目的实体(SPEs)解读
  14. 我爱赚钱吧:你知道自己建网站可以赚钱吗?①
  15. c语言小鱼的游泳时间,小鱼系列简单题参考代码
  16. 企业微信的渠道活码怎么用?到底有什么好处?
  17. Android平台下的图片/视频转Ascii码图片/视频 (一)
  18. 为何iPad 2充电快 但用不了多久?
  19. jenkins部署流程图_jenkins怎样实现自动部署的流程图
  20. 计算机网络协议分析全知识点总结兼期末复习重点

热门文章

  1. CF235C Cyclical Quest
  2. vue实现PC端和移动端的界面切换
  3. 固态硬盘打开计算机就死机,SSD固态硬盘死机卡顿无响应怎么办?SSD卡顿故障处理教程 | 麦田一棵葱...
  4. Android Runtime.getRuntime().exec()
  5. SentiLR:Linguistic Knowledge Enhanced Language Representation for Sentiment Analysis 论文阅读笔记
  6. database rough 1
  7. 广东海洋大学微型计算机控制技术,LCD显示实验(综合性)
  8. 项目踩坑随记 —— getTime() is not a function
  9. 编写程序,先将输入的一系列整数中的最小值与第一个数交换,然后将最大值与最后一个数交换,最后输出交换后的序列。
  10. 中国菜刀与一句话木马之间的原理分析