[ TUTORIAL / C++ Advanced ] Custom hardware cursor loading for Win and Mac

I decided to create this tutorial when I got answer from one of the UE4 devs that custom hardware cursor feature won’t be added and they’re working at some software implementation instead to make it possible for those who lacks special OS features knowledge.

But for those who still want lag-free custom hardware cursor I share my code which was already tested on Win 7 and OS X 10.9. Let’s go!

But first let’s define some requirements:

  • This solution is for source-based builds, because we’re gonna change engine source code.
  • It’s for 4.4 version. I haven’t tested it in 4.3 and tested windows cursor in 4.5 preview.
  • We will use .cur and .ani types for Windows platform and .png for OS X (but you can feel free to extend functionality by using third party libs like SDL_image)
  • We’ll take hotspot coordinates from cursor resource in Windows and will manually enter them in OS X.

Ok, let’s edit basic engine cursor interface and add a couple of functions there. Open file [FONT=Courier New]Engine/Source/Runtime/Core/Public/GenericPlatform/ICursor.h and add inside [FONT=Courier New]class ICursor:


public:
	/* Sets image from file for cursor of certain type */
	virtual void SetCustomCursor(EMouseCursor::Type CursorToChange, FString PathToCursor, FIntPoint CursorHotSpot) = 0;

	/* Sets default image for cursor of certain type */
	virtual void ResetCursor(EMouseCursor::Type CursorToReset) = 0;

Next, let’s implement these functions for both platforms. First lets edit [FONT=Courier New]Engine/Source/Runtime/Core/Public/Windows/WindowsCursor.h and [FONT=Courier New]Engine/Source/Runtime/Core/Private/Windows/WindowsCursor.cpp:

.h


public:
	virtual void SetCustomCursor(EMouseCursor::Type CursorToChange, FString PathToCursor, FIntPoint CursorHotSpot) override;

	virtual void ResetCursor(EMouseCursor::Type CursorToReset) override;

.cpp


void FWindowsCursor::SetCustomCursor(EMouseCursor::Type CursorToChange, FString PathToCursor, FIntPoint CursorHotSpot)
{
	// Check if file exists and it is .cur or .ani type.
	if (IFileManager::Get().FileSize(*PathToCursor) == INDEX_NONE || !(PathToCursor.EndsWith(TEXT(".cur")) || PathToCursor.EndsWith(TEXT(".ani"))))
	{
		return;
	}

	// Loading resource from file. We ignore CursorHotSpot parameter and taking hotspot coords from resource.
	HCURSOR CursorHandle = LoadCursorFromFile((LPCTSTR)*PathToCursor);
    CursorHandles[CursorToChange] = CursorHandle;
}

void FWindowsCursor::ResetCursor(EMouseCursor::Type CursorToReset)
{
	HCURSOR CursorHandle = NULL;
	switch (CursorToReset)
	{
	case EMouseCursor::None:
		// The mouse cursor will not be visible when None is used
		break;

	case EMouseCursor::Default:
		CursorHandle = ::LoadCursor(NULL, IDC_ARROW);
		break;

	case EMouseCursor::TextEditBeam:
		CursorHandle = ::LoadCursor(NULL, IDC_IBEAM);
		break;

	case EMouseCursor::ResizeLeftRight:
		CursorHandle = ::LoadCursor(NULL, IDC_SIZEWE);
		break;

	case EMouseCursor::ResizeUpDown:
		CursorHandle = ::LoadCursor(NULL, IDC_SIZENS);
		break;

	case EMouseCursor::ResizeSouthEast:
		CursorHandle = ::LoadCursor(NULL, IDC_SIZENWSE);
		break;

	case EMouseCursor::ResizeSouthWest:
		CursorHandle = ::LoadCursor(NULL, IDC_SIZENESW);
		break;

	case EMouseCursor::CardinalCross:
		CursorHandle = ::LoadCursor(NULL, IDC_SIZEALL);
		break;

	case EMouseCursor::Crosshairs:
		CursorHandle = ::LoadCursor(NULL, IDC_CROSS);
		break;

	case EMouseCursor::Hand:
		CursorHandle = ::LoadCursor(NULL, IDC_HAND);
		break;

	case EMouseCursor::GrabHand:
		CursorHandle = LoadCursorFromFile((LPCTSTR)*(FString(FPlatformProcess::BaseDir()) / FString::Printf(TEXT("%sEditor/Slate/Cursor/grabhand.cur"), *FPaths::EngineContentDir())));
		if (CursorHandle == NULL)
		{
			// Failed to load file, fall back
			CursorHandle = ::LoadCursor(NULL, IDC_HAND);
		}
		break;

	case EMouseCursor::GrabHandClosed:
		CursorHandle = LoadCursorFromFile((LPCTSTR)*(FString(FPlatformProcess::BaseDir()) / FString::Printf(TEXT("%sEditor/Slate/Cursor/grabhand_closed.cur"), *FPaths::EngineContentDir())));
		if (CursorHandle == NULL)
		{
			// Failed to load file, fall back
			CursorHandle = ::LoadCursor(NULL, IDC_HAND);
		}
		break;

	case EMouseCursor::SlashedCircle:
		CursorHandle = ::LoadCursor(NULL, IDC_NO);
		break;

	case EMouseCursor::EyeDropper:
		CursorHandle = LoadCursorFromFile((LPCTSTR)*(FString(FPlatformProcess::BaseDir()) / FString::Printf(TEXT("%sEditor/Slate/Icons/eyedropper.cur"), *FPaths::EngineContentDir())));
		break;

		// NOTE: For custom app cursors, use:
		//		CursorHandle = ::LoadCursor( InstanceHandle, (LPCWSTR)MY_RESOURCE_ID );

	default:
		// Unrecognized cursor type!
		check(0);
		break;
	}

	if (CursorHandle != NULL)
	{
		CursorHandles[CursorToReset] = CursorHandle;
	}
}

