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

朝十晚八發表於2019-06-17

一、概述

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

五、相關文章

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

以上的內容,基本上就是本篇文章的內容所有內容啦!磁力吸附功能基本完成,希望可以幫到大家。




轉載宣告:本站文章無特別說明,皆為原創,版權所有,轉載請註明:朝十晚八 or Twowords


相關文章