高仿富途牛牛-组件化(二)-磁力吸附
目录
一、概述
上一篇文章我们讲述了组件化的一些基础东西,并有了一个基本的雏形,使用过富途牛牛的同学应该对其中的gif图比较熟悉了。虽然效果糙了一点儿,但是该有的基础功能是已经有了。
- 工具栏页签拖拽
- 工具栏之间页签拖拽
- 小工具
- 多页签架构
- 小窗口
上述几个功能在上一篇文章中都已经有了,今天我们来讲述下第二个关键功能--磁力吸附和一些其他小功能
二、效果展示
磁力吸附,顾名思义就是说窗口移动时,快要接近另一个窗口边缘时,会有一种磁性,把正在拖拽的窗口直接吸过去,效果图如下图所示。
三、磁力吸附
文章最后,我列出了工程中所有的类,并做了每个类的功能说明。
本篇文章的工程代码在上一版本的基础上进行了一些优化,代码的结构也更加的清晰,阅读起来更容易,主要是增加了磁力吸附和一些同步功能。
下面来思考下磁力吸附这个功能。
首先我们来考虑下磁力吸附,什么是磁力吸附,明白我们自己的需求是什么样子的?
磁力表现出来可能像下面这样:
- 不同子窗口之间希望进行磁力吸附,也就是说窗口移动时,可以被吸附到邻近的窗口边框上
- 不同页签之间不需要关联
- 鼠标不能移动到subpanel之外
别名:被拖拽窗口(a)、吸附窗口(b)、事件处理(c)
有了清晰的需求之后,我们下面就来考虑怎么实现我们的需求,既然要做到小窗口之间进行吸附,想一想,这个事件处理不管写到a窗口还是b窗口都不是那么合适。那么可想而知,除过被拖拽的窗口a和将要的吸附窗口b之外,必然需要引入一个第三者c,进行事件处理,他不一定是一个窗口,主要是要能代理a和b的事件,并且进行各种处理即可。
有了第三者c之后,接下来我们在第三者c中去处理a的移动事件,循环去判断是否和其中某个窗口满足了吸附条件。一旦满足吸附条件,我们就触发吸附后操作
处理吸附事件时,可能像下面这样
假设我们有10个窗口,分别是a1、a2、a3、a4...a9、a10等
- 当我们拖拽a1窗口时,其他窗口都是吸附窗口(b)
- 当我们拖拽a2窗口时,a1和其他窗口都是吸附窗口(b)
- 同理,当我们拖拽其他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、限制鼠标区域
限制鼠标可移动区域的接口上边已经列出来来了,根据参数动态的去限制鼠标移动区域,或者不限制
limitcursor(bool)
当进行拖拽小窗口时,我们需要限制鼠标不能移除subpanel,如果不理解subpanel是什么东西,需要仔细去阅读下上一篇文章。
限制鼠标移动区域的代码如下所示,主要是使用了clipcursor这个win32接口,代码比较简单,这里就不做详细说明了。
void smallgroup::limitcursor(bool limit) { #ifdef q_os_win if (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上有所偏差
如上四个变量所示,当我们移动窗口时,可能会产生以下几个情况
- 没有磁力吸附,直接移动到truthpos
- 有磁力吸附,移动到被吸附的窗口边框跟前(会产生一个便宜值value,被吸过去了)
- 上一次有磁力吸附,本次不满足处理吸附,直接移动到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); } } ... } }
左侧吸附具体分两个情况
- 移动窗口a和subpanel之间的吸附
- 移动窗口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(); } }
五、相关文章
以上的内容,基本上就是本篇文章的内容所有内容啦!磁力吸附功能基本完成,希望可以帮到大家。
转载声明:本站文章无特别说明,皆为原创,版权所有,转载请注明: or twowords
上一篇: 海瑞为什么敢骂嘉靖帝?他不害怕吗