tableview动态修改和删除_实现蓝图节点的动态添加/删除Pin

v2-cf299ec9880169bc6dd5d8f2d7747619_1440w.jpg?source=172ae18b

通过本系列文章上篇的介绍,我们已经可以创建一个“没什么用”的蓝图节点了。要想让它有用,关键还是上篇中说的典型应用场景:动态添加Pin,这篇博客就来解决这个问题。

目标

和上篇一样,我还将通过一个尽量简单的节点,来说明"可动态添加/删除Pin的蓝图节点"的实现过程,让大家尽量聚焦在“蓝图自定义节点”这个主题上。

设想这样一个节点:Say Something,把输入的N个字符串连接起来,然后打印输出。也就是说,这个节点的输入Pin是可以动态添加的。我们将在上篇的那个工程基础上实现这个自定义节点。最终实现的效果如下图所示:

v2-244d9e227d360a1b58665da16dad07b8_b.jpg

下面我们还是来仔细的过一遍实现步骤吧!

创建Blueprint Graph节点类型

首先,我们还是需要创建一个class UK2Node的派生类,这个过程在上一篇中已经详细说过了,照单炒菜,很容易就创建了下图这样一个空的自定义节点,这里就不赘述了。不清楚的话,可以返回去在照着上一篇做就好了。

v2-7872109e8d5c2fd874d94610fab445cd_b.jpg

创建自定义的节点Widget

我们要动态增加Pin的话,需要在节点上显示一个"加号按钮",点击之后增加一个“input pin”。这就不能使用默认的Blueprint Graph Node Widget了,需要对其进行扩展。这个扩展的思路和前面一样,也是找到特定的基类,重载其虚函数即可,这个基类就是class SGraphNodeK2Base。我们要重载的两个核心的函数是:

  1. CreateInputSideAddButton(),创建我们需要的添加输入Pin的按钮;
  2. OnAddPin(),响应这个按钮的操作;

来看一下最简化的代码吧: SGraphNodeSaySomething.h

class SGraphNodeSaySomething : public SGraphNodeK2Base
{
public:
    SLATE_BEGIN_ARGS(SGraphNodeSaySomething){}
    SLATE_END_ARGS()

    void Construct(const FArguments& InArgs, UBPNode_SaySomething* InNode);
protected:
    virtual void CreateInputSideAddButton(TSharedPtr<SVerticalBox> InputBox) override;
    virtual FReply OnAddPin() override;
};

SGraphNodeSaySomething.cpp

void SGraphNodeSaySomething::Construct(const FArguments& InArgs, UBPNode_SaySomething* InNode)
{
    this->GraphNode = InNode;
    this->SetCursor( EMouseCursor::CardinalCross );
    this->UpdateGraphNode();
}

void SGraphNodeSaySomething::CreateInputSideAddButton(TSharedPtr<SVerticalBox> InputBox)
{
    FText Tmp = FText::FromString(TEXT("Add word"));
    TSharedRef<SWidget> AddPinButton = AddPinButtonContent(Tmp, Tmp);

    FMargin AddPinPadding = Settings->GetInputPinPadding();
    AddPinPadding.Top += 6.0f;

    InputBox->AddSlot()
    .AutoHeight()
    .VAlign(VAlign_Center)
    .Padding(AddPinPadding)
    [
        AddPinButton
    ];
}

FReply SGraphNodeSaySomething::OnAddPin()
{ }

如果你接触过Unreal Slate的话,上面这个Slate Widget的代码很容易看懂啦,如果你没有玩过Slate。。。。Slate是虚幻自己的一套 Immediate Mode UI framework,建议先过一下官方文档。

最后,因为这个基类:SGraphNodeK2Base,属于GraphEditor模块,所以要修改MyBlueprintNodeEditor.Build.cs,把它添加到PrivateDependencyModuleNames:

PrivateDependencyModuleNames.AddRange(new string[] {
            "UnrealEd",
            "GraphEditor",
            "BlueprintGraph",
            "KismetCompiler",
            "MyBlueprintNode"
        });

扩展蓝图编辑器的节点Widget

OK,上面我们已经创建了两个类,分别是:

  1. class UBPNode_SaySomething : public UK2Node
  2. class SGraphNodeSaySomething : public SGraphNodeK2Base

下面我们就需要让蓝图编辑器知道:创建UBPNode_SaySomething对象的时候,需要使用SGraphNodeSaySomething这个Widget。

添加自定义Node Widget的两种方式(参见引擎源码class FNodeFactory):

  • 重载UEdGraphNode::CreateVisualWidget()函数,例如:
TSharedPtr<SGraphNode> UNiagaraNode::CreateVisualWidget() 
{
    return SNew(SNiagaraGraphNode, this);
}
  • 使用 class FEdGraphUtilities 注册 class FGraphPanelNodeFactory对象,例如:
void FBehaviorTreeEditorModule::StartupModule()
{
    GraphPanelNodeFactory_BehaviorTree = MakeShareable( new FGraphPanelNodeFactory_BehaviorTree() );
    FEdGraphUtilities::RegisterVisualNodeFactory(GraphPanelNodeFactory_BehaviorTree);
}

在这里,我们使用第一种方式,也就是在class UBPNode_SaySomething中重载父类的虚函数CreateVisualWidget()。

TSharedPtr<SGraphNode> UBPNode_SaySomething::CreateVisualWidget() {
    return SNew(SGraphNodeSaySomething, this);
}

完成上述代码之后,运行蓝图编辑器,添加Say Something节点,就可以看到这个Widget了:

v2-4123071c51fdedb3c051f55dcfb46c7b_b.jpg

动态增加Pin

当用户点击“Add Word +”按钮时,SGraphNodeSaySomething::OnAddPin()会被调用,下面是它的实现代码:

FReply SGraphNodeSaySomething::OnAddPin()
{
    UBPNode_SaySomething* BPNode = CastChecked<UBPNode_SaySomething>(GraphNode);

    const FScopedTransaction Transaction(NSLOCTEXT("Kismet", "AddArgumentPin", "Add Argument Pin"));
    BPNode->Modify();

    BPNode->AddPinToNode();
    FBlueprintEditorUtils::MarkBlueprintAsModified(BPNode->GetBlueprint());

    UpdateGraphNode();
    GraphNode->GetGraph()->NotifyGraphChanged();

    return FReply::Handled();
}

上面这段代码主要是响应用户的UI操作,添加Pin的核心操作,还是放在UBPNode_SaySomething::AddPinToNode()这个函数里面去实现的:

void UBPNode_SaySomething::AddPinToNode()
{
    TMap<FString, FStringFormatArg> FormatArgs= {
            {TEXT("Count"), ArgPinNames.Num()}
    };
    FName NewPinName(*FString::Format(TEXT("Word {Count}"), FormatArgs));
    ArgPinNames.Add(NewPinName);

    CreatePin(EGPD_Input, UEdGraphSchema_K2::PC_String, NewPinName);
}

现在我们就可以在蓝图编辑器里面操作添加输入Pin了 :

v2-1924eeb505c3caa64f12e44ac4f0daf2_b.jpg

动态删除Pin

如果用户想要删除某个输入变量Pin,他需要在那个Pin上点击鼠标右键,呼出Context Menu,选择“删除”菜单项将其移除。下面我们就看看这个操作是如何实现的。

v2-c6cf2d263e8e3fd8b743cf4406ed5135_b.gif

我们可以通过重载void UEdGraphNode::GetContextMenuActions(const FGraphNodeContextMenuBuilder& Context) const来定制Context Menu。

void UBPNode_SaySomething::GetContextMenuActions(const FGraphNodeContextMenuBuilder & Context) const
{
    Super::GetContextMenuActions(Context);

    if (Context.bIsDebugging)
        return;

    Context.MenuBuilder->BeginSection("UBPNode_SaySomething", FText::FromString(TEXT("Say Something")));

    if (Context.Pin != nullptr)
    {
        if (Context.Pin->Direction == EGPD_Input && Context.Pin->ParentPin == nullptr)
        {
            Context.MenuBuilder->AddMenuEntry(
                FText::FromString(TEXT("Remove Word")),
                FText::FromString(TEXT("Remove Word from input")),
                FSlateIcon(),
                FUIAction(
                    FExecuteAction::CreateUObject(this, &UBPNode_SaySomething::RemoveInputPin, const_cast<UEdGraphPin*>(Context.Pin))
                )
            );
        }
    }// end of if

    Context.MenuBuilder->EndSection();
}

这个函数的实现很直白啦,就是操作MenuBuilder,添加菜单项,并绑定UIAction到成员函数UBPNode_SaySomething::RemoveInputPin,接下来就是实现这个函数了。

void UBPNode_SaySomething::RemoveInputPin(UEdGraphPin * Pin)
{
    FScopedTransaction Transaction(FText::FromString("SaySomething_RemoveInputPin"));
    Modify();

    ArgPinNames.Remove(Pin->GetFName());

    RemovePin(Pin);
    FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(GetBlueprint());
}

也很简单,就是直接调用父类的RemovePin(),并同步处理一下自己内部的状态变量就好了。

