一、概述
好久沒有做業務相關的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個步驟:
- 把頁簽的事件按鈕的父窗口中
- 重寫父類的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。移除的過程可能像下面這樣
- 隱藏被拖拽的按鈕上的占位控件
- 移除對應的面板,即中心內容窗口
- 讓面板跟隨鼠標進行移動,位置在拖拽的頁簽下方
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,暫時就不往外放了。
![]() |
![]() |
很重要--轉載聲明
-
本站文章無特別說明,皆為原創,版權所有,轉載時請用鏈接的方式,給出原文出處。同時寫上原作者:朝十晚八 or Twowords
-
如要轉載,請原文轉載,如在轉載時修改本文,請事先告知,謝絕在轉載時通過修改本文達到有利於轉載者的目的。