References:
- Original code: https://forums.wxwidgets.org/viewtopic.php?t=41893
- Hit-testing: https://www.xarg.org/book/computer-graphics/2d-hittest/
- Hit-testing in C#: https://learn.microsoft.com/en-us/dotnet/desktop/wpf/graphics-multimedia/hit-testing-in-the-visual-layer?view=netframeworkdesktop-4.8
- wxWidgets general reference book (free): https://www.wxwidgets.org/docs/book/
- Avoiding flicker: https://wiki.wxwidgets.org/Flicker-Free_Drawing
- R-tree: https://www.bartoszsypytkowski.com/r-tree/
As far as I can tell, wxWidgets does not provide general-purpose hit-testing on the client area of a window in the way that C# does [3].
This is the simplest implementation of hit-testing that I could devise. It is closely modeled on the code reference [1], but with most of the features stripped out to highlight the core features of hit-testing.
All that it does is perform a hit-test on a few lines drawn on a wxPanel. At each mouse move, a hit test is performed using the cursor location. If there is a hit on one or more of the lines, then it is or they are redrawn in red. Any line that was previously hit but is no longer is redrawn in blue.
This is a very crude version of hit-testing, where a hit is defined as the cursor position being inside the bounding box of a line. Typically, this is only the first step of a hit-test algorithm and is used to quickly find a subset of objects for more refined hit-testing. Some details of the next steps are explored in reference [2].

It’s fast because the bounding box is aligned with the coordinate system, so hit testing only requires checking if the cursor x-position is between the x-max and x-min of the bounding box and that the cursor y-position is between the y-max and y-min of the bounding box.
The three images below show what is looks like. The cursor is not visible, so you will just have to take my word for what is going on. The case with two lines hit is where the cursor is position in the overlap of the bounding boxes.
No hits:

Hit on one line:

Hit on two lines:

