高仿富途牛牛-組件化(一)-支持頁簽拖拽、增刪、小工具


一、概述

好久沒有做業務相關的UI功能了,比較炫酷的交互效果也寫的少了,最近花了2天時間寫了一個簡易的高仿富途牛牛組件化的功能,當然了這只是一個初步的效果,而且沒有做貼圖、美化等工作,但是基本的功能已經有了。本篇文章只是作為組件化的一個開始,后續還會陸續引入更多關於組件化的介紹,相信功能也會越來越豐富。除此之外,富途牛牛的一些其他高級功能也會陸續引入,不乏有k線、分時、五日、指標、自選這樣的復雜功能。

自選和k線這些東西我已經有成型的效果了,可以參考效果展示,並且已經被我封裝成控件,來需求即可定制

二、效果展示

說的再多,也不如貼一張效果圖來的實在,下邊的圖展示了附圖牛牛組件化的一個簡單demo,雖然沒有數據,但是該有的基礎交互流程已經都有了。用過富途牛牛的人應該都知道。

可能有一些人不是特別了解這個功能,這里我簡單的說一下:

  • 主窗口TemplateLayout是一個基礎的頁簽定制頁面,上邊的頁簽可以被拖拽,支持在自身組件化頁面交換位置,如果出了自身組件頁面時,它會自動生成一個新的TemplateLayout組件頁面。
  • 新的組件頁面支持頁簽欄移動窗口,注意主組件頁面不支持(需求限制)
  • 頁簽欄的最左側是一個工具按鈕,點擊可以喚醒工具箱
  • 工具箱里邊有各種小工具,當我們點擊時,就會彈出小窗口subWindow
  • 小窗口subWindow支持選中高亮
  • 不同頁簽下彈出的小窗口subWindow是獨立的,切換頁簽時,subWindow不會跟隨,也就是說在不同頁簽創建的小窗口只顯示在該頁簽下
  • 不同組件化頁面(TemplateLayout)之間的頁簽是支持互相拖拽
  • 小工具窗口和TemplateLayout窗口是一一對應關系,並且支持移動聯動,主窗口移動時,小工具自動移動;小工具移動時,主窗口不動

三、實現方案分析

寫這個demo,我基本上用了2天的時間,而且寫的過程中,代碼結構也有一些變動,達到現在這個效果

1、第一階段

當時只考慮了一個主組件化界面TemplateLayout的實現,把這個頁面總共分了這么幾個部門:主Frame、頂部工具條、頁簽窗體(支持拖拽)、中心內容窗體、工具箱、小窗體subWindow。

其中值得注意的有這么幾個地方

  • 工具箱和主窗口的聯動
  • 頁簽的拖拽效果

除過上述兩個問題以外,第一階段暫時沒有其他比較棘手的問題,簡單的頁面編寫我這里就不做過多介紹,主要分析下上邊兩個問題的處理過程,也是比較重要的處理過程

a、工具箱和主窗口的聯動

這個問題,說實話,當時真是想了好幾個小時,花費了比較長的事件。

自開始的想法就是重寫父窗口的moveEvent函數,當窗體移動時,我把移動的偏移量告知工具箱,這樣工具箱就可以進行聯動了,后來進行實現,發現工具箱移動的總是很慢,哎!moveEvent肯定是優化了一些調用,真是坑爹啊

想了一會兒了,到了吃飯時間了,算了,還是換換腦子,或許吃完飯回來就有方案了,哈哈哈,事實上還就是這樣,鎖屏去吃飯啦!!!

程序員即是閑不下來,去吃飯的路上又開始想了,不過這次我想到了方案,我們之前都是考慮怎么傳遞變化量到工具箱,那么反過來我們如果只告訴工具箱主窗口位置發生變化了呢!!!,讓工具箱自己去移動,只要保證和之前的主窗口的相對位置不變即可。你說什么?相對位置不變!,大家伙是不是也想到了呢,對了,就是這么簡單了,我們只需要在工具箱自己移動的時候更新這個相對位置即可,當主窗口移動時,我們仍然是要保證相對位置不變。

就是這么簡單!就是這么任性!

void ToolBoxDialog::LinkMove()
{
	if (QWidget * parent = parentWidget())
	{
		QPoint globalPos = parent->mapToGlobal(m_relativePos);
		move(globalPos);
	}
}

b、頁簽拖拽

