UE4 SlateUI事件机制

最近开发过程中,碰到一个比较奇怪的Bug,同事在场景中创建了个3D UI,使用的是WidgetComponent组件,然后动态设置widget实例,第一次创建的3D UI可以正常接收到鼠标事件,通过3D UI进入战斗场景后,第二场战斗的3D UI界面没法相应事件了,然后我就接住这口锅了。

wbp_path = '/Game/test_3d_ui.test_3d_ui'
# game.ui: 全局ui管理器
# create_3d_ui:加载WidgetBlueprint,并打开
widget = game.ui.create_3d_ui(wbp_path)
widget_comp.set_widget(widget)
PyObject *py_ue_set_widget(ue_PyUObject * self, PyObject * args)
{
    ue_py_check(self);

    PyObject *widget;
    if (!PyArg_ParseTuple(args, "O", &widget))
        return nullptr;
    
    UWidgetComponent *widget_component = ue_py_check_type<UWidgetComponent>(self);
    if (!widget_component)
        return PyErr_Format(PyExc_Exception, "uobject is not a UWidgetComponent");

    UUserWidget *uwidget = ue_py_check_type<UUserWidget>(widget);
    if (!uwidget)
        return PyErr_Format(PyExc_Exception, "argument2 is not a APlayerController");
    
    widget_component->SetWidget(uwidget);

    Py_RETURN_NONE;
}

然后开始看UE4源码,研究下UE4 SlateUI事件机制

按钮事件调用栈

下图是从Launch.cpp里里的GEngineLoop Tick调用Windows平台处理事件的代码,最终进入Button代码,响应OnClicked回调的调用栈。

事件来源

在Windows平台上,鼠标点击,键盘事件都是调用Windows的API,从Windows事件列表中获取的。

/** 
 * Ticks the engine loop 
 * Engine\Source\Runtime\Launch\Private\Launch.cpp
 */
void EngineTick( void )
{
    //** line:62 **//
    GEngineLoop.Tick(); 
}

/**
 * Engine\Source\Runtime\ApplicationCore\Private\Windows\WindowsPlatformApplicationMisc.cpp
 * windows 消息处理
 */
static void WinPumpMessages()
{
    {
        MSG Msg;
        while( PeekMessage(&Msg,NULL,0,0,PM_REMOVE) )
        {
            TranslateMessage( &Msg );

            //* line:108 *//
            DispatchMessage( &Msg ); 
        }
    }
}

/**
 * Engine\Source\Runtime\ApplicationCore\Private\Windows\WindowsApplication.cpp
 */
int32 FWindowsApplication::ProcessMessage( HWND hwnd, uint32 msg, WPARAM wParam, LPARAM lParam )
{
    TSharedPtr< FWindowsWindow > CurrentNativeEventWindowPtr = FindWindowByHWND( Windows, hwnd );

    if( Windows.Num() && CurrentNativeEventWindowPtr.IsValid() )
    {
        // .....

        switch(msg)
        {
            case WM_KEYDOWN:
            case WM_SYSKEYUP:
            case WM_KEYUP:
            case WM_LBUTTONDBLCLK:
            case WM_LBUTTONDOWN:
            case WM_MBUTTONDBLCLK:
            case WM_MBUTTONDOWN:
            case WM_RBUTTONDBLCLK:
            case WM_RBUTTONDOWN:
            case WM_XBUTTONDBLCLK:
            case WM_XBUTTONDOWN:
            case WM_XBUTTONUP:
            case WM_LBUTTONUP:
            case WM_MBUTTONUP:
            case WM_RBUTTONUP:
            case WM_NCMOUSEMOVE:
            case WM_MOUSEMOVE:
            case WM_MOUSEWHEEL:
    #if WINVER >= 0x0601
            case WM_TOUCH:
    #endif
                {
                    //** line:1042 **//
                    DeferMessage( CurrentNativeEventWindowPtr, hwnd, msg, wParam, lParam );
                    // Handled
                    return 0;
                }
                break;
        }
    }
}


/**
 * Engine\Source\Runtime\ApplicationCore\Private\Windows\WindowsApplication.cpp
 */
