HotPDF tách trình xem PDF Delphi của mình thành hai phần: THPDFViewerModel, một lớp thuần túy sở hữu trạng thái zoom, xoay, tìm kiếm, highlight, và điều hướng mà không phụ thuộc vào window handle nào, và THPDFViewer, một điều khiển dựa trên TScrollBox biến trạng thái đó thành các pixel. Sự tách bạch này chính là điều cho phép logic của trình xem chạy được, và được kiểm thử, mà không bao giờ cần tạo một form
Hầu hết các điều khiển trình xem tùy chỉnh không được xây dựng theo cách này. Mức zoom nằm trong một trường private của điều khiển, việc điều hướng trang giới hạn phạm vi ngay bên trong handler OnClick của một nút, và cách duy nhất để biết Ctrl+scroll có tuân thủ trần zoom hay không là chạy ứng dụng, nhấp chuột, và quan sát. Một điều khiển xây dựng theo kiểu đó hoạt động tốt cho đến khi nó cần một bộ hồi quy, hoặc một host thứ hai — một hộp thoại xem trước khi in, một dải thumbnail, một bộ duyệt hàng loạt không có cửa sổ hiển thị nào cả — và trạng thái bạn cần hóa ra lại bị hàn chết vào một TWinControl khăng khăng đòi một handle thật trước khi chịu làm bất cứ điều gì
Vì sao một điều khiển trình xem PDF cần tách MVC?
Một trình xem PDF cần kiểu tách bạch này vì trạng thái và cách trình bày của nó thay đổi vì những lý do khác nhau và với tốc độ khác nhau. Chỉ số trang, zoom, xoay view, kết quả tìm kiếm, và vùng highlight là trạng thái nghiệp vụ: chúng có thể được tính toán, xác thực, và serialize mà không cần một pixel nào trên màn hình. Việc vẽ một bitmap, bắt chuột, và vẽ một hình chữ nhật chọn kiểu marquee là các mối quan tâm về trình bày chỉ có ý nghĩa khi một điều khiển đã tồn tại. HotPDF giữ nhóm đầu tiên trong THPDFViewerModel, một lớp hoàn toàn không có tổ tiên windowing VCL nào, và nhóm thứ hai trong THPDFViewer, lớp sở hữu một instance model và phản ứng theo nó — gần với một cặp Model-View hơn là một MVC ba tầng theo sách giáo khoa, vì không có lớp Controller riêng biệt và chính THPDFViewer biến các sự kiện bàn phím và chuột thô thành các lệnh gọi model. Điều quan trọng hơn cả cái tên gọi là hướng phụ thuộc: không có gì trong THPDFViewerModel đòi hỏi một Handle, một vòng lặp message, hay một desktop hiển thị, và đó chính xác là điều cho phép bộ test riêng của HotPDF điều khiển việc chuyển trang, giới hạn zoom, các lệnh bàn phím, và chuyển đổi tọa độ hai chiều thông qua DUnitX mà không cần mở một cửa sổ nào
uses
DUnitX.TestFramework,
HPDFDoc, HPDFViewerModel;
type
[TestFixture]
TViewerModelTests = class
public
[Test]
procedure ZoomInStopsAtTheTopPresetLevel;
end;
procedure TViewerModelTests.ZoomInStopsAtTheTopPresetLevel;
var
Doc: THotPDF;
Model: THPDFViewerModel;
begin
Doc := THotPDF.Create(nil);
Model := THPDFViewerModel.Create;
try
Doc.LoadFromFile('sample.pdf');
Model.Document := Doc;
Model.Zoom := 64.0; // top of the preset table (6400%)
Model.ZoomIn; // already at the ceiling
Assert.AreEqual(64.0, Model.Zoom, 0.0001);
finally
Model.Free;
Doc.Free;
end;
end;
THPDFViewerModel thực sự sở hữu những gì
THPDFViewerModel sở hữu mọi thứ mà một trình xem cần để trả lời câu hỏi hiện tại nên hiển thị gì trên màn hình, mà không sở hữu cách vẽ nó ra sao. PageIndex, PageNumber, và PageCount theo dõi vị trí; Zoom và ZoomMode (vzmActualSize, vzmFitPage, vzmFitWidth, vzmCustom) theo dõi tỉ lệ; ViewRotation theo dõi một phép xoay trên màn hình không phá hủy, không bao giờ chạm vào entry /Rotate riêng của trang. Các phương thức điều hướng — FirstPage, PriorPage, NextPage, LastPage — và các phương thức zoom — ZoomIn, ZoomOut, duyệt qua một bảng cố định mười chín mức đặt sẵn từ 5% đến 6400% — cũng nằm ở đây, cùng với FindAll/FindNext/FindPrevious cho tìm kiếm văn bản và AddHighlightRegion/RemoveHighlightRegion/ClearHighlightRegions cho các chú thích trang lâu bền mà caller muốn giữ lại giữa các lần render. Model sở hữu cả đầu ra lẫn đầu vào: CreateCurrentPageSnapshot và CreateCurrentPageMetafile xuất chính xác trang đang hiển thị trên màn hình, và PrintCurrentView gửi chính view hiện tại đó — trang hiện tại, DPI suy ra từ zoom hiện tại, xoay hiện tại — đến một TPrinter, một tác vụ hẹp hơn, giới hạn trong phạm vi view, so với pipeline in toàn tài liệu được nói đến trong hướng dẫn in TPrinter của HotPDF. Mỗi thay đổi quan trọng cũng phát ra một sự kiện tương ứng — OnPageChange, OnZoomChange, OnSearchChange, OnHighlightChange, OnViewRotationChange — nên một bên đăng ký nhận biết được điều gì đã thay đổi mà không cần polling
THPDFViewer biết khi nào cần vẽ lại như thế nào?
THPDFViewer biết khi nào cần vẽ lại vì nó đăng ký theo model thay vì đoán mò. Constructor của THPDFViewer tạo một THPDFViewerModel riêng, sau đó nối từng sự kiện thông báo của nó — OnBeginUpdate, OnEndUpdate, OnHighlightChange, OnPageChange, OnSearchChange, OnViewRotationChange, OnZoomChange — với một handler private tương ứng. Công việc của mỗi handler đều nhỏ: gọi RefreshDocument, phương thức thực sự raster hóa trang hiện tại thông qua cùng bộ render trang có cache được mô tả trong nội bộ render trang thành bitmap của HotPDF, sau đó ghép các hộp highlight và kết quả tìm kiếm lên trên và áp dụng phép xoay view hiện tại. Các thuộc tính published như PageIndex, Zoom, ZoomMode, và ViewRotation là các bộ chuyển tiếp mỏng — getter đọc FModel.PageIndex, setter ghi FModel.PageIndex — nên từ Object Inspector hay từ code, điều khiển trông như thể nó giữ trạng thái trực tiếp, dù THPDFViewerModel mới là nơi duy nhất trạng thái đó thực sự tồn tại. Caller cũng không bị giới hạn ở tập con được chuyển tiếp: THPDFViewer phơi bày chính model thông qua một thuộc tính Model: THPDFViewerModel chỉ đọc, nên code muốn dùng FindFormFieldAt hay PrefetchCurrentPageSnapshots — không cái nào trong hai cái này được điều khiển phơi bày lại — có thể vượt qua lớp bọc và gọi thẳng model
procedure THPDFViewer.RefreshDocument;
var
Bitmap: TBitmap;
DPI: Integer;
begin
// simplified: the real method also resolves fit-mode DPI
// and composites highlight and search-hit rectangles first
if (FModel.Document = nil) or (FModel.PageIndex < 0) then Exit;
DPI := Round(96 * FModel.Zoom);
Bitmap := FModel.Document.RenderLoadedPageToBitmapCached(FModel.PageIndex, DPI);
try
FModel.ApplyViewRotation(Bitmap);
FImage.Picture.Bitmap.Assign(Bitmap);
finally
Bitmap.Free;
end;
end;
BeginUpdate và EndUpdate: chặn cơn bão vẽ lại
BeginUpdate và EndUpdate tồn tại vì một thay đổi logic đơn lẻ thường chạm vào nhiều mảnh trạng thái cùng lúc, và việc vẽ lại sau mỗi mảnh sẽ vừa lãng phí vừa gây nhiễu về mặt hình ảnh. Việc thay tài liệu đang tải là ví dụ rõ nhất: gán THPDFViewerModel.Document đặt lại phép xoay view, xóa kết quả tìm kiếm, xóa các vùng highlight, và nhảy về trang một, và mỗi bước trong số đó thông thường phát ra sự kiện thay đổi riêng của nó. THPDFViewerModel bọc chuỗi đó trong BeginUpdate/EndUpdate, một cặp đếm tham chiếu mà các lệnh gọi lồng nhau chỉ phát OnBeginUpdate tại lúc chuyển vào lệnh gọi ngoài cùng nhất và OnEndUpdate tại lúc chuyển ra khỏi nó. THPDFViewer theo dõi cùng độ sâu đó ở phía mình và bỏ qua RefreshDocument cho mỗi sự kiện nhỏ lẻ trong khi bộ đếm còn lớn hơn không, rồi vẽ lại đúng một lần khi lô thay đổi kết thúc. Các sự kiện nhỏ lẻ vẫn phát ra trong lúc lô đang chạy, nên một bên đăng ký chỉ quan tâm OnSearchChange vẫn nghe được nó; chỉ có việc vẽ lại của chính điều khiển mới bị gộp thành một lệnh gọi thay vì bốn
Việc highlight kiểu marquee ánh xạ một thao tác kéo chuột về tọa độ PDF như thế nào?
Việc highlight kiểu marquee ánh xạ một thao tác kéo chuột về tọa độ PDF thông qua một cặp phương thức model được xây dựng riêng cho chính chuyến đi khứ hồi đó: PagePointToView và ViewPointToPage. Cả hai nhận một chỉ số trang, một DPI, và một điểm, và cả hai giải quyết phép biến đổi theo hai giai đoạn — trước tiên là entry /Rotate riêng của trang và gốc tọa độ PDF ở góc dưới-trái của nó, sau đó là ViewRotation riêng biệt, không phá hủy của view và gốc tọa độ thiết bị ở góc trên-trái của trình xem — cụ thể là để chiều ngược lại có thể hoàn tác hai giai đoạn theo đúng thứ tự đảo ngược và khứ hồi chính xác qua toàn bộ mười sáu tổ hợp giữa xoay trang và xoay view. THPDFViewer gọi ViewPointToPage khi người dùng thả chuột sau khi kéo một hình chữ nhật ở chế độ tương tác vimHighlight, biến hai điểm thiết bị thành một THPDFRectangle trong không gian trang, và giao nó cho Model.AddHighlightRegion. Một chi tiết đáng biết nếu bạn xây dựng thứ gì đó tương tự: việc bắt chuột thuộc về trình xem kế thừa từ TScrollBox, không thuộc về TImage con nơi bitmap được vẽ vào, vì TControl.MouseCapture là protected và chỉ điều khiển cha mới có thể chiếm giữ nó — nên một thao tác kéo rời khỏi ranh giới của image trước khi nút chuột được nhả vẫn được giải quyết thông qua MouseMove/MouseUp đã override của chính trình xem thay vì bị điều khiển con âm thầm bỏ qua
var
ViewPt, PagePt: THPDFViewerPoint;
Rect: THPDFRectangle;
begin
ViewPt.X := 240; // device pixels inside the rendered image
ViewPt.Y := 96;
if Model.ViewPointToPage(Model.PageIndex, ViewPt, PagePt,
RenderedDPI) then // DPI you last rendered at
begin
Rect.Left := PagePt.X - 40; Rect.Bottom := PagePt.Y - 10;
Rect.Right := PagePt.X + 40; Rect.Top := PagePt.Y + 10;
Model.AddHighlightRegion(Model.PageIndex, Rect);
end;
end;
Sự tách bạch này mang lại điều gì ngoài một bộ test màu xanh
Lợi ích không chỉ giới hạn ở việc các test chạy qua trong một job CI không có phiên desktop. Vì THPDFViewer chuyển tiếp đến THPDFViewerModel thay vì nhân bản logic của nó, HotPDF đã có thể thêm một bên tiêu thụ thứ ba — THPDFViewerAction và các lớp con cụ thể như THPDFZoomInAction và THPDFFindNextAction — cắm điều hướng, zoom, tìm kiếm, và xoay vào một TActionList chuẩn của Delphi, để một nút toolbar hay một mục menu có thể điều khiển trình xem theo kiểu khai báo, tự bật/tắt dựa trên việc một trình xem có đang được phân giải làm mục tiêu của action hay không. Không lớp nào trong số đó cần biết gì về bitmap hay GDI; nó gọi Viewer.NextPage hoặc Viewer.Model.FindNext, và chuỗi sự kiện sẵn có sẽ lo phần vẽ lại. Và vì không có gì trong THPDFViewerModel tham chiếu đến TScrollBox, TImage, hay một window handle, cỗ máy trạng thái bên dưới cũng không bị hàn chết vào riêng điều khiển đó — cùng một model có thể đứng sau một bề mặt render khác mà không cần đụng đến một dòng logic điều hướng, zoom, hay tìm kiếm nào
Nơi cache render giúp ích, và nơi nó không giúp được gì
Cache render của THPDFViewerModel giúp ích trong phạm vi một tài liệu đã tải, nhưng nó không thay đổi chi phí tải tài liệu đó ngay từ đầu. CreatePageSnapshot, CreateCurrentPageSnapshot, và các phương thức prefetch PrefetchPageSnapshots/PrefetchCurrentPageSnapshots đều đi qua cùng một bộ render có cache được đánh khóa theo trang và DPI, nên việc quay lại một trang bạn đã xem ở cùng mức zoom là một lần trúng cache thay vì render lại, và việc prefetch một bán kính nhỏ các trang lân cận làm mượt trường hợp phổ biến của một người đọc lật trang tới từng trang một. Tuy nhiên, không điều nào trong số đó đụng đến chi phí của lệnh gọi LoadFromFile ban đầu, và một trình xem được xây dựng để mở bất cứ thứ gì người dùng kéo thả vào cuối cùng sẽ gặp một file đủ lớn để khiến lệnh gọi đó trở thành nút thắt cổ chai thực sự. Để biết về phương án phân tầng, dựa trên handle thay cho việc tải toàn bộ — đáng biết trước khi ngày đó đến — xem bài viết đồng hành về Direct File API cho PDF lớn
Các lớp Model và View được mô tả ở đây là hai mảnh ghép nữa của cùng bề mặt tài liệu đã tải được dùng xuyên suốt HotPDF Component dành cho Delphi và C++Builder, được xây dựng để có thể điều khiển từ một form, từ một TActionList, hoặc từ không đâu cả