【UE4 C++基础系列】:
- 【UE4】C++基础【00】——通俗易懂 用蓝图来学习 C++ 基础知识
- 【UE4】C++基础【01】——默认代码扒拉拉(初学者向)
- 【UE4】C++基础【02】——添加Slate至窗口
- 【UE4】C++基础【03】——WidgetStyle 定制Widget样式
- 【UE4】C++基础【04】——DPI屏幕适配/菜单布局
【视频教程】:
- 迪迪老湿
【导图】:
第一部分、DPI屏幕适配
一、SOverlay来做UI的布局。
【1.1】自定义SlateWidgetStyle.h
中再添加一个SlateBrush以放置屏幕中央菜单背景图片
//FShitMenuStyle 即ShitMenuWidgetStyle样式中添加两个SlateBrush,存放图片。
UPROPERTY(EditAnywhere,Category=MenuHUD)
FSlateBrush ShitMenuHUDBackGroundBrush;
UPROPERTY(EditAnywhere, Category = MenuHUD)
FSlateBrush ShitMenuBackGroundBrush;
【1.2】自定义HUDWidget.cpp
中添加Overlay层并包含两图片
【1】Overlay小知识
首先介绍一下Overlay,简单来说,Overlay就相当于一个盒子,它可以像PS图层那样布置组件,图层最上面置底,最下面置顶,我们要用的就是它的多槽,对齐和Padding功能。
比如举个例子,比如说我们的Button,Button.Style.Image是会跟Button变的,我们如果要在Button周围添加些小图片、还要给Button里面添加文字就需要用到Overlay了,毕竟Button组件只有一个槽。
我们本节用它来作为基础层来加Widget(也就是没有Canvas Panel,直接Overlay作为底层上)
【2】代码实现
这个跟UMG做UI差不多啦,网页也是,C++代码属性名和UMG Widget属性名差不多,相互对应。
SNew(SOverlay)
——从Palette拽一个Overlay到Hierarchy层级+SOverlay::Slot()
——给Overlay Widget下面放东西,添加槽.H/VAlign
——如子组件的父层Slot(Overlay Slot) 对齐方式[...]
——给UI添加Image组件,然后把WidgetStyle.SlateBrush中存的图片添加到屏幕上
注意ChildSlot[];
(分号结尾,常常加着加着就没了或忘了)
MenuStyle是从头文件来的。
const struct FShitMenuStyle* MenuStyle;
ChildSlot
[
SNew(SOverlay)
+SOverlay::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
[
SNew(SImage)
.Image(&MenuStyle->ShitMenuHUDBackGroundBrush)
]
+ SOverlay::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SNew(SImage)
.Image(&MenuStyle->ShitMenuBackGroundBrush)
]
]; //分号结尾
【1.3】ShitController源文件中把第一篇中的鼠标选项设置为不打锁。
自适应要拖动边框,所以Do Not Lock。
InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::DoNotLock);
然后编译Build一下。
【1.4】可以自定义设置图片大小
因为我们【1.2】代码中已经定义了HUD背景图铺满屏幕、Menu菜单背景图居中。所以我们可以自定义设置Menu菜单背景图的图片大小,如20、20就是个小不点,我们需要使用图片的默认尺寸大小,所以黄色小箭头归零。(也可以在代码里写死,但在SlateWidgetStyle中调节我们是可以直接预览到变化的,而且后面不是要DPI适配嘛,所以不能代码中写死。)
二、DPI Scaling 屏幕自适应
【2.1】项目默认设置
在项目设置中进行设置,默认是这样的
【操作】全部删掉,只剩下屏幕尺寸(Resolution=xxx,Scale=1.0),我们马上要通过代码来自适应。
【知识点】:
- DPI Scale Rule ,默认是Shortest Rule 根据最短边来进行计算。
屏幕尺寸比例大多为16:9,所以按它来算:
- 720:480
- 1280:720
- 1920:1080
- 3840:2160
大多屏幕都是1080P的,所以它是按照
默认的是480就停了,也就是窗口到720*480的时候就不再进行自适应缩放了。可以看到下方屏幕宽度小于480以后背景图片都没了。
其实我们只需要加个(Res=0,Scale=0)的关键帧就行了,但是为了掌握 UE4 C++基础,还得啃一下代码,两种方法其实最终效果都一样。
我们在学习网页大数据展示的时候也遇到过屏幕适配知识,可以综合着一起学习应用程序、网页、手机APP的DPI相关知识点。
【2.2】代码设置
【头文件】:
我们再在自定义SlateWidget.h中添加如下:变量和函数的声明分开,便于分辨。
private:// 函数声明
float GetUIScaler() const;
FVector2D GetViewportSize() const;
private: //变量声明
TAttribute<float>UIScaler;
【源文件】:
源文件Construct函数中:绑定缩放规则方法。
GetUIScaler方法我们会在下面实现。
UIScaler.Bind(this, &SShitMenuHUDWidget::GetUIScaler);
绑定一个任意函数,该函数将被调用来根据需要生成该属性的值。
- &——表示取函数返回的值
- SShitMenuHUDWidget::GetUIScaler—— 调用本类中的函数
- this——关键词表表示把逗号后面的值传给this绑定参数值
- UIScaler.——然后把系数值给到
float UIScaler
变量方便下面SNew(SDPIScale)调用。
跟UMG中Widget组件这个Bind类似啦。
头文件 Alt+C 源文件函数实现
- GetUIScaler函数会先调用下面的GetViewportSize函数。
float SShitMenuHUDWidget::GetUIScaler() const
{
return GetViewportSize().Y / 2160.f; //4K
}
FVector2D SShitMenuHUDWidget::GetViewportSize() const
{
FVector2D Result(3840, 2160);// 初始化声明自己的屏幕分辨率为4K。(根据自己的设置),给到Result
if (GEngine && GEngine->GameViewport)
GEngine->GameViewport->GetViewportSize(Result);//获取当前缩放比例
return Result; //返回当前的屏幕尺寸
}
三、SDPIScaler自适应类
【头文件】:UIScaler 头文件引入 SDPIScaler
#include "Widgets/Layout/SDPIScaler.h"
【源文件】:
- 把Overlay包含在DPIScaler 里面。这样所有的Widget组件都会根据UIScaler的值来进行设定。
ChildSlot
[
SNew(SDPIScaler) //创建 SDPIScaler
.DPIScale(UIScaler) //实例化并把UIScaler Ratio系数比比传给DPIScale。
[
SNew(SOverlay)
+ SOverlay::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
[
SNew(SImage)
.Image(&MenuStyle->ShitMenuHUDBackGroundBrush)
]
+ SOverlay::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SNew(SImage)
.Image(&MenuStyle->ShitMenuBackGroundBrush)
]
]
];
然后屏幕自适应就OK了。(咦,那个灰框框竟然自己神奇地消失了~)
【头文件】:
// SShitMenuHUDWidget.h
#pragma once
#include "CoreMinimal.h"
#include "Widgets/SCompoundWidget.h"
#include "Widgets/Images/SImage.h"
/**
*
*/
class TESTGAME_API SShitMenuHUDWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SShitMenuHUDWidget)
{}
SLATE_END_ARGS()
/** Constructs this widget with InArgs */
void Construct(const FArguments& InArgs);
private:
float GetUIScaler() const;
FVector2D GetViewportSize() const;
private:
TAttribute<float>UIScaler;
const struct FShitMenuStyle* MenuStyle;
};
【源文件】:
// SShitMenuHUDWidget.cpp
#include "UI/Widget/SShitMenuHUDWidget.h"
#include "SlateOptMacros.h"
#include "UI/Style/ShitStyle.h"
#include "UI/Style/ShitMenuWidgetStyle.h"
#include "Widgets/Layout/SDPIScaler.h"
#include "Engine/Engine.h"
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SShitMenuHUDWidget::Construct(const FArguments& InArgs)
{
//获取编辑器的MenuStyle
MenuStyle = &ShitStyle::Get().GetWidgetStyle<FShitMenuStyle>("BPShitStyle");
UIScaler.Bind(this, &SShitMenuHUDWidget::GetUIScaler);
ChildSlot
[
SNew(SDPIScaler)
.DPIScale(UIScaler)
[
SNew(SOverlay)
+ SOverlay::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
[
SNew(SImage)
.Image(&MenuStyle->ShitMenuHUDBackGroundBrush)
]
+ SOverlay::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SNew(SImage)
.Image(&MenuStyle->ShitMenuBackGroundBrush)
]
]
];
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
float SShitMenuHUDWidget::GetUIScaler() const
{
return GetViewportSize().Y / 2160.f;
}
FVector2D SShitMenuHUDWidget::GetViewportSize() const
{
FVector2D Result(3840, 2160);// 初始化
if (GEngine && GEngine->GameViewport)
GEngine->GameViewport->GetViewportSize(Result);//获取当前缩放比例
return Result;
}
第二部分、菜单布局实现
一、Button事件检测布局
【蓝图方法】:
我们可以通过蓝图这种方法来调节Slot参数。
【代码方法】:
【头文件】:
我们需要通过代码来实现:
- SOverlay类下的FOverlaySlot类 给到ImageSlot对象指针。(FOverlaySlot类中包含了用到的Padding、HAlign、VAlign等)
- FReply类管一些列鼠标相关的函数
private: //变量
SOverlay::FOverlaySlot * ImageSlot;
private://函数
FReply Onclick();
【源文件】:
【1】Expose(ImageSlot)
- Expose是SlotBase类中定义的(还有Attach、Detach、AttachParent等方法)
- 通过修改ImageSlot来修改Slot属性,给到那个Menu菜单栏图片(而不是背景图片哦)
+ SOverlay::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
.Expose(ImageSlot) //添加语句
[
SNew(SImage)
.Image(&MenuStyle->ShitMenuBackGroundBrush)
]
]
];
【2】实现OnClicked绑定函数
- 先把头文件中声明的OnClick方法实现,Onclick被绑定函数
- 让图片锁在Slot插槽的最右下角(编辑器WidgetStyle中改是不起效的)
- Handled让系统知道事件处理了
FReply SShitMenuHUDWidget::OnClick()
{
ImageSlot->HAlign(HAlign_Right).VAlign(VAlign_Bottom);
return FReply::Handled();
}
【3】SButton绑定OnClick事件
- 然后SButton绑定函数调用。(如果出不来那是因为你没有include)
- 注意总的ChildSlot[]; ( ]后面有个分号;,否则会报错 )
+ SOverlay::Slot()
.HAlign(HAlign_Left)
.VAlign(VAlign_Top)
[
SNew(SButton)
.OnClicked(this, &SShitMenuHUDWidget::OnClick)
]
二、背景大UI嵌入菜单中央小UI
我们之前都一直对SShitMenuHUDWidget进行操作的(我们暂且把它叫做背景UI),那个我们定义的是添加到屏幕上的。
然后我们在背景UI中先#include菜单UI。
#include "UI/Widget/SShitMenuWidget.h"
之后第三节中呢我们要对我们第一篇中创建的另外一个Widget叫做SShitMenuWidget(我们把它叫做菜单UI)进行操作。先把第一步中添加的代码先都删掉,那个Button是用来测试布局的。
【嵌入菜单UI】:
我们用SAssginNew
方法来给UI,跟之前第一篇中的Button是一样的。另一种是我们常用的SNew
【头文件】:
声明菜单指针。
TSharedPtr<class SShitMenuWidget>MenuWidget;
【源文件】:把之前那个中央的图片替换成这个菜单UI。
+ SOverlay::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SAssignNew(MenuWidget,SShitMenuWidget)
]
我们把这个菜单UI嵌入到背景UI的中间那张图片上。
三、ShitMenuWidgetStyle.h 自定义WidgetStyle中配置足够图片
本篇内容中差不多共使用到5张。
- 背景图片 1张
- 菜单背景图片 1张
- 菜单左右侧图片 2张
- 标题图片 1张
UPROPERTY(EditAnywhere,Category=MenuHUD)
FSlateBrush ShitMenuHUDBackGroundBrush;
UPROPERTY(EditAnywhere, Category = MenuHUD)
FSlateBrush ShitMenuBackGroundBrush;
UPROPERTY(EditAnywhere, Category = MenuHUD)
FSlateBrush LeftIconBrush;
UPROPERTY(EditAnywhere, Category = MenuHUD)
FSlateBrush RightIconBrush;
UPROPERTY(EditAnywhere, Category = MenuHUD)
FSlateBrush TitleBorderBrush;
四、定义菜单底图左右两侧图位置
开始定义菜单UI啦,我们先把做了三篇的背景UI先扔到一边。
【头文件】:
conststructFShitMenuStyle* MenuStyle;
——声明指针对象,方便源文件中获取图片的地址TSharedPtr<SBox>RootSizeBox;
——声明Scale Box指针,以定义ScaleBox大小。
// SShitMenuWidget.h
#pragma once
#include "CoreMinimal.h"
#include "Widgets/SCompoundWidget.h"
/**
*
*/
class TESTGAME_API SShitMenuWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SShitMenuWidget)
{}
SLATE_END_ARGS()
/** Constructs this widget with InArgs */
void Construct(const FArguments& InArgs);
private:
const struct FShitMenuStyle * MenuStyle;
TSharedPtr<SBox>RootSizeBox;
};
【源文件】:
记得包含头文件
#include"Widgets/Layout/SBox.h
——ScaleBox,它可以自定义设置Width和Height#include"UI/Style/ShitStyle.h
——以调用空类中方法#include"UI/Style/ShitMenuWidgetStyle.h
——把那个自定义WidgetStyle拿过来
- Padding就是边距,FMargin设置值。 50.f即距离上方50px。
MenuStyle = &ShitStyle::Get().GetWidgetStyle<FShitMenuStyle>("BPShitStyle");
ChildSlot
[
SAssignNew(RootSizeBox, SBox)
[
SNew(SOverlay) //创建Overlay 组件
+ SOverlay::Slot() //添加菜单背景底图
.HAlign(HAlign_Fill)//铺满
.VAlign(VAlign_Fill)
.Padding (FMargin(0.f, 50.f, 0.f, 0.f))
[
SNew(SImage)
.Image(&MenuStyle->ShitMenuBackGroundBrush)
]
+SOverlay::Slot() //添加菜单左侧图
.HAlign(HAlign_Left) //居左
.VAlign(VAlign_Center)
.Padding(FMargin(0.f, 25.f, 0.f, 0.f))
[
SNew(SImage)
.Image(&MenuStyle->LeftIconBrush)
]
+ SOverlay::Slot() //添加菜单右侧图
.HAlign(HAlign_Right) //居右
.VAlign(VAlign_Center)
.Padding(FMargin(0.f, 25.f, 0.f, 0.f))
[
SNew(SImage)
.Image(&MenuStyle->RightIconBrush)
]
];
五、定义标题框框位置
【Border】:
- Slot(XXX)就是基于父级控制当前级
- Content就是控制子级
- 结合头文件中的共享指针 用
SAssignNew
的方法是因为我们需要特定这个外面大Box的尺寸。 - ScaleBox长宽根据自己的屏幕分辨率来定。
ChildSlot
[
SAssignNew(RootSizeBox, SBox) //大菜单ScaleBox
[
SNew(SOverlay) //Overlay层叠加
+ SOverlay::Slot()
.HAlign(HAlign_Center)//标题框相对于大菜单居中
.VAlign(VAlign_Top) //标题框相对于大菜单居上
[
SNew(SBox) //居中居上的标框Box
.WidthOverride(750.f) //标题框的宽度
.HeightOverride(300.f) //标题框的高度
[
SNew(SBorder)
.BorderImage(&MenuStyle->TitleBorderBrush) //给Border添加图片
]
]
]
];
RootSizeBox->SetWidthOverride(1200.f); //设置大菜单宽度和高度
RootSizeBox->SetHeightOverride(900.f);
这里我们用到了SBox和SBorder,所以同样头文件记得哦。
#include"Widgets/Layout/SBorder.h"
#include"Widgets/Layout/SBox.h"
六、定义Text文字
【6.1】引用头文件
引入TextBlock,用到谁就引用谁。
#include "Components/TextBlock.h"
【6.2】头文件声明共享指针
因为我们会动态修改文字的内容,所以我们需要在MenuWidget头文件中把它定义成一个指针。然后SAssignNew啦。
TSharedPtr<STextBlock>TitleText;
【6.3】ShitStyle空类中定义字体样式
- 定义字体样式,浏览器搜索
免费ttf字体下载
即可。 - 然后把这个ttf字体样式导入到 “/Game/UI/Style"下面
- 还记得我们的ShitStyle嘛,就是注册注销,设置路径的那个空类。我们需要在里面设置字体路径。
- 我就直接用微软雅黑的了,导入Yes all,它会创建两个
Microsoft(Font Face)
(UI是AZ)——存ttf路径,Source FilenameMicrosoft_Font(Font)
(UI是黑色背景+白色字体样式)——一堆FontFamily,功能挺多的。
在空类源文件中 加一行代码StyleRef->Set("MenuItemFont", FSlateFontInfo("Microsoft.ttf",50));
TSharedRef<class FSlateStyleSet> ShitStyle::Create() //头文件函数声明TSharedPtr,源文件定义TSharedRef ,返回StyleRef。
{
TSharedRef<FSlateStyleSet>StyleRef = FSlateGameResources::New(ShitStyle::GetStyleSetName(), "/Game/UI/Style","/Game/UI/Style"); //调用本ShityStyle类中的GetStyleSetName函数
StyleRef->Set("MenuItemFont", FSlateFontInfo("Microsoft.ttf",50));
return StyleRef;
}
【6.4】:MenuWidget 中在小文字框内添加TextBlock
我们需要先引入#include"Components/TextBlock.h"
。
+ SOverlay::Slot()
.HAlign(HAlign_Center)//标题框相对于大菜单居中
.VAlign(VAlign_Top) //标题框相对于大菜单居上
[
SNew(SBox) //居中居上的标框Box
.WidthOverride(750.f) //标题框的宽度
.HeightOverride(300.f) //标题框的高度
[
SNew(SBorder)
.BorderImage(&MenuStyle->TitleBorderBrush) //给Border添加图片
]
]
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SAssignNew(TitleText, STextBlock) // 实例
.Font(ShitStyle::Get().GetFontStyle("MenuItemFont")) //
.Text(FText::FromString("I am 特斯拉"))
]
]
七、配置所需图片 检验测试
我们只需要设置左右图片的大小就好了,其他的大背景尺寸、菜单底图、文字框图都在代码中写死了。
另外:那个边角挺聪明的,四个朝向直接在PS中做好,要不然用代码来弄非常麻烦的,网页大数据的时候也遇到过。
我们下节再进行语言设置,还有处理文字乱码问题。
MenuWidget.h代码
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Widgets/SCompoundWidget.h"
/**
*
*/
class TESTGAME_API SShitMenuWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SShitMenuWidget)
{}
SLATE_END_ARGS()
/** Constructs this widget with InArgs */
void Construct(const FArguments& InArgs);
private:
TSharedPtr<SBox>RootSizeBox;
const struct FShitMenuStyle*MenuStyle;
TSharedPtr<STextBlock>TitleText;
};
MenuWidget.cpp代码
// Fill out your copyright notice in the Description page of Project Settings.
#include "UI/Widget/SShitMenuWidget.h"
#include "SlateOptMacros.h"
#include "Widgets/Layout/SBox.h"
#include "Widgets/Images/SImage.h"
#include "Widgets/Layout/SBorder.h"
#include "Components/TextBlock.h"
#include "UI/Style/ShitStyle.h"
#include "UI/Style/ShitMenuWidgetStyle.h"
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SShitMenuWidget::Construct(const FArguments& InArgs)
{
MenuStyle = &ShitStyle::Get().GetWidgetStyle<FShitMenuStyle>("BPShitStyle");
ChildSlot
[
SAssignNew(RootSizeBox, SBox)
[
SNew(SOverlay)
+ SOverlay::Slot()
.HAlign(HAlign_Fill)
.VAlign(VAlign_Fill)
.Padding (FMargin(0.f, 50.f, 0.f, 0.f))
[
SNew(SImage)
.Image(&MenuStyle->ShitMenuBackGroundBrush)
]
+SOverlay::Slot()
.HAlign(HAlign_Left)
.VAlign(VAlign_Center)
.Padding(FMargin(0.f, 25.f, 0.f, 0.f))
[
SNew(SImage)
.Image(&MenuStyle->LeftIconBrush)
]
+ SOverlay::Slot()
.HAlign(HAlign_Right)
.VAlign(VAlign_Center)
.Padding(FMargin(0.f, 25.f, 0.f, 0.f))
[
SNew(SImage)
.Image(&MenuStyle->RightIconBrush)
]
+ SOverlay::Slot()
.HAlign(HAlign_Center)
.VAlign(VAlign_Top)
[
SNew(SBox)
.WidthOverride(750.f)
.HeightOverride(300.f)
[
SNew(SBorder)
.BorderImage(&MenuStyle->TitleBorderBrush)
.HAlign(HAlign_Center)
.VAlign(VAlign_Center)
[
SAssignNew(TitleText, STextBlock)
.Font(ShitStyle::Get().GetFontStyle("MenuItemFont"))
.Text(FText::FromString("I Love You"))
]
]
]
]
];
RootSizeBox->SetWidthOverride(1200.f);
RootSizeBox->SetHeightOverride(900.f);
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION
小技巧:
- Tab向右缩进
- Shift+Tab向左缩进(差不多都一样,Blender Python IDE也是)
SOverlay::Slot()[]
方括号末尾添加;分号断句后可快速对齐,然后删除即可。- 编辑器中Compile貌似速度更快一些
- (在无大Bug,稍微改些代码的时候貌似这个更快)
- (而且VS中那个unabled to delete dll啥的是需要Rebuild的,通常是Build省时间,如果那种情况 VS Rebuild不成功的话,就编辑器Compile,而不用像我第二篇中提到的关闭重启了)