Обработка курсора в Firemonkey - PullRequest
1 голос
/ 11 ноября 2019

Кто-нибудь может прояснить, как работают курсоры в Delphi FMX 10.3.1? У меня есть длительное действие, и я хочу, чтобы курсор приложения отображался как crHourglass во время выполнения действия. В следующем коде я представил 3 варианта установки курсора на crHourglass.

procedure TFormMain.ActionFindExactMatchesExecute(Sender: TObject);
const
  CCursorOption= 2;
var
  IterationContextHits: TIterationContextHits;
begin
  PanelResults.SendToBack;
  PanelProgress.BringToFront;
  case CCursorOption of
    0: Self.Cursor:= crHourglass;
    1: ButtonFindExactMatches.Cursor:= crHourglass;
    2: CursorManager.SetCursor(crHourglass);
  end;
  {Create TIterationContextHits object to hold progress variables:}
  IterationContextHits:= TIterationContextHits.Create;
  try
    {Lengthy code that searches multiple files for string matches}
    {Report result of operation:}
    ShowMessage('Number of matches found: ' + IntToStr(FHitCount));
    {Update GUI:}
    DataToControls;
    PanelResults.BringToFront;
  finally
    IterationContextHits.Free;
    case CCursorOption of
      0: Self.Cursor:= crDefault;
      1: ButtonFindExactMatches.Cursor:= crDefault;
      2: CursorManager.RestorePrevCursor;
    end;
  end;
end;

В первом варианте я установил для свойства Cursor объекта MainForm значение crHourGlass, ожидая, что во время выполнения приложение отобразитсвойство InheritedCursor, которое должно выполнять поиск стека z-порядка компонента вплоть до главной формы для первого компонента, значение курсора которого не равно crDefault. Но это не работает.

Во втором варианте я установил свойство курсора кнопки, которая связана с действием. Если нажать кнопку, чтобы запустить действие, смена курсора работает. Но если действие запускается из пункта главного меню, то это не так.

В третьем варианте я использую объект класса TCursorManager, который я написал, чтобы обернуть зависящий от платформы сервис IFMXCursorService. Это в основном работает, но не всегда. Код для этого:

TCursorRecord= record
    FCursor: TCursor;
    FStartTime: integer;
  end;

  TCursorRecordArray= array of TCursorRecord;

  TCursorManager= class
  private
    FCursorService: IFMXCursorService;
    FCursorRecordStack: TCursorRecordArray;
    FCursorRecordCount: integer;
  protected
    function GetCursorTickCount: integer;
  public
    constructor Create;
    destructor Destroy; override;
    function GetCursor: TCursor;
      {Returns currently set cursor}
    procedure SetCursor(Cursor: TCursor);
      {Sets new cursor}
    function RestorePrevCursor: TCursor;
      {Restores cursor previously set using this object}
    property Cursor: TCursor read GetCursor write SetCursor;
    property CursorTickCount: integer read GetCursorTickCount;
  end;

implementation

constructor TCursorManager.Create;
var
  CurrCursorRecord: TCursorRecord;
begin
  {Create platform-dependent cursor service:}
  if TPlatformServices.Current.SupportsPlatformService(IFMXCursorService) then
    FCursorService:= TPlatformServices.Current.GetPlatformService(IFMXCursorService)
                                              as IFMXCursorService;
  {Create current cursor record:}
  CurrCursorRecord.FCursor:= FCursorService.GetCursor;
  CurrCursorRecord.FStartTime:= GetTickCount;
  {Put current cursor record onto CursorRecordStack:}
  SetLength(FCursorRecordStack, 8);
  FCursorRecordCount:= 1;
  FCursorRecordStack[0]:= CurrCursorRecord;
end;

function TCursorManager.RestorePrevCursor: TCursor;
var
  PrevCursorRecord: TCursorRecord;
begin
  if Assigned(FCursorService) then
    begin
      if FCursorRecordCount>0 then
        begin
          {Remove current cursor record from stack:}
          FCursorRecordCount:= FCursorRecordCount - 1;
          PrevCursorRecord:= FCursorRecordStack[FCursorRecordCount-1];
          {Reduce size of stack array if possible:}
          if FCursorRecordCount mod 8 = 0 then
            SetLength(FCursorRecordStack, FCursorRecordCount);
          {Update start time of new curr cursor:}
          PrevCursorRecord.FStartTime:= GetTickCount;
          {Set previous cursor in system:}
          FCursorService.SetCursor(PrevCursorRecord.FCursor);
          {Return prev cursor:}
          Result:= PrevCursorRecord.FCursor;
        end;
    end;
end;

