Teknik Makale

Bir Delphi Yerinde Form Düzenleyicisinde OnExit Yeniden Girişimi

PDF Library for Delphi'ı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

TPDFlibViewer'ın canlı bir VCL düzenleyiciyi işlenmiş PDF sayfa bitmap'inin üzerine yerleştirmesini ve OnKeyDown ile OnExit'i görüntüleyici işleyicilerine bağlamasını gösteren şema
BeginEditFormField sayfanın üzerine odaklı gerçek bir bileşen yerleştirir ve her bu tür düzenleyici OnExit taşıyarak gelir — yeniden girişin içinden geçtiği kapı

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. PDF Library for Delphi'ı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

// Naif sürüm: incelemede sorunsuz okunur, yalnızca gerçek yazma hızında başarısız olur
procedure TMyPdfViewer.EditorExit(Sender: TObject);
begin
  CommitEditor;               // hâlâ FEditor'ın kendi OnExit'i içinde çalışıyor
end;

procedure TMyPdfViewer.CommitEditor;
begin
  if not Assigned(FEditor) then
    Exit;
  SaveFieldValue(FEditor.Text);
  FEditor.Parent := nil;      // odaklanmış kontrol burada yeniden ebeveynlendirildi: OnExit
                               // yeniden tetiklenir, aynı metoda yeniden girer
  FEditor.Free;                // yığında daha aşağıdaki bir çağıran hâlâ
  FEditor := nil;              // kendi OnExit işleyicisinin içindeyken serbest bırakıldı
end;

Kontrole dokunmadan önce referansı nil yapın

PDF Library for Delphi'ı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;                      // yeniden giren bir çağrı buraya iner ve durur
  FInplaceEditor := nil;       // kontrol hiç dokunulmadan önce ayır
  if Save then
    SaveEditorValue(Editor);   // güvenli: FInplaceEditor zaten nil
  Editor.Visible := False;
  Editor.Parent := nil;        // OnExit'i yeniden tetikleyebilir; yukarıdaki koruma
                                // bu yeniden giren çağrıyı bir hiç-işlem'e dönüştürür
  ReapDeadEditor;               // son döngüde park edileni serbest bırak
  FDeadEditor := Editor;        // bunu burada serbest bırakmak yerine park et
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ü PDF Library for Delphi, 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. PDF Library for Delphi 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;          // artık güvenli: bu kontrolün kendi OnExit'i
    FDeadEditor := nil;        // en az bir düzenleme döngüsü önce tamamlandı
  end;
end;

function TPDFlibViewer.BeginEditFormField(FieldIndex: Integer): Integer;
var
  Edit: TEdit;
begin
  Result := 0;
  CommitInplaceEditor(True);   // hâlâ açık olan her ne düzenleyiciyse onu boşalt
  // ... alan araması ve dikdörtgen dönüşümü atlandı ...
  ReapDeadEditor;              // artık son döngünün park edilmiş düzenleyicisini serbest bırakmak güvenli
  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?

PDF Library for Delphi'ı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

PDF Library for Delphi akış şeması: odaklı bir düzenleyicide Editor.Parent'ın nil'e ayarlanmasının OnExit'i yeniden tetikleyerek ilk çağrı dönmeden CommitInplaceEditor'a yeniden girdiğini gösterir
Tek bir yeniden ebeveynleme ataması, işlemi kendisine geri gönderir; hem çift yazma hem de erken Free hata kiplerini açar

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, PDF Library for Delphi 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

PDF Library for Delphi: Denetime dokunmadan önce FInplaceEditor'ı temizleyen, yeniden girişli OnExit'i no-op sayan ve Free'yi FDeadEditor yuvası üzerinden erteleyen sıralı altı adımlı tamamlama dizisi
Referansı önce nil yapmak, her yeniden giren çağrıyı anında sonlandırır; bekletilen düzenleyici yuvası ise her Free'yi yığınlarının sessiz olduğu bir döngüye taşı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 PDF Library for Delphi ile birlikte gönderilen etkileşimli görüntüleyici kontrolünün bir parçasıdır