【Qt 6】讀寫剪貼簿

東邪獨孤發表於2023-05-13

剪貼簿是個啥就不用多介紹了,最直觀的功能是實現應用程式之間資料共享。就是我們們常說的“複製”、“貼上”功能。

在 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、新增原始碼、連結相關的庫。

 

好了,完工了,我們們試試。

先執行一個程式例項,輸入相關資訊,點複製按鈕。

然後,關閉這個程式重新執行,或者再執行一個新程式例項,點貼上按鈕。

這樣,就完成了自定義資料的複製和貼上。

 

相關文章