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

1、概述

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

自選和k線這些東西我已經有成型的效果了,能夠參考效果展現,而且已經被我封裝成控件,來需求便可定製函數

2、效果展現

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

可能有一些人不是特別瞭解這個功能,這裏我簡單的說一下:組件化

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

3、實現方案分析

寫這個demo,我基本上用了2天的時間,並且寫的過程當中,代碼結構也有一些變更,達到如今這個效果佈局

一、第一階段

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

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

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

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

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);
}

有了上面這個步驟以後,咱們接下來只須要專心的處理鼠標事件便可

一、鼠標按下
鼠標按下時,咱們這裏須要記錄鼠標按下的位置,方便後續移動咱們的頁籤。

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

二、鼠標移動
當鼠標被按下時,而且進行了移動,這個時候的處理過程就比較複雜了,這裏咱們涉及到2種效果

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

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

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

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

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

三、鼠標擡起
當咱們移動頁籤時,無非就三種狀況

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

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

仔細回想下咱們把按鈕的事件都安裝給了本身的父窗口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窗體中的按鈕,這樣事件就達到了統一。

二、第二階段

爲了解決不一樣工具欄之間能夠相互拖拽頁籤,這裏咱們引入了第三方類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;

三、第三階段

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

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

下面是工程中包含的全部類:

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




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

若是您以爲文章不錯,不妨給個 打賞,寫做不易,感謝各位的支持。您的支持是我最大的動力,謝謝!!!




很重要--轉載聲明

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

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

相關文章
相關標籤/搜索