{"id":3082,"date":"2021-01-02T13:54:51","date_gmt":"2021-01-02T05:54:51","guid":{"rendered":"http:\/\/blog.coolcoding.cn\/?p=3082"},"modified":"2021-01-02T19:43:58","modified_gmt":"2021-01-02T11:43:58","slug":"creating-new-toolbar-buttons","status":"publish","type":"post","link":"https:\/\/blog.coolcoding.cn\/?p=3082","title":{"rendered":"Creating new toolbar buttons"},"content":{"rendered":"\n<p> If you have created a custom tool or window for display within the editor, you probably need some way to let the user make it appear. The easiest way to do this is to create a toolbar customization that adds a new toolbar button, and have it display your window when clicked. Create a new engine module by following the previous recipe, as we&#8217;ll need it to initialize our toolbar customization. <\/p>\n\n\n\n<h1 class=\"wp-block-heading\">How to do it&#8230;<\/h1>\n\n\n\n<ol><li>Inside of the&nbsp;Chapter_10Editor&nbsp;folder, create a new header file,&nbsp;CookbookCommands.h, and insert the following class declaration:<\/li><\/ol>\n\n\n\n<pre class=\"wp-block-preformatted\">#pragma once<br>#include \"Commands.h\"<br>#include \"EditorStyleSet.h\"<br><br><br>class FCookbookCommands : public TCommands&lt;FCookbookCommands&gt;<br>{<br>public:<br>  FCookbookCommands()<br>    : TCommands&lt;FCookbookCommands&gt;( <br>      FName(TEXT(\"UE4_Cookbook\")), <br>      FText::FromString(\"Cookbook Commands\"), <br>      NAME_None, <br>      FEditorStyle::GetStyleSetName()) <br>  {<br>  };<br><br>  virtual void RegisterCommands() override;<br><br>  TSharedPtr&lt;FUICommandInfo&gt; MyButton;<br>  <br>  TSharedPtr&lt;FUICommandInfo&gt; MyMenuButton;<br>};<\/pre>\n\n\n\n<ol><li>Implement the new class by placing the following in the&nbsp;.cpp&nbsp;file:<\/li><\/ol>\n\n\n\n<pre class=\"wp-block-preformatted\">#include \"CookbookCommands.h\"<br>#include \"Chapter_10Editor.h\"<br>#include \"Commands.h\"<br><br><br>void FCookbookCommands::RegisterCommands()<br>{<br>#define LOCTEXT_NAMESPACE \"\"<br>  UI_COMMAND(MyButton, \"Cookbook\", \"Demo Cookbook Toolbar Command\", EUserInterfaceActionType::Button, FInputGesture());<br>  UI_COMMAND(MyMenuButton, \"Cookbook\", \"Demo Cookbook Toolbar Command\", EUserInterfaceActionType::Button, FInputGesture());<br>#undef LOCTEXT_NAMESPACE<br>}<br><br><\/pre>\n\n\n\n<ol><li>Next, we will need to update our module class (Chapter_10Editor.h) to the following:<\/li><\/ol>\n\n\n\n<pre class=\"wp-block-preformatted\">#pragma once<br><br>#include \"Engine.h\"<br>#include \"Modules\/ModuleInterface.h\"<br>#include \"Modules\/ModuleManager.h\"<br>#include \"UnrealEd.h\"<br><strong>#include \"CookbookCommands.h\"<\/strong><br><strong>#include \"Editor\/MainFrame\/Public\/Interfaces\/IMainFrameModule.h\"<\/strong><br><br> <br>class FChapter_10EditorModule: public IModuleInterface <br>{ <br><strong>    virtual void StartupModule() override;<\/strong><br><strong>    virtual void ShutdownModule() override;<\/strong><br><br><strong>    TSharedPtr&lt;FExtender&gt; ToolbarExtender;<\/strong><br><strong>    TSharedPtr&lt;const FExtensionBase&gt; Extension;<\/strong><br><br><strong>    void MyButton_Clicked()<\/strong><br><strong>    {<\/strong><br>        <br><strong>        TSharedRef&lt;SWindow&gt; CookbookWindow = SNew(SWindow)<\/strong><br><strong>            .Title(FText::FromString(TEXT(\"Cookbook Window\")))<\/strong><br><strong>            .ClientSize(FVector2D(800, 400))<\/strong><br><strong>            .SupportsMaximize(false)<\/strong><br><strong>            .SupportsMinimize(false);<\/strong><br><br><strong>        IMainFrameModule&amp; MainFrameModule =<\/strong><br><strong>            FModuleManager::LoadModuleChecked&lt;IMainFrameModule&gt;<\/strong><br><strong>            (TEXT(\"MainFrame\"));<\/strong><br><br><strong>        if (MainFrameModule.GetParentWindow().IsValid())<\/strong><br><strong>        {<\/strong><br><strong>            FSlateApplication::Get().AddWindowAsNativeChild<\/strong><br><strong>            (CookbookWindow, MainFrameModule.GetParentWindow()<\/strong><br><strong>                .ToSharedRef());<\/strong><br><strong>        }<\/strong><br><strong>        else<\/strong><br><strong>        {<\/strong><br><strong>            FSlateApplication::Get().AddWindow(CookbookWindow);<\/strong><br><strong>        }<\/strong><br>        <br><strong>    };<\/strong><br><br><strong>    void AddToolbarExtension(FToolBarBuilder &amp;builder)<\/strong><br><strong>    {<\/strong><br>        <br><strong>        FSlateIcon IconBrush =<\/strong><br><strong>            FSlateIcon(FEditorStyle::GetStyleSetName(),<\/strong><br><strong>                \"LevelEditor.ViewOptions\",<\/strong><br><strong>                \"LevelEditor.ViewOptions.Small\"); builder.AddToolBarButton(FCookbookCommands::Get()<\/strong><br><strong>                    .MyButton, NAME_None, FText::FromString(\"My Button\"),<\/strong><br><strong>                    FText::FromString(\"Click me to display a message\"),<\/strong><br><strong>                    IconBrush, NAME_None);<\/strong><br>        <br><strong>    };<\/strong><br>}; <\/pre>\n\n\n\n<p>Be sure to&nbsp;#include&nbsp;the header file for your command class as well.<\/p>\n\n\n\n<ol><li>We now need to implement&nbsp;StartupModule&nbsp;and&nbsp;ShutdownModule:<\/li><\/ol>\n\n\n\n<pre class=\"wp-block-preformatted\">#include \"Chapter_10Editor.h\" <br>#include \"Modules\/ModuleManager.h\"<br>#include \"Modules\/ModuleInterface.h\"<br><strong>#include \"LevelEditor.h\" <\/strong><br><strong>#include \"SlateBasics.h\" <\/strong><br><strong>#include \"MultiBoxExtender.h\" <\/strong><br><strong>#include \"CookbookCommands.h\" <\/strong><br><br>IMPLEMENT_GAME_MODULE(FChapter_10EditorModule, Chapter_10Editor)<br><br><strong>void FChapter_10EditorModule::StartupModule()<\/strong><br><strong>{<\/strong><br>    <br><strong>    FCookbookCommands::Register();<\/strong><br>   <br><strong>    TSharedPtr&lt;FUICommandList&gt; CommandList = MakeShareable(new FUICommandList());<\/strong><br>    <br><strong>    CommandList-&gt;MapAction(FCookbookCommands::Get().MyButton, FExecuteAction::CreateRaw(this, &amp;FChapter_10EditorModule::MyButton_Clicked), FCanExecuteAction());<\/strong><br>    <br>    <br><strong>    ToolbarExtender = MakeShareable(new FExtender());<\/strong><br><br><strong>    FLevelEditorModule&amp; LevelEditorModule = FModuleManager::LoadModuleChecked&lt;FLevelEditorModule&gt;( \"LevelEditor\" );<\/strong><br><br><strong>    Extension = ToolbarExtender-&gt;AddToolBarExtension(\"Compile\", EExtensionHook::Before, CommandList, FToolBarExtensionDelegate::CreateRaw(this, &amp;FChapter_10EditorModule::AddToolbarExtension)); <\/strong><br>    <br>    <br><strong>    LevelEditorModule.GetToolBarExtensibilityManager()-&gt;AddExtender(ToolbarExtender);<\/strong><br>    <br>        <br><strong>}<\/strong><br><br><strong>void FChapter_10EditorModule::ShutdownModule()<\/strong><br><strong>{<\/strong><br>    <br><strong>    ToolbarExtender-&gt;RemoveExtension(Extension.ToSharedRef());<\/strong><br>    <br><strong>    Extension.Reset();<\/strong><br><strong>    ToolbarExtender.Reset();<\/strong><br>    <br><strong>}<\/strong><\/pre>\n\n\n\n<ol><li>Regenerate your project piles if needed, compile your project from Visual Studio and start the editor.<\/li><li>Verify that there&#8217;s a new button on the toolbar in the main level editor, which can be clicked on to open a new window:<\/li><\/ol>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"http:\/\/blog.coolcoding.cn\/wp-content\/uploads\/2021\/01\/6f9e599c-aee8-4438-a084-5350a219f1ea.png\" alt=\"\"\/><\/figure>\n\n\n\n<h1 class=\"wp-block-heading\">How it works&#8230;<\/h1>\n\n\n\n<p>Unreal&#8217;s editor UI is based on the concept of commands. Commands are a design pattern that allows looser coupling between the UI and the actions that it needs to perform.<\/p>\n\n\n\n<p>To create a class that contains a set of commands, it is necessary to inherit from&nbsp;TCommands.<\/p>\n\n\n\n<p>TCommands&nbsp;is a template class that leverages the&nbsp;<strong>Curiously Recurring Template Pattern<\/strong>&nbsp;(<strong>CRTP<\/strong>). The CRTP is used commonly throughout&nbsp;Slate&nbsp;UI code as a means&nbsp;of creating compile-time polymorphism.<\/p>\n\n\n\n<p>In the initializer list for&nbsp;FCookbookCommands&nbsp;constructor, we invoke the parent class constructor, passing in a number of parameters:<\/p>\n\n\n\n<ul><li>The first parameter is the name of the command set, and is a simple&nbsp;FName.<\/li><li>The second parameter is a tooltip\/human readable string, and, as such, uses&nbsp;FText&nbsp;so that it can support localization if necessary.<\/li><li>If there&#8217;s a parent group of commands, the third parameter contains the name of the group. Otherwise, it contains&nbsp;NAME_None.<\/li><li>The final parameter for the constructor is the Slate Style set that contains any command icons that the command set will be using.<\/li><\/ul>\n\n\n\n<p>The&nbsp;RegisterCommands()&nbsp;function allows&nbsp;TCommands-derived classes to create any command objects that they require. The resulting&nbsp;FUICommandInfo&nbsp;instances that are returned from that function are stored inside the&nbsp;Commands&nbsp;class as members so that UI elements or functions can be bound to the commands.<\/p>\n\n\n\n<p>This is why we have the member variable&nbsp;TSharedPtr&lt;FUICommandInfo&gt; MyButton.<\/p>\n\n\n\n<p>In the implementation for the class, we simply need to create our commands in&nbsp;RegisterCommands.<\/p>\n\n\n\n<p>The&nbsp;UI_COMMAND&nbsp;macro that was used to create an instance of&nbsp;FUICommandInfo&nbsp;expects a localization namespace to be defined, even if it is just an empty default namespace. As a result, we need to enclose our&nbsp;UI_COMMAND&nbsp;calls with&nbsp;#defines&nbsp;to set a valid value for&nbsp;LOCTEXT_NAMESPACE,&nbsp;even if we don&#8217;t intend to use localization.<\/p>\n\n\n\n<p>The actual&nbsp;UI_COMMAND&nbsp;macro takes a number of parameters:<\/p>\n\n\n\n<ul><li>The first parameter is the variable to store the&nbsp;FUICommandInfo&nbsp;in<\/li><li>The second parameter is a human-readable name for the command<\/li><li>The third parameter is a description for the command<\/li><li>The fourth parameter is&nbsp;EUserInterfaceActionType<\/li><\/ul>\n\n\n\n<p>This enumeration essentially specifies what sort of button is being created. It supports&nbsp;Button,&nbsp;ToggleButton,&nbsp;RadioButton, and&nbsp;Check&nbsp;as valid types.<\/p>\n\n\n\n<p>Buttons are simple generic buttons. A toggle button stores on and off states. The radio button is similar to a toggle, but is grouped with other radio buttons, and only one can be enabled at a time. Lastly, the checkbox displays a read-only checkbox that&#8217;s adjacent to the button.<\/p>\n\n\n\n<p>The last parameter for&nbsp;UI_COMMAND&nbsp;is the input chord, or the combination of keys that are required to activate the command.<\/p>\n\n\n\n<p>This parameter is primarily useful for defining key combinations for hotkeys linked to the command in question, rather than buttons. As a result, we use an empty&nbsp;InputGesture.<\/p>\n\n\n\n<p>So, we now have a set of commands, but we haven&#8217;t told the engine we want to&nbsp;add the set to the commands that show on the toolbar. We also haven&#8217;t set up&nbsp;what actually happens when the button is clicked. To do this, we need to perform some initialization when our module begins, so we place some code into&nbsp;the&nbsp;StartupModule\/ShutdownModule&nbsp;functions.<\/p>\n\n\n\n<p>Inside&nbsp;StartupModule, we call the static&nbsp;Register&nbsp;function on the commands class that we defined earlier.<\/p>\n\n\n\n<p>We then create a shared pointer to a list of commands using the&nbsp;MakeShareable&nbsp;function.<\/p>\n\n\n\n<p>In the command list, we use&nbsp;MapAction&nbsp;to create a mapping, or association, between the&nbsp;UICommandInfo&nbsp;object, which we set as a member of the&nbsp;FCookbookCommands, and the actual function we want to execute when the command is invoked.<\/p>\n\n\n\n<p>You&#8217;ll note that we don&#8217;t explicitly set anything regarding what could be used to invoke the command here.<\/p>\n\n\n\n<p>To perform this mapping, we call the&nbsp;MapAction&nbsp;function. The first parameter to&nbsp;MapAction&nbsp;is a&nbsp;FUICommandInfo&nbsp;object, which we can retrieve from&nbsp;FCookbookCommands&nbsp;by using its static&nbsp;Get()&nbsp;method to retrieve the instance.<\/p>\n\n\n\n<p>FCookbookCommands&nbsp;is implemented as a singleton&nbsp;\u2013&nbsp;a class with a single instance that exists throughout the application. You&#8217;ll see the pattern in most places&nbsp;\u2013&nbsp;there&#8217;s a static&nbsp;Get()&nbsp;method available in the engine.<\/p>\n\n\n\n<p>The second parameter of the&nbsp;MapAction&nbsp;function is a delegate bound to the function to be invoked when the command is executed.<\/p>\n\n\n\n<p>Because&nbsp;Chapter_10EditorModule&nbsp;is a raw C++ class rather than a&nbsp;UObject, and we want to invoke a member function rather than a&nbsp;static&nbsp;function, we use&nbsp;CreateRaw&nbsp;to create a new delegate that&#8217;s bound to a raw C++ member function.<\/p>\n\n\n\n<p>CreateRaw&nbsp;expects a pointer to the object instance, and a function reference to the function to invoke on that pointer.<\/p>\n\n\n\n<p>The third parameter for&nbsp;MapAction&nbsp;is a delegate to call to test if the action can be executed. Because we want the command to be executable all the time, we can use&nbsp;a simple predefined delegate that always returns&nbsp;true.<\/p>\n\n\n\n<p>With an association created between our command and the action it should call, we now need to actually tell the extension system that we want to add new commands to the toolbar.<\/p>\n\n\n\n<p>We can do this via the&nbsp;FExtender&nbsp;class, which can be used to extend menus, context menus, or toolbars.<\/p>\n\n\n\n<p>We initially create an instance of&nbsp;FExtender&nbsp;as a shared pointer so that our extensions are uninitialized when the module is shut down.<\/p>\n\n\n\n<p>We then call&nbsp;AddToolBarExtension&nbsp;on our new extender, storing the results in a shared pointer so that we can remove it on module uninitialization.<\/p>\n\n\n\n<p>First argument of&nbsp;AddToolBarExtension&nbsp;is the name of the extension point where we want to add our extension.<\/p>\n\n\n\n<p>To find where we want to place our extension, we first need to turn on the display of extension points within the editor UI.<\/p>\n\n\n\n<p>To do so, open&nbsp;Editor Preferences&nbsp;in the&nbsp;Edit&nbsp;menu within the editor:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"http:\/\/blog.coolcoding.cn\/wp-content\/uploads\/2021\/01\/733efb6c-4df7-4663-b51c-2629dabb48c2.png\" alt=\"\"\/><\/figure>\n\n\n\n<p>Open&nbsp;General&nbsp;|&nbsp;Miscellaneous&nbsp;and select&nbsp;Display UIExtension Points:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"http:\/\/blog.coolcoding.cn\/wp-content\/uploads\/2021\/01\/a7875429-2beb-4350-8952-b13d6b5f419d.png\" alt=\"\"\/><\/figure>\n\n\n\n<p>Restart the editor, and you should see green text overlaid on the Editor UI, as shown in the following screenshot:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/learning.oreilly.com\/library\/view\/unreal-engine-4x\/9781789809503\/assets\/9cd351fc-a334-42c3-8155-24fa0772578c.png\" alt=\"\"\/><\/figure>\n\n\n\n<p>Green text overlaying the Editor UI<\/p>\n\n\n\n<p>The green text indicates&nbsp;UIExtensionPoint, and the text&#8217;s value is the string we should provide to the&nbsp;AddToolBarExtension&nbsp;function.<\/p>\n\n\n\n<p>We&#8217;re going to add our extension to the&nbsp;Compile&nbsp;extension point in this recipe, but of course, you could use any other extension point you wish.<\/p>\n\n\n\n<p>It&#8217;s important to note that adding a toolbar extension to a menu extension point will fail silently, and vice versa.<\/p>\n\n\n\n<p>The second parameter to&nbsp;AddToolBarExtension&nbsp;is a location anchor relative to the extension point that&#8217;s specified. We&#8217;ve selected&nbsp;FExtensionHook::Before, so our icon will be displayed before the compile point.<\/p>\n\n\n\n<p>The next parameter is our command list that contains mapped actions.<\/p>\n\n\n\n<p>Finally, the last parameter is a delegate that is responsible for actually adding UI controls to the toolbar at the extension point and the anchor that we specified earlier.<\/p>\n\n\n\n<p>The delegate is bound to a function that has the form void (*func) (FToolBarBuilder&nbsp;and&nbsp;builder). In this instance, it is a function called&nbsp;AddToolbarExtension, which is&nbsp;defined in our module class.<\/p>\n\n\n\n<p>When the function is invoked, calling commands on the&nbsp;builder&nbsp;that adds UI elements will apply those elements to the location in the UI we specified.<\/p>\n\n\n\n<p>Lastly, we need to load the level editor module within this function so that we can&nbsp;add our extender to the main toolbar within the level editor.<\/p>\n\n\n\n<p>As usual, we can use&nbsp;ModuleManager&nbsp;to load a module and return a reference to it.<\/p>\n\n\n\n<p>With that reference in hand, we can get the Toolbar Extensibility Manager for the module, and tell it to add our Extender.<\/p>\n\n\n\n<p>While this may seem cumbersome at first, the intention is to allow you to apply the same toolbar extension to multiple toolbars in different modules, if you would like to create a consistent UI layout between different editor windows.<\/p>\n\n\n\n<p>The counterpart to initializing our extension, of course, is removing it when our module is unloaded. To do that, we remove our extension from the extender, then null the shared pointers for both Extender and extension, thus reclaiming their memory allocation.<\/p>\n\n\n\n<p>The&nbsp;AddToolBarExtension&nbsp;function within the editor module is the one that is responsible for actually adding UI elements to the toolbar that can invoke our commands.<\/p>\n\n\n\n<p>It does this by calling functions on the&nbsp;FToolBarBuilder&nbsp;instance that&#8217;s passed in as a function parameter.<\/p>\n\n\n\n<p>First, we retrieve an appropriate icon for our new toolbar button using the&nbsp;FSlateIcon&nbsp;constructor. Then, with the icon loaded, we invoke&nbsp;AddToolBarButton&nbsp;on the&nbsp;builder&nbsp;instance.<\/p>\n\n\n\n<p>AddToolbarButton&nbsp;has a number of parameters. The first parameter is the command to bind to&nbsp;\u2013&nbsp;you&#8217;ll notice it&#8217;s the same&nbsp;MyButton&nbsp;member that we accessed earlier when binding the action to the command. The second parameter is an override for the extension hook we specified earlier, but we don&#8217;t want to override that, so we can use&nbsp;NAME_None. The third parameter is a label override for the new button that we create. Parameter four is a tooltip for the new button. The second to last parameter is the button&#8217;s icon, and the last parameter is a name that&#8217;s used to refer to this button element for highlighting support if you wish to use the in-editor tutorial framework.<\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>If you have created a custom tool or window for display [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[27,1],"tags":[26],"_links":{"self":[{"href":"https:\/\/blog.coolcoding.cn\/index.php?rest_route=\/wp\/v2\/posts\/3082"}],"collection":[{"href":"https:\/\/blog.coolcoding.cn\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.coolcoding.cn\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.coolcoding.cn\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.coolcoding.cn\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=3082"}],"version-history":[{"count":1,"href":"https:\/\/blog.coolcoding.cn\/index.php?rest_route=\/wp\/v2\/posts\/3082\/revisions"}],"predecessor-version":[{"id":3094,"href":"https:\/\/blog.coolcoding.cn\/index.php?rest_route=\/wp\/v2\/posts\/3082\/revisions\/3094"}],"wp:attachment":[{"href":"https:\/\/blog.coolcoding.cn\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=3082"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.coolcoding.cn\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=3082"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.coolcoding.cn\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=3082"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}