How to select on which monitor to display the game ?

Hello,

I am trying to build a settings menu for my game and I would like to add the option to select on which monitor the game should be displayed in case the player has several monitors. Then it would be great to retrieve the list of supported resolutions for that selected screen.

Many games have this setting and nowadays it is very widely spread to have more than 1 monitor.

Is it something possible ? How can I create that ?

Right know I am only able to retrieve supported resolutions for the primary monitor with this function:
http://api.unrealengine.com/INT/BlueprintAPI/Rendering/GetSupportedFullscreenResolution-/index.htm

Is this still relevant to you? Are you able to work with C++? If so, a starting point would be looking at FDisplayMetrics, FMonitorInfo and FSlateApplication, which provides access to the former two. FMonitorInfo in particular contains the ID and name of a monitor, and whether it’s the primary one.

Hi ramesjandi,
Thank you for your answer. I was looking for a solution using only blueprints as I’m not familiar with C++, but I guess I will have to look into C++ since I cannot find any plugin for this usage.

Is this still relevant to you? Are you able to work with C++? If so, a starting point would be looking at FDisplayMetrics, FMonitorInfo and FSlateApplication, which provides access to the former two. FMonitorInfo in particular contains the ID and name of a monitor, and whether it’s the primary one.

Hi @and-rad have you done this before? I haven’t been able to find info on this anywhere else. I have two options in my head:

  1. create a new application window (slate window?) on a desired monitor, and somehow register that new window as the primary fullscreen window. No idea how though.

  2. Choose another monitor to be the primary monitor and then refresh the display metrics and then somehow magically move the window.

Would you mind elaborating on these steps, or perhaps share on what you have done?

This is the function that we’re using to get all connected monitors. It’s part of an in-house plugin.

void UPoetGameUserSettings::GetAvailableDisplays(TArray<FDisplayInfo>& OutDisplays, int32& OutCurrentIndex) const
{
	OutDisplays.Reset();
	FDisplayMetrics DisplayMetrics;
	FSlateApplication::Get().GetDisplayMetrics(DisplayMetrics);

	int32 PrimaryIndex = 0;
	for (int32 i = 0; i < DisplayMetrics.MonitorInfo.Num(); ++i)
	{
		const FMonitorInfo& I = DisplayMetrics.MonitorInfo[i];
		OutDisplays.Add({ I.ID, I.Name, I.bIsPrimary });
		if (I.bIsPrimary)
		{
			PrimaryIndex = i;
		}
	}

	OutCurrentIndex = DisplayMetrics.MonitorInfo.IsValidIndex(Display) ? Display : PrimaryIndex;
}

The monitor to be used is stored in the property int32 Display, refrencing an index in this array. When it’s time to apply those settings, the game window is moved to the monitor’s position:

void UPoetGameUserSettings::ApplyNonResolutionSettings()
{
	Super::ApplyNonResolutionSettings();

	...

#if !WITH_EDITOR
	FDisplayMetrics DisplayMetrics;
	FSlateApplication::Get().GetDisplayMetrics(DisplayMetrics);
	if (DisplayMetrics.MonitorInfo.IsValidIndex(Display))
	{
		const FMonitorInfo& Info = DisplayMetrics.MonitorInfo[Display];
		const FIntPoint Res = GetScreenResolution();
		if (Res.X > Info.NativeWidth || Res.Y > Info.NativeHeight)
		{
			SetScreenResolution(FIntPoint(Info.NativeWidth, Info.NativeHeight));
		}

		SetWindowPosition(Info.WorkArea.Left, Info.WorkArea.Top);
		if (GEngine && GEngine->GameViewport)
		{
			ApplyWindowPosition();
		}
		else
		{
			DelegateHandleViewportCreated = UGameViewportClient::OnViewportCreated().AddUObject(this, &UPoetGameUserSettings::ApplyWindowPosition);
		}
	}
#endif
}

The WITH_EDITOR check is there to prevent the window from moving when we’re working in the editor.

Edit: Oh yeah, and FDisplayInfo is a custom struct to store just the data we’re interested in:

USTRUCT(BlueprintType)
struct FDisplayInfo
{
	GENERATED_BODY()

	UPROPERTY(BlueprintReadOnly)
	FString ID;

	UPROPERTY(BlueprintReadOnly)
	FString Name;

