Qt 是一個跨平臺C++圖形介面開發庫,利用Qt可以快速開發跨平臺表單應用程式,在Qt中我們可以通過拖拽的方式將不同元件放到指定的位置,實現圖形化開發極大的方便了開發效率,本章將重點介紹StandardItemModel
資料模型元件的常用方法及靈活運用。
QStandardItemModel
是 Qt 中用於儲存標準項資料的模型類之一,它繼承自 QAbstractItemModel
類。這個模型提供了一種靈活的方式來組織和管理資料,適用於各種檢視類(比如 QTreeView
、QListView
、QTableView
等)。該元件是標準的以項資料為單位的基於M/V模型的一種標準資料管理方式。
Model/View 是Qt中的一種資料編排結構,其中Model
代表模型而View
則代表檢視,檢視是顯示和編輯資料的介面元件,而模型則是檢視與原始資料之間的介面,通常該類結構都是用在資料庫中較多,例如模型結構負責讀取或寫入資料庫,檢視結構則負責展示資料,其條理清晰,編寫程式碼便於維護。Model/View架構是Qt中資料與介面分離的核心設計模式,為開發者提供了一種清晰而靈活的方式來管理和展示資料。
資料模型元件通常會配合TableView
等相關元件一起使用,首先繪製UI
介面,介面中包含頂部ToolBar
元件,底部是一個TableView
檢視表格,最下方是一個PlainTextEdit
文字方塊,如下圖所示;
如上圖所示ToolBar
元件中我們繫結了一些快捷鍵及ICO
圖示,這些資訊通過圖形化的方式進行了關聯;
為了能充分展示QStandardItemModel
模型元件的使用,我們首先簡單的的介紹一下該元件的常用方法與描述,下面是 QStandardItemModel
類的一些常用方法,說明和概述:
方法 | 描述 |
---|---|
QStandardItemModel(int rows, int columns, QObject *parent = nullptr) |
建構函式,建立一個具有指定行數和列數的 QStandardItemModel 物件。 |
virtual ~QStandardItemModel() |
虛解構函式,釋放 QStandardItemModel 物件及其所有子項。 |
int rowCount(const QModelIndex &parent = QModelIndex()) const |
返回指定父項的行數。如果 parent 為無效索引,則返回根項的行數。 |
int columnCount(const QModelIndex &parent = QModelIndex()) const |
返回指定父項的列數。如果 parent 為無效索引,則返回根項的列數。 |
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const |
返回指定行、列和父項的索引。 |
QModelIndex parent(const QModelIndex &child) const |
返回指定子項的父項的索引。如果子項沒有父項,則返回無效索引。 |
Qt::ItemFlags flags(const QModelIndex &index) const |
返回指定索引處項的標誌,用於指示該項的狀態和行為。 |
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const |
返回指定索引處項的資料。role 引數指定要獲取的資料的角色,如 Qt::DisplayRole 表示顯示文字。 |
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) |
設定指定索引處項的資料。如果設定成功,則返回 true ,否則返回 false 。 |
bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) |
在指定父項下插入行。返回 true 表示成功。 |
bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) |
從指定父項中移除行。返回 true 表示成功。 |
QModelIndexList match(const QModelIndex &start, int role, const QVariant &value, int hits = 1, Qt::MatchFlags flags = Qt::MatchFlags(Qt::MatchStartsWithQt::MatchWrap)) const |
從模型中匹配指定的字串等變數。 |
bool insertColumns(int column, int count, const QModelIndex &parent = QModelIndex()) |
在指定父項下插入列。返回 true 表示成功。 |
bool removeColumns(int column, int count, const QModelIndex &parent = QModelIndex()) |
從指定父項中移除列。返回 true 表示成功。 |
Qt::DropActions supportedDropActions() const |
返回模型支援的拖放操作。 |
Qt::DropActions supportedDragActions() const |
返回模型支援的拖動操作。 |
QStringList mimeTypes() const |
返回模型支援的 MIME 型別列表。 |
QMimeData *mimeData(const QModelIndexList &indexes) const |
返回包含指定索引項資料的 MIME 資料物件。 |
bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) |
處理拖放操作中的 MIME 資料。返回 true 表示成功。 |
以上是 QStandardItemModel
類的一些常用方法,通過這些方法,可以對模型進行增刪改查等操作,並與檢視進行互動。
首先筆者先來演示一下如何將tableView
元件與QStandardItemModel
元件進行繫結操作,其實繫結很簡單隻需要呼叫ui->tableView->setModel
即可將tableView
元件與model
資料集進行繫結,當繫結後,模型中的資料發生變化則會自動重新整理到View
元件中,我們就無需關心介面中的元件如何顯示了,這個現實過程交給Model
對映吧。
如下所示的程式碼片段是一個使用 QStandardItemModel
的例子,演示瞭如何建立一個帶有表頭和初始資料的 QTableView
。
以下是程式碼片段的一些說明:
QStandardItemModel
物件,並設定列數為 3。QTableView
。這樣,就建立了一個包含表頭和資料的 QTableView
,並將其顯示在 MainWindow
中。
// 預設建構函式
MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{
ui->setupUi(this);
QStandardItemModel *model = new QStandardItemModel();
// 初始化tableView表頭
model->setColumnCount(3);
model->setHeaderData(0,Qt::Horizontal,QString("賬號"));
model->setHeaderData(1,Qt::Horizontal,QString("使用者"));
model->setHeaderData(2,Qt::Horizontal,QString("年齡"));
ui->tableView->setModel(model);
ui->tableView->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft); // 表頭居左顯示
// 設定列寬
ui->tableView->setColumnWidth(0,101);
ui->tableView->setColumnWidth(1,102);
// 迴圈初始化設定模型
for(int i = 0; i < 5; i++)
{
model->setItem(i,0,new QStandardItem("20210506"));
// 設定字元顏色
model->item(i,0)->setForeground(QBrush(QColor(255, 0, 0)));
// 設定字元位置
model->item(i,0)->setTextAlignment(Qt::AlignCenter);
model->setItem(i,1,new QStandardItem(QString("lyshark")));
model->setItem(i,2,new QStandardItem(QString("24")));
}
}
執行後讀者可觀察TableView
表格中的變化情況,如下圖所示;
接著,我們來看下如何對本專案中UI
表格進行初始化,在MainWindow
建構函式中,我們首先建立一個QStandardItemModel
用於儲存表格資料,以及一個QItemSelectionModel
用於處理表格中的選擇操作,並將它們關聯到TableView
元件上。在視窗初始化時,除了開啟檔案的操作外,禁用了其他所有Action
選項。建立狀態列元件,包括顯示當前檔案、當前單元格位置和單元格內容的QLabel
元件。
// 預設建構函式
MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow)
{
ui->setupUi(this);
// 初始化部分
model = new QStandardItemModel(3,FixedColumnCount,this); // 資料模型初始化
selection = new QItemSelectionModel(model); // Item選擇模型
// 為TableView設定資料模型
ui->tableView->setModel(model); // 設定資料模型
ui->tableView->setSelectionModel(selection); // 設定選擇模型
// 預設禁用所有Action選項,只保留開啟
ui->actionSave->setEnabled(false);
ui->actionView->setEnabled(false);
ui->actionAppend->setEnabled(false);
ui->actionDelete->setEnabled(false);
ui->actionInsert->setEnabled(false);
// 建立狀態列元件,主要來顯示單元格位置
LabCurFile = new QLabel("當前檔案:",this);
LabCurFile->setMinimumWidth(200);
LabCellPos = new QLabel("當前單元格:",this);
LabCellPos->setMinimumWidth(180);
LabCellPos->setAlignment(Qt::AlignHCenter);
LabCellText = new QLabel("單元格內容:",this);
LabCellText->setMinimumWidth(150);
ui->statusbar->addWidget(LabCurFile);
ui->statusbar->addWidget(LabCellPos);
ui->statusbar->addWidget(LabCellText);
// 選擇當前單元格變化時的訊號與槽
connect(selection,SIGNAL(currentChanged(QModelIndex,QModelIndex)),this,SLOT(on_currentChanged(QModelIndex,QModelIndex)));
}
MainWindow::~MainWindow()
{
delete ui;
}
如上程式碼中,我們還將選擇模型的currentChanged
訊號連線到了槽函數on_currentChanged
上面,這個槽函數主要用於實現,當選擇單元格變化時則響應,並將當前單元格變化重新整理到底部的StatusBar
元件上,程式碼如下所示;
// 【選中單元格時響應】:選擇單元格變化時的響應,通過在建構函式中繫結訊號和槽函數實現觸發
void MainWindow::on_currentChanged(const QModelIndex ¤t, const QModelIndex &previous)
{
Q_UNUSED(previous);
if (current.isValid()) // 當前模型索引有效
{
LabCellPos->setText(QString::asprintf("當前單元格:%d行,%d列",current.row(),current.column())); // 顯示模型索引的行和列號
QStandardItem *aItem;
aItem=model->itemFromIndex(current); // 從模型索引獲得Item
this->LabCellText->setText("單元格內容:"+aItem->text()); // 顯示item的文字內容
}
}
讀者可自行執行這段程式,當執行後首先會初始化表格的長度及寬度,且頁面中禁用了其他按鈕,只能選擇開啟檔案選項,如下圖所示;
當讀者點選開啟檔案時,首先會觸發on_actionOpen_triggered
槽函數,在該函數內,通過QCoreApplication::applicationDirPath()
獲取應用程式的路徑,並通過QFileDialog::getOpenFileName()
檔案對話方塊讓使用者選擇一個資料檔案(*.txt
)。如果使用者選擇了檔案,就以唯讀文字方式開啟該檔案,讀取檔案內容到一個字串列表 fFileContent
中,並顯示到 plainTextEdit
文字方塊中。
當讀取結束後,直接關閉檔案,並呼叫 iniModelFromStringList
函數,該函數根據字串列表的內容初始化資料模型。隨即啟用工具列中的其他Action
選項,包括儲存、檢視、追加、刪除和插入。並在狀態列顯示當前開啟的檔案路徑。
該函數實現了開啟檔案後的一系列操作,包括讀取檔案內容、更新UI顯示和初始化資料模型。
// 【開啟檔案】:當工具列中開啟檔案被點選後則觸發
void MainWindow::on_actionOpen_triggered()
{
QString curPath=QCoreApplication::applicationDirPath(); // 獲取應用程式的路徑
// 呼叫開啟檔案對話方塊開啟一個檔案
QString aFileName=QFileDialog::getOpenFileName(this,"開啟一個檔案",curPath,"資料檔案(*.txt);;所有檔案(*.*)");
if (aFileName.isEmpty())
{
return; // 如果未選擇檔案則退出
}
QStringList fFileContent; // 檔案內容字串列表
QFile aFile(aFileName); // 以檔案方式讀出
if (aFile.open(QIODevice::ReadOnly | QIODevice::Text)) // 以唯讀文字方式開啟檔案
{
QTextStream aStream(&aFile); // 用文字流讀取檔案
ui->plainTextEdit->clear(); // 清空列表
// 迴圈讀取只要不為空
while (!aStream.atEnd())
{
QString str=aStream.readLine(); // 讀取檔案的一行
ui->plainTextEdit->appendPlainText(str); // 新增到文字方塊顯示
fFileContent.append(str); // 新增到StringList
}
aFile.close(); // 關閉檔案
iniModelFromStringList(fFileContent); // 從StringList的內容初始化資料模型
}
// 開啟檔案完成後,就可以將Action全部開啟了
ui->actionSave->setEnabled(true);
ui->actionView->setEnabled(true);
ui->actionAppend->setEnabled(true);
ui->actionDelete->setEnabled(true);
ui->actionInsert->setEnabled(true);
// 開啟檔案成功後,設定狀態列當前檔案列
this->LabCurFile->setText("當前檔案:"+aFileName);//狀態列顯示
}
在上述槽函數中並沒有分析iniModelFromStringList(fFileContent)
函數的具體實現細節,該函數用於從傳入的字串列表 aFileContent
中獲取資料,並將資料初始化到 TableView
模型中。
具體步驟如下:
rowCnt
,第一行是標題。rowCnt-1
,因為第一行是標題。header
,並將其分割成一個字串列表 headerList
,作為模型的水平表頭標籤。tmpList
。QStandardItem
。QStandardItem
,並設定為可檢查狀態。根據資料判斷是否選中,並設定相應的檢查狀態。QStandardItem
設定到模型的相應行列位置。這個函數主要完成了從字串列表中獲取資料並初始化到 TableView
模型的過程,包括表頭的設定、資料的提取和狀態的處理。
// 【初始化填充TableView】:從傳入的StringList中獲取資料,並將資料初始化到TableView模型中
void MainWindow::iniModelFromStringList(QStringList& aFileContent)
{
int rowCnt=aFileContent.count(); // 文字行數,第1行是標題
model->setRowCount(rowCnt-1); // 實際資料行數,要在標題上減去1
// 設定表頭
QString header=aFileContent.at(0); // 第1行是表頭
// 一個或多個空格、TAB等分隔符隔開的字串、分解為一個StringList
QStringList headerList=header.split(QRegExp("\\s+"),QString::SkipEmptyParts);
model->setHorizontalHeaderLabels(headerList); // 設定表頭文字
// 設定表格中的資料
int x = 0,y = 0;
QStandardItem *Item;
// 有多少列資料就回圈多少次
for(x=1; x < rowCnt; x++)
{
QString LineText = aFileContent.at(x); // 獲取資料區的一行
// 一個或多個空格、TAB等分隔符隔開的字串、分解為一個StringList
QStringList tmpList=LineText.split(QRegExp("\\s+"),QString::SkipEmptyParts);
// 迴圈列數,也就是迴圈FixedColumnCount,其中tmpList中的內容也是.
for(y=0; y < FixedColumnCount-1; y++)
{
Item = new QStandardItem(tmpList.at(y)); // 建立item
model->setItem(x-1,y,Item); // 為模型的某個行列位置設定Item
}
// 最後一個資料需要取出來判斷,並單獨設定狀態
Item=new QStandardItem(headerList.at(y)); // 最後一列是Checkable,需要設定
Item->setCheckable(true); // 設定為Checkable
// 判斷最後一個數值是否為0
if (tmpList.at(y) == "0")
Item->setCheckState(Qt::Unchecked); // 根據資料設定check狀態
else
Item->setCheckState(Qt::Checked);
model->setItem(x-1,y,Item); // 為模型的某個行列位置設定Item
}
}
讀者可自行執行程式,當程式執行後預設只能點選開啟按鈕,點選開啟按鈕後可以選擇專案中的data.txt
文字檔案,此時就可以將文字中的內容對映到元件中,其輸出效果如下圖所示;
接著我們來看下儲存檔案與預覽TableView
檢視的實現方法,其實儲存檔案與預覽是一個功能,唯一的區別是儲存檔案重新整理到檔案中,而預覽則是重新整理到了PlainTextEdit
文字方塊內,但其兩個本質上是一個功能,此處筆者就以儲存檔案為例來說明如何實現的。
首先,在程式碼中同樣是獲取應用程式路徑,同樣是開啟檔案唯一不同的是這裡使用了getSaveFileName
也標誌著是開啟一個儲存對話方塊,這裡還使用了QFile::Open
函數,並設定了QIODevice::ReadWrite
寫入模式,接著定義了QTextStream
文字流,第一次迴圈將表頭先追加到流中,最後model->rowCount()
迴圈表格元素次數,並依次追加文字流到檔案。
步驟總結起來如下:
QFile
開啟檔案,以讀寫、覆蓋原有內容的方式開啟檔案。QTextStream
以文字流的方式讀取檔案。\t\t
分隔,寫入檔案。\t\t
分隔,寫入檔案。最後一列根據選中狀態寫入 1
或 0
。plainTextEdit
文字方塊中。這個函數主要完成了將 TableView
模型中的資料儲存到檔案的過程,包括檔案的選擇、開啟和寫入。
// 【儲存檔案】:當儲存檔案被點選後觸發
void MainWindow::on_actionSave_triggered()
{
QString curPath=QCoreApplication::applicationDirPath(); // 獲取應用程式的路徑
// 呼叫開啟檔案對話方塊選擇一個檔案
QString aFileName=QFileDialog::getSaveFileName(this,tr("選擇一個檔案"),curPath,"資料檔案(*.txt);;所有檔案(*.*)");
if (aFileName.isEmpty()) // 未選擇檔案則直接退出
{
return;
}
QFile aFile(aFileName);
// 以讀寫、覆蓋原有內容方式開啟檔案
if (!(aFile.open(QIODevice::ReadWrite | QIODevice::Text | QIODevice::Truncate)))
return;
QTextStream aStream(&aFile); // 用文字流讀取檔案
QStandardItem *Item;
QString str;
int x = 0,y = 0;
ui->plainTextEdit->clear();
// 獲取表頭文字
for (x=0; x<model->columnCount(); x++)
{
Item=model->horizontalHeaderItem(x); // 獲取表頭的項資料
str= str + Item->text() + "\t\t"; // 以TAB製表符隔開
}
aStream << str << "\n"; // 檔案裡需要加入換行符\n
ui->plainTextEdit->appendPlainText(str);
// 獲取資料區文字
for ( x=0; x < model->rowCount(); x++)
{
str = "";
for( y=0; y < model->columnCount()-1; y++)
{
Item=model->item(x,y);
str=str + Item->text() + QString::asprintf("\t\t");
}
// 對最後一列需要轉換一下,如果判斷為選中則寫1否則寫0
Item=model->item(x,y);
if (Item->checkState()==Qt::Checked)
{
str= str + "1";
}
else
{
str= str + "0";
}
ui->plainTextEdit->appendPlainText(str);
aStream << str << "\n";
}
}
執行程式後,讀者可以點選儲存檔案按鈕,並將其儲存到任意位置,此時開啟檔案,可看到如下圖所示的效果;
首先來解釋一下如何新增一行新的行,其實新增與插入原理一致,唯一的區別在於,新增一行新的資料是在行尾加入,這個可以使用model->columnCount()
來得到行尾,而插入則是在選中當前selection->currentIndex()
行的下方加入行,其他的方式是完全一致的。
如下所示的函數用於在 TableView
中追加一行資料,具體步驟如下:
QList
容器 ItemList
用於儲存一行資料的 QStandardItem
。FixedColumnCount-1
列的資料,每列的資料都是 "測試(追加行)"。model->headerData
獲取。將該項設定為可選,並新增到 ItemList
中。model->insertRow
插入一行,該行的資料由 ItemList
決定。ModelIndex
。這個函數主要用於模擬在 TableView
中追加一行資料,其中包括普通文字和可選框資料。
// 【新增一行】:為TableView新增一行資料(在檔案末尾插入)
void MainWindow::on_actionAppend_triggered()
{
QList<QStandardItem *> ItemList; // 建立臨時容器
QStandardItem *Item;
// 模擬新增一列的資料
for(int x=0; x<FixedColumnCount-1; x++)
{
Item = new QStandardItem("測試(追加行)"); // 迴圈建立每一列
ItemList << Item; // 新增到連結串列中
}
// 建立最後一個列元素,由於是選擇框所以需要單獨建立
// 1.獲取到最後一列的表頭下標,最後下標為6
QString str = model->headerData(model->columnCount()-1,Qt::Horizontal,Qt::DisplayRole).toString();
Item=new QStandardItem(str); // 建立 "是否合格" 欄位
Item->setCheckable(true); // 設定狀態為真
ItemList << Item; // 最後一個選項追加進去
model->insertRow(model->rowCount(),ItemList); // 插入一行,需要每個Cell的Item
QModelIndex curIndex=model->index(model->rowCount()-1,0); // 建立最後一行的ModelIndex
selection->clearSelection(); // 清空當前選中項
selection->setCurrentIndex(curIndex,QItemSelectionModel::Select); // 設定當前選中項為當前選擇行
}
對於刪除來說則更容易實現,只需要通過呼叫selection->currentIndex()
獲取噹噹前單元格模型索引,並通過呼叫model->removeRow
來實現一處即可,此處需要區別一下是不是最後一行,如果是最後一行則直接刪除即可,如果不是則需要在刪除資料後通過setCurrentIndex
將索引設定到前一個或第一個元素上,且核心程式碼如下所示;
// 【刪除一行】:刪除選中行
void MainWindow::on_actionDelete_triggered()
{
QModelIndex curIndex = selection->currentIndex(); // 獲取當前選擇單元格的模型索引
// 先判斷是不是最後一行
if (curIndex.row()==model->rowCount()-1)
{
model->removeRow(curIndex.row()); // 刪除最後一行
}
else
{
model->removeRow(curIndex.row()); // 刪除一行,並重新設定當前選擇行
selection->setCurrentIndex(curIndex,QItemSelectionModel::Select);
}
}
讀者可自行點選新增一行與插入行,觀察變化則可以理解兩者的區別,如下圖所示;
格式設定也是非常常用的功能,例如在Office
中就有表格元素居中、表格左對齊、表格右對齊、字型加粗顯示等,在Qt中Table
表格就預設自帶了這些功能的支援,通過直接呼叫setTextAlignment
並傳入Qt::AlignHCenter
居中、Qt::AlignLeft
用於左對齊、Qt::AlignRight
用於右對齊、而對於加粗顯示只需要通過呼叫setFont
將加粗厚的文字重新整理到表格中即可,這些功能具備相似性,如下是完整的程式碼實現;
// 設定表格居中對齊
void MainWindow::on_pushButton_clicked()
{
if (!selection->hasSelection())
{
return;
}
QModelIndexList selectedIndex=selection->selectedIndexes();
QModelIndex Index;
QStandardItem *Item;
for (int i=0; i<selectedIndex.count(); i++)
{
Index=selectedIndex.at(i);
Item=model->itemFromIndex(Index);
Item->setTextAlignment(Qt::AlignHCenter);
}
}
// 設定表格左對齊
void MainWindow::on_pushButton_2_clicked()
{
// 沒有選擇的項
if (!selection->hasSelection())
{
return;
}
// 獲取選擇的單元格的模型索引列表,可以是多選
QModelIndexList selectedIndex=selection->selectedIndexes();
for (int i=0;i<selectedIndex.count();i++)
{
QModelIndex aIndex=selectedIndex.at(i); // 獲取其中的一個模型索引
QStandardItem* aItem=model->itemFromIndex(aIndex); // 獲取一個單元格的項資料物件
aItem->setTextAlignment(Qt::AlignLeft); // 設定文字對齊方式
}
}
// 設定表格右對齊
void MainWindow::on_pushButton_3_clicked()
{
if (!selection->hasSelection())
{
return;
}
QModelIndexList selectedIndex=selection->selectedIndexes();
QModelIndex aIndex;
QStandardItem *aItem;
for (int i=0;i<selectedIndex.count();i++)
{
aIndex=selectedIndex.at(i);
aItem=model->itemFromIndex(aIndex);
aItem->setTextAlignment(Qt::AlignRight);
}
}
// 設定字型加粗顯示
void MainWindow::on_pushButton_4_clicked()
{
if (!selection->hasSelection())
{
return;
}
// 獲取選擇單元格的模型索引列表
QModelIndexList selectedIndex=selection->selectedIndexes();
for (int i=0;i<selectedIndex.count();i++)
{
QModelIndex aIndex=selectedIndex.at(i); // 獲取一個模型索引
QStandardItem* aItem=model->itemFromIndex(aIndex); // 獲取項資料
QFont font=aItem->font(); // 獲取字型
font.setBold(true); // 設定字型是否粗體
aItem->setFont(font); // 重新設定字型
}
}
讀者可依此點選下圖的四個按鈕來實習那對不同表格元素的個性化處理,當然如果需要儲存這些狀態,則還需要單獨儲存表格中的狀態值,在執行程式後依次設定即可;