Техническа статия

Reentrancy на OnExit при редактор на PDF поле в Delphi

Reentrancy на OnExit при in-place редактор на PDF form field възниква, когато TPDFlibViewer затваря редактора от неговия собствен OnExit event и промяната на Visible или Parent предизвиква второ извикване

Проблемът често се появява при бързо преминаване с Tab през текстови полета. Старият editor все още държи фокуса, когато новият field започва редактиране, затова commit логиката трябва да е безопасна при повторно влизане

Как TPDFlibViewer поставя истински editor върху rendered page

TPDFlibViewer рендерира PDF страницата като bitmap и по подразбиране не превръща полетата в живи VCL контроли. BeginEditFormField създава подходящ TEdit, TMemo или TComboBox върху координатите на полето

Защо скриването на editor извиква OnExit втори път?

VCL TWinControl може да премести фокуса синхронно при промяна на Visible или Parent на фокусиран control. Това извиква OnExit, докато CommitInplaceEditor вече изпълнява първото затваряне

Какво се обърква при повторно влизане в CommitInplaceEditor?

Наивният commit може да запише стойността два пъти или да освободи control, докато неговият event stack още работи. Резултатът е повредена стойност, достъп до освободена памет или нестабилност, която зависи от фокуса

// 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;

Занулете reference преди да докоснете control-а

PDFlibPas първо записва editor в локална променлива, занулява полето, което сочи към активния control, и едва след това променя Visible или Parent. Така повторният OnExit вижда nil и приключва безопасно

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 представя реалната логика, която различава TComboBox, TMemo и TEdit и извлича текущата стойност преди control-ът да бъде скрит

Никога не освобождавайте control от неговия собствен event

TPDFlibViewer.CommitInplaceEditor не извиква директно Editor.Free. Освобождаването се отлага за безопасна точка след приключване на dispatch-а, защото текущият stack frame може още да използва control-а

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;

Защо това се проявява най-силно при бърза Tab навигация?

FocusNextFormField, добавен във v3.226.0 за Tab и Shift+Tab навигация, извиква BeginEditFormField за следващото поле след CommitInplaceEditor. Всеки Tab натиск повтаря detach-then-touch последователността, докато старият editor все още е фокусиран

Точният момент на OnExit зависи от focus и window-handle състоянието и може да се промени под debugger или при repaint. Затова guard трябва да е коректен по конструкция чрез ранно зануляване, а не да разчита на случайния ред на събитията

Същият модел важи за всяка editing surface, която поставя жив VCL control върху rendered content. Нулирайте reference преди операция, която може да предизвика exit event, и не извиквайте Free от път, който може да се изпълнява вътре в dispatch-а Свързана статия Свързана статия

In-place form field editing, Tab навигацията и reentrancy-safe commit пътят са част от интерактивния viewer control на PDFlibPas за Delphi и C++Builder Свързана статия