Ok, now it’s Mac’s turn. Edit [FONT=Courier New]Engine/Source/Runtime/Core/Public/Mac/MacCursor.h and [FONT=Courier New]Engine/Source/Runtime/Core/Private/Mac/MacCursor.cpp:

.h


public:
	virtual void SetCustomCursor(EMouseCursor::Type CursorToChange, FString PathToCursor, FIntPoint CursorHotSpot);

	virtual void ResetCursor(EMouseCursor::Type CursorToReset);

.cpp


void FMacCursor::SetCustomCursor(EMouseCursor::Type CursorToChange, FString PathToCursor, FIntPoint CursorHotSpot)
{
    // Check if file exists and is png type
	if (IFileManager::Get().FileSize(*PathToCursor) == INDEX_NONE || !PathToCursor.EndsWith(TEXT(".png")))
	{
		return;
	}

	// Loading cursor image from file
    NSImage *CursorImage = [NSImage alloc] initWithContentsOfFile:PathToCursor.GetNSString()];
	NSCursor *CursorHandle = [NSCursor alloc] initWithImage:CursorImage hotSpot : NSMakePoint(CursorHotSpot.X, CursorHotSpot.Y)];
	[CursorImage release];

	CursorHandles[CursorToChange] = CursorHandle;
}

void FMacCursor::ResetCursor(EMouseCursor::Type CursorToReset)
{
	NSCursor *CursorHandle = NULL;

	switch (CursorToReset)
	{
	case EMouseCursor::None:
		break;

	case EMouseCursor::Default:
		CursorHandle = [NSCursor arrowCursor];
		break;

	case EMouseCursor::TextEditBeam:
		CursorHandle = [NSCursor IBeamCursor];
		break;

	case EMouseCursor::ResizeLeftRight:
		CursorHandle = [NSCursor resizeLeftRightCursor];
		break;

	case EMouseCursor::ResizeUpDown:
		CursorHandle = [NSCursor resizeUpDownCursor];
		break;

	case EMouseCursor::ResizeSouthEast:
	{
		FString Path = FString::Printf(TEXT("%s%sEditor/Slate/Cursor/SouthEastCursor.png"), FPlatformProcess::BaseDir(), *FPaths::EngineContentDir());
		NSImage* CursorImage = [NSImage alloc] initWithContentsOfFile:Path.GetNSString()];
		CursorHandle = [NSCursor alloc] initWithImage:CursorImage hotSpot : NSMakePoint(8, 8)];
		[CursorImage release];
		break;
	}

	case EMouseCursor::ResizeSouthWest:
	{
		FString Path = FString::Printf(TEXT("%s%sEditor/Slate/Cursor/SouthWestCursor.png"), FPlatformProcess::BaseDir(), *FPaths::EngineContentDir());
		NSImage* CursorImage = [NSImage alloc] initWithContentsOfFile:Path.GetNSString()];
		CursorHandle = [NSCursor alloc] initWithImage:CursorImage hotSpot : NSMakePoint(8, 8)];
		[CursorImage release];
		break;
	}

	case EMouseCursor::CardinalCross:
	{
		FString Path = FString::Printf(TEXT("%s%sEditor/Slate/Cursor/CardinalCrossCursor.png"), FPlatformProcess::BaseDir(), *FPaths::EngineContentDir());
		NSImage* CursorImage = [NSImage alloc] initWithContentsOfFile:Path.GetNSString()];
		CursorHandle = [NSCursor alloc] initWithImage:CursorImage hotSpot : NSMakePoint(8, 8)];
		[CursorImage release];
		break;
	}

	case EMouseCursor::Crosshairs:
		CursorHandle = [NSCursor crosshairCursor];
		break;

	case EMouseCursor::Hand:
		CursorHandle = [NSCursor pointingHandCursor];
		break;

	case EMouseCursor::GrabHand:
		CursorHandle = [NSCursor openHandCursor];
		break;

	case EMouseCursor::GrabHandClosed:
		CursorHandle = [NSCursor closedHandCursor];
		break;

	case EMouseCursor::SlashedCircle:
		CursorHandle = [NSCursor operationNotAllowedCursor];
		break;

	case EMouseCursor::EyeDropper:
	{
		FString Path = FString::Printf(TEXT("%s%sEditor/Slate/Cursor/EyeDropperCursor.png"), FPlatformProcess::BaseDir(), *FPaths::EngineContentDir());
		NSImage* CursorImage = [NSImage alloc] initWithContentsOfFile:Path.GetNSString()];
		CursorHandle = [NSCursor alloc] initWithImage:CursorImage hotSpot : NSMakePoint(1, 17)];
		[CursorImage release];
		break;
	}

	default:
		// Unrecognized cursor type!
		check(0);
		break;
	}

	if (CursorHandle != NULL)
	{
		CursorHandles[CursorToReset] = CursorHandle;
	}
}