	UPROPERTY(BlueprintReadOnly)
	bool bIsPrimary;
};
3 Likes

Thank you for the very descriptive answer. Would you mind explaining a bit about what the function ApplyWindowPosition does? I’m having trouble capturing the mouse inside the window after it switches monitor, maybe there’s something in there that I need to take into account? At least, it’s not really clear to me why I need the GEngine here… :slight_smile:

Correct me if I’m wrong @and-rad , and otherwise for the public enjoyment - I did manage to get something working. So in case the next person is wondering:

UCLASS(BlueprintType)
class MY_GRAPHICS_API UMyGraphicsManager final : public UObject
{
	GENERATED_BODY()

public:
	UFUNCTION(BlueprintCallable, Category=GF_CATEGORY_GRAPHICS)
	void Init();

	UFUNCTION(BlueprintCallable, Category=GF_CATEGORY_GRAPHICS)
	TArray<FString> GetAvailableMonitorNames(int32& OutCurrentIndex);

	UFUNCTION(BlueprintCallable, Category=GF_CATEGORY_GRAPHICS)
	void SelectMonitor(const FString& Name);
private:
	void ApplyWindowPosition();
	FDelegateHandle mDelegateHandleViewportCreated;

	UPROPERTY()
	UGameUserSettings* mGameUserSettings;

	// Check for command-line overrides when applying graphics user settings
	static constexpr bool skCheckForCommandLineOverrides = false;

	// Custom class that wraps around an object inherited from USaveGame,
	// so we can store settings not covered by UGameUserSettings, such as
	// name of selected monitor!
	UPROPERTY()
	UMyGraphicsSaveGame* mGraphicsSaveGame;
}

I was looking for something that could be used in a widget blueprint so that I could create a menu for switching monitor. Our internal usage of this class is more complicated, but I thought I’d show something more usable for others. The implementation:

void UMyGraphicsManager::Init()
{
	mGameUserSettings = UGameUserSettings::GetGameUserSettings();
	mGameUserSettings->LoadSettings(ForceReload);
	mGameUserSettings->ApplySettings(skCheckForCommandLineOverrides);
	mGraphicsSaveGame->Init();
	mGraphicsSaveGame->LoadData();
}

TArray<FString> UMyGraphicsManager::GetAvailableMonitorNames(int32& OutCurrentIndex)
{
	FDisplayMetrics displayMetrics;
	FSlateApplication::Get().GetDisplayMetrics(displayMetrics);

	const FString& savedMonitorName = mGraphicsSaveGame->GetData()->MonitorName;
	const int32 num = displayMetrics.MonitorInfo.Num();

	int32 chosenIndex = -1;
	int32 primaryIndex = 0;

	TArray<FString> ret;
	for (int32 i = 0; i < num; i++)
	{
		FMonitorInfo& info = displayMetrics.MonitorInfo[i];
		if (info.Name == savedMonitorName)
		{
			chosenIndex = i;
		}
		if (info.bIsPrimary) { primaryIndex = i; }
		ret.Add(info.Name);
	}

	OutCurrentIndex = (chosenIndex < 0) ? primaryIndex : chosenIndex;
	return ret;
}

void UMyGraphicsManager::SelectMonitor(const FString& Name)
{
#if !WITH_EDITOR
	FDisplayMetrics displayMetrics;
	FSlateApplication::Get().GetDisplayMetrics(displayMetrics);
	const int32 num = displayMetrics.MonitorInfo.Num();

	int32 chosenIndex = -1;
	int32 primaryIndex = 0;

	for (int32 i = 0; i < num; i++)
	{
		FMonitorInfo& info = displayMetrics.MonitorInfo[i];
		if (info.Name == Name)
		{
			chosenIndex = i;
			break;
		}
		if (info.bIsPrimary) { primaryIndex = i; }
	}

	int32 finalIndex = (chosenIndex < 0) ? primaryIndex : chosenIndex;
	if (chosenIndex < 0 && !Name.IsEmpty())
	{
		// NOTE: Log something here
	}

	// TODO: Some of these things are probably not relevant for windowed windows!

	const FMonitorInfo& info = displayMetrics.MonitorInfo[finalIndex];
	const FIntPoint resolution = mGameUserSettings->GetScreenResolution();
	if (resolution.X > info.NativeWidth || resolution.Y > info.NativeHeight)
	{
		mGameUserSettings->SetScreenResolution({ info.NativeWidth, info.NativeHeight });
	}
	mGameUserSettings->SetWindowPosition(info.WorkArea.Left, info.WorkArea.Top);

	if (GEngine && GEngine->GameViewport)
	{
		ApplyWindowPosition();
	}
	else
	{
		mDelegateHandleViewportCreated = UGameViewportClient::OnViewportCreated().AddUObject(
			this, &UMyGraphicsManager::ApplyWindowPosition);
	}

	mGraphicsSaveGame->GetData()->MonitorName = info.Name;
	mGraphicsSaveGame->SaveData();
#endif
}

