1. Tại sao việc đọc Excel bằng Qt lại bị treo như trình chiếu?
Khi lần đầu sử dụng Qt để thao tác với bảng Excel, tôi đã hào hứng viết một đoạn lặp để đọc từng ô. Kết quả là khi mở file chứa 5000 dòng dữ liệu, thanh tiến trình di chuyển chậm như rùa bò, con trỏ chuột biến thành vòng tròn màu, và chương trình hoàn toàn bị treo - tình huống này chắc hẳn nhiều bạn mới tiếp cận với thao tác Excel trên Qt đều đã gặp phải.
Vấn đề nằm ở chi phí gọi COM interface. Qt sử dụng QAxObject để gọi COM interface của Excel, mỗi lần đọc một ô giống như gọi điện cho Excel: "Xin hỏi giá trị của ô A1 là gì?", "Bây giờ xin cho biết giá trị của ô A2"... Loại giao tiếp liên tiến trình tần suất cao này tạo ra chi phí khổng lồ. Thực tế đo lường cho thấy việc đọc 1000 ô mất 2.3 giây, trong khi cùng dữ liệu đó nếu đọc theo phạm vi chỉ mất 0.05 giây, chênh lệch tới 46 lần!
Tệ hơn nữa, nhiều nhà phát triển mắc phải hai lỗi điển hình sau:
- Mỗi lần đọc/ghi đều tạo mới instance của ứng dụng Excel, tương tự như việc đóng mở phần mềm Excel liên tục
- Sử dụng
dynamicCallđể lấy từng ô một, giống như múc nước biển bằng thìa thay vì mở cống xả
// Ví dụ sai: đọc từng ô một (tốc độ rùa)
QAxObject *worksheet = workbook->querySubObject("Worksheets(int)", 1);
for(int row=1; row<=5000; row++){
for(int col=1; col<=10; col++){
QAxObject *cell = worksheet->querySubObject("Cells(int,int)", row, col);
QString value = cell->dynamicCall("Value()").toString(); // điểm nghẽn chết người
delete cell;
}
}
2. Ba kỹ thuật tối ưu hiệu suất
2.1 Đọc theo phạm vi: từ múc nước đến máy bơm
COM interface của Excel cung cấp đối tượng Range, cho phép đọc dữ liệu theo vùng hình chữ nhật một lần. Điều này giống như thay thìa bằng máy bơm - chúng ta không còn hỏi từng ô giá trị là gì, mà nói: "Xin vui lòng gửi gói dữ liệu từ vùng A1 đến J5000 cho tôi".
Tốc độ của đoạn mã được tối ưu hóa ngay lập tức:
// Cách đúng: đọc theo phạm vi (tốc độ闪电)
QAxObject *usedRange = worksheet->querySubObject("UsedRange");
QAxObject *rows = usedRange->querySubObject("Rows");
QAxObject *columns = usedRange->querySubObject("Columns");
int rowCount = rows->property("Count").toInt();
int colCount = columns->property("Count").toInt();
// Lấy tất cả dữ liệu cùng một lúc
QVariant var = usedRange->dynamicCall("Value()");
QVariantList allData = var.toList();
Kết quả so sánh thực tế rất ấn tượng:
| Lượng dữ liệu | Đọc từng ô | Đọc theo phạm vi | Tăng tốc |
|---|---|---|---|
| 1000 dòng | 2.3s | 0.05s | 46x |
| 10000 dòng | 23s | 0.4s | 57x |
2.2 Tái sử dụng instance Excel: đừng khởi động Excel liên tục
Trong nhiều ví dụ hướng dẫn, bạn sẽ thấy mẫu mã như sau:
void readExcel(){
QAxObject excel("Excel.Application");
// mã thao tác...
excel.dynamicCall("Quit()");
}
Khi xử lý hàng loạt, điều này tương đương với việc đóng mở Excel liên tục - giống như mỗi lần đổ nước đều phải mở cửa tủ lạnh rồi đóng lại. Cách đúng là duy trì singleton:
// Duy trì instance Excel toàn cục
static QAxObject *g_excel = nullptr;
void initExcel(){
if(!g_excel){
g_excel = new QAxObject("Excel.Application");
g_excel->setProperty("Visible", false);
}
}
void cleanupExcel(){
if(g_excel){
g_excel->dynamicCall("Quit()");
delete g_excel;
g_excel = nullptr;
}
}
2.3 Đọc bất đồng bộ: giữ UI mượt mà
Ngay cả khi tối ưu hóa phương thức đọc, xử lý 100,000+ dòng dữ liệu vẫn có thể làm chặn giao diện. Lúc này cần kết hợp đa luồng + tín hiệu slot:
class ExcelWorker : public QObject {
Q_OBJECT
public slots:
void readData(const QString &filePath){
// thao tác tốn thời gian để ở đây
QVariantList data = readExcelRange(filePath);
emit dataReady(data);
}
signals:
void dataReady(const QVariantList &);
};
// Trong luồng chính
QThread *thread = new QThread;
ExcelWorker *worker = new ExcelWorker;
worker->moveToThread(thread);
connect(thread, &QThread::started, [=](){ worker->readData("data.xlsx"); });
connect(worker, &ExcelWorker::dataReady, this, &MainWindow::handleData);
thread->start();
3. Kỹ thuật nâng cao trong thực chiến
3.1 Tối ưu bộ nhớ: đọc file lớn theo từng phần
Khi gặp file Excel trên 50MB, ngay cả việc đọc theo phạm vi cũng có thể gây tràn bộ nhớ. Lúc này cần chiến lược đọc theo từng phần:
const int BATCH_SIZE = 5000; // Xử lý 5000 dòng mỗi lần
int totalRows = getTotalRowCount();
for(int startRow=1; startRow<=totalRows; startRow+=BATCH_SIZE){
int endRow = qMin(startRow+BATCH_SIZE-1, totalRows);
QString range = QString("A%1:Z%2").arg(startRow).arg(endRow);
QAxObject *rangeObj = sheet->querySubObject("Range(const QString&)", range);
QVariant batchData = rangeObj->dynamicCall("Value()");
processBatchData(batchData);
}
3.2 Xử lý lỗi: yếu tố bắt buộc cho tính ổn định
thao tác Excel có thể gặp nhiều tình huống bất ngờ:
- File đang được sử dụng
- Định dạng không tương thích
- Thiếu quyền truy cập
Phải thêm xử lý lỗi đầy đủ:
bool safeReadExcel(const QString &path){
try {
QAxObject *workbook = excel->querySubObject("Workbooks")->querySubObject("Open(const QString&)", path);
if(!workbook) throw std::runtime_error("Không thể mở sổ làm việc");
// mã thao tác thực tế...
workbook->dynamicCall("Close(Boolean)", false);
return true;
} catch(const std::exception &e) {
qCritical() << "Thao tác Excel thất bại:" << e.what();
return false;
}
}
3.3 Tiền xử lý định dạng: bí quyết tăng tốc
Nếu chỉ cần dữ liệu mà không quan tâm đến kiểu dáng, tắt các tính năng này trước có thể tăng tốc độ:
// Cấu hình tối ưu instance Excel
excel->setProperty("ScreenUpdating", false);
excel->setProperty("EnableEvents", false);
excel->setProperty("Calculation", -4135); // xlCalculationManual
4. Bảng so sánh hiệu suất thực tế
Tôi đã thử ba cách đọc cùng một file Excel chứa 100,000 dòng dữ liệu:
- Phương pháp gốc: đọc từng ô một
- Tối ưu cơ bản: đọc theo phạm vi + tái sử dụng instance
- Giải pháp tối ưu nhất: đọc theo phạm vi + tái sử dụng instance + bất đồng bộ + tối ưu định dạng
Môi trường thử nghiệm:
- CPU: i7-11800H
- RAM: 32GB
- File Excel: 108MB
Kết quả so sánh:
| Phương án | Thời gian | Bộ nhớ sử dụng | UI bị lag |
|---|---|---|---|
| Phương pháp gốc | 4 phút 23s | 1.2GB | Đóng băng hoàn toàn |
| Tối ưu cơ bản | 2.8s | 580MB | Lag nhẹ |
| Giải pháp tối ưu | 1.4s | 350MB | Mượt hoàn toàn |
Lưu ý đặc biệt: nếu lượng dữ liệu trên 1 triệu dòng, nên cân nhắc sử dụng trực tiếp các thư viện chuyên nghiệp như libxlsxwriter, hoặc chuyển đổi Excel sang CSV trước khi xử lý. Bởi vì thao tác Excel với Qt về cơ bản vẫn đang sử dụng COM interface để giao tiếp với tiến trình Excel, các giới hạn vật lý không thể vượt qua.
5. Hướng dẫn tránh lỗi
Sau khi triển khai giải pháp Qt + Excel cho nhiều doanh nghiệp, tôi tổng hợp được những kinh nghiệm xương máu này:
- Giết tiến trình triệt để: ngay cả khi gọi Quit(), tiến trình Excel có thể còn sót lại. Cách an toàn là kết thúc bằng:
system("taskkill /f /im excel.exe");
- Bẫy kiểu dữ liệu: số trong Excel có thể bị QVariant chuyển thành double dẫn đến mất độ chính xác, nên dùng
toString()để xử lý thống nhất - Vấn đề cài đặt khu vực: một số khu vực Excel mặc định dùng dấu phẩy làm thập phân, cần thiết lập trước khi đọc:
QAxObject *application = excel->querySubObject("Application");
application->setProperty("UseSystemSeparators", false);
application->setProperty("DecimalSeparator", ".");
- Cấm kỵ đa luồng: QAxObject không an toàn về luồng, phải tạo và hủy trong cùng một luồng
Cuối cùng, chia sẻ một mẹo gỡ lỗi: trong giai đoạn phát triển có thể tạm thời đặt excel->setProperty("Visible", true), như vậy có thể thấy quá trình thao tác thực tế của Excel, giúp dễ dàng xác định vấn đề.