编写自定义属性检查器-验证值时如何处理就地编辑器焦点? [英] Writing a custom property inspector - How to handle inplace editor focus when validating values?

查看:52
本文介绍了编写自定义属性检查器-验证值时如何处理就地编辑器焦点?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试编写自己的简单属性检查器,但我面临着一个困难而又令人困惑的问题。首先,尽管让我说我的组件不是要使用或处理组件属性,而是允许向其添加自定义值。我组件的完整源代码在问题的后面,一旦将其安装在软件包中并从一个新的空项目中运行,它应该看起来像这样:

I am trying to write my own simple property inspector but I am facing a difficult and rather confusing problem. First though let me say that my component is not meant to work with or handle component properties, instead it will allow adding custom values to it. The full source code of my component is further down the question and it should look something like this once you have installed it in a package and run it from a new empty project:

问题是有关使用就地编辑器和验证属性值的信息。想法是,如果属性值无效,则向用户显示一条消息,通知他们该值不能被接受,然后将焦点放回到最初集中在行和就位编辑器上。

The issue is regarding the use of inplace editors and validating the property values. The idea is, if a property value is not valid then show a message to the user notifying them that the value cannot be accepted, then focus back to the row and inplace editor that was originally focused on.

我们实际上可以使用Delphi自己的对象检查器来说明我要寻找的行为,例如,尝试在 Name 属性中编写一个字符串,该字符串不能被接受,然后单击远离对象检查器。显示一条消息,并在关闭消息时将其焦点移回到名称行。

We can actually use Delphi's very own Object Inspector to illustrate the behavior I am looking for, for example try writing a string in the Name property that cannot be accepted then click away from the Object Inspector. A message is shown and upon closing it, it will focus back to the Name row.

没有任何代码,这个问题变得太模糊了,但是由于组件的性质,我试图编写它也很大。为了问题和示例的目的,我已尽可能将其简化。我敢肯定会有一些评论问我为什么不这样做或不这样做,但重要的是要知道我不是Delphi专家,并且经常做出错误的决定和选择,但我总是愿意学习,所以所有评论欢迎,尤其是如果它有助于找到我的解决方案。

The question becomes too vague without any code but due to the nature of the component I am trying to write it's also quite large. I have stripped it down as much as possible for the purpose of the question and example. I am sure there will be some comments asking me why I didn't do this or do that instead but it's important to know that I am no Delphi expert and often I make wrong decisions and choices but I am always willing to learn so all comments are welcomed, especially if it aids in finding my solution.

unit MyInspector;

interface

uses
  Winapi.Windows,
  Winapi.Messages,
  System.Classes,
  System.SysUtils,
  Vcl.Controls,
  Vcl.Dialogs,
  Vcl.StdCtrls,
  Vcl.Graphics,
  Vcl.Forms;