在做這個組件化拖拽之前,我也簡單的做過一些拖拽功能,因此這里做這個東西也不困難。首先就是我們的技術方案選擇了,Qt他為我們提供了一些拖拽上的功能實現,但是定制化不強,一旦我們想做的更美觀,交互上更豐富一些,使用原有的操作邏輯就會遇到一些困難,因此這里我們完全自己實現一個拖拽的效果

看這里-我們通過過濾鼠標的3種事件,來模擬拖拽。哪三種事件呢?鼠標按下、鼠標移動和鼠標抬起

下面我們分析下具體的事件處理流程,既然我們想要過濾這三種事件,那么我們就必須要找一個過濾對象,來過濾所有的頁簽按鈕事件,這里很自然的我就想到了他們的父窗口DragTabWidget,了解Qt的事件循環的人應該都知道接下來我要干什么了,2個步驟:

  1. 把頁簽的事件按鈕的父窗口中
  2. 重寫父類的EventFilter函數

代碼可能會像下面這樣

//按鈕把自己的事件先讓父類處理 this這里表示父窗口
tabButton->installEventFilter(this);

//父類重寫eventFilter函數
bool DragTabWidget::eventFilter(QObject * watched, QEvent * event)
{
	TabButton * button = dynamic_cast<TabButton *>(watched);
	if (button && m_buttonMaps.contains(button->GetID()))
	{
	}
	
	return __super::eventFilter(watched, event);
}

有了上面這個步驟之后,我們接下來只需要專心的處理鼠標事件即可

1、鼠標按下
鼠標按下時,我們這里需要記錄鼠標按下的位置,方便后續移動我們的頁簽。

if (event->type() == QEvent::MouseButtonPress)
{
	QMouseEvent * mouseEvent = static_cast<QMouseEvent *>(event);
	m_pressGlobalPos = button->mapToGlobal(mouseEvent->pos());
	m_pressButtonPos = button->pos();
}

2、鼠標移動
當鼠標被按下時,並且進行了移動,這個時候的處理過程就比較復雜了,這里我們涉及到2種效果

首先就是我們的頁簽在自身的工具欄種移動,這樣的情況比較好處理一些,我們只需要把根據當前的鼠標位置移動我們的占用控件和被拖拽的頁簽,

占位控件:就是效果展示圖中,我們看到的空白區域,主要是告訴使用者,當鼠標抬起時,控件將會在被移動到這個地方

這里邊有一個小技巧:就是我們拖拽頁簽的時候,我們構造一個被拖拽頁簽的圖片,跟隨鼠標移動即可,這樣省時、省力又省心,最主要還是bug少。

其次呢當我們的鼠標移除工具欄時,我們的頁簽也是要跟着被拖出原有的組件化頁面的,也就是原有的TemplateLayout。移除的過程可能像下面這樣

  1. 隱藏被拖拽的按鈕上的占位控件
  2. 移除對應的面板,即中心內容窗口
  3. 讓面板跟隨鼠標進行移動,位置在拖拽的頁簽下方
if (event->type() == QEvent::MouseMove)
{
	if (IsMoveTab())
	{
		//移動當前選中按鈕
		MoveIn(button);
		MoveSnapSeat(button);
	}
	else
	{
		//把當前選中按鈕拖出布局
		MoveOut(button);
	}
}

3、鼠標抬起
當我們移動頁簽時,無非就三種情況

  • 原有工具欄移動
  • 新建一個組件化頁面
  • 移動到另一個組件化頁面

處理代碼如下,這是第一個階段的代碼,有點兒問題,不支持第三種情況。

仔細回想下我們把按鈕的事件都安裝給了自己的父窗口DragTabWidget,可想而知不同DragTabWidget之間可能也不能進行交互了,因為他們走的都是自己的事件循環、和處理邏輯。

if (event->type() == QEvent::MouseButtonRelease)
{
	if (IsMoveTab())
	{
		//移動拖拽的按鈕到占位位置
		MoveDragButton();
	}
	else
	{
		//添加新的獨立窗口
		//button 添加到新的TemplateLayout布局中
		emit ButtonMoveOutCompleted(button->GetID());

		//原有布局中 如果只剩下一個按鈕 關閉按鈕將不讓使用
		if (m_buttonMaps.size() == 1)
		{
			m_buttonMaps.values().first()->SetCloseEanble(false);
		}
	}
}