void UMyGraphicsManager::ApplyWindowPosition()
{
	TSharedPtr<SWindow> window = GEngine->GameViewport->GetWindow();
	window->MoveWindowTo(mGameUserSettings->GetWindowPosition());
	window->Resize(mGameUserSettings->GetScreenResolution());
	window->SetWindowMode(EWindowMode::Fullscreen);

	mGameUserSettings->SetFullscreenMode(EWindowMode::Fullscreen);
	mGameUserSettings->ApplyResolutionSettings(skCheckForCommandLineOverrides);

	if (mDelegateHandleViewportCreated.IsValid())
	{
		UGameViewportClient::OnViewportCreated().Remove(mDelegateHandleViewportCreated);
		mDelegateHandleViewportCreated.Reset();
	}
}

Slightly verbose post, but I wanted to show my findings as complete as possible. I find this setup extremely complicated, and I’m probably making a lot of mistakes. But for now it seems to be working okay “on my machine”.


Next thing should be to set up some feedback from the UMyGraphicsManager to the blueprint widget, maybe in some sort of callback/delegate, so that the manager can inform the widget to refresh its contents.

Any suggestions are very welcome! :slight_smile: (@Roy_Wierer.Seda145 would you happen to know a good solution for this?)

Solution for what? :slight_smile: You said you got it working?

Anyhow the delegate is easy to set up for blueprints. On the header side you write

DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnMyDelegate)

UPROPERTY(BlueprintAssignable, Category = "Delegates")
	FOnMyDelegate OnMyDelegate;

on the cpp binding method of USomeClass to that delegate

OnMyDelegate.AddDynamic(this, &USomeClass::ActOnSomeDelegate);

I suppose you already know that.

I’m just wondering why would you use a savegame uobject why you can write directly to ini files just like GameUserSettings does, and why use monitor names if you can assume indexes. But if it works it works no need to perfect.

1 Like

Hi @Roy_Wierer.Seda145 thank you for answering!

My solution is not working exactly, because if I start up on another display than the primary display then it never gets focus - seemingly no matter what I do. I can’t click on it with the mouse, and I can’t Alt-Tab into it. So there’s some startup logic that I don’t quite understand.

And no, I didn’t actually know how to do the delegate. I will try it. Thanks! :slight_smile:


Is it better to write directly to ini files than using a SaveGame object? I think I have come to the conclusion that it’s better to make a class that derives from GameUserSettings instead of creating a custom SaveGame object.

Wrt saving monitor name, I only did it because indices might change if the user unplugs a monitor or something like that. Saving the monitor name works fine, and if the saved monitor name doesn’t exist I fallback to primary monitor. This system works, except that I can’t get focus on the monitor…

Haven’t ran into this issue yet, that sounds like something that just shouldn’t happen out of the box on a clean project. My thoughts is that you might be able to set display preferences through the RHI library but I don’t immediately see anything here. I access that through the global pointer GDynamicRHI when I need to play with resolution values.

It’s simpler to do, read, write, share. It can be accessable to end users by just notepad. I’d say it is the preferred method and the gameusersettings does this as well. Must say I avoid anything binary when I can as well because of versioning software and because unreal too often screws them binary files up. The old input system and many project settings also write to ini. Should be:

GConfig->SetString(*Section, *Setting, *Value, GGameUserSettingsIni);
GConfig->Flush(true, GGameUserSettingsIni);

Some others have similar questions and attempts

You don’t have to inherit from it, it’s possible but personally I just write to ini files from my subsystem that manages certain additional settings. I regard the gameusersettings as a tool I control from that subsystem.

