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

1、概述

上一篇文章高仿富途牛牛-組件化(一)-支持頁籤拖拽、增刪、小工具咱們講述了組件化的一些基礎東西,並有了一個基本的雛形,使用過富途牛牛的同窗應該對其中的gif圖比較熟悉了。雖然效果糙了一點兒,可是該有的基礎功能是已經有了。架構

  • 工具欄頁籤拖拽
  • 工具欄之間頁籤拖拽
  • 小工具
  • 多頁籤架構
  • 小窗口

上述幾個功能在上一篇文章中都已經有了,今天咱們來說述下第二個關鍵功能--磁力吸附和一些其餘小功能ide

2、效果展現

磁力吸附,顧名思義就是說窗口移動時,快要接近另外一個窗口邊緣時,會有一種磁性,把正在拖拽的窗口直接吸過去,效果圖以下圖所示。

函數






3、磁力吸附

高仿富途牛牛-組件化(一)-支持頁籤拖拽、增刪、小工具文章最後,我列出了工程中全部的類,並作了每一個類的功能說明。工具

本篇文章的工程代碼在上一版本的基礎上進行了一些優化,代碼的結構也更加的清晰,閱讀起來更容易,主要是增長了磁力吸附和一些同步功能。組件化

下面來思考下磁力吸附這個功能。優化

首先咱們來考慮下磁力吸附,什麼是磁力吸附,明白咱們本身的需求是什麼樣子的?this

磁力表現出來可能像下面這樣:spa

  1. 不一樣子窗口之間但願進行磁力吸附,也就是說窗口移動時,能夠被吸附到鄰近的窗口邊框上
  2. 不一樣頁籤之間不須要關聯
  3. 鼠標不能移動到subPanel以外

別名:被拖拽窗口(A)、吸附窗口(B)、事件處理(C)代理

有了清晰的需求以後,咱們下面就來考慮怎麼實現咱們的需求,既然要作到小窗口之間進行吸附,想想,這個事件處理無論寫到A窗口仍是B窗口都不是那麼合適。那麼可想而知,除過被拖拽的窗口A和將要的吸附窗口B以外,必然須要引入一個第三者C,進行事件處理,他不必定是一個窗口,主要是要能代理A和B的事件,而且進行各類處理便可。

有了第三者C以後,接下來咱們在第三者C中去處理A的移動事件,循環去判斷是否和其中某個窗口知足了吸附條件。一旦知足吸附條件,咱們就觸發吸附後操做

處理吸附事件時,可能像下面這樣

假設咱們有10個窗口,分別是A一、A二、A三、A4...A九、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. 獲取最鄰近的可被吸附的窗口

一、限制鼠標區域

限制鼠標可移動區域的接口上邊已經列出來來了,根據參數動態的去限制鼠標移動區域,或者不限制

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 
}

二、修正窗口能夠移動的區域

看到這個標題是否是有點兒蒙圈,其實這個也很簡單,這裏主要說明的是,咱們移動小窗口時,小窗口不能移出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;
}

三、獲取最鄰近的可被吸附的窗口

磁力吸附最複雜的地方可能就是這個功能了,當咱們移動一個窗口時,咱們須要判斷各類狀況,而後去修正咱們的位置。

劃重點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;
}

4、其餘

工具箱窗口和工具欄工具按鈕聯動,按理說這個功能屬於比較常見的功能,可是這裏我也想拿出來跟你們分享下,這裏我主要是藉助了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();
    }
}

5、相關文章

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

以上的內容,基本上就是本篇文章的內容全部內容啦!磁力吸附功能基本完成,但願能夠幫到你們。


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




很重要--轉載聲明

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

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

相關文章
相關標籤/搜索