PDFlibPas'ın TPDFlibViewer kontrolü, yerinde bir form alanı düzenleyicisini düzenleyicinin kendi OnExit olayı üzerinden işler ve bu tasarım seçimi klasik bir Delphi VCL tuzağını gizler: odaklanılmış bir kontrolü kendi OnExit işleyicisinin içinden gizlemek, ebeveyn değiştirmek veya yok etmek, ilk çağrı dönmeden önce OnExit'i ikinci kez tetikleyebilir ve işleme mantığını kendi içine geri gönderebilir
Bunun ürettiği hata temiz bir yeniden üretime direnir. Bir kullanıcı taranmış bir başvuru formundaki bir dizi metin alanında hızlıca Tab tuşuyla dolaşır ve arada bir görüntüleyici bir erişim ihlali fırlatır veya daha kötüsü, çalışmaya devam ederken sessizce yanlış değeri iki tab geride bir alana yazar. Bunu talep üzerine yeniden üretin ve hata geriye dönüp bakıldığında bariz görünür; onu tek bir müşterinin çökme raporundan kovalayın ve bir hayalet gibi görünür, çünkü ikinci OnExit'in gerçekten tetiklenip tetiklenmediği, alan türü, yazma hızı ve o anda mesaj kuyruğunun başka ne yaptığıyla değişen pencere-tutamacı ve odak zamanlamasına bağlıdır
TPDFlibViewer render edilmiş bir sayfanın üzerine gerçek bir düzenleyiciyi nasıl koyar
TPDFlibViewer, her PDF sayfasını bir bitmap'e render eder ve varsayılan olarak form alanlarını canlı VCL kontrollerine dönüştürmez; bu yüzden BeginEditFormField, iki dünyayı köprüleyen metottur: bir alan indeksiyle çağrıldığında, alanın dikdörtgenini bulur ve onu istemci koordinatlarına dönüştürür, sonra bir metin alanı için o dikdörtgenin üzerine gerçek bir TEdit veya TMemo, veya bir seçim alanı için csDropDownList stilinde bir TComboBox bırakır, alanın mevcut değeri zaten yüklenmiş olarak. ISO 32000-2 §12.7, bir PDF içinde bir metin veya seçim form alanının ne olduğunu tanımlar, ama o spesifikasyonda hiçbir şey bir Windows uygulamasının birinin birine yazmasına nasıl izin vermesi gerektiğini söylemez ve BeginEditFormField'ın var olma nedeni tam olarak o boşluğu doldurmaktır. Hem OnKeyDown hem de OnExit, TPDFlibViewer'ın kurduğu her düzenleyicide aynı iki görüntüleyici metoduna, InplaceEditorKeyDown ve InplaceEditorExit'e bağlanır; bu eşleştirme, etkileşimli form doldurma v3.220.0'da ilk indiğinden beri değişmeden gönderilmiştir ve sorunun başladığı yer OnExit'tir
Düzenleyiciyi gizlemek OnExit'i neden ikinci kez tetikliyor?
VCL'deki TWinControl, odaklanılmış bir kontrolde Visible veya Parent'a bir değişikliği, odağı ondan hemen uzaklaştırmak için bir neden olarak ele alır ve odağı bir kontrolden uzaklaştırmak, o kontrolün OnExit olayını tam olarak tetikleyen şeydir, eşzamanlı olarak, onu tetikleyen özellik atamasının kendisi bile dönmeden önce. PDFlibPas'ın yerinde düzenleyiciyi kapatmak ve değerini form alanına geri yazmak için kullandığı metot olan CommitInplaceEditor, çıkış yolunda tam olarak bu iki şeyi yapmak zorundadır: kontrolün sayfanın üzerine çizmeyi durdurması ve girdi almayı durdurması için Editor.Visible'ı False'a ve Editor.Parent'ı nil'e ayarlamak. Düzenleyici hâlâ odaktayken bunlardan birini yapın, ki kullanıcı onu az önce terk ettiğinden neredeyse her zaman odaktadır, ve OnExit, o düzenleyicinin OnExit'inin tetikleyeceği son şey olması gereken tam çağrının ortasında yeniden tetiklenir
CommitInplaceEditor kendi kendine yeniden girdiğinde ne yanlış gider?
Naif bir işleme metodu bunun bedelini iki şekilden birinde öder. Ya alanın değerini iki kez yazar, bir kez orijinal çağrıdan ve bir kez ilki kendi durumuna dokunmayı bitirmeden önce sıkışıp kalan yeniden giren çağrıdan, ya da bir çerçeve çağrı yığınının daha aşağısında hâlâ o aynı kontrolün kendi olay işleyicisinin içindeyken düzenleyici kontrolünü serbest bırakmaya çalışır ki bu, VCL'de tanımsız bir bölgedir ve neredeyse herhangi bir satıra işaret edebilen, mutlaka gerçekte neden olan satır olmayan bir erişim ihlali olarak ortaya çıkar. Hiçbir başarısızlık bunu tetiklemek için büyük bir forma ihtiyaç duymaz; kullanıcı ikinci alanı, işletim sisteminin birincisinden odak mesajlarını hâlâ geri sarmakta olacağı kadar hızlı terk ederse, iki alanlı bir belge yeterlidir
// Naive version: reads fine in review, fails only under real typing speed
procedure TMyPdfViewer.EditorExit(Sender: TObject);
begin
CommitEditor; // still running inside FEditor's own OnExit
end;
procedure TMyPdfViewer.CommitEditor;
begin
if not Assigned(FEditor) then
Exit;
SaveFieldValue(FEditor.Text);
FEditor.Parent := nil; // focused control reparented here: OnExit
// fires again, re-entering this same method
FEditor.Free; // freed while a caller further down the
FEditor := nil; // stack is still inside its OnExit handler
end;
Kontrole dokunmadan önce referansı nil yapın
PDFlibPas'ın gönderdiği düzeltme tek bir yeniden sıralamadır: düzenleyiciyi yerel bir değişkende yakalayın, ona işaret eden alanı temizleyin ve yalnızca o zaman kontrolün özelliklerini değiştirmeye başlayın. CommitInplaceEditor, FInplaceEditor'ı yerel bir Editor değişkenine okur, FInplaceEditor'ı hemen nil'e ayarlar ve yalnızca bundan sonra Editor.Visible ve Editor.Parent'ı atar. Bu iki atamadan biri tarafından tetiklenen yeniden giren bir çağrı, FInplaceEditor'ın kendisini okur, onu zaten nil olarak bulur ve Editor'a dokunabilmeden veya alanın değerini ikinci kez yazabilmeden ilk satırında çıkar
procedure TPDFlibViewer.CommitInplaceEditor(Save: Boolean);
var
Editor: TWinControl;
begin
Editor := FInplaceEditor;
if not Assigned(Editor) then
Exit; // a reentrant call lands here and stops
FInplaceEditor := nil; // detach before the control is touched at all
if Save then
SaveEditorValue(Editor); // safe: FInplaceEditor is already nil
Editor.Visible := False;
Editor.Parent := nil; // may fire OnExit again; the guard above
// turns that reentrant call into a no-op
ReapDeadEditor; // free whatever was parked last cycle
FDeadEditor := Editor; // park this one instead of freeing it here
end;
O listedeki SaveEditorValue, Editor'ın bir TComboBox mu, bir TMemo mu, yoksa bir TEdit mi olduğunu kontrol eden ve buna göre değerini okuyan gerçek dal için yer tutar, çünkü PDFlibPas, alanın bir metin alanı mı yoksa bir seçim alanı mı olduğuna bağlı olarak farklı bir kontrol oluşturur. Koruma hangi dalın çalıştığını umursamaz, yalnızca OnExit'i tetikleyebilecek herhangi bir şey çalışmadan önce FInplaceEditor'ın nil olduğunu umursar ki bu, metodun geri kalanını aksi halde doğal olan herhangi bir stille yazmayı güvenli kılan tek sıralama kısıtlamasıdır
Bir kontrolü asla kendi olayının içinden serbest bırakmayın
TPDFlibViewer.CommitInplaceEditor hiçbir zaman doğrudan Editor.Free'yi çağırmaz ve bu kasıtlıdır: aynı kontrolün kendi olay gönderimine ait bir yığın çerçevesi, onu serbest bırakan çağrının üzerinde hâlâ geri sarılıyor olabilirken bir kontrolü serbest bırakmak güvensizdir, yeniden giren OnExit olsun olmasın. PDFlibPas bunun yerine ayrılmış düzenleyiciyi tek yuvalı bir park yerine, FDeadEditor'a verir; önceki düzenleme döngüsünden orada oturan her ne varsa onu, bir sonraki BeginEditFormField'ın başında ve bir kez daha CloseDocument'ten çağrılan küçük bir yardımcı, ReapDeadEditor aracılığıyla serbest bırakır; görüntüleyicinin oluşturduğu her düzenleyici de görüntüleyicinin kendisine aittir, TEdit.Create(nil) değil TEdit.Create(Self); bu yüzden görüntüleyici yok edildiğinde FDeadEditor'da hâlâ park halinde olan bir kontrol bile sızmak yerine sıradan VCL bileşen sahipliğiyle temizlenir
procedure TPDFlibViewer.ReapDeadEditor;
begin
if Assigned(FDeadEditor) then
begin
FDeadEditor.Free; // safe now: this control's own OnExit
FDeadEditor := nil; // finished at least one edit cycle ago
end;
end;
function TPDFlibViewer.BeginEditFormField(FieldIndex: Integer): Integer;
var
Edit: TEdit;
begin
Result := 0;
CommitInplaceEditor(True); // flush whatever editor is still open
// ... field lookup and rectangle conversion omitted ...
ReapDeadEditor; // now safe to free last cycle's parked editor
Edit := TEdit.Create(Self);
Edit.Parent := Self;
Edit.OnExit := InplaceEditorExit;
FInplaceEditor := Edit;
FInplaceEditor.SetFocus;
Result := 1;
end;
Bu neden en çok hızlı Tab gezinmesi sırasında ortaya çıkıyor?
PDFlibPas'ın v3.226.0'da bir form genelinde Tab ve Shift+Tab gezinmesini yönlendirmek için eklediği metot olan FocusNextFormField, her tek atlamada bir sonraki uygun alan için BeginEditFormField'ı çağırır ve BeginEditFormField, önceki alanın açık bıraktığı her ne düzenleyiciyse onu boşaltmak için CommitInplaceEditor(True)'yu çağırarak açılır. Bu, çok alanlı bir formu doldururken bir kullanıcının yaptığı her Tab basışının, yukarıda açıklanan tam ayır-sonra-dokun dizisini bir kez çalıştırdığı anlamına gelir ki bu, bir kontrolün Visible ve Parent değiştiği anda gerçekten odaklanmış olmasının en olası olduğu kod yoludur, çünkü Tab, giden düzenleyicinin yenisi onu isteyene kadar odağı tutmasını neredeyse garantileyen tek etkileşimdir
Bunların hiçbiri hatayı göstermeyi güvenilir kılmaz ve bunu geçiştirmek yerine açıkça söylemeye değer. Belirli bir Visible veya Parent atamasının gerçekten eşzamanlı bir OnExit'i zorlayıp zorlamadığı, bir hata ayıklayıcının yalnızca bağlanarak değiştirdiği, ilgisiz bir yeniden boyama veya zamanlayıcının bozabileceği ve TEdit, TMemo veya TComboBox'tan hangisinin oyunda olan kontrol olduğuna bağlı olarak farklı davranan odak ve pencere-tutamacı durumuna bağlıdır. Yalnızca bazen çalıştırılan bir koruma, bu türden bir kusurun kod incelemesinden ve manuel testten aynı şekilde sağ çıkabilmesinin nedenidir ve düzeltmenin de yapı gereği doğru olması gerekmesinin nedenidir, referansı her şeyden önce nil yapmak, bir avuç manuel test geçişinin gözlemlediği her ne davranışsa ona göre doğru olması değil
Bu düzeltmenin genel şekli tek bir görüntüleyici kontrolünün çok ötesine seyahat eder. Yalnızca bir PDF form alanı değil, render edilmiş içeriğin üzerine canlı bir VCL kontrolü katmanlayarak kurulan herhangi bir özel düzenleme yüzeyi, kapat-ve-işle mantığı hem açık bir kullanıcı eylemi hem de örtük bir odak değişikliği tarafından tetiklenebildiği anda aynı tehlikeyi miras alır ve aynı iki parçalı yanıt uygulanır: aktif kontrolü tanımlayan referansı, onun kendi çıkış olayını tetikleyebilecek herhangi bir şey yapmadan önce temizleyin ve o kontrolün kendi olay gönderiminin altında hâlâ çalışıyor olabilecek bir kod yolundan asla Free'yi çağırmayın. TPDFlibViewer'ın daha geniş form-doldurma ve render yüzeyi, hangi kontrol türünü hangi alan için göstereceğine nasıl karar verdiği dahil, PDFlibPas ile Delphi VCL'de etkileşimli bir PDF görüntüleyici kontrolü kurma genel bakışında ele alınmıştır ve SetFormFieldValueAndRefresh'in her işlenen düzenlemede geçersiz kılmak zorunda olduğu sayfa bitmap önbelleği, görüntüleyicinin monitör başına DPI disk sayfa önbelleği üzerine parçada ayrıca ele alınmıştır
Yerinde form alanı düzenleme, Tab ile yönlendirilen alan gezinmesi ve ikisinin arkasındaki yeniden-giriş-güvenli işleme yolu, sayfa render, açıklama ve form-alanı API yüzeyinin geri kalanının yanında, Delphi ve C++Builder için PDF kütüphanesi PDFlibPas ile birlikte gönderilen etkileşimli görüntüleyici kontrolünün bir parçasıdır