處理 TreeView 的 Drag 與 Drop
前言
在先前的 TreeView 的基本應用 與 在 TreeView 使用右鍵選單 裡介紹了 TreeView 的常用功能,但少提到了一個常用的功能,就是 Drag 與 Drop ,節點的資料能夠直接在 Widget 上編輯是一個非常方便的功能,在此把學習的過程做個紀錄。
內容
先到 [ GitLab ] HelloQt 下載範例,這次應用的專案路徑
(HelloQt' directory)/TreeView/DragAndDrop,執行結果如下
|
範例的執行結果 |
範例會給予預設的節點,並可以在 Widget 上透過拖曳(Drag)來編輯節點。這次雖然只是處理 Drag 與 Drop 的事件,但不幸的是 Qt 並沒有提供透過 Connect 事件的方式來處理,這次使用的方式是透過"繼承"的方式來處理 Drag 與 Drop 的事件,這種透過繼承的方式可以參考 特製 Widget ,兩者的差別是該篇繼承的是 QWidget ,而本篇繼承的是 QTreeView,在新增 Class 的精靈介面可以參考下圖
|
新增繼承 QTreeView 的類別 |
精靈介面加完後的宣告程式碼並不完整,像是 include 的相關程式碼並不會自動被加入,可以比對範例的宣告來看差異是什麼,接著就直接看到宣告的部分,程式碼如下
#include <QTreeView>
#include <QVector>
#include <QDragEnterEvent>
#include <QDragMoveEvent>
#include <QDragLeaveEvent>
#include <QDropEvent>
#include <QStandardItemModel>
class MyTreeView : public QTreeView
{
Q_OBJECT
public:
explicit MyTreeView(QWidget *parent = nullptr);
//
void CustomSetModel(QStandardItemModel* pModel);
void HideColumnByList();
protected:
void dragEnterEvent(QDragEnterEvent *event) override;
void dragMoveEvent(QDragMoveEvent *event) override;
void dragLeaveEvent(QDragLeaveEvent *event) override;
void dropEvent(QDropEvent *event) override;
//
private:
QVector<int> m_cHideColumnList;
};
處理 Drag 與 Drop 的事件有 dragEngterEvent() 、 dragMoveEvent() 、 dragLeaveEvent() 與 dropEvent() , CustomSetModel() 與 HideColumnByList() 是自己定義的 Function ,待會兒在實作的部分會進一步說明它們的功能,接下來就看到實作的部分,先看到自訂的 Function ,如下
void MyTreeView::CustomSetModel(QStandardItemModel *pModel)
{
this->setModel( pModel );
HideColumnByList();
}
void MyTreeView::HideColumnByList()
{
for( auto i = 0 ; i < m_cHideColumnList.size() ; i++ )
{
this->hideColumn( m_cHideColumnList[i] );
}
}
HideColumnByList() 的部分會依據成員"m_cHideColumn"裡所存的要隱藏的 Column 來喚起 QTreeView::hideColumn() ,為什麼要提供這個 Function 呢?因為 QTreeView 在處理某些事件後會把隱藏的 Column 再次變成顯示 ,所以需要這個 Function 來恢復我們期望隱藏的 Column ,CustomSetModel() 會在喚起 QTreeView::setModel() 後喚起 HideColumnByList() 隱藏指定的 Column ,接著看到主程式的 MainWindow::MainWindow() ,如下
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
//
//
QStandardItemModel* pModel = new QStandardItemModel( ui->treeView );
pModel->setHorizontalHeaderLabels( QStringList()<<QStringLiteral( "Name" )<<QStringLiteral( "Comment" ) );
QStandardItem* pItemRoot1 = new QStandardItem( QStringLiteral( "root1" ) );
//first
pItemRoot1->setChild( 0 , 0 , new QStandardItem( QStringLiteral( "child" ) ) );
pItemRoot1->setChild( 0 , 1 , new QStandardItem( QStringLiteral( "I'm child" ) ) );
//second
pItemRoot1->setChild( 1 , 0 , new QStandardItem( QStringLiteral( "child1" ) ) );
//
pModel->appendRow( pItemRoot1 );
//
QStandardItem* pItemRoot2 = new QStandardItem( QStringLiteral( "root2" ) );
pItemRoot2->setChild( 0 , 0 , new QStandardItem( QStringLiteral( "child" ) ) );
pItemRoot2->setChild( 0 , 1 , new QStandardItem( QStringLiteral( "I'm child" ) ) );
//
pModel->appendRow( pItemRoot2 );
//
ui->treeView->CustomSetModel( pModel );
}
前面就單純的加入預設節點不再贅述,可以參考 TreeView 的基本應用 ,重點在最後一行使用的是 MyTreeView::CustomSetModel() 而非 QTreeView::setModel() ,這是因為這樣可以隱藏指定的 Column 。接著看到 MyTreeView::MyTreeView() ,如下
MyTreeView::MyTreeView(QWidget *parent):
QTreeView(parent)
{
this->setDragEnabled( true );
this->setDragDropMode( QAbstractItemView::InternalMove );
//
this->setHeaderHidden( true );
this->setSelectionMode( QAbstractItemView::ExtendedSelection );
//
m_cHideColumnList.push_back( 1 );
}
由於 QTreeView 預設是把 Drag 與 Drop 的功能關閉,所以透過 setDragEnabled() 與 setDragDropMode() 來開啟,接著透過 setHeaderHidden() 來關閉上方的欄位,然後透過 setSelectionMode() 來開啟可多選,最後在成員"m_cHideColumnList"加入要隱藏的 Column ,最後就直接看到 Drag 和 Drop 事件的實作,如下
void MyTreeView::dragEnterEvent(QDragEnterEvent *event)
{
qDebug( "dragEnterEvent" );
//
QTreeView::dragEnterEvent( event );
}
void MyTreeView::dragMoveEvent(QDragMoveEvent *event)
{
qDebug( "dragMoveEvent" );
//
QTreeView::dragMoveEvent( event );
}
void MyTreeView::dragLeaveEvent(QDragLeaveEvent *event)
{
qDebug( "dragLeaveEvent" );
//
QTreeView::dragLeaveEvent( event );
}
void MyTreeView::dropEvent(QDropEvent *event)
{
qDebug( "dropEvent" );
QModelIndex index = indexAt(event->pos() );
qDebug( "dropEvent node:%s" , index.data().toString().toLocal8Bit().data() );
DropIndicatorPosition dropIndicator = dropIndicatorPosition();
switch ( dropIndicator ) {
case DropIndicatorPosition::OnItem:
qDebug("onItem");
break;
case DropIndicatorPosition::AboveItem:
qDebug("AboveItem");
break;
case DropIndicatorPosition::BelowItem:
qDebug("BelowItem");
break;
defadefault:
qDebug("Unknown DropIndicatorPosition!");
break;
}
//
for( auto i = 0 ; i < this->selectedIndexes().size() ; i++ )
{
if( this->selectedIndexes().at(i).column() != 0 )
continue;
//
qDebug( "source node:%s" , this->selectedIndexes().at(i).data().toString().toLocal8Bit().data() );
}
//
HideColumnByList();
//
QTreeView::dropEvent(event);
}
dragEngterEvent() 、 dragMoveEvent() 與 dragLeaveEvent() 由於這次用不到,所以只列印偵錯訊息,這樣方便了解事件觸發的時機,所以範例重點會放在 dropEvent() ,這裡說明一下,範例希望得到的資訊是把那些節點丟到某個節點 的相關資訊,但不幸的是 QDropEvent 裡所提供的資訊並不完整,必須搭配 QTreeView 的相關 Funciton 才能達到目的,要得到被丟到的節點資訊 可以透過 QTreeEvent::Pos() 與 QTreeView::indexAt() 來的到資料,但丟到某個節點的想法有點瑕疵,如果丟到 root 那這個節點會是無效的嗎?答案是不會,因為 Qt 的這裡設計得不是很直覺, QTreeView 提供 DropIndicatorPoisition 來說明"丟到這個節點的裡面"、"丟到這個節點的前面"與"丟到這個節點的後面",來解說要丟到哪個節點的詳細狀況,接著來接解決如何取得要丟的節點,這個資訊會存在 QTreeView::selectedIndexs() ,其實就是最後選擇的節點,因為之前有開啟多選,所以這裡可能會有多個節點,最後透過 HideColumnByList() 來隱藏指定的 Column ,最後的 QTreeView::dropEvent() 是執行 DropEvent 的預設行為,但這個預設行為並不包含把節點的資料變成丟完後的資料,整個 dropEvent() 會發生在資料被修改完之前,所以要注意這個事件是發生在丟完資料之前,也就是 QTreeView 的樹節點資料都還沒改變時。
參考資料
[ doc.qt.io ] QTreeView Class
相關文章與資料
[ GitLab ] HelloQt
特製 Widget
TreeView 的基本應用
在 TreeView 使用右鍵選單