So, those are code snippets for two platforms. Now we just need to make our Slate system use it. The best place to get reference of our Platform application with cursor class is SlateApplication class. We’re gonna edit [FONT=Courier New]Engine/Source/Runtime/Slate/Public/Framework/Application/SlateApplication.h and [FONT=Courier New]Engine/Source/Runtime/Slate/Private/Framework/Application/SlateApplication.cpp:

.h (put everything inside of [FONT=Courier New]class SLATE_API FSlateApplication)


	// Custom Cursors setup
public:
	/**
	* Sets the image of the cursor.
	*
	* @param CursorToChange		The type of cursor to set image for.
	* @param PathToImageFile	The path to the cursor image file.
	* @param CursorHotSpotThe coordinates of cursor hotspot
	*/
	virtual void SetCustomCursor(EMouseCursor::Type CursorToChange, FString PathToImageFile, FIntPoint CursorHotSpot = FIntPoint(0,0));

	/**
	* Sets the default image of cursor.
	*
	* @param CursorToReset		The type of cursor to set its default image for.
	*/
	virtual void ResetCursor(EMouseCursor::Type CursorToReset);

	// End Custom Cursor Setup

.cpp


void FSlateApplication::SetCustomCursor(EMouseCursor::Type CursorToChange, FString PathToImageFile, FIntPoint CursorHotSpot)
{
	if (PlatformApplication->Cursor.IsValid())
	{
		PlatformApplication->Cursor.Get()->SetCustomCursor(CursorToChange, PathToImageFile, CursorHotSpot);
	}
}

void FSlateApplication::ResetCursor(EMouseCursor::Type CursorToReset)
{
	if (PlatformApplication->Cursor.IsValid())
	{
		PlatformApplication->Cursor.Get()->ResetCursor(CursorToReset);
	}
}

That’s it! Done. Build your engine and enjoy. =)

Here’s an example of how to use it. Let’s assume that we are on Windows platform and we have set of cursors in our [FONT=Courier New]Content/Cursors folder:


FString Path = FPaths::GameContentDir() / "Cursors";