The lines are created and drawn during the application initialization (cApp::OnInit) and are fixed. The lines are created in cAppFrame::buildGeometry, and drawn in cAppFrame::draw.
Information on each line is stored in a structure (LineSegment) that contains the start and end points of the line and a boolean (m_Hit) that records if the line has been hit or not. As each line is created, it is added to a vector of lines (LineVector). This vector is used within a class (LineList) that provides the following functions:
- AddLine adds a line to the vector
- IsHit checks if the line was hit
- SetHit sets the line as hit
- SetNotHit sets the line as not hit
- size returns the vector length
- GetBndBox returns a wxRect that in the line’s bounding box
- sp returns the starting point of the line
- ep returns the ending point of the line
When the lines are drawn on the client area, the hit/not hit boolean is used to set the line color. Initially, no lines are hit so they are all drawn in blue.
Hit-testing is performed in response to a mouse movement event. Hit-testing involves stepping through the LineVector and checking if the mouse position is within the bounding rectangle of each line. If it is, m_Hit is set to true. If it isn’t, m_Hit is set to false. Actually, m_Hit is only set if the new condition is different from the last hit-testing. This was done to avoid redrawing at every mouse move.
If there are a large number of objects to be hit-tested, a more efficient data-structure than a vector may be needed. Something like an R-tree or similar, for example [6].
That’s about it.
Other Comments/Issues
- To avoid flickering, the wxEraseEvent is handled with a handler that does nothing. I cannot find the reference where I found this idea, but it is discussed in references [4] and [5].
- If you resize the window and make it larger, the wxMemoryDC bitmap is not resized, so the edge of the background drawn in the CAppFrame constructor will be visible.
Code Listing
The code consists of two files: a header file (.h) and a code file (.cpp).
Header file (cMain.h):
#pragma once
#include <wx/wx.h>
#include <wx/dcbuffer.h>
struct LineSegment
{
LineSegment(const wxPoint& pt1, const wxPoint& pt2)
: x1(pt1.x), y1(pt1.y), x2(pt2.x), y2(pt2.y) {}
wxInt32 x1, y1, x2, y2;
bool m_Hit = false;
};
typedef wxVector<LineSegment> LineVector;
class LineList
{
public:
LineList() { }
void AddLine(const wxPoint& pt1, const wxPoint& pt2) { m_lineVec.push_back(LineSegment(pt1, pt2)); }
bool IsHit(int i) const { return m_lineVec[i].m_Hit; }
void SetHit(int i) { m_lineVec[i].m_Hit = true; }
void SetNotHit(int i) { m_lineVec[i].m_Hit = false; }
const wxInt32 size() { return m_lineVec.size(); }
wxRect GetBndBox(int i) { return wxRect(wxPoint(m_lineVec[i].x1, m_lineVec[i].y1), wxPoint(m_lineVec[i].x2, m_lineVec[i].y2)); }
wxPoint sp(int i) { return wxPoint(m_lineVec[i].x1, m_lineVec[i].y1); }
wxPoint ep(int i) { return wxPoint(m_lineVec[i].x2, m_lineVec[i].y2); }
private:
LineVector m_lineVec;
};
class cApp : public wxApp
{
public:
virtual bool OnInit() override;
};
class cAppFrame : public wxFrame
{
public:
cAppFrame(const wxString& title);
void buildGeometry();
void draw();
private:
void OnPaint(wxPaintEvent& event);
void OnErase(wxEraseEvent& event) {};
void OnMotion(wxMouseEvent& event);
wxPanel* m_canvas;
wxBitmap m_canvasBitmap;
int m_canvasWidth, m_canvasHeight;
LineList m_LineList;
};
Code file (cMain.cpp):
#include "cMain.h"
bool cApp::OnInit()
{
cAppFrame* mainFrame = new cAppFrame(wxT("Line"));
mainFrame->buildGeometry();
mainFrame->draw();
mainFrame->Show(true);
return true;
}
void cAppFrame::buildGeometry()
{
wxPoint sp = wxPoint(50, 60);
wxPoint ep = wxPoint(150, 160);
m_LineList.AddLine(sp, ep);
sp = wxPoint(250, 80);
ep = wxPoint(200, 10);
m_LineList.AddLine(sp, ep);
sp = wxPoint(40, 200);
ep = wxPoint(70, 140);
m_LineList.AddLine(sp, ep);
}
void cAppFrame::draw()
{
wxMemoryDC dc(m_canvasBitmap);
wxPen red = wxPen(*wxRED_PEN);
wxPen blue = wxPen(*wxBLUE_PEN);
const wxInt32 count = m_LineList.size();
for (int i = 0; i < count; i++)
{
if (m_LineList.IsHit(i))
{
dc.SetPen(red);
}
else
{
dc.SetPen(blue);
}
dc.DrawLine(m_LineList.sp(i), m_LineList.ep(i));
}
m_canvas->Refresh();
}
void cAppFrame::OnPaint(wxPaintEvent& event)
{
wxPaintDC(m_canvas).DrawBitmap(m_canvasBitmap, 0, 0);
}
void cAppFrame::OnMotion(wxMouseEvent& event)
{
wxPoint mousePos = wxPoint(event.GetX(),event.GetY());
wxRect boundingBox;
const wxInt32 count = m_LineList.size();
for (int i = 0; i < count; i++)
{
bool forceDraw = false;
boundingBox = m_LineList.GetBndBox(i);
if (boundingBox.Contains(mousePos))
{
if (!m_LineList.IsHit(i))
{
forceDraw = true;
m_LineList.SetHit(i);
}
}
else
{
if (m_LineList.IsHit(i))
{
forceDraw = true;
m_LineList.SetNotHit(i);
}
}
if (forceDraw) { this->draw(); }
}
}
cAppFrame::cAppFrame(const wxString& title)
:wxFrame(NULL, wxID_ANY, title, wxDefaultPosition, wxSize(600, 600))
{
m_canvas = new wxPanel(this);
this->Layout();
this->SetBackgroundStyle(wxBG_STYLE_PAINT);
m_canvasWidth = m_canvas->GetSize().GetWidth();
m_canvasHeight = m_canvas->GetSize().GetHeight();
m_canvasBitmap = wxBitmap(m_canvasWidth, m_canvasHeight, 24);
wxMemoryDC dc(m_canvasBitmap);
dc.SetPen(*wxWHITE_PEN);
dc.SetBrush(*wxWHITE_BRUSH);
dc.DrawRectangle(0, 0, m_canvasWidth, m_canvasHeight);
m_canvas->Bind(wxEVT_PAINT, &cAppFrame::OnPaint, this);
m_canvas->Bind(wxEVT_ERASE_BACKGROUND, &cAppFrame::OnErase, this);
m_canvas->Bind(wxEVT_MOTION, &cAppFrame::OnMotion, this);
}
wxIMPLEMENT_APP(cApp);
