一、概述
上一篇文章高仿富途牛牛-元件化(一)-支援頁籤拖拽、增刪、小工具我們講述了元件化的一些基礎東西,並有了一個基本的雛形,使用過富途牛牛的同學應該對其中的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