如何将屏幕坐标转换为控件坐标

6
我需要的代码能够拦截平板电脑上同时发生的多次点击。在一篇关于如何处理多个OnMouseDown同时发生的问题(不可能)的之前的问题中,提供了一个链接来解决Delphi-Android中如何处理多点触控的问题。然而,这段代码返回屏幕坐标系下的(x, y)位置,我不知道如何将其转换为特定控件的本地坐标系。Delphi的文档提到了一个ScreenToClient函数,但它只能将屏幕坐标系转换为窗体坐标系,对于Android来说几乎没什么用处(文档是关于XE2的,但该函数仍存在于XE5中,但已经从FMX.Platform移动到了FMX.Form)。
在FMX中是否有将屏幕坐标转换为TControl坐标的简单方法,就像在VCL中一样?当然,我可以“去除”控件的父级,记录其左上角坐标,并对每个父级重复此操作,直到到达基础表单,但这相当繁琐。
我的当前方法是获取TControl(实际上是TPanel)的顶部(x,y)坐标,该TControl是要被点击的控件(实际上是TRectangle's的子项),并将这些坐标加起来以检查单击是否在该矩形内。请参见下面的示例代码。
procedure TKeyBoard.process_touch (Event: TTouchEvent; status_byte: Int32);
var
  key: TKey;
  p, q: TPointF;
  i: Integer;
  x, y: single;
begin
// Check whether at least one event is present. If so, i points to the last event
   i := Length (Event.Points) - 1;
   if i < 0 then Exit;

// Get (x, y) coordinates from event. It's in screen coordinates
   x := Event.Points [i].Position.X;
   y := Event.Points [i].Position.Y;

// FControl is a reference to the panel holding the keys
// Find its rectangle position and convert to screen coordinates
   p := TPointF.Create (FControl.Position.X, FControl.Position.Y);
   q := TPointF.Create (p.X + FControl.Width, p.Y + FControl.Height);
   p := Application.MainForm.ClientToScreen (p);
   q := Application.MainForm.ClientToScreen (q);

   logd ('control [%.0f %.0f - %.0f, %.0f]', [FControl.Position.X, FControl.Position.Y, FControl.Width, FControl.Height]);
   logd ('to screen [%.0f %.0f - %.0f, %.0f]', [p.X, p.Y, q.X, q.Y]);

// Determine whether a black key has been pressed
   for i := Low (Fkeys) to High (FKeys) do
   begin
      if not cOctave_Major [i mod nOctave] then
      begin
         key := FKeys [i];

         logd ('%d (%.0f, %.0f) - (%.0f, %.0f) (%.0f, %.0f)', [i, x, y,
                  key.Position.X + p.X,             key.Position.Y + p.Y,
                  key.Position.X + P.X + Key.Width, key.Position.Y + p.Y + key.Height]);

         if (x >= key.Position.X + p.X) and (x <= key.Position.X + p.X + Key.Width) and
            (y >= key.Position.Y + p.Y) and (y <= key.Position.Y + p.Y + key.Height)
            then break;
         key := nil;
      end; // if
   end; // for

// if not, check whether a white key has been pressed
   if key = nil then
   begin
      logd ('Major');
      for i := Low (Fkeys) to High (FKeys) do
      begin
         if cOctave_Major [i mod nOctave] then
         begin
            key := FKeys [i];

            logd ('%d (%.0f, %.0f) - (%.0f, %.0f) (%.0f, %.0f)', [i, x, y,
                  key.Position.X + p.X,             key.Position.Y + p.Y,
                  key.Position.X + P.X + Key.Width, key.Position.Y + p.Y + key.Height]);

            if (x >= key.Position.X + p.X) and (x <= key.Position.X + p.X + Key.Width) and
               (y >= key.Position.Y + p.Y) and (y <= key.Position.Y + p.Y + key.Height)
               then break;
            key := nil;
         end; // if
      end; // for
   end; // if


   if key <> nil
      then putShort (status_byte, key.Note, 127);
   if key <> nil
      then logd (' found %s', [key.Text.Text]);
end; // process_touch //

这段代码实际上很混乱,因为它假设父控件的父级是Application.MainForm,但这并不一定是正确的。另一个观察结果是,标签可能在Y轴方向上仍然略有偏差。因此,我想直接将屏幕坐标转换为控件坐标以解决这个问题。
编辑2: 我尝试使用@Sentient建议的IsMouseOver检查每个关键控件,但奇怪的是,只有在处理MouseUp事件时才会产生真值。

1
使用控件的.IsMouseOver是否可行?我不知道当同时存在多个触摸时,它是否能正确注册。 - Sentient
哎呀,这在安卓上不起作用。它是在MouseUp事件期间触发的,而不是在MouseDown期间触发的。 - Arnold
1个回答

13

我是你正在使用的多点触控代码的作者。当我看到你在处理坐标时遇到了困难,我想到了一些解决方案并更新了代码,现在它可以提供被触摸的控制和相对坐标。如果你想知道如何使用它,那很简单。

关于此问题的博客文章在这里:

http://www.cromis.net/blog/2014/02/multi-touch-touched-control-and-relative-coordinates/

解决这个问题的代码看起来像这样:

  if Screen.ActiveForm <> nil then
  begin
    for I := 0 to Length(Event.Points) - 1 do
    begin
      Control := Screen.ActiveForm.ObjectAtPoint(Event.Points[I].Position);

      if Control <> nil then
      begin
        Event.Points[I].Control := Control as TFmxObject;
        Event.Points[I].RelPosition := Control.ScreenToLocal(Event.Points[I].Position);
      end
      else
      begin
        Event.Points[I].Control := Screen.ActiveForm;
        Event.Points[I].RelPosition := Screen.ActiveForm.ScreenToClient(Event.Points[I].Position);
      end;
    end;
  end;

2
“我是作者…”总是一个很好的开始来回答SO问题。+1。相关的一系列博客文章也很有趣。 - David
非常感谢您的详细解释。看起来您已经解决了从屏幕坐标到窗口坐标的转换问题。根据我理解您的博客,您也将此代码用于音乐键盘。您是否有这样的印象,即此代码的反应速度实际上比OnMouseDown事件处理程序要慢? - Arnold
是的,我用它来做音乐键盘。我正在将名为Piano Wizard的应用程序移植到iOS。至于速度,我认为它不应该比OnMouseDown慢。 - Runner
这段代码运行良好!感谢您的工作,它解决了我找到正确密钥的问题。我不知道有一个ObjectAtPoint方法,非常有用。一个非常小的细节:在Windows或macOS下编译时,有一个没有任何单元的“uses”子句会导致编译器错误。注释掉“uses”就可以编译通过了。出于好奇,有一个问题:我不需要您跟踪的历史记录,Points数组中的最后一个元素就足够了。您跟踪历史记录的原因是什么? - Arnold
1
很高兴能够帮忙。我会检查Windows和MacOS的编译情况。感谢您的提示。至于历史记录,没有特别的原因。我自己不保留历史记录,iOS和Android都将其作为系统的一部分存储,并且您可以在触摸事件中获取该数据。因此,我向用户提供了这些数据,以防有用的时候。 - Runner
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接