1. 程式人生 > >Qt quick實現無邊框可拖拽風格

Qt quick實現無邊框可拖拽風格

由來:

    現在大量的軟體採用簡潔的風格,即移除系統的標題欄及邊框並自己實現標題欄以達到定製的目的,由於我們寫要用Qt寫一個類似IM的軟體,故有此需求。

方法:

去除系統邊框比較簡單,setFlags(Qt::Window | Qt::FramelessWindowHint);即可,但下一步要實現窗體的拖拽和縮放。

1,最簡單的方法:

    這種方法比較簡單直觀,即點選滑鼠的時候記錄下滑鼠位置,然後在滑鼠移動的時候對窗體進行移動。示例程式碼如下:

 MouseArea { //為視窗新增滑鼠事件
    property int xMouse //儲存滑鼠x座標
    property int
yMouse //儲存滑鼠y座標 anchors.fill: parent acceptedButtons: Qt.LeftButton //只處理滑鼠左鍵 drag.filterChildren: true onPressed: { //接收滑鼠按下事件 xMouse = mouse.x yMouse = mouse.y } onPositionChanged: { //滑鼠按下後改變位置 loginScreen.x = loginScreen.x + (mouse.x - xMouse) loginScreen.y = loginScreen.y + (mouse
.y - yMouse) } }

    以上方法簡單直觀,但當窗體比較複雜的時候將發生窗體移動比較遲鈍,甚至,當從上標題快速拖拽窗體時,滑鼠移出窗體,而窗體不再移動的問題。
    網上的一個案例,大家可以參考http://blog.sina.com.cn/s/blog_a6fb6cc90101au8r.html

2,改進方案:

    觀察系統預設的帶標題欄的窗體,發現,拖動窗體時,窗體並不移動,而是由一個矩形框隨著滑鼠移動,當鬆開滑鼠後窗體才進行一次性地移動。
    於是,有了一個可行的方案(因為後面有更優的方案,但是如果後面的方案不行,這個方案可能成為最後的方案),當拖拽的時候繪製一個移動的和原窗體等大小的矩形進行移動,最後再一次性地移動(相信系統的拖拽也是採用類似的方案)。具體實施略,可以自行測試。

3,繼續改進方案,應該是一個較優方案:

當視窗確定滑鼠位置時,Windows向視窗傳送WM_NCHITTEST訊息,可以處理該訊息,使得只要滑鼠在視窗內,Windows便認為滑鼠在標題條上。這需要過載CWnd類處理WM_NCHITTEST訊息的OnNcHitTest函式,在函式中呼叫父類的該函式,如果返回HTCLIENT,說明滑鼠在視窗客戶區內,使過載函式返回HTCAPTION,使Windows誤認為滑鼠處於標題條上。

UINT MyWndDlg::OnNcHitTest(CPoint point)
{
    // 取得滑鼠所在的視窗區域
    UINT nHitTest = CDialog::OnNcHitTest(point);

    // 如果滑鼠在視窗客戶區,則返回標題條代號給Windows
    // 使Windows按滑鼠在標題條上類進行處理,即可單擊移動視窗
    return (nHitTest==HTCLIENT) ? HTCAPTION : nHitTest;
}

    知道了原理咱便可以開工了!!
    接著又發現瞭如下例子:http://blog.csdn.net/kfbyj/article/details/9284923,不過貌似博主文章是在Qt4時寫的,現在早就沒有MainWindow::winEvent(MSG *message, long *result)函數了呀!!
    不過既然Qt堵了一扇門,他一定會開啟另一扇窗,於是有了nativeEvent函式~~
    參看該部落格http://blog.csdn.net/tujiaw/article/details/43836039,哈哈,貌似一切都搞定了~ 確實,我們可以開始寫自己的無邊框窗體了~~
    上程式碼:

#include "framelessWindow.h"
#include <QQuickItem>

FramelessWindow::FramelessWindow(QWindow *parent) : QQuickWindow(parent)
{
    setFlags(Qt::Window | Qt::FramelessWindowHint);
}

FramelessWindow::~FramelessWindow()
{

}

void FramelessWindow::setTitleBar(QQuickItem *titleBar)
{
    m_pTitleBar = titleBar;
}

