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;
}
}