Kontrolnik TPDFlibViewer v PDF Library for Delphi potrdi urejanje polja obrazca na mestu prek dogodka OnExit samega urejevalnika, pri čemer ta oblikovna odločitev skriva klasično past Delphija VCL: skrivanje, ponovno dodeljevanje nadrejenega ali uničenje fokusiranega kontrolnika iz njegovega lastnega obravnavalnika OnExit lahko sproži OnExit še drugič, preden se prvi klic vrne, zato se logika potrditve vrne sama vase
Napako, ki pri tem nastane, je težko zanesljivo ponoviti. Uporabnik hitro preklaplja med zaporedjem besedilnih polj na optično prebranem obrazcu in pregledovalnik občasno povzroči izjemo zaradi kršitve dostopa ali, še slabše, deluje naprej in tiho zapiše napačno vrednost v polje dve tabulatorski mesti nazaj. Pri ponovitvi na zahtevo je napaka za nazaj videti očitna, pri iskanju vzroka v poročilu o sesutju ene same stranke pa je videti kot prikazen, saj je od tega, ali se drugi OnExit dejansko sproži, odvisna časovna usklajenost ročaja okna in fokusa, ki se spreminja glede na vrsto polja, hitrost tipkanja in vse drugo, kar v tistem trenutku počne čakalna vrsta sporočil
Kako TPDFlibViewer postavi pravi urejevalnik na izrisano stran
TPDFlibViewer vsako stran PDF izriše v bitno sliko in privzeto ne pretvori polj obrazca v žive kontrolnike VCL, zato je BeginEditFormField metoda, ki poveže oba svetova: ob klicu z indeksom polja poišče pravokotnik polja in ga pretvori v koordinate odjemalca, nato pa za besedilno polje na ta pravokotnik postavi pravi TEdit ali TMemo oziroma za izbirno polje TComboBox v slogu csDropDownList, pri čemer je trenutna vrednost polja že naložena. ISO 32000-2 §12.7 opredeljuje, kaj je besedilno ali izbirno polje obrazca znotraj PDF, vendar ta specifikacija ne določa, kako naj aplikacija Windows uporabniku omogoči vnos, in prav to vrzel zapolnjuje BeginEditFormField. Dogodka OnKeyDown in OnExit sta pri vsakem urejevalniku, ki ga ustvari TPDFlibViewer, povezana z istima metodama pregledovalnika, InplaceEditorKeyDown in InplaceEditorExit, ta povezava pa je od prve uvedbe interaktivnega izpolnjevanja obrazcev v v3.220.0 ostala nespremenjena, težava pa se začne pri OnExit
Zakaj skrivanje urejevalnika drugič sproži OnExit?
TWinControl v VCL spremembo lastnosti Visible ali Parent na fokusiranem kontrolniku obravnava kot razlog za takojšen premik fokusa, premik fokusa s kontrolnika pa je natančno tisto, kar sinhrono sproži njegov dogodek OnExit, še preden se vrne dodelitev lastnosti, ki je to sprožila. CommitInplaceEditor, metoda, ki jo PDF Library for Delphi uporablja za zapiranje urejevalnika na mestu in zapis njegove vrednosti nazaj v polje obrazca, mora ob izhodu narediti prav ti dve stvari: nastaviti Editor.Visible na False in Editor.Parent na nil, da se kontrolnik preneha izrisovati nad stranjo in preneha sprejemati vnos. Če katero od teh dejanj izvedete, ko ima urejevalnik še vedno fokus, kar se skoraj vedno zgodi, ker ga je uporabnik pravkar zapustil, se OnExit znova sproži sredi klica, za katerega naj bi bilo to zadnje dejanje, ki ga je sprožil OnExit tega urejevalnika
Kaj se zgodi, ko CommitInplaceEditor znova vstopi vase?
Naivna metoda potrditve za to plača na enega od dveh načinov. Ali dvakrat zapiše vrednost polja, prvič iz prvotnega klica in drugič iz ponovnega klica, ki se prikrade, preden prvi klic konča spreminjanje lastnega stanja, ali pa poskuša sprostiti kontrolnik urejevalnika, medtem ko je okvir nižje na skladu klicev še vedno znotraj obravnavalnika dogodka tega istega kontrolnika, kar je na območju nedefiniranega vedenja VCL in se pokaže kot izjema zaradi kršitve dostopa, ki lahko kaže skoraj na katero koli vrstico, ne nujno na tisto, ki jo je dejansko povzročila. Za nobeno od teh napak ni potreben velik obrazec; dovolj je dokument z dvema poljema, če uporabnik drugo polje zapusti dovolj hitro, da operacijski sistem še vedno razveljavlja sporočila o fokusu prvega
// Naivna različica: pri pregledu kode je videti v redu, odpove šele pri resnični hitrosti tipkanja
procedure TMyPdfViewer.EditorExit(Sender: TObject);
begin
CommitEditor; // še vedno teče znotraj lastnega OnExit FEditor
end;
procedure TMyPdfViewer.CommitEditor;
begin
if not Assigned(FEditor) then
Exit;
SaveFieldValue(FEditor.Text);
FEditor.Parent := nil; // fokusiranemu kontrolniku se tu zamenja nadrejeni: OnExit
// se sproži znova, s ponovnim vstopom v to isto metodo
FEditor.Free; // sproščen, medtem ko je klicatelj nižje na skladu
FEditor := nil; // še vedno znotraj njegovega obravnavalnika OnExit
end;
Preden se dotaknete kontrolnika, nastavite sklic na nil
Popravek, ki ga je poslal PDF Library for Delphi, je eno samo preurejanje: zajemite urejevalnik v lokalno spremenljivko, počistite polje, ki kaže nanj, in šele nato začnite spreminjati lastnosti kontrolnika. CommitInplaceEditor prebere FInplaceEditor v lokalno spremenljivko Editor, takoj nastavi FInplaceEditor na nil in šele nato dodeli Editor.Visible in Editor.Parent. Ponovni klic, ki ga sproži katera od teh dveh dodelitev, sam prebere FInplaceEditor, ugotovi, da je že nil, in se konča že v prvi vrstici, preden bi se lahko dotaknil Editor ali drugič zapisal vrednost polja
procedure TPDFlibViewer.CommitInplaceEditor(Save: Boolean);
var
Editor: TWinControl;
begin
Editor := FInplaceEditor;
if not Assigned(Editor) then
Exit; // ponovni klic pristane tukaj in se ustavi
FInplaceEditor := nil; // odklopi, preden se kontrolnika sploh dotaknemo
if Save then
SaveEditorValue(Editor); // varno: FInplaceEditor je že nil
Editor.Visible := False;
Editor.Parent := nil; // lahko znova sproži OnExit; varovalo zgoraj
// ta ponovni klic spremeni v prazno dejanje
ReapDeadEditor; // sprosti, kar je ostalo parkirano od prejšnjega cikla
FDeadEditor := Editor; // to raje parkiraj, namesto da bi ga sprostil tukaj
end;
SaveEditorValue v tem izseku predstavlja pravo vejo, ki preveri, ali je Editor tipa TComboBox, TMemo ali TEdit, in ustrezno prebere njegovo vrednost, saj PDF Library for Delphi ustvari drug kontrolnik glede na to, ali je polje besedilno ali izbirno. Varovalo ne skrbi za to, katera veja se izvede, temveč le za to, da je FInplaceEditor nastavljen na nil, preden se izvede kar koli, kar lahko sproži OnExit, in prav ta vrstni red je omejitev, zaradi katere je preostanek metode varen za zapis v sicer naravnem slogu
Kontrolnika nikoli ne sprostite iz njegovega lastnega dogodka
TPDFlibViewer.CommitInplaceEditor nikoli neposredno ne pokliče Editor.Free in to je namerno: sproščanje kontrolnika ni varno, dokler se okvir sklada, ki pripada odpošiljanju dogodka tega istega kontrolnika, morda še razpleta nad klicem, ki ga sprošča, ne glede na ponovno vstopanje OnExit. PDF Library for Delphi zato ločeni urejevalnik preda enojnemu odlagalnemu mestu FDeadEditor in vsebino, ki je tam ostala iz prejšnjega cikla urejanja, sprosti z majhnim pomočnikom ReapDeadEditor, ki se pokliče na začetku naslednjega BeginEditFormField in še enkrat iz CloseDocument; vsak urejevalnik, ki ga ustvari pregledovalnik, je poleg tega v lasti samega pregledovalnika, TEdit.Create(Self) namesto TEdit.Create(nil), zato tudi kontrolnik, ki ob uničenju pregledovalnika še vedno čaka v FDeadEditor, prevzame običajno lastništvo komponent VCL, namesto da bi ostal nezaseden
procedure TPDFlibViewer.ReapDeadEditor;
begin
if Assigned(FDeadEditor) then
begin
FDeadEditor.Free; // zdaj varno: lasten OnExit tega kontrolnika
FDeadEditor := nil; // je končal vsaj en cikel urejanja nazaj
end;
end;
function TPDFlibViewer.BeginEditFormField(FieldIndex: Integer): Integer;
var
Edit: TEdit;
begin
Result := 0;
CommitInplaceEditor(True); // izprazni urejevalnik, ki je še odprt
// ... iskanje polja in pretvorba pravokotnika izpuščena ...
ReapDeadEditor; // zdaj varno sprostiti parkirani urejevalnik prejšnjega cikla
Edit := TEdit.Create(Self);
Edit.Parent := Self;
Edit.OnExit := InplaceEditorExit;
FInplaceEditor := Edit;
FInplaceEditor.SetFocus;
Result := 1;
end;
Zakaj se to najmočneje pokaže pri hitrem krmarjenju s tabulatorsko tipko?
FocusNextFormField, metoda, ki jo je PDF Library for Delphi dodal v v3.226.0 za krmarjenje s Tab in Shift+Tab po obrazcu, ob vsakem posameznem prehodu pokliče BeginEditFormField za naslednje ustrezno polje, BeginEditFormField pa se začne s klicem CommitInplaceEditor(True), da izprazni urejevalnik, ki ga je pustilo odprto prejšnje polje. To pomeni, da vsak pritisk tipke Tab med izpolnjevanjem obrazca z več polji enkrat izvede natanko opisano zaporedje ločitve in dotika, kar je prav koda, pri kateri je najverjetneje, da ima kontrolnik v trenutku spremembe Visible in Parent še vedno pravi fokus, saj je Tab tista interakcija, pri kateri je skoraj zagotovljeno, da bo izhodni urejevalnik obdržal fokus vse do trenutka, ko ga zahteva novi
Nič od tega ne naredi napake zanesljivo dokazljive in to je vredno povedati jasno, namesto da bi to olepševali. Ali določena dodelitev Visible ali Parent dejansko prisili sinhroni OnExit, je odvisno od stanja fokusa in ročaja okna, ki ga razhroščevalnik spremeni že s svojo priključitvijo, ki ga lahko zmoti nepovezano vnovično izrisovanje ali časovnik in ki se obnaša drugače glede na to, ali je uporabljeni kontrolnik TEdit, TMemo ali TComboBox. Varovalo, ki se izvede le občasno, je razlog, da takšna napaka preživi tako pregled kode kot ročno testiranje, hkrati pa je to razlog, da mora biti popravek pravilen po konstrukciji, tako da se sklic nastavi na nil, preden se zgodi kar koli drugega, ne pa pravilen le zaradi vedenja, ki ga je pokazalo nekaj naključnih ročnih testov
Splošna oblika tega popravka se dobro prenese daleč onkraj enega kontrolnika pregledovalnika. Vsaka površina za urejanje po meri, zgrajena s prekrivanjem živega kontrolnika VCL čez izrisano vsebino, ne le polje obrazca PDF, podeduje isto nevarnost, ko lahko njeno logiko zapiranja in potrditve sprožita tako izrecno dejanje uporabnika kot implicitna sprememba fokusa, zato velja isti dvodelni odgovor: počistite sklic, ki določa aktivni kontrolnik, preden storite kar koli, kar bi lahko sprožilo njegov lastni izhodni dogodek, in nikoli ne pokličite Free iz poti kode, ki bi lahko še vedno tekla pod odpošiljanjem dogodka tega kontrolnika. Širša površina TPDFlibViewer za izpolnjevanje in izris obrazcev, vključno z načinom odločanja, kateri tip kontrolnika se prikaže za posamezno polje, je opisana v pregledu izdelave interaktivnega kontrolnika za pregledovanje PDF v Delphi VCL s PDF Library for Delphi, predpomnilnik bitnih slik strani, ki ga mora SetFormFieldValueAndRefresh razveljaviti ob vsaki potrjeni spremembi, pa je ločeno opisan v članku o diskovnem predpomnilniku strani pregledovalnika z DPI za posamezen monitor
Urejanje polj obrazca na mestu, krmarjenje po poljih s tipko Tab in pot za potrditev, varna pred ponovnim vstopanjem, so del interaktivnega kontrolnika pregledovalnika, ki je priložen PDF Library for Delphi, knjižnici PDF za Delphi in C++Builder, skupaj s preostalim API za izris strani, opombe in polja obrazcev