type
  TMyInspectorItems = class(TObject)
  private
    FPropertyNames: TStringList;
    FPropertyValues: TStringList;

    procedure AddItem(APropName, APropValue: string);
    procedure Clear;
  public
    constructor Create;
    destructor Destroy; override;
  end;

  TOnMouseMoveEvent = procedure(Sender: TObject; X, Y: Integer) of object;
  TOnSelectRowEvent = procedure(Sender: TObject; PropName, PropValue: string; RowIndex: Integer) of object;

  TMyCustomInspector = class(TGraphicControl)
  private
    FInspectorItems: TMyInspectorItems;
    FOnMouseMove: TOnMouseMoveEvent;
    FOnSelectRow: TOnSelectRowEvent;

    FRowCount: Integer;
    FNamesFont: TFont;
    FValuesFont: TFont;

    FSelectedRow: Integer;

    procedure SetNamesFont(const AValue: TFont);
    procedure SetValuesFont(const AValue: TFont);

    procedure CalculateInspectorHeight;
    function GetMousePosition: TPoint;
    function MousePositionToRowIndex: Integer;
    function RowIndexToMousePosition(ARowIndex: Integer): Integer;
    function GetRowHeight: Integer;
    function GetValueRowWidth: Integer;
    function RowExists(ARowIndex: Integer): Boolean;
    function IsRowSelected: Boolean;

  protected
    procedure Loaded; override;
    procedure Paint; override;
    procedure WMKeyDown(var Message: TMessage); message WM_KEYDOWN;
    procedure WMMouseDown(var Message: TMessage); message WM_LBUTTONDOWN;
    procedure WMMouseMove(var Message: TMessage); message WM_MOUSEMOVE;
    procedure WMMouseUp(var Message: TMessage); message WM_LBUTTONUP;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

    function RowCount: Integer;

    property Items: TMyInspectorItems read FInspectorItems write FInspectorItems;
    property OnMouseMove: TOnMouseMoveEvent read FOnMouseMove write FOnMouseMove;
    property OnSelectRow: TOnSelectRowEvent read FOnSelectRow write FOnSelectRow;
  published
    property Align;
  end;

  TMyPropertyInspector = class(TScrollBox)
  private
    FInspector: TMyCustomInspector;
    FInplaceStringEditor: TEdit;

    FSelectedRowName: string;
    FLastSelectedRowName: string;
    FLastSelectedRow: Integer;

    function SetPropertyValue(RevertToPreviousValueOnFail: Boolean): Boolean;

    procedure InplaceStringEditorEnter(Sender: TObject);
    procedure InplaceStringEditorExit(Sender: TObject);
    procedure InplaceStringEditorKeyPress(Sender: TObject; var Key: Char);
    procedure SelectRow(Sender: TObject; PropName, PropValue: string; RowIndex: Integer);
    function ValidateStringValue(Value: string): Boolean;
  protected
    procedure Loaded; override;
    procedure WMSize(var Message: TMessage); message WM_SIZE;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;

    procedure AddItem(APropName, APropValue: string);
    function GetSelectedPropertyName: string;
    function GetSelectedPropertyValue: string;
    function RowCount: Integer;
  end;

var
  FCanSelect: Boolean;

implementation

{ TMyInspectorItems }

constructor TMyInspectorItems.Create;
begin
  inherited Create;
  FPropertyNames  := TStringList.Create;
  FPropertyValues := TStringList.Create;
end;

destructor TMyInspectorItems.Destroy;
begin
  FPropertyNames.Free;
  FPropertyValues.Free;
  inherited Destroy;
end;

procedure TMyInspectorItems.AddItem(APropName, APropValue: string);
begin
  FPropertyNames.Add(APropName);
  FPropertyValues.Add(APropValue);
end;

procedure TMyInspectorItems.Clear;
begin
  FPropertyNames.Clear;
  FPropertyValues.Clear;
end;

{ TMyCustomInspector }

constructor TMyCustomInspector.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);

  FInspectorItems     := TMyInspectorItems.Create;

  FNamesFont          := TFont.Create;
  FNamesFont.Color    := clWindowText;
  FNamesFont.Name     := 'Segoe UI';
  FNamesFont.Size     := 9;
  FNamesFont.Style    := [];

  FValuesFont         := TFont.Create;
  FValuesFont.Color   := clNavy;
  FValuesFont.Name    := 'Segoe UI';
  FValuesFont.Size    := 9;
  FValuesFont.Style   := [];
end;

destructor TMyCustomInspector.Destroy;
begin
  FInspectorItems.Free;
  FNamesFont.Free;
  FValuesFont.Free;
  inherited Destroy;
end;

procedure TMyCustomInspector.Loaded;
begin
  inherited Loaded;
end;

