K.Y.さん、しんちょくどーですか?

仕事や趣味で知ったこと、つまずいたことを書いています

【UE5】ListView のラップ(Wrap)指定時のフォーカス移動をきちんとループさせる

この記事は Unreal Engine (UE) Advent Calendar 2025 シリーズ 4 の 23 日目の記事です。


ListView の UI ナビゲーションで「ラップ(Wrap)」を指定すると、リストの端でフォーカスが反対側にループする...と思っていたのですが、試してみると期待したようには動きませんでした。

実際には、上下(または左右)移動で端に到達した際、フォーカスが先頭/末尾に戻らず、そのまま移動が止まっているように見えます。

この記事では、こうした ListView のラップ指定時の挙動について、改善方法を紹介します。

確認環境

  • UE5.7.1
  • C++ プロジェクト

改善前/改善後(動画)

ラップ指定時の挙動について、改善前後の様子は以下の動画のとおりです。
(リスト内の項目がフォーカスを受けると、緑色に変わるようにしています。)

youtu.be

改善方法(C++ 必須)

UI ナビゲーションでラップが指定されている場合でも、ListView では端を越えるナビゲーションが先頭/末尾へループする挙動にはなっていませんでした。

そのため、ListView を継承し OnNavigation(Slate 側)で、リスト端に到達した際のフォーカス移動を反対側の項目へ差し替える処理を実装します。

Slate 側の挙動を拡張するため、今回の方法では C++ が必須となります。

Slate / SlateCore の有効化

Slate のナビゲーション処理を拡張するため、Slate UI を使用できるように設定しておきます。
[プロジェクト名].Build.cs にある以下の行を有効化します。
(行自体がコメントアウトされているので、コメントを外します)

// Uncomment if you are using Slate UI
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });

ListView を継承したクラスの作成

次に、ListView を継承したクラスを作成し、ナビゲーション処理を拡張します。

以下は、ラップが指定されている方向の入力に対して、リスト端に到達した場合に反対側の項目へフォーカスを移動する実装例です。

#pragma once

#include "CoreMinimal.h"
#include "Components/ListView.h"
#include "MyListView.generated.h"

template <typename ItemType>
class SMyListView : public SListView<ItemType>
{
public:
  virtual FNavigationReply OnNavigation(const FGeometry& MyGeometry, const FNavigationEvent& InNavigationEvent) override
  {
    if (this->HasValidItemsSource() && this->bHandleDirectionalNavigation && (this->bHandleGamepadEvents || InNavigationEvent.GetNavigationGenesis() != ENavigationGenesis::Controller))
    {
      const TArrayView<const ItemType> ItemsSourceRef = this->GetItems();
      const int32 NumItems = ItemsSourceRef.Num();
      if (0 < NumItems)
      {
        // ナビゲーションで「ラップ」が指定された方向の入力のみ処理
        const EUINavigation NavigationType = InNavigationEvent.GetNavigationType();
        const TSharedPtr<FNavigationMetaData> NavigationMetaData = this->GetMetaData<FNavigationMetaData>();
      if (NavigationMetaData.IsValid() && NavigationMetaData->GetBoundaryRule(NavigationType) == EUINavigationRule::Wrap)
        {
          const int32 CurSelectionIndex = ItemsSourceRef.Find(TListTypeTraits<ItemType>::NullableItemTypeConvertToItemType(this->SelectorItem));
          if (CurSelectionIndex != INDEX_NONE)
          {
            const int32 NumItemsPerLine = this->GetNumItemsPerLine();
            int32 AttemptSelectIndex = CurSelectionIndex;

            // リストの方向(垂直/水平)に応じた入力方向にナビゲーション
            if ((this->Orientation == Orient_Vertical && NavigationType == EUINavigation::Up) ||
              (this->Orientation == Orient_Horizontal && NavigationType == EUINavigation::Left))
            {
              AttemptSelectIndex = CurSelectionIndex - NumItemsPerLine;
            }
            else if ((this->Orientation == Orient_Vertical && NavigationType == EUINavigation::Down) ||
                (this->Orientation == Orient_Horizontal && NavigationType == EUINavigation::Right))
            {
              AttemptSelectIndex = CurSelectionIndex + NumItemsPerLine;
            }

            if (!ItemsSourceRef.IsValidIndex(AttemptSelectIndex) && this->bIsGamepadScrollingEnabled)
            {
              // ナビゲーション先がリストの端を越えたなら逆側に移動
              AttemptSelectIndex = (AttemptSelectIndex + NumItems) % NumItems;

              this->NavigationSelect(ItemsSourceRef[AttemptSelectIndex], InNavigationEvent);
              return FNavigationReply::Explicit(nullptr);
            }
          }
        }
      }
    }

    return SListView<ItemType>::OnNavigation(MyGeometry, InNavigationEvent);
  }
};

UCLASS()
class UMyListView : public UListView
{
GENERATED_BODY()

protected:
  virtual TSharedRef<STableViewBase> RebuildListWidget() override
  {
    return ConstructListView<SMyListView>();
  }
};

UMG で使用する

ビルドしてエディターを起動すると、UMG のパレットに "MyListView"(自前で作成した ListView)が表示されます。

通常の ListView の代わりに、こちらを使用するだけで OK です。

余談 - ScrollBox について

ScrollBox でも、UI ナビゲーションでラップを指定した際に、期待したようなループ挙動にはなっていませんでした。

この挙動の改善については、CommonHierarchicalScrollBox を使用するのがお手軽なのでおすすめです。

SCommonHierarchicalScrollBox::OnNavigation を見ると、ループ処理が拡張されているのがわかります。
今回の MyListView の実装も、この処理を参考にしています。

あとがき

Widget BP のみで解決できないかも試してみましたが、うまくいかず断念しました...

ナビゲーションをカスタムに変更し、今回紹介した C++ コードと似たような処理を BP 側で組むことでループ自体は実現できましたが、ループ直後のフォーカスイベントが発火しない問題が発生しました。

BP オンリーでうまく解決できる方法があれば、ぜひ教えてください。


記事の内容について、誤字脱字、内容の誤り、感想などありましたら気軽にコメントしていただけると嬉しいです。(このブログでも SNS でも歓迎です。)