实现这个蓝图节点的编译

通过前面的步骤,蓝图编辑器的扩展就全部完成了,接下来就是最后一步了,通过扩展蓝图编译过程来实现这个节点的实际功能。

我们延续上篇的思路来实现这个节点的功能,也就是重载UK2Node::ExpandNode()函数。

核心的问题是如何把当前的所有的输入的Pin组合起来? 答案很简单,把所有输入的Pin做成一个TArray<FString>,这样就可以传入到一个UFunction来调用。

首先我们在 class UMyBlueprintFunctionLibrary 中添加一个函数:

UCLASS()
class MYBLUEPRINTNODE_API UMyBlueprintFunctionLibrary : public UBlueprintFunctionLibrary
{
    GENERATED_BODY()

public:
    UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true"))
        static void SaySomething_Internal(const TArray<FString>& InWords);
};

然后,仍然与上篇相同,使用一个 class UK2Node_CallFunction 节点实例对象来调用这个UFunction,不同的是,我们需要使用一个 class UK2Node_MakeArray 节点的实例来把收集所有的动态生成的输入Pin。下面是实现的代码:

void UBPNode_SaySomething::ExpandNode(FKismetCompilerContext & CompilerContext, UEdGraph * SourceGraph) {
    Super::ExpandNode(CompilerContext, SourceGraph);

    UEdGraphPin* ExecPin = GetExecPin();
    UEdGraphPin* ThenPin = GetThenPin();
    if (ExecPin && ThenPin) {

        // create a CallFunction node
        FName MyFunctionName = GET_FUNCTION_NAME_CHECKED(UMyBlueprintFunctionLibrary, SaySomething_Internal);

        UK2Node_CallFunction* CallFuncNode = CompilerContext.SpawnIntermediateNode<UK2Node_CallFunction>(this, SourceGraph);
        CallFuncNode->FunctionReference.SetExternalMember(MyFunctionName, UBPNode_SaySomething::StaticClass());
        CallFuncNode->AllocateDefaultPins();

        // move exec pins
        CompilerContext.MovePinLinksToIntermediate(*ExecPin, *(CallFuncNode->GetExecPin()));
        CompilerContext.MovePinLinksToIntermediate(*ThenPin, *(CallFuncNode->GetThenPin()));

        // create a "Make Array" node to compile all args
        UK2Node_MakeArray* MakeArrayNode = CompilerContext.SpawnIntermediateNode<UK2Node_MakeArray>(this, SourceGraph);
        MakeArrayNode->AllocateDefaultPins();

        // Connect Make Array output to function arg
        UEdGraphPin* ArrayOut = MakeArrayNode->GetOutputPin();
        UEdGraphPin* FuncArgPin = CallFuncNode->FindPinChecked(TEXT("InWords"));
        ArrayOut->MakeLinkTo(FuncArgPin);

        // This will set the "Make Array" node's type, only works if one pin is connected.
        MakeArrayNode->PinConnectionListChanged(ArrayOut);

        // connect all arg pin to Make Array input
        for (int32 i = 0; i < ArgPinNames.Num(); i++) {

            // Make Array node has one input by default
            if (i > 0)
                MakeArrayNode->AddInputPin();

            // find the input pin on the "Make Array" node by index.
            const FString PinName = FString::Printf(TEXT("[%d]"), i);
            UEdGraphPin* ArrayInputPin = MakeArrayNode->FindPinChecked(PinName);

            // move input word to array 
            UEdGraphPin* MyInputPin = FindPinChecked(ArgPinNames[i], EGPD_Input);
            CompilerContext.MovePinLinksToIntermediate(*MyInputPin, *ArrayInputPin);
        }// end of for
    }

    // break any links to the expanded node
    BreakAllNodeLinks();
}

核心步骤来讲解一下:

  1. 创建了一个class UK2Node_CallFunction的实例,然后把自身节点的两端的Exec Pin重定向到这个Node的两端;
  2. 使用“函数参数名称”找到UK2Node_CallFunction节点的输入Pin,把它连接到一个新建的UK2Node_MakeArray的节点实例上;
  3. 把自己所有的输入变量Pin重定向到UK2Node_MakeArray的输入上(需要为它动态添加新的Pin);

结束语

今天涉及到的class稍微有点多,我整理了一个UML静态结构图,看看这几个classes直接的关系以及它们所在的模块。完整源代码仍然是在我的GitHub:

neil3d/UnrealCookBook​github.com
v2-cef39435c6108e198781ab301a57e9dd_ipico.jpg

v2-def64ca006584bdde49cc4e40a6086d6_b.jpg