procedure TMyCustomInspector.Paint;

  procedure DrawBackground;
  begin
    Canvas.Brush.Color := clWindow;
    Canvas.Brush.Style := bsSolid;
    Canvas.FillRect(Rect(0, 0, Self.Width, Self.Height));
  end;

  procedure DrawNamesBackground;
  begin
    Canvas.Brush.Color := clWindow;
    Canvas.Brush.Style := bsSolid;
    Canvas.FillRect(Rect(0, 0, Self.Width div 2, Self.Height));
  end;

  procedure DrawNamesSelection;
  begin
    if (FRowCount > -1) and (RowExists(MousePositionToRowIndex)) then
    begin
      Canvas.Brush.Color := $00E0E0E0;
      Canvas.Brush.Style := bsSolid;
      Canvas.FillRect(Rect(0, RowIndexToMousePosition(FSelectedRow),
        Self.Width div 2, RowIndexToMousePosition(FSelectedRow) + GetRowHeight));
    end;
  end;

  procedure DrawNamesText;
  var
    I: Integer;
    Y: Integer;
  begin
    FRowCount := FInspectorItems.FPropertyNames.Count;

    Canvas.Brush.Style  := bsClear;
    Canvas.Font.Color   := FNamesFont.Color;
    Canvas.Font.Name    := FNamesFont.Name;
    Canvas.Font.Size    := FNamesFont.Size;

    Y := 0;
    for I := 0 to FInspectorItems.FPropertyNames.Count -1 do
    begin
      Canvas.TextOut(2, Y, FInspectorItems.FPropertyNames.Strings[I]);
      Inc(Y, GetRowHeight);
    end;
  end;

  procedure DrawValuesBackground;
  begin
    Canvas.Brush.Color := clWindow;
    Canvas.Brush.Style := bsSolid;
    Canvas.FillRect(Rect(Self.Width div 2, 0, Self.Width, Self.Height));
  end;

  procedure DrawValuesSelection;
  begin
    if (FRowCount > -1) and (RowExists(MousePositionToRowIndex)) then
    begin
      Canvas.DrawFocusRect(Rect(Self.Width div 2, RowIndexToMousePosition(FSelectedRow),
        Self.Width, RowIndexToMousePosition(FSelectedRow) + GetRowHeight));
    end;
  end;

  procedure DrawValues;
  var
    I, Y: Integer;
  begin
    FRowCount := FInspectorItems.FPropertyValues.Count;

    Y := 0;
    for I := 0 to FInspectorItems.FPropertyValues.Count -1 do
    begin
      Canvas.Brush.Style  := bsClear;
      Canvas.Font.Color   := FValuesFont.Color;
      Canvas.Font.Name    := FValuesFont.Name;
      Canvas.Font.Size    := FValuesFont.Size;

      Canvas.TextOut(Self.Width div 2 + 2, Y + 1, FInspectorItems.FPropertyValues.Strings[I]);
      Inc(Y, GetRowHeight);
    end;
  end;

begin
  DrawNamesBackground;
  DrawNamesSelection;
  DrawNamesText;
  DrawValuesBackground;
  DrawValuesSelection;
  DrawValues;
end;

procedure TMyCustomInspector.WMKeyDown(var Message: TMessage);
begin
  inherited;

  case Message.WParam of
    VK_DOWN:
    begin

    end;
  end;
end;

procedure TMyCustomInspector.WMMouseDown(var Message: TMessage);
begin
  inherited;

  Parent.SetFocus;

  FSelectedRow := MousePositionToRowIndex;

  if FSelectedRow <> -1 then
  begin
    if Assigned(FOnSelectRow) then
    begin
      FOnSelectRow(Self, FInspectorItems.FPropertyNames.Strings[FSelectedRow],
        FInspectorItems.FPropertyValues.Strings[FSelectedRow], FSelectedRow);
    end;
  end;

  Invalidate;
end;

procedure TMyCustomInspector.WMMouseMove(var Message: TMessage);
begin
  inherited;

  if Assigned(FOnMouseMove) then
  begin
    FOnMouseMove(Self, GetMousePosition.X, GetMousePosition.Y);
  end;
end;

procedure TMyCustomInspector.WMMouseUp(var Message: TMessage);
begin
  inherited;
