#(52):使用拖放
拖放(Drag and Drop),通常會簡稱為 DnD,是現代軟件開發中必不可少的一項技術。它提供了一種能夠在應用程序內部甚至是應用程序之間進行信息交換的機制。操作系統與應用程序之間進行的剪貼板內容的交換,也可以被認為是拖放的一部分。
拖放其實是由兩部分組成的:拖動和釋放。拖動是將被拖放對象進行移動,釋放是將被拖放對象放下。前者是一個按下鼠標按鍵并移動的過程,后者是一個松開鼠標按鍵的過程;通常這兩個操作之間的鼠標按鍵是被一直按下的。當然,這只是一種普遍的情況,其它情況還是要看應用程序的具體實現。對于 Qt 而言,一個組件既可以作為被拖動對象進行拖動,也可以作為釋放掉的目的地對象,或者二者都是。
在下面的例子中(來自 C++ GUI Programming with Qt4, 2nd Edition),我們將創建一個程序,將操作系統中的文本文件拖進來,然后在窗口中讀取內容。
~~~
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()`兩個函數。顧名思義,前者是拖放進入的事件,后者是釋放鼠標的事件。
~~~
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`處理。
~~~
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/](http://www.iana.org/assignments/media-types/)。MIME 類型為 text/uri-list 通常用來描述一個 URI 列表。這些 URI 可以是文件名,可以是 URL 或者其它的資源描述符。如果發現用戶拖放的是一個 text/uri-list 數據(即文件名),我們便接受這個動作。
~~~
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。在這個例子中,我們將創建左右兩個并列的列表,可以實現二者之間數據的相互拖動。
~~~
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`的并列。
~~~
ProjectListWidget::ProjectListWidget(QWidget *parent)
: QListWidget(parent)
{
setAcceptDrops(true);
}
~~~
構造函數我們設置了`setAcceptDrops()`,使`ProjectListWidget`能夠支持拖動操作。
~~~
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 會在拖放對象不需要的時候釋放掉。
~~~
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()`中我們檢查的是釋放的對象:這二者是不同的。
附件:[ProjectChooser](http://files.devbean.net/code/ProjectChooser.zip)
- (1)序
- (2)Qt 簡介
- (3)Hello, world!
- (4)信號槽
- (5)自定義信號槽
- (6)Qt 模塊簡介
- (7)MainWindow 簡介
- (8)添加動作
- (9)資源文件
- (10)對象模型
- (11)布局管理器
- (12)菜單欄、工具欄和狀態欄
- (13)對話框簡介
- (14)對話框數據傳遞
- (15)標準對話框 QMessageBox
- (16)深入 Qt5 信號槽新語法
- (17)文件對話框
- (18)事件
- (19)事件的接受與忽略
- (21)事件過濾器
- (22)事件總結
- (23)自定義事件
- (24)Qt 繪制系統簡介
- (25)畫刷和畫筆
- (26)反走樣
- (27)漸變
- (28)坐標系統
- (29)繪制設備
- (30)Graphics View Framework
- (31)貪吃蛇游戲(1)
- (32)貪吃蛇游戲(2)
- (33)貪吃蛇游戲(3)
- (34)貪吃蛇游戲(4)
- (35)文件
- (36)二進制文件讀寫
- (37)文本文件讀寫
- (38)存儲容器
- (39)遍歷容器
- (40)隱式數據共享
- (41)model/view 架構
- (42)QListWidget、QTreeWidget 和 QTableWidget
- (43)QStringListModel
- (44)QFileSystemModel
- (45)模型
- (46)視圖和委托
- (47)視圖選擇
- (48)QSortFilterProxyModel
- (49)自定義只讀模型
- (50)自定義可編輯模型
- (51)布爾表達式樹模型
- (52)使用拖放
- (53)自定義拖放數據
- (54)剪貼板
- (55)數據庫操作
- (56)使用模型操作數據庫
- (57)可視化顯示數據庫數據
- (58)編輯數據庫外鍵
- (59)使用流處理 XML
- (60)使用 DOM 處理 XML
- (61)使用 SAX 處理 XML
- (62)保存 XML
- (63)使用 QJson 處理 JSON
- (64)使用 QJsonDocument 處理 JSON
- (65)訪問網絡(1)
- (66)訪問網絡(2)
- (67)訪問網絡(3)
- (68)訪問網絡(4)
- (69)進程
- (70)進程間通信
- (71)線程簡介
- (72)線程和事件循環
- (73)Qt 線程相關類
- (74)線程和 QObject
- (75)線程總結
- (76)QML 和 QtQuick 2
- (77)QML 語法
- (78)QML 基本元素
- (79)QML 組件
- (80)定位器
- (81)元素布局
- (82)輸入元素
- (83)Qt Quick Controls
- (84)Repeater
- (85)動態視圖
- (86)視圖代理
- (87)模型-視圖高級技術
- (88)Canvas
- (89)Canvas(續)