這不第一個版本有了解決不了的問題,這里我才進行了適當的重構,引入第二個版本。

第二個版本的修改主要是,按鈕的事件不在交給父窗口去優先處理了,而是交給一個第三者,這個第三者接收了所有的按鈕事件,包括不同DragTabWidget窗體中的按鈕,這樣事件就達到了統一。

2、第二階段

為了解決不同工具欄之間可以相互拖拽頁簽,這里我們引入了第三方類TabMoveManager,這個類專門負責處理所有的按鈕事件,處理過程和第一階段類型,這里我把關鍵部分的代碼貼出來

bool TabMoveManager::eventFilter(QObject * watched, QEvent * event)
{
	TabButton * button = dynamic_cast<TabButton *>(watched);
	if (button && m_tabButtonMap.contains(button->GetID()))
	{
		if (event->type() == QEvent::MouseButtonPress)
		{
			QMouseEvent * mouseEvent = static_cast<QMouseEvent *>(event);
			m_pressGlobalPos = mouseEvent->globalPos();
			m_pressButtonPos = button->mapToParent(QPoint(0, 0));
			
			//記錄從當前被拖拽的tab和模板
			ResolveDragTab(button);
		}
		else if (event->type() == QEvent::MouseMove)
		{
			if (IsMoveTab())
			{
				//移動當前選中按鈕
				MoveIn(button);
				MoveSnapSeat(button);
			}
			else
			{
				//把當前選中按鈕拖出布局
				MoveOut(button);
			}
		}
		else if (event->type() == QEvent::MouseButtonRelease)
		{
			if (IsMoveTab())
			{
				//移動拖拽的按鈕到候選模板占位位置
				MoveDragButton();
			}
			else //空白處 釋放鼠標 需要新構造一個TemplateLayout
			{
				emit NeedNewTemplateLayout(button->GetID());
			}
			//清理原有模板布局
			if (m_pDragTemplate != m_pCandidateTemplate && m_pCandidateTemplate != nullptr)
			{
				m_pDragTemplate->CleanUP(button->GetID());
			}

			//原有布局中 如果只剩下一個按鈕 關閉按鈕將不讓使用
			if (m_pDragTabWidget->GetButtonCount() == 1)
			{
				m_pDragTabWidget->GetToolButton()->SetCloseEanble(false);
			}
            ···
		}
	}

	return __super::eventFilter(watched, event);
}

除了處理拖拽事件以外,他還起到了一個容器的作用,所有的subPanel我都注冊到這個類中,其他的地方都可以通過這個類來獲取指定Panel

void RegisterSubPanel(const QString &, SubContentWidget *);
void UnregisterSybPanel(const QString &);

SubContentWidget * GetSubPanel(const QString &) const;

3、第三階段

前兩個階段已經把主要的功能基本都實現了,但是代碼結構上,包括一些類負責的功能上還是有一些問題。

第三階段主要對代碼進行了細微的重構,工程代碼結構下圖所示

下面是工程中包含的所有類:

  • ContentPanel:中心窗口,里邊包含了一個個SubPanel,每個SubPanel對應一個頁簽。該類的內存中記錄了很多窗體指針,但只負責內存維護,不對窗體的打開、關閉進行操作
  • DragTabWidget:頁簽所屬窗體,支持移動整個組件化窗口
  • DragToolBar:工具欄,內部包括調出工具箱的按鈕和DragTabWidget
  • SmallWidget:小窗體,支持各種小工具頁面
  • TabButton:頁簽
  • TabMoveManager:靜態單例,負責處理頁簽移動事件
  • TemplateLayout:組件化窗口
  • ToolBoxDialog:工具箱
  • TemplateDefine:頭文件,定義了很多變量




以上代碼基本把視線組件化功能的主要邏輯講完了,具體的代碼量比較大,而且是一個不成形的demo,暫時就不往外放了。

如果您覺得文章不錯,不妨給個 打賞,寫作不易,感謝各位的支持。您的支持是我最大的動力,謝謝!!!




很重要--轉載聲明

  1. 本站文章無特別說明,皆為原創,版權所有,轉載時請用鏈接的方式,給出原文出處。同時寫上原作者:朝十晚八 or Twowords

  2. 如要轉載,請原文轉載,如在轉載時修改本文,請事先告知,謝絕在轉載時通過修改本文達到有利於轉載者的目的。



免責聲明!

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



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