procedure TCursorManager.SetCursor(Cursor: TCursor);
var
  NewCursorRecord: TCursorRecord;
begin
  if Assigned(FCursorService) then
    begin
      {Set up new CursorRecord:}
      NewCursorRecord.FCursor:= Cursor;
      NewCursorRecord.FStartTime:= GetTickCount;
      {Add new cursor record to stack:}
      if FCursorRecordCount= Length(FCursorRecordStack) then
        SetLength(FCursorRecordStack, FCursorRecordCount + 8);
      Inc(FCursorRecordCount);
      FCursorRecordStack[FCursorRecordCount-1]:= NewCursorRecord;
      {Call system procedure to set cursor:}
      FCursorService.SetCursor(Cursor);
    end;
end;

Какой самый простой способ достичь того, чего я хочу достичь?

1 Ответ

1 голос
/ 14 ноября 2019

Проблема, по-видимому, объясняется следующими фактами. Поведение курсора описывается в справке RAD Studio следующим образом:

Если для курсора установлен курсор по умолчанию, этот элемент управления может отображать другой курсор, когда указатель мыши находится над ним. Фактический курсор, который отображает этот элемент управления, является курсором, определенным в InheritedCursor, свойстве только для чтения, которое рассчитывается не только по значению Cursor в этом элементе управления, но также и по значению Cursor в любом предке этого элемента управления (parent, grand). родитель и так далее до родительской формы).

Это реализовано в следующем методе:

procedure TControl.SetCursor(const Value: TCursor);
var
  CursorService: IFMXCursorService;
begin
  if FCursor <> Value then
  begin
    FCursor := Value;
    if FCursor <> crDefault then
      RefreshInheritedCursor
    else
    begin
      if Parent <> nil then
        RefreshInheritedCursor
      else
        FInheritedCursor := crDefault;
    end;

    if IsMouseOver and not (csLoading in ComponentState) and not (csDesigning in ComponentState) and
      TPlatformServices.Current.SupportsPlatformService(IFMXCursorService, CursorService) then
      CursorService.SetCursor(FInheritedCursor);
  end;
end;

При щелчке мышью по компоненту, который наследуется от класса TControl, вызывается описанная выше процедура. Если IsMouseOver имеет значение True, смена курсора работает. Поэтому вариант 1 работает, когда кнопка нажата, потому что мышь находится над ней, когда она нажата. Но процедура не вызывается при нажатии на элемент меню, связанный с действием, потому что в этом случае мышь находится не над кнопкой, а над элементом меню.

Можно было бы подумать, что вариант 0 должен работатьпотому что там, где на форме щелкают мышью, форма всегда находится под мышью. Но TForm не наследуется от TControl, а только от TFMXObject. Метод TCustomForm.SetCursor просто присваивает значение курсора полю, не вызывая код, реализующий поведение, описанное в файлах справки. Следовательно, опция 0 не работает. Такое поведение, по-видимому, не соответствует описанному в файле справки, в котором говорится, что InheritedCursor должен искать курсор не по умолчанию вплоть до формы предка. Кажется, здесь есть возможности для улучшения реализации FMX!

Что касается подхода в Варианте 2, он на самом деле не работает должным образом. Песочные часы показываются кратко, пока не появится PanelProgress. Это заставляет курсор переключиться обратно на crDefault.

С учетом этих ограничений единственное решение, которое мне удалось найти, - это добавить новую кнопку в PanelProgress с пометкой «Пуск» и переместить большую часть кода. ранее в ActionFindExactMatchesExecute в обработчик события OnClick для новой кнопки. ActionFindExactMatchesExecute становится:

procedure TFormMain.ActionFindExactMatchesExecute(Sender: TObject);
begin
  PanelResults.SendToBack;
  PanelProgress.BringToFront;
end;

, а код ButtonStartClick:

procedure TFormMain.ButtonStartClick(Sender: TObject);
var
  IterationContextHits: TIterationContextHits;
begin
  ButtonStart.Cursor:= crHourglass;
  {…}
  Try
    {…}
    ShowMessage('Number of matches found: ' + IntToStr(FHitCount));
    {Update GUI:}
    DataToControls;
    PanelResults.BringToFront;
  finally
    IterationContextHits.Free;
    ButtonStart.Cursor:= crDefault;
  end;
end;

С этими изменениями в зависимости от того, какой компонент связан с действием, все, что происходит, - это то, что PanelProgress выводится на экран. Тогда есть только один способ для запуска длинного кода, то есть нажать ButtonStart, поэтому мышь обязательно находится над ButtonStart, поэтому Control.IsMouseOver имеет значение true. Следовательно, курсор песочных часов показан, однако действие запущено.

...