0

I am trying to define an area in the shape of a triangle and check if the mouse is in it. I can find if the mouse is in a certain square area using the code below. My program needs to detect the mouse in a triangle or a more complex shape.

if (Mouse.CursorPos.X < 20) or (50 > tbmn.Left + tbmn.Width) or (Mouse.CursorPos.Y < 20) or (Mouse.CursorPos.Y > tbmn.Top + 60) then 
begin

end;

So basically, what I want to do is have a shape anywhere on the screen and check if the mouse is in it.

Is there a way to easily calculate a region of the screen and detect if the mouse is present in it?

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
Codex
  • 101
  • 1
  • 11
  • https://stackoverflow.com/questions/217578/how-can-i-determine-whether-a-2d-point-is-within-a-polygon – Andreas Rejbrand Nov 13 '21 at 11:14
  • There are multiple ways of achieving this. What is your end goal? Perhaps your end goal could be achieved with a completely different approach. – SilverWarior Nov 13 '21 at 11:24
  • @SilverWarior I have a circle divided in 8 pieces. Each piece is a button that I click on. I want to know above which button my mouse is. The buttons are 8 triangles. – Codex Nov 13 '21 at 11:47
  • If you only need to determine if a point is within a triangle, you can of course do it naïvely, as I do here: https://stackoverflow.com/a/7224075/282848 (`GetTriangleAt` function). – Andreas Rejbrand Nov 13 '21 at 12:23
  • And in the context of GDI, you have the [`PtInRegion`](https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-ptinregion) function. – Andreas Rejbrand Nov 13 '21 at 16:28
  • @Codex how are you creating the circle and buttons to begin with? Whatever API you are using for that task might provide its own way of performong coordinate hit testing. – Remy Lebeau Nov 13 '21 at 17:34
  • If you have circle divided into 8 pieces then doing triangle detection is not best approach. Why? First triangle won't cover the whole piece of your circle so if user clicks on the very edge on the circle the click won't be recognized. Second reason is that you would have to perform this is the mouse within a triangle 8 times (once for each section of the circle). You are better of by creating a vector between center of the circle and the mouse position and then calculating the angle of that vector which will tell you which section/button is in that direction ... – SilverWarior Nov 14 '21 at 01:53
  • ... Then you just calculate the length of that said vector to see if the mouse cursor is over the circle or outside of the circle. If it is smaller than circle radius then the mouse cursor is over circle and if it is greater then it is outside of the circle. And with additional smaller diameter you can easily turn your pie buttons into one of those button wheels that are so commonly used in games these days. Check [this answer](https://stackoverflow.com/a/15596654/3636228) to see how to calculate the vector angle and use Pythagorean theorem for calculating vector distance. – SilverWarior Nov 14 '21 at 02:03

2 Answers2

1

Asuming you have a component where you draw a triangle inside and only want to have the component detect mouse hit when the cursor is over the visible part of the shape then you could do something like this:

Have an alpha layer on the component being drawn. Then intercept the Windows CM_HITTEST message. in the hit test message procedure you then check if the alpha value is 0. If it is 0 then the mouse is over an area with some visible color value.

Type  
  TSomeComponent = class(TGraphicControl)
  private
    FPNG : TGraphic;
    procedure CMHitTest(var Message: TCMHitTest); message CM_HITTEST;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure Paint; Override;
  end;

procedure Register;

implementation
uses
  GR32,  GR32_Brushes,Winapi.Windows;   

procedure TSomeComponent.CMHitTest(var Message: TCMHitTest);
var
  colorEntry: TColor32Entry;
  bmp : TBitmap32;
begin
  bmp := TBitmap32.Create();
  try
    try
      bmp.Assign(FPNG);
      colorEntry := TColor32Entry(bmp.Pixels[Message.XPos,Message.YPos]);

      if colorEntry.A <> 0 then
           Message.Result := HTCLIENT
      else
           Message.Result := HTNOWHERE;
    except
      Message.Result := HTCLIENT;
    end;
  finally
    bmp.Free;
  end;
end;
Jeppe Clausen
  • 166
  • 10
1

You can use region functions from WinApi. Here are example for simple triangle:

function PtInTriangle(ptX,ptY,X1,Y1,X2,Y2,X3,Y3:integer):Boolean;
var rgn:THandle; pts:array [0..2] of TPoint;
begin
  pts[0].X:=X1; pts[0].Y:=Y1;
  pts[1].X:=X2; pts[1].Y:=Y2;
  pts[2].X:=X3; pts[2].Y:=Y3;
  rgn := CreatePolygonRgn( pts[0], 3, WINDING);
  Result := PtInRegion(rgn, ptX, ptY);
  DeleteObject(rgn);
end;

This function takes about ~30..40us on my machine, and PtInRegion() takes only ~10% of this time (so, you can optimize it by caching Region object). Here are code with simple bencmark:

function PtInTriangle(ptX,ptY,X1,Y1,X2,Y2,X3,Y3:integer):Boolean;
var rgn:THandle; pts:array [0..2] of TPoint;
    t,t1,t2,t3:Int64;
begin
  // Create region
  QueryPerformanceCounter(t);
    pts[0].X:=X1; pts[0].Y:=Y1;
    pts[1].X:=X2; pts[1].Y:=Y2;
    pts[2].X:=X3; pts[2].Y:=Y3;
    rgn := CreatePolygonRgn( pts[0], 3, WINDING);
  QueryPerformanceCounter(t1); Dec(t1,t);
  // Check point
  QueryPerformanceCounter(t);
    Result := PtInRegion(rgn, ptX, ptY);
  QueryPerformanceCounter(t2); Dec(t2,t);
  // Delete region
  QueryPerformanceCounter(t);
    DeleteObject(rgn);
  QueryPerformanceCounter(t3); Dec(t3,t);
  // Debug output
  QueryPerformanceFrequency(t);
  OutputDebugString(PChar(Format('All:%d(%.1fus)  Create:%d  PtInRect:%d(%.1f%%)  Delete:%d',
    [t1+t2+t3,(t1+t2+t3)/t*1E6,t1,t2,t2*100/(t1+t2+t3),t3])));
end;

Also, you can create complex regions with CreatePolyPolygonRgn() or CombineRgn().