拖放(Drag and Drop),一般會簡稱爲 DnD,是現代軟件開發中必不可少的一項技術。它提供了一種可以在應用程序內部甚至是應用程序之間進行信息交換的機制。操做系統與應用程序之間進行的剪貼板內容的交換,也能夠被認爲是拖放的一部分。函數
拖放實際上是由兩部分組成的:拖動和釋放。拖動是將被拖放對象進行移動,釋放是將被拖放對象放下。前者是一個按下鼠標按鍵並移動的過程,後者是一個鬆開鼠標按鍵的過程;一般這兩個操做之間的鼠標按鍵是被一直按下的。固然,這只是一種廣泛的狀況,其它狀況仍是要看應用程序的具體實現。對於 Qt 而言,一個組件既能夠做爲被拖動對象進行拖動,也能夠做爲釋放掉的目的地對象,或者兩者都是。this
在下面的例子中(來自 C++ GUI Programming with Qt4, 2nd Edition),咱們將建立一個程序,將操做系統中的文本文件拖進來,而後在窗口中讀取內容。url
1操作系統 2翻譯 3code 4orm 5對象 6繼承 7事件 8 9 10 11 12 13 |
class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = 0); ~MainWindow(); protected: void dragEnterEvent(QDragEnterEvent *event); void dropEvent(QDropEvent *event); private: bool readFile(const QString &fileName); QTextEdit *textEdit; }; |
注意到咱們須要重寫dragEnterEvent()
和dropEvent()
兩個函數。顧名思義,前者是拖放進入的事件,後者是釋放鼠標的事件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { textEdit = new QTextEdit; setCentralWidget(textEdit);
textEdit->setAcceptDrops(false); setAcceptDrops(true);
setWindowTitle(tr("Text Editor")); }
MainWindow::~MainWindow() { } |
在構造函數中,咱們建立了QTextEdit
的對象。默認狀況下,QTextEdit
能夠接受從其它應用程序拖放過來的文本類型的數據。若是用戶把一個文件拖到這面,默認會把文件名插入到光標位置。可是咱們但願讓MainWindow
讀取文件內容,而不是僅僅插入文件名,因此咱們在MainWindow
中加入了拖放操做。首先要把QTextEdit
的setAcceptDrops()
函數置爲 false,而且把MainWindow
的setAcceptDrops()
置爲 true,這樣咱們就可以讓MainWindow
截獲拖放事件,而不是交給QTextEdit
處理。
1 2 3 4 5 6 |
void MainWindow::dragEnterEvent(QDragEnterEvent *event) { if (event->mimeData()->hasFormat("text/uri-list")) { event->acceptProposedAction(); } } |
當用戶將對象拖動到組件上面時,系統會回調dragEnterEvent()
函數。若是咱們在事件處理代碼中調用acceptProposeAction()
函數,就能夠向用戶暗示,你能夠將拖動的對象放在這個組件上。默認狀況下,組件是不會接受拖放的。若是咱們調用了這個函數,那麼 Qt 會自動以光標樣式的變化來提示用戶是否能夠將對象放在組件上。在這裏,咱們但願告訴用戶,窗口能夠接受拖放,可是咱們僅接受某一種類型的文件,而不是所有文件。咱們首先檢查拖放文件的 MIME 類型信息。MIME 類型由 Internet Assigned Numbers Authority (IANA) 定義,Qt 的拖放事件使用 MIME 類型來判斷拖放對象的類型。關於 MIME 類型的詳細信息,請參考 http://www.iana.org/assignments/media-types/。MIME 類型爲 text/uri-list 一般用來描述一個 URI 列表。這些 URI 能夠是文件名,能夠是 URL 或者其它的資源描述符。若是發現用戶拖放的是一個 text/uri-list 數據(即文件名),咱們便接受這個動做。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
void MainWindow::dropEvent(QDropEvent *event) { QList<QUrl> urls = event->mimeData()->urls(); if (urls.isEmpty()) { return; }
QString fileName = urls.first().toLocalFile(); if (fileName.isEmpty()) { return; }
if (readFile(fileName)) { setWindowTitle(tr("%1 - %2").arg(fileName, tr("Drag File"))); } }
bool MainWindow::readFile(const QString &fileName) { bool r = false; QFile file(fileName); QString content; if(file.open(QIODevice::ReadOnly)) { content = file.readAll(); r = true; } textEdit->setText(content); return r; } |
當用戶將對象釋放到組件上面時,系統回調dropEvent()
函數。咱們使用QMimeData::urls()
來得到QUrl
的一個列表。一般,這種拖動應該只有一個文件,可是也不排除多個文件一塊兒拖動。所以咱們須要檢查這個列表是否爲空,若是不爲空,則取出第一個,不然當即返回。最後咱們調用readFile()
函數讀取文件內容。這個函數的內容很簡單,咱們前面也講解過有關文件的操做,這裏再也不贅述。如今能夠運行下看看效果了。
接下來的例子也是來自 C++ GUI Programming with Qt4, 2nd Edition。在這個例子中,咱們將建立左右兩個並列的列表,能夠實現兩者之間數據的相互拖動。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class ProjectListWidget : public QListWidget { Q_OBJECT public: ProjectListWidget(QWidget *parent = 0); protected: void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); void dragEnterEvent(QDragEnterEvent *event); void dragMoveEvent(QDragMoveEvent *event); void dropEvent(QDropEvent *event); private: void performDrag(); QPoint startPos; }; |
ProjectListWidget
是咱們的列表的實現。這個類繼承自QListWidget
。在最終的程序中,將會是兩個ProjectListWidget
的並列。
1 2 3 4 5 |
ProjectListWidget::ProjectListWidget(QWidget *parent) : QListWidget(parent) { setAcceptDrops(true); } |
構造函數咱們設置了setAcceptDrops()
,使ProjectListWidget
可以支持拖動操做。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
void ProjectListWidget::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) startPos = event->pos(); QListWidget::mousePressEvent(event); }
void ProjectListWidget::mouseMoveEvent(QMouseEvent *event) { if (event->buttons() & Qt::LeftButton) { int distance = (event->pos() - startPos).manhattanLength(); if (distance >= QApplication::startDragDistance()) performDrag(); } QListWidget::mouseMoveEvent(event); }
void ProjectListWidget::performDrag() { QListWidgetItem *item = currentItem(); if (item) { QMimeData *mimeData = new QMimeData; mimeData->setText(item->text());
QDrag *drag = new QDrag(this); drag->setMimeData(mimeData); drag->setPixmap(QPixmap(":/images/person.png")); if (drag->exec(Qt::MoveAction) == Qt::MoveAction) delete item; } } |
mousePressEvent()
函數中,咱們檢測鼠標左鍵點擊,若是是的話就記錄下當前位置。須要注意的是,這個函數最後須要調用系統自帶的處理函數,以便實現一般的那種操做。這在一些重寫事件的函數中都是須要注意的,前面咱們已經反覆強調過這一點。
mouseMoveEvent()
函數判斷了,若是鼠標在移動的時候一直按住左鍵(也就是 if 裏面的內容),那麼就計算一個manhattanLength()
值。從字面上翻譯,這是個「曼哈頓長度」。首先來看看event.pos() - startPos
是什麼。在mousePressEvent()
函數中,咱們將鼠標按下的座標記錄爲 startPos,而event.pos()
則是鼠標當前的座標:一個點減去另一個點,這就是一個位移向量。所謂曼哈頓距離就是兩點之間的距離(按照勾股定理進行計算而來),也就是這個向量的長度。而後繼續判斷,若是大於QApplication::startDragDistance()
,咱們才進行釋放的操做。固然,最後仍是要調用系統默認的鼠標拖動函數。這一判斷的意義在於,防止用戶由於手的抖動等因素形成的鼠標拖動。用戶必須將鼠標拖動一段距離以後,咱們才認爲他是但願進行拖動操做,而這一距離就是QApplication::startDragDistance()
提供的,這個值一般是 4px。
performDrag()
開始處理拖放的過程。這裏,咱們要建立一個QDrag
對象,將 this 做爲 parent。QDrag
使用QMimeData
存儲數據。例如咱們使用QMimeData::setText()
函數將一個字符串存儲爲 text/plain 類型的數據。QMimeData
提供了不少函數,用於存儲諸如 URL、顏色等類型的數據。使用QDrag::setPixmap()
則能夠設置拖動發生時鼠標的樣式。QDrag::exec()
會阻塞拖動的操做,直到用戶完成操做或者取消操做。它接受不一樣類型的動做做爲參數,返回值是真正執行的動做。這些動做的類型通常爲Qt::CopyAction
,Qt::MoveAction
和Qt::LinkAction
。返回值會有這幾種動做,同時還會有一個Qt::IgnoreAction
用於表示用戶取消了拖放。這些動做取決於拖放源對象容許的類型,目的對象接受的類型以及拖放時按下的鍵盤按鍵。在exec()
調用以後,Qt 會在拖放對象不須要的時候釋放掉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
void ProjectListWidget::dragMoveEvent(QDragMoveEvent *event) { ProjectListWidget *source = qobject_cast(event->source()); if (source && source != this) { event->setDropAction(Qt::MoveAction); event->accept(); } }
void ProjectListWidget::dropEvent(QDropEvent *event) { ProjectListWidget *source = qobject_cast(event->source()); if (source && source != this) { addItem(event->mimeData()->text()); event->setDropAction(Qt::MoveAction); event->accept(); } } |
dragMoveEvent()
和dropEvent()
類似。首先判斷事件的來源(source),因爲咱們是兩個ProjectListWidget
之間相互拖動,因此來源應該是ProjectListWidget
類型的(固然,這個 source 不能是本身,因此咱們還得判斷source != this
)。dragMoveEvent()
中咱們檢查的是被拖動的對象;dropEvent()
中咱們檢查的是釋放的對象:這兩者是不一樣的。