- UE5 Common UI お試し ① ボタンのアイコンを出し分ける - You are done!
- UE5 Common UI お試し ② デフォルトナビゲーション (Default Click Action) - You are done!
- UE5 Common UI お試し ③ デフォルトナビゲーション (Default Back Action) - You are done!
- UE5 Common UI お試し ④ Action Bar - You are done!
上記の続きです。
今回は機能別サンプルでも紹介されている Popup のような、複数の Widget を扱う場合の最初のステップとして Visibility の制御を見ていきます。
Popup を作る
Widget の作成
WBP_Popup
という名前で作成しています。例によって CommonActivatableWidget
を親にします。
内部はそれ程工夫はありませんが、Close Popup
ボタンは通常の Button
です。(CommonButtonBase
ではない)
また、上部に CommonActionWidget
だけ配置しており、ボタンではありませんが Back のナビゲーションを案内しています。
以下に主要な Common UI 関連の設定を貼ります。
WBP_Popup |
CommonActionWidget_87 |
---|---|
まず、ActionWidget_BackIcon
に関してですが、こちらは Back のアイコンを出したいだけの設定です。
つづいて WBP_Popup
ですが、こちらは Back に反応するための Is Back Handler
のチェックをしています。また、WBP_Menu
では true
になっていた Auto Activate
ですが、今回は WBP_Menu
から Activate
を呼び出すため false
になっています。
続いて Activated Visibility / Deactivated Visibility
ですが、こちらはアクティベート時に表示、非アクティブ時には消すという Popup としてはそのままの挙動にしています。
グラフ
コメント通りです。初期の非表示はもう少しスマートな方法があるのかもしれませんが、機能別サンプルもこうなっていたので一旦こうしています。
ActivatedVisibility / DeactivatedVisibility
ここで先程出てきたプロパティの確認をしておきます。まず定義ですが CommonActivatableWidget.h
ですね。
UPROPERTY(EditAnywhere, Category = Activation, meta = (InlineEditConditionToggle = "ActivatedVisibility")) bool bSetVisibilityOnActivated = false; UPROPERTY(EditAnywhere, Category = Activation, meta = (EditCondition = "bSetVisibilityOnActivated")) ESlateVisibility ActivatedVisibility = ESlateVisibility::SelfHitTestInvisible; UPROPERTY(EditAnywhere, Category = Activation, meta = (InlineEditConditionToggle = "DeactivatedVisibility")) bool bSetVisibilityOnDeactivated = false; UPROPERTY(EditAnywhere, Category = Activation, meta = (EditCondition = "bSetVisibilityOnDeactivated")) ESlateVisibility DeactivatedVisibility = ESlateVisibility::Collapsed;
Editor の UI 通りです。
続いて、これらのプロパティが利用されている箇所を見ていきます。まずは予想しやすい OnActivated / OnDeactivated
時です。
void UCommonActivatableWidget::NativeOnActivated() { if (ensureMsgf(bIsActive, TEXT("[%s] has called NativeOnActivated, but isn't actually activated! Never call this directly - call ActivateWidget()"))) { if (bSetVisibilityOnActivated) { SetVisibility(ActivatedVisibility); UE_LOG(LogCommonUI, Verbose, TEXT("[%s] set visibility to [%s] on activation"), *GetName(), *StaticEnum<ESlateVisibility>()->GetDisplayValueAsText(ActivatedVisibility).ToString()); } BP_OnActivated(); OnActivated().Broadcast(); BP_OnWidgetActivated.Broadcast(); } }
void UCommonActivatableWidget::NativeOnDeactivated() { if (ensure(!bIsActive)) { if (bSetVisibilityOnDeactivated) { SetVisibility(DeactivatedVisibility); UE_LOG(LogCommonUI, Verbose, TEXT("[%s] set visibility to [%d] on deactivation"), *GetName(), *StaticEnum<ESlateVisibility>()->GetDisplayValueAsText(DeactivatedVisibility).ToString()); } // Cancel any holds that were active ClearActiveHoldInputs(); BP_OnDeactivated(); OnDeactivated().Broadcast(); BP_OnWidgetDeactivated.Broadcast(); } }
上記を見ての通りですが、そもそもこの設定がなければ SetVisibility
が呼び出されないため表示は一切変更されないことになります。
続いて以下の箇所でも利用されていますが、これは後ほど出てくるためここではスキップします。
void UCommonActivatableWidget::HandleVisibilityBoundWidgetActivations() { ESlateVisibility OldDeactivatedVisibility = DeactivatedBindVisibility; OldDeactivatedVisibility = bSetVisibilityOnActivated && IsActivated() ? ActivatedVisibility : OldDeactivatedVisibility; OldDeactivatedVisibility = bSetVisibilityOnDeactivated && !IsActivated() ? DeactivatedVisibility : OldDeactivatedVisibility;
最後に、一応 ESlateVisibility
の確認をしておきます。(私はよく内容を忘れます。。)
UENUM(BlueprintType) enum class ESlateVisibility : uint8 { /** Visible and hit-testable (can interact with cursor). Default value. */ Visible, /** Not visible and takes up no space in the layout (obviously not hit-testable). */ Collapsed, /** Not visible but occupies layout space (obviously not hit-testable). */ Hidden, /** Visible but not hit-testable (cannot interact with cursor) and children in the hierarchy (if any) are also not hit-testable. */ HitTestInvisible UMETA(DisplayName = "Not Hit-Testable (Self & All Children)"), /** Visible but not hit-testable (cannot interact with cursor) and doesn't affect hit-testing on children (if any). */ SelfHitTestInvisible UMETA(DisplayName = "Not Hit-Testable (Self Only)") };
Popup を Menu から呼び出す
WBP_Popup
を WBP_Menu
のボタンから呼び出し表示できるようにします。
Widget の修正
ここまで利用してきた WBP_Menu
からはレイアウトやボタンの機能を変えています。
CommonButton_Open1, 2, 3
これは CommonButtonBase
で作成しています。
全て以下のように Triggering Input Action
の設定はなく、Default Click Action に反応するようにしています。
いずれも WBP_Popup
の Activate Widget
を呼び出すようになっています。
WBP_Popup の設定
前述の WBP_Popup
自体の設定と同じなのですが、ここでも Activated Visibility / Deactivated Visibility
の指定をしないと有効にならなかったので一応 WBP_Menu
に配置した側での設定も貼っておきます。
また、Popup はサイズはコンテンツに合わせていて、全画面にはしていません。(つまり、Popup が表示されても裏側の Menu が触れる状態)
現状の動作
ここまでの設定で Open Window
ボタンを押すと Popup が表示され、Popup で Close Popup
または Back に対応するキーを押すと Popup が消えるという動作になり意図通りです。
加えて、Popup 表示時に Back キーを押して消えるのは Popup だけであり、適切に Back キーの入力がルーティングされていることが確認できます。
Menu と Popup の Visibility 制御
ただ、現状だと Popup 表示時に Menu のボタンも依然としてされてしまいます。よって、ここからは「Popup 表示時 (Activated) には Menu は触れ無くする (Deactivated) 」部分を書いていきます。
Menu の実装
やることは WBP_Menu
お Event Construct
から以下のノードを設定するだけです。
これだけでは意味が分からないので詳細を見ていきます。
SetBindVisibilities / BindVisibilityToActivation
いずれも CommonActivatableWidget
の関数です。
まず SetBindVisibilities
ですが、ここで入力で受け取った設定値を変数に入れています。
void UCommonActivatableWidget::SetBindVisibilities(ESlateVisibility OnActivatedVisibility, ESlateVisibility OnDeactivatedVisibility, bool bInAllActive) { ActivatedBindVisibility = OnActivatedVisibility; DeactivatedBindVisibility = OnDeactivatedVisibility; bAllActive = bInAllActive; }
続いて BindVisibility の対象となる Widet を登録します(今回の例でいえば WBP_Popup
です)。
VisibilityBoundWidgets
に重複無く追加した上で、対象の Widget の Activated
, Deactivated
イベントに自身の HandleVisibilityBoundWidgetActivations
を関連付けています。そして最後に一度呼び出しています。
void UCommonActivatableWidget::BindVisibilityToActivation(UCommonActivatableWidget* ActivatableWidget) { if (ActivatableWidget && !VisibilityBoundWidgets.Contains(ActivatableWidget)) { VisibilityBoundWidgets.Add(ActivatableWidget); ActivatableWidget->OnActivated().AddUObject(this, &UCommonActivatableWidget::HandleVisibilityBoundWidgetActivations); ActivatableWidget->OnDeactivated().AddUObject(this, &UCommonActivatableWidget::HandleVisibilityBoundWidgetActivations); HandleVisibilityBoundWidgetActivations(); } }
上述の HandleVisibilityBoundWidgetActivations
は以下です。上記の通りであれば、最初の一回以外は、登録した Widet の Activate/Deactivate
に反応して呼び出されることになります。
void UCommonActivatableWidget::HandleVisibilityBoundWidgetActivations() { ESlateVisibility OldDeactivatedVisibility = DeactivatedBindVisibility; OldDeactivatedVisibility = bSetVisibilityOnActivated && IsActivated() ? ActivatedVisibility : OldDeactivatedVisibility; OldDeactivatedVisibility = bSetVisibilityOnDeactivated && !IsActivated() ? DeactivatedVisibility : OldDeactivatedVisibility; for (const TWeakObjectPtr<UCommonActivatableWidget>& VisibilityBoundWidget : VisibilityBoundWidgets) { if (VisibilityBoundWidget.IsValid()) { if (bAllActive) { if (!VisibilityBoundWidget->IsActivated()) { SetVisibility(OldDeactivatedVisibility); return; } } else { if (VisibilityBoundWidget->IsActivated()) { SetVisibility(ActivatedBindVisibility); return; } } } } SetVisibility(bAllActive ? ActivatedBindVisibility : OldDeactivatedVisibility); }
私が混乱してきたので改めてですが、変数と Editor 上での関係です。
ActivatedVisibility DeactivatedVisibility |
|
ActivatedBindVisibility DeactivatedBindVisibility bAllActive |
SetBindVisibilities で設定される |
ややこしいのですが、上記の設定の場合、この Widet の自身の ActivatedVisibility/DeactivatedVisibility
は直感的(アクティブで表示、非アクティブで非表示)ですが、BindVisibility
の場合は全く反対の設定を行うことに留意します。Bind 対象の Widget の Active/Deactive に対応するためです。
続いて、最初の 3 行がどのように判定されるのかを簡単なデシジョンテーブルで書いてみました。
1 | 2 | 3 | 4 | |
---|---|---|---|---|
IsActivated() | Y | Y | N | N |
bSetVisibilityOnActivated | Y | N | - | - |
bSetVisibilityOnDeactivated | - | - | Y | N |
OldDeactivatedVisibility | ActivatedVisibility | DeactivatedBindVisibility | DeactivatedVisibility | DeactivatedBindVisibility |
簡単には、自身の Activated/Deactivated Visibility の設定が有効であれば、それと自身のアクティブ状態を対応させ、逆に無効であれば DeactivatedBindVisibility
を優先するというものです。
さて、for
文を見る前に bAllActive
を確認しておきます。
/** True if we should switch to activated visibility only when all bound widgets are active */ bool bAllActive = true;
VisibilityBoundWidgets
が全て Activated
の場合にのみ、この Widget の activated visibility を変えるということのようです。
改めて for
文以降のみ貼ります。
for (const TWeakObjectPtr<UCommonActivatableWidget>& VisibilityBoundWidget : VisibilityBoundWidgets) { if (VisibilityBoundWidget.IsValid()) { if (bAllActive) { if (!VisibilityBoundWidget->IsActivated()) { SetVisibility(OldDeactivatedVisibility); return; } } else { if (VisibilityBoundWidget->IsActivated()) { SetVisibility(ActivatedBindVisibility); return; } } } } SetVisibility(bAllActive ? ActivatedBindVisibility : OldDeactivatedVisibility);
bAllActive
の場合には、先程のコメントにあった通り、Activated
ではない VisibilityBoundWidget
を検出した時点で OldDeactivatedVisibility
を適用し return
しています。
逆に bAllActive
でない場合には、一つでも Activated
なものを検出すると、ActivatedBindVisibility
を適用しています。(上記の設定でいえば、一つでも登録した Widget がアクティブになれば、自身は Not Hit-Testable
にするということです)
最後の一文は for
文の条件節に引っかからなかった場合ですが、(有効な VisibilityBoundWidget
が 0 の場合の除けば) bAllActive
が true
で全ての登録 Widget が Activated
だった場合と、 bAllActive
が false
で、全ての登録 Widget が Deactivated
の場合なので、この式で十分のように見えます。
現状の動作
この状態だと、Popup が出ている時に、Back キーは効くのですが、Close Popup
ボタンが押せないという状態になります。
というのも、Set Bind Visibilities
の On Activated Visibility
を All Children
も含めてヒットしない設定にしているため、WBP_Menu
の子として配置している WBP_Popup
もヒットしなくなっています。
一方で、これを Self
のみヒットしなくすると、結局 Open Window
のボタンが押せるままになってしまうので意味がありません。
続く
ということで、長くなってしまったので一度切ります。。
上記を回避するために現状の WBP_Menu
と WBP_Popup
の包含関係を修正し、一枚外側に Widget を入れて並列に並べることが必要になりそうです。
実は機能別サンプルの方は最初から最初からそうなっているのですが、今回は自分で再現するにあたり、自分が理解できている機能だけを追いながら組んでいったのですがそれがミスの原因になりました。