FSlateApplication::Get().SetCustomCursor(EMouseCursor::Default,			Path / "Default.cur");
FSlateApplication::Get().SetCustomCursor(EMouseCursor::TextEditBeam,		Path / "TextEditBeam.cur");
FSlateApplication::Get().SetCustomCursor(EMouseCursor::ResizeLeftRight,		Path / "ResizeLeftRight.cur");
FSlateApplication::Get().SetCustomCursor(EMouseCursor::ResizeUpDown,		Path / "ResizeUpDown.cur");
FSlateApplication::Get().SetCustomCursor(EMouseCursor::ResizeSouthEast,		Path / "ResizeSouthEast.cur");
FSlateApplication::Get().SetCustomCursor(EMouseCursor::ResizeSouthWest,		Path / "ResizeSouthWest.cur");
FSlateApplication::Get().SetCustomCursor(EMouseCursor::CardinalCross,		Path / "CardinalCross.cur");
FSlateApplication::Get().SetCustomCursor(EMouseCursor::Crosshairs,		Path / "Crosshairs.cur");
FSlateApplication::Get().SetCustomCursor(EMouseCursor::Hand,			Path / "Hand.cur");
FSlateApplication::Get().SetCustomCursor(EMouseCursor::GrabHand,		Path / "GrabHand.cur");
FSlateApplication::Get().SetCustomCursor(EMouseCursor::GrabHandClosed,		Path / "GrabHandClosed.cur");
FSlateApplication::Get().SetCustomCursor(EMouseCursor::SlashedCircle,		Path / "SlashedCircle.cur");
FSlateApplication::Get().SetCustomCursor(EMouseCursor::EyeDropper,		Path / "EyeDropper.cur");

[HR][/HR]
P.S. I tried to make Linux implementation but could make it just half-working. The cursor image is loaded but its colors are blended and cursor looks weird. I don’t have enough knowledge so I leave it for those who can do it.
I included SDL_image library in Engine source and changed [FONT=Courier New]Engine/Source/Runtime/Core/Core.Build.cs to add it in AddThirdPartyPrivateStaticDependencies.
Here are LinuxCursor .h and cpp snippets:

.h


public:
	virtual void SetCustomCursor(EMouseCursor::Type CursorToChange, FString PathToCursor, FIntPoint CursorHotSpot) override;

	virtual void ResetCursor(EMouseCursor::Type CursorToReset) override;

.cpp


void FLinuxCursor::SetCustomCursor(EMouseCursor::Type CursorToChange, FString PathToCursor, FIntPoint CursorHotSpot)
{
    // Check if file exists
	if (IFileManager::Get().FileSize(*PathToCursor) == INDEX_NONE) return;

	SDL_RWops* Src = SDL_RWFromFile(TCHAR_TO_UTF8(*PathToCursor), "rb");

	if (IMG_isPNG(Src))
	{
		SDL_Surface* CursorImage = IMG_LoadTyped_RW(Src, 1, "PNG");
		SDL_HCursor  CursorHandle = SDL_CreateColorCursor(CursorImage, CursorHotSpot.X, CursorHotSpot.Y);
		CursorHandles[CursorToChange] = CursorHandle;
	}
	else
	{
		SDL_FreeRW(Src);
	}
}

void FLinuxCursor::ResetCursor(EMouseCursor::Type CursorToReset)
{
    SDL_HCursor CursorHandle = NULL;
    
    switch( CursorToReset )
    {
		case EMouseCursor::None:
			// The mouse cursor will not be visible when None is used
			break;
            
		case EMouseCursor::Default:
			CursorHandle = SDL_CreateSystemCursor( SDL_SYSTEM_CURSOR_ARROW );
			break;
            
		case EMouseCursor::TextEditBeam:
			CursorHandle = SDL_CreateSystemCursor( SDL_SYSTEM_CURSOR_IBEAM );
			break;
            
		case EMouseCursor::ResizeLeftRight:
			CursorHandle = SDL_CreateSystemCursor( SDL_SYSTEM_CURSOR_SIZEWE );
			break;
            
		case EMouseCursor::ResizeUpDown:
			CursorHandle = SDL_CreateSystemCursor( SDL_SYSTEM_CURSOR_SIZENS );
			break;
            
		case EMouseCursor::ResizeSouthEast:
			CursorHandle = SDL_CreateSystemCursor( SDL_SYSTEM_CURSOR_SIZENWSE );
			break;
            
		case EMouseCursor::ResizeSouthWest:
			CursorHandle = SDL_CreateSystemCursor( SDL_SYSTEM_CURSOR_SIZENESW );
			break;
            
		case EMouseCursor::CardinalCross:
			CursorHandle = SDL_CreateSystemCursor( SDL_SYSTEM_CURSOR_SIZEALL );
			break;
            
		case EMouseCursor::Crosshairs:
			CursorHandle = SDL_CreateSystemCursor( SDL_SYSTEM_CURSOR_CROSSHAIR );
			break;
            
		case EMouseCursor::Hand:
			CursorHandle = SDL_CreateSystemCursor( SDL_SYSTEM_CURSOR_HAND );
			break;
            
		case EMouseCursor::GrabHand:
			//CursorHandle = LoadCursorFromFile((LPCTSTR)*(FString( FPlatformProcess::BaseDir() ) / FString::Printf( TEXT("%sEditor/Slate/Old/grabhand.cur"), *FPaths::EngineContentDir() )));
			//if (CursorHandle == NULL)
			//{
			//	// Failed to load file, fall back
            CursorHandle = SDL_CreateSystemCursor( SDL_SYSTEM_CURSOR_HAND );
			//}
			break;
            
		case EMouseCursor::GrabHandClosed:
			//CursorHandle = LoadCursorFromFile((LPCTSTR)*(FString( FPlatformProcess::BaseDir() ) / FString::Printf( TEXT("%sEditor/Slate/Old/grabhand_closed.cur"), *FPaths::EngineContentDir() )));
			//if (CursorHandle == NULL)
			//{
			//	// Failed to load file, fall back
            CursorHandle = SDL_CreateSystemCursor( SDL_SYSTEM_CURSOR_HAND );
			//}
			break;
            
		case EMouseCursor::SlashedCircle:
			CursorHandle = SDL_CreateSystemCursor( SDL_SYSTEM_CURSOR_NO );
			break;
            
		case EMouseCursor::EyeDropper:
			//CursorHandle = LoadCursorFromFile((LPCTSTR)*(FString( FPlatformProcess::BaseDir() ) / FString::Printf( TEXT("%sEditor/Slate/Icons/eyedropper.cur"), *FPaths::EngineContentDir() )));
			break;
            
			// NOTE: For custom app cursors, use:
			//		CursorHandle = ::LoadCursor( InstanceHandle, (LPCWSTR)MY_RESOURCE_ID );
            
		default:
			// Unrecognized cursor type!
			check( 0 );
			break;
    }
    
    if(CursorHandle != NULL)
    {
        CursorHandles[CursorToReset] = CursorHandle;
    }
}