至此,通过派生class UK2Node和class SGraphNodeK2Base来扩展Blueprint Graph Editor,我们可以自己定义蓝图节点,以及编辑器中的Node Widget,可以添加按钮,以及其他任何你想要做的东西。通过这个定制化的Node Widget,可以实现编辑时对Blueprint Graph Node的交互控制啦~。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 可以使用Qt的Model/View框架,在TableModel中添加数据,并在TableView中显示出来。以下是一个简单的示例代码: ``` // 创建一个TableModel QStandardItemModel *model = new QStandardItemModel(); // 添加表头 model->setHorizontalHeaderLabels(QStringList() << "列1" << "列2" << "列3"); // 添加数据 QList<QStandardItem *> rowItems; rowItems.append(new QStandardItem("行1列1")); rowItems.append(new QStandardItem("行1列2")); rowItems.append(new QStandardItem("行1列3")); model->appendRow(rowItems); rowItems.clear(); rowItems.append(new QStandardItem("行2列1")); rowItems.append(new QStandardItem("行2列2")); rowItems.append(new QStandardItem("行2列3")); model->appendRow(rowItems); // 在TableView中显示TableModel QTableView *tableView = new QTableView(); tableView->setModel(model); tableView->show(); ``` 在TableModel中使用appendRow()方法可以添加一行数据。在上面的示例代码中,我们添加了两行数据,每行数据有三列。最后,将TableView设置为该TableModel,并调用show()方法显示出来即可。 ### 回答2: 在Qt中,可以通过使用QTableView实现TableView添加数据。首先,我们需要创建一个QStandardItemModel模型,并将其设置为QTableView的模型。然后,可以使用QStandardItem来创建和设置要显示的数据项。最后,将数据项添加到模型中。 下面是一个示例代码: ```python # 导入需要的模块 from PyQt5.QtWidgets import QApplication, QTableView from PyQt5.QtGui import QStandardItemModel, QStandardItem import sys # 创建应用程序对象 app = QApplication(sys.argv) # 创建一个QTableView和一个QStandardItemModel模型 table_view = QTableView() model = QStandardItemModel() # 设置QTableView的模型为QStandardItemModel table_view.setModel(model) # 创建和设置要显示的数据项 item1 = QStandardItem("数据1") item2 = QStandardItem("数据2") item3 = QStandardItem("数据3") # 将数据项添加到模型中 model.appendRow([item1, item2, item3]) # 显示QTableView table_view.show() # 运行应用程序 sys.exit(app.exec_()) ``` 在上面的示例中,我们创建了一个QTableView和一个QStandardItemModel模型。然后,使用QStandardItem类创建了三个数据项,并将它们添加到模型中。最后,将模型设置为QTableView的模型,并显示QTableView。当运行这段代码时,将在QTableView中显示包含三个数据项的一行数据。 ### 回答3: 在Qt中实现TableView添加数据可以按照以下步骤进行: 1. 创建TableView和Model对象:首先,我们需要创建一个TableView和一个Model对象,并将Model对象与TableView绑定。可以使用QTableView类和QStandardItemModel类来实现。 2. 设置Model的表头:使用setHorizontalHeaderLabels()方法来设置Model的表头。 3. 添加数据到Model中:使用Model的insertRow()方法来插入一行数据,然后使用Model的setData()方法来设置每个单元格的数据。 4. 刷新TableView:使用TableView的reset()方法来刷新TableView,使其显示最新的数据。 下面是一个示例代码,演示如何在TableView添加数据: ```cpp #include <QApplication> #include <QTableView> #include <QStandardItemModel> int main(int argc, char *argv[]) { QApplication app(argc, argv); // 创建TableView和Model对象 QTableView tableView; QStandardItemModel model; // 将Model对象与TableView绑定 tableView.setModel(&model); // 设置Model的表头 model.setHorizontalHeaderLabels({"姓名", "年龄"}); // 添加数据到Model中 model.insertRow(0); model.setData(model.index(0, 0), "张三"); model.setData(model.index(0, 1), 25); model.insertRow(1); model.setData(model.index(1, 0), "李四"); model.setData(model.index(1, 1), 30); // 刷新TableView tableView.reset(); // 显示TableView tableView.show(); return app.exec(); } ``` 在上述代码中,我们首先创建了一个TableView和一个Model对象,并将Model对象与TableView绑定。然后,我们使用setHorizontalHeaderLabels()方法设置Model的表头,使用insertRow()方法插入行数据,使用setData()方法设置每个单元格的数据。最后,我们使用reset()方法刷新TableView,使其显示刚刚添加的数据。执行代码后,会弹出一个包含添加数据的TableView窗口。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值