end;

procedure TMyCustomInspector.SetNamesFont(const AValue: TFont);
begin
  FNamesFont.Assign(AValue);
  Invalidate;
end;

procedure TMyCustomInspector.SetValuesFont(const AValue: TFont);
begin
  FValuesFont.Assign(AValue);
  Invalidate;
end;

procedure TMyCustomInspector.CalculateInspectorHeight;
var
  I, Y: Integer;
begin
  FRowCount := FInspectorItems.FPropertyNames.Count;

  Y := GetRowHeight;
  for I := 0 to FRowCount -1 do
  begin
    Inc(Y, GetRowHeight);
  end;

  if Self.Height <> Y then
    Self.Height := Y;
end;

function TMyCustomInspector.GetMousePosition: TPoint;
var
  Pt: TPoint;
begin
  Pt := Mouse.CursorPos;
  Pt := ScreenToClient(Pt);
  Result := Pt;
end;

function TMyCustomInspector.MousePositionToRowIndex: Integer;
begin
  Result := GetMousePosition.Y div GetRowHeight;
end;

function TMyCustomInspector.RowIndexToMousePosition(
  ARowIndex: Integer): Integer;
begin
  Result := ARowIndex * GetRowHeight;
end;

function TMyCustomInspector.GetRowHeight: Integer;
begin
  Result := FNamesFont.Size * 2 + 1;
end;

function TMyCustomInspector.GetValueRowWidth: Integer;
begin
  Result := Self.Width div 2;
end;

function TMyCustomInspector.RowCount: Integer;
begin
  Result := FRowCount;
end;

function TMyCustomInspector.RowExists(ARowIndex: Integer): Boolean;
begin
  Result := MousePositionToRowIndex < RowCount;
end;

function TMyCustomInspector.IsRowSelected: Boolean;
begin
  Result := FSelectedRow <> -1;
end;

{ TMyPropertyInspector }

constructor TMyPropertyInspector.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);

  Self.DoubleBuffered               := True;
  Self.Height                       := 150;
  Self.HorzScrollBar.Visible        := False;
  Self.TabStop                      := True; // needed to receive focus
  Self.Width                        := 250;

  FInspector                        := TMyCustomInspector.Create(Self);
  FInspector.Parent                 := Self;
  FInspector.Align                  := alTop;
  FInspector.Height                 := 0;
  FInspector.OnSelectRow            := SelectRow;

  FInplaceStringEditor              := TEdit.Create(Self);
  FInplaceStringEditor.Parent       := Self;
  FInplaceStringEditor.BorderStyle  := bsNone;
  FInplaceStringEditor.Color        := clWindow;
  FInplaceStringEditor.Height       := 0;
  FInplaceStringEditor.Left         := 0;
  FInplaceStringEditor.Name         := 'MyPropInspectorInplaceStringEditor';
  FInplaceStringEditor.Top          := 0;
  FInplaceStringEditor.Visible      := False;
  FInplaceStringEditor.Width        := 0;
  FInplaceStringEditor.Font.Assign(FInspector.FValuesFont);

  FInplaceStringEditor.OnEnter      := InplaceStringEditorEnter;
  FInplaceStringEditor.OnExit       := InplaceStringEditorExit;
  FInplaceStringEditor.OnKeyPress   := InplaceStringEditorKeyPress;

  FCanSelect                        := True;
end;

destructor TMyPropertyInspector.Destroy;
begin
  FInspector.Free;
  FInplaceStringEditor.Free;
  inherited Destroy;
end;

procedure TMyPropertyInspector.Loaded;
begin
  inherited Loaded;
end;

procedure TMyPropertyInspector.WMSize(var Message: TMessage);
begin
  FInspector.Width := Self.Width;
  Invalidate;
end;


procedure TMyPropertyInspector.AddItem(APropName, APropValue: string);
begin
  FInspector.CalculateInspectorHeight;
  FInspector.Items.AddItem(APropName, APropValue);
  FInspector.Invalidate;
  Self.Invalidate;
