高仿富途牛牛-組件化(二)-磁力吸附


一、概述

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

如上四個變量所示,當我們移動窗口時,可能會產生以下幾個情況

  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. 如要轉載,請原文轉載,如在轉載時修改本文,請事先告知,謝絕在轉載時通過修改本文達到有利於轉載者的目的。



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM