剪貼簿是個啥就不用多介紹了,最直觀的功能是實現應用程式之間資料共享。就是我們們常說的“複製”、“貼上”功能。
在 Qt 中,QClipboard 類提供了相關 API 讓應用程式具備讀/寫剪貼簿的能力。資料透過 QMimeData 類包裝。該類使用 MIME 型別來標識資料。比如,要包裝的資料是純文字內容,就使用 text/plain;如果是 PNG 影像資料,就用 image/png。當然,自定義型別也是可以的,如 application/xxx。
QMimeData 的核心方法是 setData 和 data。setData 方法用來放入資料,data 方法用來取出資料。setData 方法的簽名如下:
void setData(const QString &mimetype, const QByteArray &data);
mimetype 引數為字串,指定資料的 MIME 型別;data 引數就是資料本尊,型別為位元組序列。透過 setData 方法的簽名,我們們也能知道,QMimeData 類可以放任意內容。要獲取資料時,data 方法需要透過 MIME 型別來檢索。
為了便於存取常見的資料——如文字、影像、HTML文字等,QMimeData 類提供一些封裝好的方法成員:
文字 | setText | 設定普通文字 |
text | 獲取普通文字 | |
hasText
|
判斷是否存在文字資料 | |
HTML文字 |
setHtml
|
設定 HTML 文字 |
html
|
獲取HTML文字 | |
hasHtml
|
判斷是否存在 HTML 文字資料 | |
URL |
setUrls
|
設定 URL 列表,引數為 QList<QUrl> |
urls | 獲取 URL 列表 | |
hasUrls
|
檢測是否存在 URL 列表 | |
影像 |
setImageData
|
設定影像資料 |
imageData
|
獲取影像資料 | |
hasImage
|
判斷是否存在影像資料 | |
顏色 |
setColorData
|
設定顏色資料 |
colorData
|
獲取顏色資料 | |
hasColor
|
是否存在顏色資料 |
QClipboard 類不能直接例項化使用,它由 QGuiApplication 類的靜態成員 clipboard 公開。該靜態成員返回 QClipboard 類的指標,程式程式碼將透過這個指標來訪問 QClipboard 物件。由於 QApplication 類派生自 QGuiApplication,當然也繼承了 clipboard 成員。
下面做一個簡單的練習:複製和貼上文字。
MyWindow 類的標頭檔案。
class MyWindow : public QWidget { Q_OBJECT public: MyWindow(QWidget* parent = nullptr); ~MyWindow(); private: void _initUi(); // 私有方法 // 下面是私有欄位 QLineEdit* _txtInput; QLabel* _lbTxt; QPushButton* _btnCopy; QPushButton* _btnPaste; // 用來佈局控制元件的 QGridLayout* _layout; // 下面成員響應 clicked 訊號 void onCopy(); void onPaste(); };
_initUi 方法負責初始化視窗上的東西。這個視窗有四個元件:一個 QLineEdit 用來輸入文字;一個 QLabel 用來顯示文字;然後是兩個按鈕—— 執行“複製”和“貼上”操作。
後面兩個方法 onCopy 和 onPaste 分別與兩個按鈕的 clicked 訊號繫結。
建構函式的實現比較簡單,就是呼叫 _initUi 方法。
MyWindow::MyWindow(QWidget* parent) : QWidget::QWidget(parent) { // 初始化UI this -> _initUi(); } MyWindow::~MyWindow() { }
解構函式這裡啥也不做。
下面是 _intUi 的實現程式碼。
void MyWindow::_initUi() { // 設定一下視窗 this->setWindowTitle("複製貼上文字"); this->setGeometry(560, 480, 320, 150); this->setMinimumSize(300, 150); _txtInput = new QLineEdit(); _lbTxt = new QLabel(); _btnCopy = new QPushButton("複製"); _btnPaste = new QPushButton("貼上"); _layout = new QGridLayout(this); // 設定空白 _layout->setSpacing(12); // 放置各控制元件 _layout->addWidget(_txtInput, 0, 0); _layout->addWidget(_btnCopy, 1, 0); _layout->addWidget(_lbTxt, 0, 2); _layout->addWidget(_btnPaste, 1, 2); _layout->setColumnStretch(0, 2); _layout->setColumnStretch(1, 1); _layout->setColumnStretch(2, 2); // 繫結訊號和槽 connect(_btnCopy, &QPushButton::clicked, this, &MyWindow::onCopy); connect(_btnPaste, &QPushButton::clicked, this, &MyWindow::onPaste); }
QGridLayout 類也是一個元件,以網格方式佈局各元件。網格的行和列是自動劃分的。上面程式碼中其實用到了三列兩行:
1、QLineEdit 在第一列第一行;
2、第二列空著,沒放東西;
3、 QLabel 元件在第三列第一行;
4、“複製”按鈕在第一列第二行;
5、“貼上”按鈕在第二行第二行。
這三行是設定空間比例的。
_layout->setColumnStretch(0, 2); _layout->setColumnStretch(1, 1); _layout->setColumnStretch(2, 2);
這意思就是,列的總寬平均分為4份,第一列和第三列都佔兩份,第二列只佔一份。
隨後是與 clicked 訊號繫結的兩個私有方法。
void MyWindow::onCopy() { // 獲得 QClipboard 的引用 QClipboard* clboard = QApplication::clipboard(); // 設定文字資料 clboard -> setText(_txtInput -> text()); } void MyWindow::onPaste() { // 過程差不多 QClipboard* cb = QApplication::clipboard(); QString s = cb->text(); // 顯示貼上的文字 _lbTxt->setText(s); }
最後是 main 函式的程式碼:
int main(int argc, char** argv) { QApplication app(argc, argv); MyWindow win; win.show(); return app.exec(); }
執行程式,先輸入一些文字,點選“複製”;再點選“貼上”,被複制的文字就會顯示出來了。
這個例子用了 QClipboard 類公開的封裝方法,不需要操作 QMimeData 類。針對常用的資料格式,可直接用。
1、text、setText:設定或獲取文字;
2、setImage 和 image:設定或獲取影像(QImage型別);
3、setPixmap 和 pixmap:設定或獲取影像(QPixmap型別)。
後面兩個是讀寫影像的。
對於影像資料的複製和貼上,操作流程差不多,大夥伴有興趣可以試試。
前文提到過,除了常見的資料格式外,QMimeData 允許自定義格式,用 MIME 來標識。
接下來,我們們做個練習,複製和貼上生日賀卡資訊。假設生日賀卡資訊包括姓名、生日、祝福語。我們們用自定義的資料格式將其複製,也可以貼上到應用程式上。MIME 型別為 application/bug。
以下是自定義視窗類的標頭檔案定義。
#ifndef APP_H #define APP_H #include <qwidget.h> #include <qpushbutton.h> #include <qlineedit.h> #include <qdatetimeedit.h> class CustWind : public QWidget { Q_OBJECT public: CustWind(QWidget* parent = nullptr); private: QLineEdit* txtName; QLineEdit* txtWish; QDateEdit* txtDate; QPushButton* btnCopy; QPushButton* btnPaste; // 與 clicked 訊號繫結的方法(Slots) void onCopy(); void onPaste(); }; #endif
在包含標頭檔案時,用帶 .h 字尾和不用後是一樣的,既可以用 <QWidget> 也可以用 <qwidget.h>,只是作相容之用。
在建構函式中,初始化各類可視物件。
CustWind::CustWind(QWidget *parent) : QWidget::QWidget(parent) { // 初始化元件 txtName = new QLineEdit(); txtWish = new QLineEdit(); txtDate = new QDateEdit(); // 有效日期範圍 txtDate -> setMinimumDate(QDate(1950, 1, 1)); txtDate -> setMaximumDate(QDate(2085, 12, 31)); btnCopy = new QPushButton("複製生日賀卡"); btnPaste = new QPushButton("貼上生日賀卡"); // 佈局 QFormLayout* layout = new QFormLayout(this); layout->addRow("姓名:", txtName); layout->addRow("生日:", txtDate); layout->addRow("祝福語:", txtWish); QVBoxLayout *sublayout = new QVBoxLayout(); sublayout->addWidget(btnCopy); sublayout->addWidget(btnPaste); layout ->addRow(sublayout); // 連線訊號和槽 connect(btnCopy,&QPushButton::clicked,this,&CustWind::onCopy); connect(btnPaste,&QPushButton::clicked,this,&CustWind::onPaste); }
這個例子我們們用 QFormLayout 來佈局介面。其含義和 HTML 中 <form> 元素差不多,即表單。
下面重點說兩個 slot 方法。
第一個是 onCopy ,由複製按鈕的點選觸發。
void CustWind::onCopy() { // 獲取資料 QString name = txtName->text(); QString wish = txtWish->text(); QDate birthdate = txtDate->date(); if(name.isEmpty()) { return; //姓名是空的 } // 開始序列化 QByteArray data; QBuffer buff(&data); // buffer 要先開啟 buff.open(QBuffer::WriteOnly); QDataStream output(&buff); // 寫入資料 output << name << birthdate << wish; buff.close(); // 包裝資料 QMimeData packet; // 錯誤 packet.setData("application/bug", data); // 把資料放到剪貼簿 QClipboard* cb = QApplication::clipboard(); cb->setMimeData(&packet); QMessageBox::information(this,"恭喜","生日賀卡複製成功",QMessageBox::Ok); }
Qt 裡面常用 QDataStream 類來做序列化和反序列化操作。由於它有運算子過載,我們可以使用 C++ 入門時最熟悉的 <<、>> 運算子來輸入輸出。向 QDataStream 物件寫入資料時:
dataStream << a << b << c;
反序列化時:
dataStream >> a >> b >> c;
運算子很TM生動形象地描述出資料的流動方向。注意輸入和輸出時,資料的順序必須一致。
QMimeData 類用 setData 方法設定自定義資料時,引數接收的型別是 QByteArray(位元組陣列)而不是 QDataStream 物件,因此,我們要用 QBuffer 類來中轉一下。
1、建立 QByteArray 例項;
2、建立 QBuffer 例項,關聯 QByteArray 例項;
3、建立 QDataStream 例項,關聯 QBuffer 例項。
QDataStream 類需要 QIODevice 的派生類來完成讀寫操作,而 QByteArray 類不是 QIODevice 的子類,故要用 QBuffer 來過渡一下。當然了,老周這裡為了讓這個思路更清晰一些,所以“中規中矩”地寫程式碼。其實,QDataStream 類有接收 QByteArray 型別的引數的,可以省略 QBuffer 的程式碼。QDataStream 類內部自動建立 QBuffer 物件。
更正:上面 onCopy 方法的程式碼有問題,當資料被“貼上”(讀取)後會引發空指標引用,致使程式崩掉。為了給大夥們提供參考,避免犯同類錯誤,所以,老周沒有刪除上面的程式碼。現在我放出修改後的 onCopy 方法。
void CustWind::onCopy() { …… // 包裝資料 QMimeData* packet = new QMimeData; packet -> setData("application/bug", data); // 把資料放到剪貼簿 QClipboard* cb = QApplication::clipboard(); cb->setMimeData(packet); …… }
問題出在建立 QMimeData 例項時,我第一次的程式碼是棧上分配例項的,Qt 無法在後續的事件迴圈中管理其記憶體。所以,QMimeData 要用指標型別,以 new 運算子來建立例項。
第二個 slot 方法是 onPaste,實現賀卡的貼上。
void CustWind::onPaste() { // 訪問剪貼簿 QClipboard* cb=QApplication::clipboard(); const QMimeData* dataPack = cb->mimeData(); // 判斷一下有沒有我們要的資料 if(!dataPack->hasFormat("application/bug")) { return; } // 取出資料 QByteArray data = dataPack->data("application/bug"); // 反序列化 QBuffer buff(&data); // 記得先開啟 buffer buff.open(QBuffer::ReadOnly); QDataStream input(&buff); // 注意讀的順序 QString name; QDate birth; QString wish; input >> name >> birth >> wish; // 顯示資料 this->txtName->setText(name); this->txtDate->setDate(birth); this->txtWish->setText(wish); }
反序列化的原理與序列化是一樣的,只是反向操作罷了。注意讀寫資料的順序,寫的時候是姓名 - 生日 - 祝福語,讀的時候也必須按這個順序。
最後,是 main 函式。
int main(int argc, char* argv[]) { QApplication myapp(argc, argv); CustWind mwindow; mwindow.show(); return myapp.exec(); }
CMake 檔案(CMakeLists.txt)就按照標準文件上直接抄就行了。
cmake_minimum_required(VERSION 3.20) project(demo LANGUAGES CXX) find_package( Qt6 REQUIRED COMPONENTS Core Gui Widgets ) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) add_executable(demo app.h app.cpp) target_link_libraries( demo PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets )
重點就是三步:
1、宣告 CMake 文件支援的版本,應用專案的名稱(target)。
2、auto moc 一定要開啟,否則編譯時會掛。
3、新增原始碼、連結相關的庫。
好了,完工了,我們們試試。
先執行一個程式例項,輸入相關資訊,點複製按鈕。
然後,關閉這個程式重新執行,或者再執行一個新程式例項,點貼上按鈕。
這樣,就完成了自定義資料的複製和貼上。