int32 FWindowsApplication::ProcessDeferredMessage( const FDeferredWindowsMessage& DeferredMessage )
{
    if ( Windows.Num() && DeferredMessage.NativeWindow.IsValid() )
    {
        HWND hwnd = DeferredMessage.hWND;
        uint32 msg = DeferredMessage.Message;
        WPARAM wParam = DeferredMessage.wParam;
        LPARAM lParam = DeferredMessage.lParam;

        switch(msg)
        {
            case WM_LBUTTONDBLCLK:
            case WM_LBUTTONDOWN:
            case WM_MBUTTONDBLCLK:
            case WM_MBUTTONDOWN:
            case WM_RBUTTONDBLCLK:
            case WM_RBUTTONDOWN:
            case WM_XBUTTONDBLCLK:
            case WM_XBUTTONDOWN:
            case WM_LBUTTONUP:
            case WM_MBUTTONUP:
            case WM_RBUTTONUP:
            case WM_XBUTTONUP:
                {
                    POINT CursorPoint;
                    CursorPoint.x = GET_X_LPARAM(lParam);
                    CursorPoint.y = GET_Y_LPARAM(lParam); 

                    ClientToScreen(hwnd, &CursorPoint);

                    const FVector2D CursorPos(CursorPoint.x, CursorPoint.y);

                    EMouseButtons::Type MouseButton = EMouseButtons::Invalid;
                    bool bDoubleClick = false;
                    bool bMouseUp = false;
                    switch(msg)
                    {
                        case WM_LBUTTONDBLCLK:
                            bDoubleClick = true;
                            MouseButton = EMouseButtons::Left;
                            break;
                        case WM_LBUTTONUP:
                            bMouseUp = true;
                            MouseButton = EMouseButtons::Left;
                            break;
                        case WM_LBUTTONDOWN:
                            MouseButton = EMouseButtons::Left;
                            break;
                        
                        // ...
                        default:
                            check(0);
                    }

                    if (bMouseUp)
                    {
                        //** line:2183 **//
                        return MessageHandler->OnMouseUp( MouseButton, CursorPos ) ? 0 : 1;
                    }
                    else if (bDoubleClick)
                    {
                        MessageHandler->OnMouseDoubleClick( CurrentNativeEventWindowPtr, MouseButton, CursorPos );
                    }
                    else
                    {
                        MessageHandler->OnMouseDown( CurrentNativeEventWindowPtr, MouseButton, CursorPos );
                    }
                    return 0;
                }
                break;
            }
        }
    }
}

随后代码进入SlateApplication中,对事件进行封装,然后开始找到响应的Widget,调用对应的响应函数,并最终响应事件。


/*  ==================================
    前面都是从Windows事件队列获取消息
    并对消息进行处理,后面开始进入最难
    的地方了
    ==================================
 */

/**
 * Engine\Source\Runtime\Slate\Private\Framework\Application\SlateApplication.cpp
 */
bool FSlateApplication::OnMouseUp( const EMouseButtons::Type Button, const FVector2D CursorPos )
{
    // convert left mouse click to touch event if we are faking it	
    if (IsFakingTouchEvents() && Button == EMouseButtons::Left)
    {
        bIsFakingTouched = false;
        
        //** line:5305 **//
        return OnTouchEnded(PlatformApplication->Cursor->GetPosition(), 0, 0);
    }

    FKey Key = TranslateMouseButtonToKey( Button );

    FPointerEvent MouseEvent(
        GetUserIndexForMouse(),
        CursorPointerIndex,
        CursorPos,
        GetLastCursorPos(),
        PressedMouseButtons,
        Key,
        0,
        PlatformApplication->GetModifierKeys()
        );

    return ProcessMouseButtonUpEvent( MouseEvent );
}

/**
 * Engine\Source\Runtime\Slate\Private\Framework\Application\SlateApplication.cpp
 */
bool FSlateApplication::OnTouchEnded( const FVector2D& Location, int32 TouchIndex, int32 ControllerId )
{
    TSharedRef<FSlateUser> User = GetOrCreateUser(ControllerId);
    if (User->IsTouchPointerActive(TouchIndex))
    {
        FPointerEvent PointerEvent(
            ControllerId,
            TouchIndex,
            Location,
            Location,
            0.0f,
            true);

        //** line:5912 **//
        ProcessTouchEndedEvent(PointerEvent);

#if WITH_SLATE_DEBUGGING
        ensure(!User->IsTouchPointerActive(TouchIndex));
#endif

        return true;
    }

    return false;
}

