Kontrolnik TPDFlibViewer v PDFlibPas 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 PDFlibPas 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
// 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;
Preden se dotaknete kontrolnika, nastavite sklic na nil
Popravek, ki ga je poslal PDFlibPas, 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; // 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;
SaveEditorValue v tem izseku predstavlja pravo vejo, ki preveri, ali je Editor tipa TComboBox, TMemo ali TEdit, in ustrezno prebere njegovo vrednost, saj PDFlibPas 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. PDFlibPas 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; // 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;
Zakaj se to najmočneje pokaže pri hitrem krmarjenju s tabulatorsko tipko?
FocusNextFormField, metoda, ki jo je PDFlibPas 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 PDFlibPas, 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 PDFlibPas, knjižnici PDF za Delphi in C++Builder, skupaj s preostalim API za izris strani, opombe in polja obrazcev