end;

function TMyPropertyInspector.GetSelectedPropertyName: string;
begin
  Result := '';

  if FInspector.FSelectedRow <> -1 then
  begin
    Result := FInspector.FInspectorItems.FPropertyNames.Strings[FInspector.FSelectedRow];
  end;
end;

function TMyPropertyInspector.GetSelectedPropertyValue: string;
begin
  Result := '';

  if FInspector.FSelectedRow <> -1 then
  begin
    Result := FInspector.FInspectorItems.FPropertyValues.Strings[FInspector.FSelectedRow];
  end;
end;

function TMyPropertyInspector.RowCount: Integer;
begin
  Result := FInspector.RowCount;
end;

procedure TMyPropertyInspector.InplaceStringEditorEnter(Sender: TObject);
begin
  FCanSelect := False;
  FLastSelectedRow := FInplaceStringEditor.Tag;
end;

procedure TMyPropertyInspector.InplaceStringEditorExit(Sender: TObject);
begin
  if SetPropertyValue(True) then
  begin
    FCanSelect := True;
  end;
end;

procedure TMyPropertyInspector.InplaceStringEditorKeyPress(Sender: TObject;
  var Key: Char);
begin
  if Key = Chr(VK_RETURN) then
  begin
    Key := #0;
    FInplaceStringEditor.SelectAll;
  end;
end;

procedure TMyPropertyInspector.SelectRow(Sender: TObject; PropName, PropValue: string; RowIndex: Integer);
begin
  FSelectedRowName     := PropName;
  FLastSelectedRowName := PropName;

  FInplaceStringEditor.Height   := FInspector.GetRowHeight - 2;
  FInplaceStringEditor.Left     := Self.Width div 2;
  FInplaceStringEditor.Tag      := RowIndex;
  FInplaceStringEditor.Text     := GetSelectedPropertyValue;
  FInplaceStringEditor.Top      := FInspector.RowIndexToMousePosition(FInspector.FSelectedRow) + 1 - Self.VertScrollBar.Position;
  FInplaceStringEditor.Visible  := True;
  FInplaceStringEditor.Width    := FInspector.GetValueRowWidth - 3;
  FInplaceStringEditor.SetFocus;
  FInplaceStringEditor.SelectAll;
end;

function TMyPropertyInspector.SetPropertyValue(
  RevertToPreviousValueOnFail: Boolean): Boolean;
var
  S: string;
begin
  Result := False;

  S := FInplaceStringEditor.Text;

  if ValidateStringValue(S) then
  begin
    Result := True;
  end
  else
  begin
    ShowMessage('"' + S + '"' + 'is not a valid value.');
    Result := False;
  end;
end;

function TMyPropertyInspector.ValidateStringValue(Value: string): Boolean;
begin
  // a quick and dirty way of testing for a valid string value, here we just
  // look for strings that are not zero length.
  Result := Length(Value) > 0;
end;

end.



问题(详细)



混乱我全都归结到谁首先获得关注,以及如何正确处理和响应它。因为我是自定义绘制行,所以我确定在单击检查器控件时鼠标的位置,然后绘制选定的行以显示此行。但是,在处理就地编辑器时,尤其是 OnEnter OnExit 事件,我遇到了各种时髦的问题,其中例如,在某些情况下,我陷入了反复显示的验证错误消息的循环中(因为焦点从我的检查器切换到就地编辑器,来回切换)。

Problem (detailed)

The confusion I have all comes down to who receives focus first and how to handle and respond to it correctly. Because I am custom drawing my rows I determine where the mouse is when clicking on the inspector control and then I draw the selected row to show this. When handling the inplace editors however, especially the OnEnter and OnExit event I have been facing all kinds of funky problems where in some cases I have been stuck in a cycle of the validate error message repeatedly showing for example (because focus is switching from my inspector to the inplace editor and back and forth).

