上一篇文章高仿富途牛牛-組件化(一)-支持頁籤拖拽、增刪、小工具咱們講述了組件化的一些基礎東西,並有了一個基本的雛形,使用過富途牛牛的同窗應該對其中的gif圖比較熟悉了。雖然效果糙了一點兒,可是該有的基礎功能是已經有了。架構
上述幾個功能在上一篇文章中都已經有了,今天咱們來說述下第二個關鍵功能--磁力吸附和一些其餘小功能ide
磁力吸附,顧名思義就是說窗口移動時,快要接近另外一個窗口邊緣時,會有一種磁性,把正在拖拽的窗口直接吸過去,效果圖以下圖所示。
函數
高仿富途牛牛-組件化(一)-支持頁籤拖拽、增刪、小工具文章最後,我列出了工程中全部的類,並作了每一個類的功能說明。工具
本篇文章的工程代碼在上一版本的基礎上進行了一些優化,代碼的結構也更加的清晰,閱讀起來更容易,主要是增長了磁力吸附和一些同步功能。組件化
下面來思考下磁力吸附這個功能。優化
首先咱們來考慮下磁力吸附,什麼是磁力吸附,明白咱們本身的需求是什麼樣子的?this
磁力表現出來可能像下面這樣:spa
別名:被拖拽窗口(A)、吸附窗口(B)、事件處理(C)代理
有了清晰的需求以後,咱們下面就來考慮怎麼實現咱們的需求,既然要作到小窗口之間進行吸附,想想,這個事件處理無論寫到A窗口仍是B窗口都不是那麼合適。那麼可想而知,除過被拖拽的窗口A和將要的吸附窗口B以外,必然須要引入一個第三者C,進行事件處理,他不必定是一個窗口,主要是要能代理A和B的事件,而且進行各類處理便可。
有了第三者C以後,接下來咱們在第三者C中去處理A的移動事件,循環去判斷是否和其中某個窗口知足了吸附條件。一旦知足吸附條件,咱們就觸發吸附後操做
處理吸附事件時,可能像下面這樣
假設咱們有10個窗口,分別是A一、A二、A三、A4...A九、A10等
當要引入第三者窗口時,咱們可能須要思考以下幾個問題
思考如上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點
限制鼠標可移動區域的接口上邊已經列出來來了,根據參數動態的去限制鼠標移動區域,或者不限制
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 }
看到這個標題是否是有點兒蒙圈,其實這個也很簡單,這裏主要說明的是,咱們移動小窗口時,小窗口不能移出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; }
磁力吸附最複雜的地方可能就是這個功能了,當咱們移動一個窗口時,咱們須要判斷各類狀況,而後去修正咱們的位置。
劃重點1:磁力吸附是說當咱們靠近某個小窗口邊框時,咱們拖拽的窗口能夠被吸附過去,可是須要特別注意,咱們實際移動的距離根本沒有到達那麼多,所以,當咱們鼠標稍微往遠移動一下,窗口應該像被彈開同樣。
劃重點2:要實現重點1,那麼咱們在移動窗口時,就須要有必定的技巧,須要記錄小窗口開始移動的位置,和當前移動的距離。根據移動後的距離判斷是否能夠被吸附,若是被吸附了,那麼咱們直接把窗口移動多一點(或者少一點)距離,達到吸附的位置,可是實際上這個時候,咱們鼠標移動的距離並不等於咱們實際移動的距離,這樣是爲了當咱們鼠標在次偏移時,咱們能夠繼續去判斷是否知足吸附條件,若是不知足則按實際的移動距離。這樣就達到了被彈開的視覺效果
上邊的描述可能理解起來會比較費勁,這裏我在用公式說明下,理解不了就多看幾遍吧
startMovePos:開始移動時,鼠標按下的位置
offsetPos:鼠標當前位置距離開始移動時的位置之間的距離
truthPos:按照鼠標位移,將要移動到的位置。
movePos:窗口將要被移動到的位置。磁力吸附後,會在truthPos上有所誤差
如上四個變量所示,當咱們移動窗口時,可能會產生如下幾個狀況
磁力吸附須要處理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面板的左邊框,同理其餘邊框都是同樣
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; }
循環判斷其餘可被吸附的窗口,找到一個距離最近可悲吸附的窗口,而後進行位置修正。當函數返回爲真時,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(); } }
以上的內容,基本上就是本篇文章的內容全部內容啦!磁力吸附功能基本完成,但願能夠幫到你們。
![]() |
![]() |
很重要--轉載聲明