/**
 * Engine\Source\Runtime\Slate\Private\Framework\Application\SlateApplication.cpp
 */
bool FSlateApplication::ProcessMouseButtonUpEvent( const FPointerEvent& MouseEvent )
{
    // ...
    // An empty widget path is passed in.  As an optimization, one will be generated only if a captured mouse event isn't routed
    FWidgetPath EmptyPath;
    //** line:5356 **//
    const bool bHandled = RoutePointerUpEvent( EmptyPath, MouseEvent ).IsEventHandled();

    if ( bIsCursorUser && PressedMouseButtons.Num() == 0 )
    {
        PlatformApplication->SetCapture( nullptr );
    }

    return bHandled;
}

/**
 * Engine\Source\Runtime\Slate\Private\Framework\Application\SlateApplication.cpp
 */
FReply FSlateApplication::RoutePointerUpEvent(const FWidgetPath& WidgetsUnderPointer, const FPointerEvent& PointerEvent)
{
    TScopeCounter<int32> BeginInput(ProcessingInput);

    FReply Reply = FReply::Unhandled();
    TSharedRef<FSlateUser> SlateUser = GetOrCreateUser(PointerEvent);
    TSharedPtr<FDragDropOperation> LocalDragDropContent;

    if (SlateUser->HasCapture(PointerEvent.GetPointerIndex()))
    {
        FWidgetPath MouseCaptorPath = SlateUser->GetCaptorPath(PointerEvent.GetPointerIndex(), 
            FWeakWidgetPath::EInterruptedPathHandling::Truncate, &PointerEvent);
        if ( ensureMsgf(MouseCaptorPath.Widgets.Num() > 0, TEXT("A window had a widget with mouse capture. 
            That entire window has been dismissed before the mouse up could be processed.")) )
        {
            // Switch worlds widgets in the current path
            FScopedSwitchWorldHack SwitchWorld( MouseCaptorPath );

            //** line:4815 **//
            Reply =
                FEventRouter::Route<FReply>( this, FEventRouter::FToLeafmostPolicy(MouseCaptorPath), PointerEvent, 
                    [this]( const FArrangedWidget& TargetWidget, const FPointerEvent& Event )
                {
                    FReply TempReply = FReply::Unhandled();
                    if (Event.IsTouchEvent())
                    {
                        TempReply = TargetWidget.Widget->OnTouchEnded(TargetWidget.Geometry, Event);
                    }

                    if (!Event.IsTouchEvent() || (!TempReply.IsEventHandled() && this->bTouchFallbackToMouse))
                    {
                        TempReply = TargetWidget.Widget->OnMouseButtonUp( TargetWidget.Geometry, Event );
                    }
                    
                    if ( Event.IsTouchEvent() && !IsFakingTouchEvents() )
                    {
                        // Generate a Leave event when a touch ends as well, since a 
                        // touch can enter a widget and then end inside it
                        TargetWidget.Widget->OnMouseLeave(Event);
                    }

                    return TempReply;
                }, ESlateDebuggingInputEvent::MouseButtonUp);
        }
    }
    else
    {
        if (!LocalWidgetsUnderPointer.IsValid())
        {
            // 更新屏幕坐标区域中的widget
            LocalWidgetsUnderPointer = LocateWindowUnderMouse(PointerEvent.GetScreenSpacePosition(), 
                GetInteractiveTopLevelWindows(), false, SlateUser->GetUserIndex());
        }
    }
}

/**
* Route an event based on the Routing Policy.
* Engine\Source\Runtime\Slate\Private\Framework\Application\SlateApplication.cpp
*/
template< typename ReplyType, typename RoutingPolicyType, typename EventType, typename FuncType >
static ReplyType Route( FSlateApplication* ThisApplication, RoutingPolicyType RoutingPolicy, EventType EventCopy, 
    const FuncType& Lambda, ESlateDebuggingInputEvent DebuggingInputEvent)
{
    ReplyType Reply = ReplyType::Unhandled();
    const FWidgetPath& RoutingPath = RoutingPolicy.GetRoutingPath();
    const FWidgetPath* WidgetsUnderCursor = RoutingPolicy.GetWidgetsUnderCursor();

    EventCopy.SetEventPath( RoutingPath );

    for (; !Reply.IsEventHandled() && RoutingPolicy.ShouldKeepGoing(); RoutingPolicy.Next())
    {
        const FWidgetAndPointer& ArrangedWidget = RoutingPolicy.GetWidget();

#if PLATFORM_COMPILER_HAS_IF_CONSTEXPR
        if constexpr (Translate<EventType>::TranslationNeeded())
        {
            const EventType TranslatedEvent = Translate<EventType>::PointerEvent(ArrangedWidget.PointerPosition, EventCopy);

            //** line:378 **//
            Reply = Lambda(ArrangedWidget, TranslatedEvent).SetHandler(ArrangedWidget.Widget);
            ProcessReply(ThisApplication, RoutingPath, Reply, WidgetsUnderCursor, &TranslatedEvent);
        }
        else
        {
            Reply = Lambda(ArrangedWidget, EventCopy).SetHandler(ArrangedWidget.Widget);
            ProcessReply(ThisApplication, RoutingPath, Reply, WidgetsUnderCursor, &EventCopy);
        }
#else
        const EventType TranslatedEvent = Translate<EventType>::PointerEvent(ArrangedWidget.PointerPosition, EventCopy);
        Reply = Lambda(ArrangedWidget, TranslatedEvent).SetHandler(ArrangedWidget.Widget);
        ProcessReply(ThisApplication, RoutingPath, Reply, WidgetsUnderCursor, &TranslatedEvent);
#endif
    }

    return Reply;
}

/**
* Engine\Source\Runtime\Slate\Private\Framework\Application\SlateApplication.cpp
*/
FReply FSlateApplication::RoutePointerUpEvent(const FWidgetPath& WidgetsUnderPointer, const FPointerEvent& PointerEvent)
{
    // ...

    // Switch worlds widgets in the current path
    FScopedSwitchWorldHack SwitchWorld( MouseCaptorPath );

    //** line:4815 **//
    Reply = FEventRouter::Route<FReply>( this, FEventRouter::FToLeafmostPolicy(MouseCaptorPath), PointerEvent, 
        [this]( const FArrangedWidget& TargetWidget, const FPointerEvent& Event )
        {
            FReply TempReply = FReply::Unhandled();
            if (Event.IsTouchEvent())
            {
                TempReply = TargetWidget.Widget->OnTouchEnded(TargetWidget.Geometry, Event);
            }

            if (!Event.IsTouchEvent() || (!TempReply.IsEventHandled() && this->bTouchFallbackToMouse))
            {
                //** line:4829 **//
                TempReply = TargetWidget.Widget->OnMouseButtonUp( TargetWidget.Geometry, Event );
            }

            if ( Event.IsTouchEvent() && !IsFakingTouchEvents() )
            {
                // Generate a Leave event when a touch ends as well, since a 
                // touch can enter a widget and then end inside it
                TargetWidget.Widget->OnMouseLeave(Event);
            }

            return TempReply;
        }, ESlateDebuggingInputEvent::MouseButtonUp);
}

/**
* Engine\Source\Runtime\Slate\Private\Widgets\Input\SButton.cpp
*/
FReply SButton::OnMouseButtonUp( const FGeometry& MyGeometry, const FPointerEvent& MouseEvent )
{
	FReply Reply = FReply::Unhandled();
    // ...

    //** line:304 **//
    Reply = ExecuteOnClick();
}

/**
* Engine\Source\Runtime\Slate\Private\Widgets\Input\SButton.cpp
*/
FReply SButton::ExecuteOnClick()
{
    if (OnClicked.IsBound())
    {
        //** line:385 **//
        FReply Reply = OnClicked.Execute();

        return Reply;
    }
    else
    {
        return FReply::Handled();
    }
}


/**
* Engine\Source\Runtime\UMG\Private\Components\Button.cpp
*/
FReply UButton::SlateHandleClicked()
{

    //** line:203 **//
    OnClicked.Broadcast();

    return FReply::Handled();
}

获取响应控件

UE4中,为了方便获取鼠标响应控件,会将屏幕区域划分成一个一个区域,然后按照区域,将控件划分到对应的区域中管理,一个控件可能会被划分到多个区域中。例如:1920 * 1080 分辨率会被划分成 15 * 9 个Cell。详细代码参见如下:

/**
* Engine\Source\Runtime\SlateCore\Private\Input\HittestGrid.cpp
*/
// 屏幕分区大小
const FVector2D CellSize(128.0f, 128.0f);

// 计算屏幕分区个数
bool FHittestGrid::SetHittestArea(const FVector2D& HittestPositionInDesktop, const FVector2D& HittestDimensions,
    const FVector2D& HitestOffsetInWindow)
{
    bool bWasCleared = false;

    // If the size of the hit test area changes we need to clear it out
    if (GridSize != HittestDimensions)
    {
        GridSize = HittestDimensions;
        NumCells = FIntPoint(FMath::CeilToInt(GridSize.X / CellSize.X), FMath::CeilToInt(GridSize.Y / CellSize.Y));
        
        const int32 NewTotalCells = NumCells.X * NumCells.Y;
        ClearInternal(NewTotalCells);

        bWasCleared = true;
    }

    GridOrigin = HittestPositionInDesktop;
    GridWindowOrigin = HitestOffsetInWindow;

    return bWasCleared;
}

// 通过屏幕坐标获取对应分割区Cell坐标
FIntPoint FHittestGrid::GetCellCoordinate(FVector2D Position) const
{
    return FIntPoint(
        FMath::Min(FMath::Max(FMath::FloorToInt(Position.X / CellSize.X), 0), NumCells.X - 1),
        FMath::Min(FMath::Max(FMath::FloorToInt(Position.Y / CellSize.Y), 0), NumCells.Y - 1));
}

HittestGrid 每帧都会刷新,刷新堆栈如下:

每帧从SWindow根节点开始绘制,调用SetHittestArea函数,刷新HittestGrid:

int32 SWindow::PaintWindow( double CurrentTime, float DeltaTime, FSlateWindowElementList& OutDrawElements, 
    const FWidgetStyle& InWidgetStyle, bool bParentEnabled )
{
    // 更新HittestArea屏幕大小
    const bool HittestCleared = HittestGrid->SetHittestArea(GetPositionInScreen(), GetViewportSize());
    FPaintArgs PaintArgs(nullptr, GetHittestGrid(), GetPositionInScreen(), CurrentTime, DeltaTime);
    FSlateInvalidationContext Context(OutDrawElements, InWidgetStyle);
    Context.bParentEnabled = bParentEnabled;
    Context.PaintArgs = &PaintArgs;

    // 开始绘制窗口界面
    FSlateInvalidationResult Result = PaintInvalidationRoot(Context);
}

根节点开始Paint后,会以深度优先方式遍历所有子节点,并调用子节点的Paint函数

/**
* Engine\Source\Runtime\SlateCore\Private\Widgets\SWidget.cpp
*/
int32 SWidget::Paint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, 
    const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, 
    int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const
{
    // ...
    OutDrawElements.PushPaintingWidget(*this, LayerId, PersistentState.CachedElementHandle);

    if (bOutgoingHittestability)
    {
        //** line:1344 **//
        Args.GetHittestGrid().AddWidget(MutableThis, 0, LayerId, FastPathProxyHandle.GetIndex());
    }

    // ...
    // Paint the geometry of this widget.
    int32 NewLayerId = OnPaint(UpdatedArgs, AllottedGeometry, CullingBounds, OutDrawElements, LayerId, 
        ContentWidgetStyle, bParentEnabled);
}

然后再调用FHittestGrid::AddWidget函数,对每个Widget进行区域划分,将Widget加入对应的Cell中。

void FHittestGrid::AddWidget(const TSharedRef<SWidget>& InWidget, int32 InBatchPriorityGroup, 
    int32 InLayerId, int32 InSecondarySort)
{
    // Widget不可见,直接返回
    if (!InWidget->GetVisibility().IsHitTestVisible())
    {
        return;
    }

    FGeometry GridSpaceGeometry = InWidget->GetPaintSpaceGeometry();
    GridSpaceGeometry.AppendTransform(FSlateLayoutTransform(-GridWindowOrigin));

    const FSlateRect BoundingRect = GridSpaceGeometry.GetRenderBoundingRect();

    // 获取Widget最左上角跟最右下角的Cell Index
    // 后面循环将Widget加入到对应的Cell区域
    const FIntPoint UpperLeftCell = GetCellCoordinate(BoundingRect.GetTopLeft());
    const FIntPoint LowerRightCell = GetCellCoordinate(BoundingRect.GetBottomRight());

    if (bAddWidget)
    {
        int32& WidgetIndex = WidgetMap.Add(&*InWidget);
        for (int32 XIndex = UpperLeftCell.X; XIndex <= LowerRightCell.X; ++XIndex)
        {
            for (int32 YIndex = UpperLeftCell.Y; YIndex <= LowerRightCell.Y; ++YIndex)
            {
                if (IsValidCellCoord(XIndex, YIndex))
                {
                    CellAt(XIndex, YIndex).AddIndex(WidgetIndex);
                }
            }
        }
    }
}

TODO:

FWidgetPath FSlateApplication::LocateWidgetInWindow(FVector2D ScreenspaceMouseCoordinate, const TSharedRef<SWindow>& Window, bool bIgnoreEnabledStatus, int32 UserIndex) const
{
    const bool bAcceptsInput = Window->IsVisible() && (Window->AcceptsInput() || IsWindowHousingInteractiveTooltip(Window));
    if (bAcceptsInput && Window->IsScreenspaceMouseWithin(ScreenspaceMouseCoordinate))
    {
        TArray<FWidgetAndPointer> WidgetsAndCursors = Window->GetHittestGrid().GetBubblePath(ScreenspaceMouseCoordinate, GetCursorRadius(), bIgnoreEnabledStatus, UserIndex);
        return FWidgetPath(MoveTemp(WidgetsAndCursors));
    }
    else
    {
        return FWidgetPath();
    }
}



TArray<FWidgetAndPointer> FHittestGrid::GetBubblePath(FVector2D DesktopSpaceCoordinate, float CursorRadius, bool bIgnoreEnabledStatus, int32 UserIndex)
{
    checkSlow(IsInGameThread());

    const FVector2D CursorPositionInGrid = DesktopSpaceCoordinate - GridOrigin;

    if (WidgetArray.Num() > 0 && Cells.Num() > 0)
    {
        FGridTestingParams TestingParams;
        TestingParams.CursorPositionInGrid = CursorPositionInGrid;
        TestingParams.CellCoord = GetCellCoordinate(CursorPositionInGrid);
        TestingParams.Radius = 0.0f;
        TestingParams.bTestWidgetIsInteractive = false;

        // First add the exact point test results
        const FIndexAndDistance BestHit = GetHitIndexFromCellIndex(TestingParams);
        if (BestHit.IsValid())
        {
            const FWidgetData& BestHitWidgetData = BestHit.GetWidgetData();
            const TSharedPtr<SWidget> FirstHitWidget = BestHitWidgetData.GetWidget();
            // Make Sure we landed on a valid widget
            if (FirstHitWidget.IsValid() && IsCompatibleUserIndex(UserIndex, BestHitWidgetData.UserIndex))
            {
                TArray<FWidgetAndPointer> Path;

                TSharedPtr<SWidget> CurWidget = FirstHitWidget;
                while (CurWidget.IsValid())
                {
                    FGeometry DesktopSpaceGeometry = CurWidget->GetPaintSpaceGeometry();
                    DesktopSpaceGeometry.AppendTransform(FSlateLayoutTransform(GridOrigin - GridWindowOrigin));

                    Path.Emplace(FArrangedWidget(CurWidget.ToSharedRef(), DesktopSpaceGeometry), TSharedPtr<FVirtualPointerPosition>());
                    CurWidget = CurWidget->Advanced_GetPaintParentWidget();
                }

                if (!Path.Last().Widget->Advanced_IsWindow())
                {
                    return TArray<FWidgetAndPointer>();
                }

                Algo::Reverse(Path);

                bool bRemovedDisabledWidgets = false;
                if (!bIgnoreEnabledStatus)
                {
                    // @todo It might be more correct to remove all disabled widgets and non-hit testable widgets.  It doesn't make sense to have a hit test invisible widget as a leaf in the path
                    // and that can happen if we remove a disabled widget. Furthermore if we did this we could then append custom paths in all cases since the leaf most widget would be hit testable
                    // For backwards compatibility changing this could be risky
                    const int32 DisabledWidgetIndex = Path.IndexOfByPredicate([](const FArrangedWidget& SomeWidget) { return !SomeWidget.Widget->IsEnabled(); });
                    if (DisabledWidgetIndex != INDEX_NONE)
                    {
                        bRemovedDisabledWidgets = true;
                        Path.RemoveAt(DisabledWidgetIndex, Path.Num() - DisabledWidgetIndex);
                    }
                }

                if (!bRemovedDisabledWidgets && Path.Num() > 0)
                {
                    if (BestHitWidgetData.CustomPath.IsValid())
                    {
                        const TArray<FWidgetAndPointer> BubblePathExtension = BestHitWidgetData.CustomPath.Pin()->GetBubblePathAndVirtualCursors(FirstHitWidget->GetTickSpaceGeometry(), DesktopSpaceCoordinate, bIgnoreEnabledStatus);
                        Path.Append(BubblePathExtension);
                    }
                }
    
                return Path;
            }
        }
    }

    return TArray<FWidgetAndPointer>();
}



FHittestGrid::FIndexAndDistance FHittestGrid::GetHitIndexFromCellIndex(const FGridTestingParams& Params) const
{
    //check if the cell coord 
    if (IsValidCellCoord(Params.CellCoord))
    {
        // Get the cell and sort it 
        FCollapsedWidgetsArray WidgetIndexes;
        GetCollapsedWidgets(WidgetIndexes, Params.CellCoord.X, Params.CellCoord.Y);

        // Consider front-most widgets first for hittesting.
        for (int32 i = WidgetIndexes.Num() - 1; i >= 0; --i)
        {
            check(WidgetIndexes[i].IsValid());
            const FWidgetData& TestCandidate = WidgetIndexes[i].GetWidgetData();
            const TSharedPtr<SWidget> TestWidget = TestCandidate.GetWidget();

            // When performing a point hittest, accept all hittestable widgets.
            // When performing a hittest with a radius, only grab interactive widgets.
            const bool bIsValidWidget = TestWidget.IsValid() && (!Params.bTestWidgetIsInteractive || TestWidget->IsInteractable());
            if (bIsValidWidget)
            {
                const FVector2D WindowSpaceCoordinate = Params.CursorPositionInGrid + GridWindowOrigin;

                const FGeometry& TestGeometry = TestWidget->GetPaintSpaceGeometry();

                bool bPointInsideClipMasks = true;

                if (WidgetIndexes[i].GetCullingRect().IsValid())
                {
                    bPointInsideClipMasks = WidgetIndexes[i].GetCullingRect().ContainsPoint(WindowSpaceCoordinate);
                }

                if (bPointInsideClipMasks)
                {
                    const TOptional<FSlateClippingState>& WidgetClippingState = TestWidget->GetCurrentClippingState();
                    if (WidgetClippingState.IsSet())
                    {
                        // TODO: Solve non-zero radius cursors?
                        bPointInsideClipMasks = WidgetClippingState->IsPointInside(WindowSpaceCoordinate);
                    }
                }

                if (bPointInsideClipMasks)
                {
                    // Compute the render space clipping rect (FGeometry exposes a layout space clipping rect).
                    const FSlateRotatedRect WindowOrientedClipRect = TransformRect(
                        Concatenate(
                            Inverse(TestGeometry.GetAccumulatedLayoutTransform()),
                            TestGeometry.GetAccumulatedRenderTransform()),
                        FSlateRotatedRect(TestGeometry.GetLayoutBoundingRect())
                    );

                    if (IsOverlappingSlateRotatedRect(WindowSpaceCoordinate, Params.Radius, WindowOrientedClipRect))
                    {
                        // For non-0 radii also record the distance to cursor's center so that we can pick the closest hit from the results.
                        const bool bNeedsDistanceSearch = Params.Radius > 0.0f;
                        const float DistSq = (bNeedsDistanceSearch) ? DistanceSqToSlateRotatedRect(WindowSpaceCoordinate, WindowOrientedClipRect) : 0.0f;
                        return FIndexAndDistance(WidgetIndexes[i], DistSq);
                    }
                }
            }
        }
    }

    return FIndexAndDistance();
}

响应事件