2020年10月13日 星期二

處理 TreeView 的 Drag 與 Drop

處理 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 使用右鍵選單

沒有留言:

張貼留言