Edit*

You might want to see this:

GitHub - YawLighthouse/UMG-Slate-Compendium: A Compendium Information Guide for UMG and Slate in Unreal Engine.

Before diving into that see if your problem exists on a clean project, or if it’s your own bug.

1 Like

Thank you for the hints regarding ini file configs. I will try them out.

Regarding the issues I’m having, it could be an engine bug somewhere. It’s alluded here:

In particular:

Bumping this: from my testing it seems that this issue only occurs if you are using a second monitor? When I run my game on my main monitor, it behaves fine, but if I move it to my second monitor and start changing resolution, then the mouse gets super offset.

From what I can tell, whatever is in charge of mouse position uses the resolution of the first monitor, even when the game is running on a different one with a different resolution?

Maybe there’s a discrepancy between mouse coordinates in UMG and in the viewport. I’m not sure how to “frame” this correctly…

But it does seem like whenever the fullscreen window is moved to another screen with different DPI-scaling that the mouse coordinate system is not being updated properly.
I’m open to ideas how to approach this. For now I will continue to try stuff. :slight_smile:

Also, this issue only occurs when EWindowMode is Fullscreen. For WindowedFullscreen it works, for some reason.


Edit

After “trying stuff”, I have found this logic inside SlateApplication.cpp, seemingly suspicious:

FPointerEvent FSlateApplication::TransformPointerEvent(const FPointerEvent& PointerEvent, const TSharedPtr<SWindow>& Window) const
{
	FPointerEvent TransformedPointerEvent = PointerEvent;
	if (Window)
	{
		if (TransformFullscreenMouseInput && !GIsEditor && Window->GetWindowMode() == EWindowMode::Fullscreen)
		{
			// Screen space mapping scales everything. When window resolution doesn't match platform resolution, 
			// this causes offset cursor hit-tests in fullscreen. Correct in slate since we are first window-aware slate processor.
			FVector2f WindowSize = Window->GetSizeInScreen();
			FVector2f DisplaySize = { (float)CachedDisplayMetrics.PrimaryDisplayWidth, (float)CachedDisplayMetrics.PrimaryDisplayHeight };

			TransformedPointerEvent = FPointerEvent(PointerEvent, PointerEvent.GetScreenSpacePosition() * WindowSize / DisplaySize, PointerEvent.GetLastScreenSpacePosition() * WindowSize / DisplaySize);
		}
	}

	return TransformedPointerEvent;
}

If EWindowMode is Fullscreen, then no matter what the mouse position is offset by the PrimaryDisplay(Width|Height). This at least could explain why it works correctly for WindowedFullscreen. Exact same code is in FSlateApplication::LocateWidgetInWindow.

@Roy_Wierer.Seda145 do you think I’m on to something here? I’m not building the engine from source, so I can’t really change this. I could make a pull request somehow and get other people to review this. I have tried modifying the fields inside CachedDisplayMetrics to set primary display width/height to whatever is current window size, but unfortunately that didn’t solve the problem.

So it seems I can add a variable inside <project>/Config/DefaultEngine.ini:

[ConsoleVariables]
Slate.Transform.FullscreenMouseInput=False

This fixes the problem!

But also, this variable is true by default, and given this description:
"Set true to transform mouse input to account for viewport stretching at fullscreen resolutions not natively supported by the monitor."

So it may be a non-solution. But whatever the case, I still think this is an engine bug. At least the text description should be changed to "[...] not natively supported by the primary monitor."

1 Like

I can’t tell, the system is much much bigger than this. You can only debug it all. Try it on a clean project.

Maybe this is silly but if a user has multiple monitors, and you only display the game on one monitor, why not let the OS handle it and let the user set his main monitor in Windows?

As a user of multiple monitors I can give feedback that I use the secondary purely to display the stuff I don’t want to clutter my main, like logs. If for some reason my secondary was a game monitor or TV I’d just set that to my main for games in Windows.

1 Like

Yes that is a valid point. For some reason I decided it would be cool to have a graphics menu where the user may choose a monitor, and I kind of want that to work as well. Now it works, as long as I use fullscreen resolutions supported by the monitor. Of course, I have created a bug report here, maybe someone from Epic will read it and investigate.

Again, thank you for all your help!