要在运行时填充检查器,您可以执行以下操作:

To populate my inspector at runtime you can do the following:

procedure TForm1.Button1Click(Sender: TObject);
begin
  MyPropertyInspector1.AddItem('A', 'Some Text');
  MyPropertyInspector1.AddItem('B', 'Hello World');
  MyPropertyInspector1.AddItem('C', 'Blah Blah');
  MyPropertyInspector1.AddItem('D', 'The Sky is Blue');
  MyPropertyInspector1.AddItem('E', 'Another String');
end;

您可以尝试以下操作:


  • 单击一行

  • 从就地编辑器中删除内容

  • 选择另一行

  • 出现验证错误消息框(尚未关闭)

  • 在消息框仍然可见的情况下,将鼠标移到另一行上

  • 现在按Enter键以关闭消息框

  • 您将注意到所选行现已移至鼠标所在的位置

  • Click on a row
  • Delete the contents from the inplace editor
  • Select another row
  • The validate error message box appears (don't close it yet)
  • With the message box still visible, move your mouse over another row
  • Now press Enter to close the message box
  • You will notice the selected row has now moved to where the mouse was

我需要的是显示并关闭验证消息框后,我需要将焦点重新设置为首先验证的行。这令人困惑,因为似乎(或我认为)在 WMMouseDown(var Message:TMessage);之后调用了就地编辑器 OnExit 检查器的代码。

What I need is after the validate message box has shown and closed, I need to set the focus back to the row that was been validated in the first place. It gets confusing because it seems (or so I think) that the inplace editors OnExit is been called after the WMMouseDown(var Message: TMessage); code of my inspector.

如果问题仍然不清楚,我可以尽可能简单地说,Delphi对象检查器的行为就是正在尝试将其实施到我的组件中。您在就地编辑器中输入一个值,如果该值未通过验证,则显示一个消息框,然后将焦点集中到最后选择的行。一旦焦点转移到就地编辑器上,就应进行就地编辑器验证。

To put it as simple as I can if the question remains unclear, the behavior of the Delphi Object Inspector is what I am trying to implement into my component. You enter a value into the inplace editors, if it fails the validation then display a messagebox and then focus back to the row that was last selected. The inplace editor validation should occur as soon as focus is switched away from the inplace editor.

推荐答案


我只是似乎无法弄清楚什么是第一个,什么是阻塞事件被触发,这使我感到困惑,因为绘制所选行的方式取决于单击检查器控件时鼠标的位置。

I just can't seem to figure out what is been called first and what is blocking events been fired, it becomes confusing because the way I draw my selected row is determined by where the mouse was when clicking on the inspector control.

这是您的事件流:


  • TMyCustomInspector.WMMouseDown 称为

  • TMyCustomInspector.WMMouseDown is called

  1. 其中, Parent.SetFocus 称为


    • 焦点从Edit控件中移出,并调用 TMyPropertyInspector.InplaceStringEditorExit

    • 消息对话框由 SetPropertyValue

  1. Therein, Parent.SetFocus is called
    • The focus is removed from the Edit control and TMyPropertyInspector.InplaceStringEditorExit is called
    • The message dialog is shown by SetPropertyValue


您需要做的是防止在验证失败的情况下重置 FSelectedRow 。所有必需的成分已经存在,只需添加以下条件即可:

What you need to is to prevent FSelectedRow being reset in case of validation did not succeed. All needed ingredients are already there, just add this one condition:

  if FCanSelect then
    FSelectedRow := MousePositionToRowIndex;

一些说明:


  • 使 FCanSelect TMyCustomInspector

  • 您需要检查 TMyCustomInspector.MousePositionToRowIndex 中的限制,以便返回 -1

  • Make FCanSelect a protected or private field of TMyCustomInspector,
  • You need to check for limits in TMyCustomInspector.MousePositionToRowIndex in order to return -1.

这篇关于编写自定义属性检查器-验证值时如何处理就地编辑器焦点?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