Nice work, this seems really useful for getting that extra level of polish in games that use a cursor.

Thanks a lot for sharing! Very cool.

Update: Just tried this out under Windows 8.1 64-bit and it worked exactly as advertised. Thanks again!

Yes, I can also add that this works on versions up to 4.6.1 as intended. Thanks to Epic who doesn’t play with this part of code making this solution stable.

I added the mentioned code and rebuilt the engine. I don’t know what to do with the last part:

Where to put this?
And where do I put my .cur files? In the engine/content/cursors or in my project folder/content/cursors ?

You put this anywhere inside your code where you want your cursors to be set. It could be some init function or player choosing a faction procedure and his GUI changes according to that or anywhere else.

FPaths::GameContentDir() return a path to your cooked Content folder so in my example you should put cur files in Content/Cursor folder and include that folder in project build settings to be copied to cooked content folder.
Or you can use any other path of your file system. If you can get your game install folder other way you can use that.

If I put this in my HUD-Class, I am getting "use of undefined type ‘FSlateApplication’

Did you set up Slate correctly?
You need to add

PublicDependencyModuleNames.AddRange(new string] { "Slate", "SlateCore" });

to your project’s Build.cs file. And also you need to include SlateBasics.h in your source file.
I purposely omitted that in example assuming Slate is already set up.

Thanks Tim! That works now.

I actually tried another thing to re-enabling mouse-look after disabling the cursor (without having to click) writing the following lines:

That didn’t work for me. But I think this is getting OT.

EDIT: Nevermind, it works… :slight_smile:

Just wanted to confirm that this still works with the most recent dev source build. Thanks a bunch!

This is awsome@! yes, awsome!
Thanks for sharing.

For the people that have implemented this, I am curious if you are having a similar issue. If the mouse state changes and the mouse does not change position, it doesn’t update. For instance, in our game, if a unit moves under the mouse, the call to change the cursor fires and but the actual image updating isn’t done until the mouse moves at least 1 pixel. I have more time to look into it today so I will figure out exactly what is going wrong and post back here. In the meantime however, if anyone has come across a similar problem and knows how to fix it, please share.

Sorry for being absent. Email notifications stopped coming. Don’t know why.

For cursor not redrawn fix you could use pseudo movement for mouse cursor by using: [FONT=Courier New]FSlateApplication::Get()->SetCursorPos(MouseCoordinates), where [FONT=Courier New]MouseCoordinates is current mouse coordinates you get by [FONT=Courier New]GetMousePosition(). This should force cursor redraw.
I can’t check if that works now because currently I don’t have a working copy of UE build. But if my memory serves me right I fixed that cursor behaviour by using method I’ve just explained.