bool FramelessWindow::nativeEvent(const QByteArray &eventType, void *message, long *result)
{
    Q_UNUSED(eventType);
    if ( windowState() && Qt::WindowMaximized) {
        return false;
    }

    const int HIT_BORDER = 8;
    const MSG *msg=static_cast<MSG*>(message);
    if(msg->message == WM_NCHITTEST) {
        int xPos = ((int)(short)LOWORD(msg->lParam)) - this->frameGeometry().x();
        int yPos = ((int)(short)HIWORD(msg->lParam)) - this->frameGeometry().y();

        if (m_pTitleBar && m_pTitleBar->contains(QPointF(xPos,yPos))) {
            *result = HTCAPTION;
        }
        if (m_pTitleBar) {
            if (m_pTitleBar->contains(QPointF(xPos,yPos)) && !m_pTitleBar->childAt(xPos,yPos)) {
                *result = HTCAPTION;
                return true;
            }
        } else {
            /*
            if(contentItem()->childItems()[0]->childAt(xPos,yPos) == 0)
            {
                *result = HTCAPTION;
            }*/
        }

        auto child = contentItem()->childAt(xPos,yPos);
        if(child)
        {
            if (child != m_pTitleBar) {
                return false;
            } else {
                if (m_pTitleBar && !m_pTitleBar->childAt(xPos,yPos)) {
                    *result = HTCAPTION;
                    return true;
                }
                return false;
            }
        }

        if(xPos > 0 && xPos < HIT_BORDER) {
            *result = HTLEFT;
        }
        if(xPos > (this->width() - HIT_BORDER) && xPos < (this->width() - 0)) {
            *result = HTRIGHT;
        }
        if(yPos > 0 && yPos < HIT_BORDER) {
            *result = HTTOP;
        }
        if(yPos > (this->height() - HIT_BORDER) && yPos < (this->height() - 0)) {
            *result = HTBOTTOM;
        }
        if(xPos > 0 && xPos < HIT_BORDER && yPos > 0 && yPos < HIT_BORDER) {
            *result = HTTOPLEFT;
        }
        if(xPos > (this->width() - HIT_BORDER) && xPos < (this->width() - 0) && yPos > 0 && yPos < HIT_BORDER) {
            *result = HTTOPRIGHT;
        }
        if(xPos > 0 && xPos < HIT_BORDER && yPos > (this->height() - HIT_BORDER) && yPos < (this->height() - 0)) {
            *result = HTBOTTOMLEFT;
        }
        if(xPos > (this->width() - HIT_BORDER) && xPos < (this->width() - 0) && yPos > (this->height() - HIT_BORDER) && yPos < (this->height() - 0)) {
            *result = HTBOTTOMRIGHT;
        }
        return true;
    }
    return false;
}

    大體和前人的程式碼類似,略有些小改動,下面會說這些改動的原因。

4,進一步,將c++類匯出到qml中使用:

    其實下一步相對來說比較簡單,套路即可:在main函式中加入如下程式碼:

qmlRegisterType(“com.FramelessWindow”, 1, 0, “FramelessWindow”);

話說要記得FramelessWindow得繼承自QObject才能匯出哦,還有,我們的類得是用在主窗體下,所以繼承自QQuickWindow

#ifndef MYWINDOW_H
#define MYWINDOW_H

#include <QQuickWindow>

class FramelessWindow : public QQuickWindow
{
    Q_OBJECT
public:
    explicit FramelessWindow(QWindow *parent = 0);
    ~FramelessWindow();
    Q_INVOKABLE void setTitleBar(QQuickItem *titleBar);
signals:

public slots:

    // QWidget interface
protected:
    bool nativeEvent(const QByteArray &eventType, void *message, long *result);
private:
    QQuickItem *m_pTitleBar;
};

#endif // MYWINDOW_H

    於是一切搞定,最後附上具體使用的qml

import QtQuick.Window 2.2
import QtWebEngine 1.2
import QtQuick 2.5
import QtQuick.Controls 1.4
import QtWebChannel 1.0
import com.FramelessWindow 1.0

FramelessWindow {
    width: 500
    height: 859
    id:window

    TitleBar {
        id: titleBar
        anchors.top: parent.top
        anchors.left: parent.left
        width: parent.width
        height: 100
        color: "blue"
    }

    Component.onCompleted: {
        window.setTitleBar(titleBar)
    }    
}

    看,我們指定了一個TitleBar(其實就是一個Rectangle),注意setTitleBar一定得是Q_INVOKABLE或者定義為slot,否則qml中無法呼叫!
還有這段

if (m_pTitleBar && m_pTitleBar->contains(QPointF(xPos,yPos))) {
    *result = HTCAPTION;
}
if (m_pTitleBar) {
    if (m_pTitleBar->contains(QPointF(xPos,yPos)) && !m_pTitleBar->childAt(xPos,yPos)) {
        *result = HTCAPTION;
        return true;
    }
} else {
    /*
    if(contentItem()->childItems()[0]->childAt(xPos,yPos) == 0)
    {
        *result = HTCAPTION;
    }*/
}

    用於判斷是否設定了titlebar,而且點選位置十分在控制元件下,比較簡單~~