• ue4 SNew背后的逻辑


    ue4的ui库Slate体系非常庞大,即使是在创建对象这一小事上,也是相当复杂:

    SLATECORE_API TSharedRef<SWidget> SNullWidget::NullWidget = SNew(SNullWidgetContent).Visibility(EVisibility::Hidden);

    所有SWidget体系内的对象,都要用SNew这个宏来创建,它的内容是:

    #define SNew( WidgetType, ... ) 
        MakeTDecl<WidgetType>( #WidgetType, __FILE__, __LINE__, RequiredArgs::MakeRequiredArgs(__VA_ARGS__) ) <<= TYPENAME_OUTSIDE_TEMPLATE WidgetType::FArguments()

    这里做了两件事:

    1、先是调用MakeTDecl创建了一个【widget wrapper】

    2、再调用该wrapper重载的<<=操作符,实并将一个FArguments类型的默认对象做为参数传进去。注意这里这个FArguments类型不是全局类,而是内嵌于WidgetType里的,也就是说每个widget子类都可以定义自己的初始化参数类。

    那么继续跟踪1,看看MakeTDecl在干啥:

    template<typename WidgetType, typename RequiredArgsPayloadType>
    TDecl<WidgetType, RequiredArgsPayloadType> MakeTDecl( const ANSICHAR* InType, const ANSICHAR* InFile, int32 OnLine, RequiredArgsPayloadType&& InRequiredArgs )
    {
        return TDecl<WidgetType, RequiredArgsPayloadType>(InType, InFile, OnLine, Forward<RequiredArgsPayloadType>(InRequiredArgs));
    }

    它实际就是一层模块包装,生成了一个TDecl类型对象,也就是上面说的wrapper,<<=重载也就是在它身上调的。

    这里值得注意的是第4个参数,实参是:RequiredArgs::MakeRequiredArgs(__VA_ARGS__),由于__VA_ARGS__是个变参宏,就意味着MakeRequiredArgs必须要有支持多个参数的版本

    事实上确实如此,UE4已经写了从无参到5个参数的6种变体,如果今后有更多参数的调用出现了,自然还要再加,下面贴出前几个看看:

    FORCEINLINE T0RequiredArgs MakeRequiredArgs()
        {
            return T0RequiredArgs();
        }
    
        template<typename Arg0Type>
        T1RequiredArgs<Arg0Type&&> MakeRequiredArgs(Arg0Type&& InArg0)
        {
            return T1RequiredArgs<Arg0Type&&>(Forward<Arg0Type>(InArg0));
        }
    
        template<typename Arg0Type, typename Arg1Type>
        T2RequiredArgs<Arg0Type&&, Arg1Type&&> MakeRequiredArgs(Arg0Type&& InArg0, Arg1Type&& InArg1)
        {
            return T2RequiredArgs<Arg0Type&&, Arg1Type&&>(Forward<Arg0Type>(InArg0), Forward<Arg1Type>(InArg1));
        }

    不同版本的返回值类型也是不一样的,也分别定义了从T0RequiredArgs 到 T5RequiredArgs 共6个类型,也都是简单的参数打包结构体,只看一个T2吧:

    template<typename Arg0Type, typename Arg1Type>
        struct T2RequiredArgs
        {
            T2RequiredArgs(Arg0Type&& InArg0, Arg1Type&& InArg1)
                : Arg0(InArg0)
                , Arg1(InArg1)
            {
            }
    
            template<class WidgetType>
            void CallConstruct(const TSharedRef<WidgetType>& OnWidget, const typename WidgetType::FArguments& WithNamedArgs) const
            {
                // YOUR WIDGET MUST IMPLEMENT Construct(const FArguments& InArgs)
                OnWidget->Construct(WithNamedArgs, Forward<Arg0Type>(Arg0), Forward<Arg1Type>(Arg1));
                OnWidget->CacheVolatility();
            }
    
            Arg0Type& Arg0;
            Arg1Type& Arg1;
        };

    回到前面第1点,MakeTDecl返回了一个TDecl对象,而这个对象的类型也是模板化的,除了要SNew的Widget类型本身,还有一个打包着变参参数的RequiredArgsPayloadType,而该类型是T0RequiredArgs 到 T5RequiredArgs其中之一。

    那么TDecl是什么,又如何构造的呢:

    template<class WidgetType, typename RequiredArgsPayloadType>
    struct TDecl
    {
        TDecl( const ANSICHAR* InType, const ANSICHAR* InFile, int32 OnLine, RequiredArgsPayloadType&& InRequiredArgs )
            : _Widget( TWidgetAllocator<WidgetType, TIsDerivedFrom<WidgetType, SUserWidget>::IsDerived >::PrivateAllocateWidget() )
            , _RequiredArgs(InRequiredArgs)
        {
            _Widget->SetDebugInfo( InType, InFile, OnLine );
        }
    
        const TSharedRef<WidgetType> _Widget;
        RequiredArgsPayloadType& _RequiredArgs;
    };

    在这里也是做了两件事,一是把SNew传入的参数存起来放在_RequiredArgs属性上以备后用,二是通过PrivateAllocateWidget创建了widget实例

    而这个PrivateAllocateWidget的前缀也是非常拗口:

    TWidgetAllocator<WidgetType, TIsDerivedFrom<WidgetType, SUserWidget>::IsDerived >

    这个TWidgetAllocator就是起到一个【萃取器】的作用,给各个Widget子类提供了一个模板重载的机会,让它们可以定义自身实例的特殊创建方式

    下面是该萃取器的默认实现:

    template<typename WidgetType, bool IsDerived>
    struct TWidgetAllocator
    {
        static TSharedRef<WidgetType> PrivateAllocateWidget()
        {
            return MakeShareable( new WidgetType() );
        }
    };

    也就是没做什么特殊处理,直接new出来了。然而全局搜索,并未发现有任何子类重载过以给予特殊逻辑。当然这里留下了接口,方便以后扩展。

    再看第2点,也就是对 TDecl.operator<<=(FArguments& InArgs) 的调用:

    TSharedRef<WidgetType> operator<<=( const typename WidgetType::FArguments& InArgs ) const
        {
            //@todo UMG: This should be removed in favor of all widgets calling their superclass construct.
            _Widget->SWidgetConstruct(
                InArgs._ToolTipText,
                InArgs._ToolTip ,
                InArgs._Cursor ,
                InArgs._IsEnabled ,
                InArgs._Visibility,
                InArgs._RenderTransform,
                InArgs._RenderTransformPivot,
                InArgs._Tag,
                InArgs._ForceVolatile,
                InArgs.MetaData );
    
            _RequiredArgs.CallConstruct(_Widget, InArgs);
    
            return _Widget;
        }

    这里也有两个关注点:

    一是_RequiredArgs.CallConstruct调用,代码上上面已经贴过,再回顾一下:

     template<class WidgetType>
            void CallConstruct(const TSharedRef<WidgetType>& OnWidget, const typename WidgetType::FArguments& WithNamedArgs) const
            {
                // YOUR WIDGET MUST IMPLEMENT Construct(const FArguments& InArgs)
                OnWidget->Construct(WithNamedArgs, Forward<Arg0Type>(Arg0), Forward<Arg1Type>(Arg1));
                OnWidget->CacheVolatility();
            }

    内部就是将自己保存的参数再转调到Widget->Construct(...)

    然而_RequiredArgs是个模板类型,可能表示0~5个参数,那么每一个类型里要转调的Widget->Construct的参数个数(和类型)也是不一样的

    这就是说,当你用SNew(YourWidget,A,B,C)去创建一个YourWidget实例时,最终层层展开的模板代码,会要求YourWidget类上,必须有一个接受【FArguments,A,B,C】为参数的Construct版本。

    这种用法很有趣,其规则就是:谁调用谁实现,而中间层只管转发,如果匹配不上,那是使用方的责任。

    搜索代码可以找到其中的例子:

    class SPaperExtractSpritesViewport : public SPaperEditorViewport
    {
        void Construct(const FArguments& InArgs, UTexture2D* Texture, const TArray<FPaperExtractedSprite>& ExtractedSprites, const class UPaperExtractSpritesSettings* Settings, class SPaperExtractSpritesDialog* InDialog);
        ...
    }
    
    TSharedRef<SPaperExtractSpritesViewport> Viewport = SNew(SPaperExtractSpritesViewport, SourceTexture, ExtractedSprites, ExtractSpriteSettings, this);

    只有自己先实现了相应参数版本的Construct,才能在SNew中传递相应的实参来构造。

    二是关于SWidget上的SWidgetConstruct和Construct的关系。

    这个<<=重载里的代码正是先后调了俩:首先是->SWidgetConstruct,然后通过转发又调了->Construct

    SWidgetConstruct里面并没有什么东西,直接就调了Construct,然而此Construct并非上面说的与SNew参数匹配的那版,而仅仅是一个与SWidgetConstruct签名完全相同的壳,其中的内容也非常简单,只是把参数保存到相应成员变量里。

    而真正的SNew对应版Construct,才是各Widget子类真正做初始化的地方

    挖了这么多,就是一个小小SNew的实现。。还有更多的Slate Trick等待发掘。。

  • 相关阅读:
    xp 安装 win7 64
    局域网内传输文件速度慢
    安全和共享设置
    vs2005无法启动
    dos快速通道
    xp 共享 guest
    Java菜题
    Java序列化总结(最全)
    Bitset改进你的程序质量
    Java反射方法总结
  • 原文地址:https://www.cnblogs.com/wellbye/p/5785550.html
Copyright © 2020-2023  润新知