Hãy hình dung một job gần như chẳng làm gì cả: mở một workbook hàng tháng, ghi ngày hôm nay vào một ô, lưu lại. Chạy nó qua một dịch vụ đủ nhiều lần và một khiếu nại vẫn cứ xuất hiện. Macro biến mất, hoặc tỷ giá hối đoái được liên kết giờ đọc ra #REF!, và đội vận hành tin chắc rằng code của bạn đã xóa chúng. Nó chẳng xóa gì cả. Điều thường xảy ra là một workbook có bật macro được xuất ra dưới một cái tên .xlsx trơn, và Excel tuân theo các quy tắc content-type của ECMA-376: một package có content type khai báo không có VBA thì không thể nạp một VBA project, bất kể các byte đó có đang nằm ngay đó hay không. Tệp không hề hỏng. Nó bị đổi tên vào một trạng thái buộc Excel phải bỏ qua một phần của nó
Macro và liên kết workbook ngoài là hai thứ mà tự động hóa làm mất đáng tin cậy nhất, vì cùng một lý do nền tảng. Cả hai đều sống bên ngoài lưới ô mà code chỉnh sửa thực sự chạm tới, nên code suy luận theo dòng và cột sẽ làm rơi mất chúng mà chẳng bao giờ phát ra một lệnh xóa nào cả. HotXLS là một thư viện thuần cho Delphi và C++Builder đọc và ghi XLS và XLSX mà không cần cài Excel, và nó xem cả hai loại tài sản này như những payload nó cố tình mang theo chứ không phải dữ liệu mà nó tình cờ sao chép. Phần tiếp theo là những gì mỗi loại cần từ đường lưu của bạn, và đâu là điểm mà các đảm bảo đó dừng lại
Vì sao hai loại tài sản này hành xử khác nhau khi bị viết lại
Một VBA project là một khối nhị phân mờ đục duy nhất. Trong một package OOXML đó là tệp vbaProject.bin; trong một tệp BIFF kiểu cũ đó là một OLE storage. Có đúng hai cách để làm mất nó: writer không bao giờ sao chép nó vào đầu ra, hoặc đầu ra nhận một kiểu tệp cấm nó. Cả hai kiểu thất bại đều toàn diện và im lặng. Project đó hoặc có mặt, hoặc không
Một liên kết ngoài hoàn toàn không phải là một blob. Đó là một đồ thị nhỏ các mối quan hệ: một đường dẫn hay URL đích trỏ tới một workbook khác, danh sách tên sheet mà đích đó phơi ra, và một cache tùy chọn chứa các giá trị lần cuối thấy được trong những sheet đó để Excel có thể hiển thị điều gì đó khi đích đang offline. Ba phần đó có vòng đời khác nhau khi bị viết lại, và một thư viện có thể trung thành giữ lại một số phần trong khi âm thầm làm rơi mất những phần khác. Sự bất đối xứng đó là phần đáng để chính xác hóa, vì không có gì trong code chỉnh sửa ô sẽ làm nó lộ ra cả
Mang một VBA project xuyên qua một lần viết lại XLSX
Ở phía XLSX, TXLSXWorkbook giữ nguyên payload macro không đổi một chữ. Thuộc tính VbaProject giữ các byte thô của vbaProject.bin bên trong một AnsiString, và một chuỗi rỗng là cách model nói rằng không có macro nào cả. Xung quanh nó là ba thao tác: HasVbaProject trả lời liệu một project có mặt hay không, ClearVbaProject cố tình xóa nó đi, và LoadVbaProjectFromFile tiêm vào một project được trích ra từ một template. Lệnh gọi cuối cùng đó đáng giá hơn vẻ ngoài của nó. Nó cho phép các workbook được tạo tự động lấy về một macro project chuẩn mà không cần kéo cả một tệp template đầy đủ xuyên suốt pipeline
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Data');
Sheet.Cells[1, 1].Value := 'Refreshed ' + DateTimeToStr(Now);
Book.LoadVbaProjectFromFile('macros\vbaProject.bin');
if not Book.HasVbaProject then
raise Exception.Create('VBA payload failed to load');
// Phần đuôi .xlsm không phải trang trí: nó chọn
// content type bật macro bên trong package.
Book.SaveAs('monthly-report.xlsm');
finally
Book.Free;
end;
end;
Dòng lưu chính là nơi cả vấn đề xoay chuyển. Một workbook giữ một VBA project phải được ghi ra với ngữ nghĩa bật macro, và HotXLS áp dụng chúng khi tên đích kết thúc bằng .xlsm. Đưa cho nó .xlsx thay vào đó thì Excel sẽ từ chối macro, dù các byte đó vẫn hiện diện đầy đủ trong package và vẫn deserialize ổn thỏa. Phần đuôi mở rộng không phải là trang trí; nó chọn content type báo cho Excel biết một VBA project được phép tồn tại. Hầu hết thời gian bạn chỉ cần mang payload đó xuyên suốt. Khi bạn cần đọc sâu vào bên trong nó, chẳng hạn để liệt kê tên module cho một báo cáo audit, ParsedVBAProject phơi ra một model module đã phân tích trong khi VbaProject vẫn giữ nguyên các byte gốc chưa đụng tới
Tái sử dụng macro từ các workbook XLS kiểu cũ
Facade BIFF phản chiếu lại bộ công cụ đó với thêm một bước. HasVBAProject dò một tệp đã nạp, SaveVBAProjectToFile ghi phần storage của project ra đĩa, và LoadVBAProjectFromFile đọc nó trở lại vào một workbook khác. Đường vòng qua một tệp trung gian biến một việc hiện đại hóa thường gặp trở nên đơn giản: nhấc macro ra khỏi một model từ thời 2003 rồi cấy chúng vào một đầu ra XLS vừa được tạo mới, không cần template gốc nào tại thời điểm chạy
var
Src, Dst: IXLSWorkbook; // tham chiếu interface: không cần tự gọi Free
begin
Src := TXLSWorkbook.Create;
if Src.Open('legacy-model.xls') <= 0 then
raise Exception.Create('Cannot open legacy model');
if Src.HasVBAProject then
Src.SaveVBAProjectToFile('extracted-vba.bin');
Dst := TXLSWorkbook.Create;
Dst.Sheets.Add.Name := 'Report2026';
Dst.LoadVBAProjectFromFile('extracted-vba.bin');
Dst.SaveAs('report-with-macros.xls');
end;
Mô hình bộ nhớ chính là cái bẫy ở đây, và nó đi ngược lại với class XLSX. TXLSWorkbook được giữ thông qua interface đếm tham chiếu IXLSWorkbook, nên bạn không bao giờ tự tay free nó; còn TXLSXWorkbook của XLSX là một object thuần mà bạn phải bọc trong try..finally rồi free. Trộn hai quy ước đó trong cùng một unit thì lỗi crash double-free sẽ theo sau. Còn một ranh giới nữa đáng tôn trọng: giữ việc trích xuất và tiêm vào bên trong một định dạng tệp duy nhất. Phần storage project của BIFF và vbaProject.bin của OOXML là anh em họ, không phải cùng một container, và một pipeline phải phát ra macro ở cả hai định dạng nên giữ một template macro riêng cho mỗi định dạng
Liên kết ngoài: bản đồ sống sót, giá trị cache thì không
Với các workbook XLSX, HotXLS phơi ra các liên kết ngoài thông qua collection ExternalLinks. Mỗi TXLSXExternalLink mang theo một Target, đường dẫn hay URL của workbook từ xa, cộng thêm một danh sách SheetNames đặt tên các sheet mà nó tham chiếu tới. Cả hai đều sống sót nguyên vẹn qua một chu kỳ mở-rồi-lưu, và bạn cũng có thể dựng một liên kết từ đầu:
var
Link: TXLSXExternalLink;
begin
Link := Book.ExternalLinks.Add('\\fileserver\finance\fx-rates-2026.xlsx');
Link.SheetNames.Add('FX');
if Book.ExternalLinks.Count > 0 then
Writeln(Format('%d external link(s): delivery requires reachable targets',
[Book.ExternalLinks.Count]));
end;
Ranh giới thực sự nằm sâu hơn một tầng so với danh sách đích. HotXLS round-trip bản đồ liên kết, nghĩa là đích và danh sách tên sheet, nhưng nó không phân tích hay viết lại các giá trị ô đã cache mà OOXML giữ trong phần tử sheetDataSet của liên kết đó. Cái cache đó là thứ cho phép Excel hiển thị một con số lần cuối biết được khi tệp nguồn đang offline, và một workbook được tạo tự động xuất ra mà không có nó. Hậu quả rơi vào người nhận, chứ không phải vào bạn. Mở một tệp như vậy khi đích không thể tới được, một laptop rời khỏi VPN hoặc một share đã bị đổi tên, và các công thức phụ thuộc vào liên kết đó sẽ giải ra thành #REF! hoặc khựng lại sau một hộp thoại cập nhật. Vậy nên hai quy tắc rút ra từ đây. Đừng hứa hẹn rằng một workbook được tạo tự động sẽ hiển thị các giá trị liên kết ngoài của nó khi offline. Và hãy đọc một ExternalLinks.Count khác 0 như một điều kiện tiên quyết khi bàn giao chứ không phải như một tính năng: mọi đích đều phải tới được từ bất cứ nơi nào tệp thực sự sẽ được mở lên
Những gì trình đọc XLS giữ nguyên byte-cho-byte
Với những cấu trúc mà nó không model hóa, phía BIFF có một câu trả lời khác: để nguyên chúng đúng như lúc tìm thấy. Pivot cache và pivot view (họ bản ghi SX*), định nghĩa QueryTable, kết nối dữ liệu ngoài, custom view, header picture, và bản ghi theme, tất cả đều đi qua một chu kỳ mở-rồi-lưu dưới dạng các khối bản ghi thô, không phân tích và không sửa đổi. Bản thân các tham chiếu ngoài cũng round-trip qua các bản ghi EXTERNSHEET và SupBook bên dưới. Không có API tạo mới có kiểu cho chúng ở phía XLS, nhưng một liên kết đã tồn tại sẽ sống sót qua chỉnh sửa mà không hề đụng tới
Việc giữ nguyên byte-cho-byte là một đảm bảo thực sự nhưng có một cạnh sắc. Vì không có gì đọc một cấu trúc đã được giữ nguyên, các chỉnh sửa của bạn không thể làm hỏng nó. Cũng vì lý do đó, không có gì cập nhật nó cả. Chèn dòng xuyên qua một vùng mà một pivot cache hay query table đã giữ nguyên đang trỏ tới, và cấu trúc đó vẫn giữ tọa độ gốc trong khi dữ liệu bên dưới dịch chuyển. Tệp vẫn là XML hay BIFF hợp lệ; ý nghĩa đã âm thầm trôi lệch, và không có lỗi nào bật lên để báo cho bạn biết. Cách bố trí đứng vững được là giữ các chỉnh sửa tạo tự động trên những sheet không chứa cấu trúc nào đã được giữ nguyên, đó cũng chính là kỷ luật bảo vệ các sheet đã khóa và đã cấu hình in ấn trong bài viết của chúng tôi về bảo vệ worksheet và page setup
Xác minh tệp mà bạn thực sự đã ghi ra
Cả hai kiểu thất bại đều im lặng vào lúc ghi, nên phép khẳng định thực sự quan trọng phải được thực hiện bằng cách mở lại đầu ra thay vì tin tưởng vào đoạn code đã tạo ra nó. Ba lượt kiểm tra bao trùm gần như mọi thứ. Mở lại tệp và xác nhận HasVbaProject vẫn trả về true bất cứ khi nào macro được kỳ vọng có mặt, điều này bắt được cả một payload bị rơi mất lẫn một phần mở rộng sai chỉ trong một lượt kiểm tra. Đọc ExternalLinks.Count và so sánh nó với số đếm từ trước khi viết lại. Sau đó mở tệp một lần trong Excel với macro bị vô hiệu hóa, vì việc kiểm tra content-type của Excel khắt khe hơn bất kỳ thư viện nào, và Excel chính là chương trình mà khách hàng của bạn sẽ dùng để đánh giá tệp
Không điều nào trong số đó đòi hỏi một lượt phân tích đầy đủ ngay từ đầu vào. Khi workbook đến với số lượng lớn và bạn chỉ cần sàng lọc xem cái nào mang nội dung cần kiểm soát, việc dò nhẹ được trình bày trong bài viết của chúng tôi về liệt kê sheet và kiểm tra workbook nhẹ nhàng cho phép bạn định tuyến các tệp mang macro và liên kết vào một pipeline khắt khe hơn trước khi lượt viết lại đầu tiên từng chạy
Có vài câu hỏi xuất hiện đủ thường xuyên để đáng trả lời trực tiếp. HotXLS không bao giờ thực thi các macro mà nó giữ lại: không có VBA runtime nào trong thư viện cả, chỉ có cơ chế để lưu trữ, sao chép, trích xuất, và tiêm project đó vào như dữ liệu. Trên một server, đó là một đặc tính bảo mật đáng nêu ra, vì một macro độc hại đi qua pipeline sẽ ở trạng thái bất động cho tới khi một Excel desktop mở tệp lên và người dùng bật content. Chuyển đổi một tệp .xlsm sang .xlsx mà vẫn giữ macro là điều không thể, và đó là quy tắc của định dạng chứ không phải giới hạn của thư viện: content type của .xlsx khai báo một workbook không macro, nên kết quả trung thực duy nhất là giữ nguyên .xlsm hoặc gọi ClearVbaProject rồi xuất ra một tệp thực sự không còn macro nào cả. Việc đổi tên âm thầm là lựa chọn duy nhất khiến chẳng ai hài lòng. Và khi các ô liên kết hiện ra #REF! sau một lần viết lại, nguyên nhân là cache giá trị bị thiếu đã bàn ở trên: tệp mới mang theo đích nhưng không mang theo các con số đã cache, nên Excel phải giải quyết nguồn ngay lúc mở, và một đường dẫn không tới được hoặc phụ thuộc môi trường sẽ đánh bại nó. Hoặc là đảm bảo đích có thể tới được, hoặc là ghi các giá trị đã tính sẵn vào ô trước khi bàn giao rồi loại bỏ hoàn toàn sự phụ thuộc đó
Chỉnh sửa workbook của người khác phần lớn là công việc giữ nguyên những thứ mà bạn không viết ra và cũng không hiểu hết. Các cơ chế round-trip cho VBA và liên kết ngoài được mô tả ở đây đi kèm sẵn trong HotXLS Delphi Component cho Delphi và C++Builder, cùng với các thuộc tính audit cho phép bạn phát hiện nội dung cần kiểm soát ngay khi một tệp vừa tới