Virtualization-free custom cursor for dpi-awareness PerMonitorV2 win32 app - to avoid ugly auto-scaling.
We obtain HCURSOR by creating an "icon", not by creating a "cursor", so we don't have to call win11 new API SetThreadCursorCreationScaling(CURSOR_CREATION_SCALING_NONE), so this program should run also on earlier versions of win11 and win10.
#pragma once #include <map> /* CCursorDpi typical usage: With visual studio resource view, create a cursor resource containing 32, 48, 64, 96 and 128 monochrome (or color) image types Construct a CCursorDpi object with the ID of the cursor resource above Call Create() on WM_CREATE Call DpiChanged() on WM_DPICHANGED Call GetHCURSOR() on WM_MOUSEMOVE to get the cursor handle for SetCursor() */ class CCursorDpi { protected: bool m_bSuccessfullyConstructed; // cursor size to image ordinal map std::map<int, int> m_mapImages; HCURSOR m_hCursor; int m_nCursorSize; bool m_bDpiChanged; HWND m_hWnd; public: CCursorDpi(int idCursor); bool IsSuccessfullyConstructed() { return m_bSuccessfullyConstructed; } ~CCursorDpi(); BOOL Create(HWND hWnd); BOOL DpiChanged(); HCURSOR GetHCURSOR(); protected: BOOL CreateCursorFromResource(int cxCursor, int nOrdinal); BOOL FindCursorSize(int cxCursorWanted, int& cxCursor, int& nOrdinal); };
#include <Windows.h> #include "CursorDpi.h" #if !defined(ASSERT) #include <cassert> #define ASSERT assert #endif #pragma pack(2) typedef struct { WORD Reserved; WORD ResType; WORD ResCount; } NEWHEADER; typedef struct { BYTE Width; BYTE Height; BYTE ColorCount; BYTE reserved; } ICONRESDIR; typedef struct { WORD Width; WORD Height; } CURSORDIR; typedef struct { union { ICONRESDIR Icon; CURSORDIR Cursor; }; WORD Planes; WORD BitCount; DWORD BytesInRes; WORD IconCursorId; } RESDIR; typedef struct { WORD xHotSpot; WORD yHotSpot; } LOCALHEADER; #pragma pack() /////////////////////////////////////////////////////////////////////////// CCursorDpi::CCursorDpi(int idCursor) : m_hCursor(NULL) , m_bDpiChanged(false) , m_nCursorSize(0) , m_hWnd(NULL) { m_bSuccessfullyConstructed = false; { // load group cursor resource HRSRC hrsrc = FindResource(NULL, MAKEINTRESOURCE(idCursor), RT_GROUP_CURSOR); if (hrsrc) { HGLOBAL h = LoadResource(NULL, hrsrc); if (h) { BYTE* p = (BYTE*)LockResource(h); if (p) { NEWHEADER* pNewHeader = (NEWHEADER*)p; if (pNewHeader->ResType == 2) { p += sizeof(NEWHEADER); RESDIR* pResDirs = (RESDIR*)p; for (int i = 0; i < (int)pNewHeader->ResCount; i++) { if (m_mapImages.find(pResDirs[i].Cursor.Width) == m_mapImages.end()) { // set image ordinal for cursor size m_mapImages[pResDirs[i].Cursor.Width] = pResDirs[i].IconCursorId; } else { ASSERT(0); // duplicate cursor size } } } else { ASSERT(pNewHeader->ResType == 2); // must be cursor return; } UnlockResource(h); } FreeResource(h); } } } if (m_mapImages.empty()) { ASSERT(0); return; } m_bSuccessfullyConstructed = true; } CCursorDpi::~CCursorDpi() { if (m_hCursor) DestroyIcon(m_hCursor); } BOOL CCursorDpi::Create(HWND hWnd) { if (m_hWnd) { ASSERT(0); // creating twice not assumed return FALSE; } m_hWnd = hWnd; return TRUE; } BOOL CCursorDpi::DpiChanged() { ASSERT(m_hWnd); m_bDpiChanged = true; return TRUE; } HCURSOR CCursorDpi::GetHCURSOR() { ASSERT(m_hWnd); if (!m_hCursor || m_bDpiChanged) { int nDpi = GetDpiForWindow(m_hWnd); int cxCursorWanted = GetSystemMetricsForDpi(SM_CXCURSOR, nDpi); int cxCursor, nOrdinal; FindCursorSize(cxCursorWanted, cxCursor, nOrdinal); if (m_nCursorSize != cxCursor) { CreateCursorFromResource(cxCursor, nOrdinal); } m_bDpiChanged = false; } return m_hCursor; } BOOL CCursorDpi::CreateCursorFromResource(int cxCursor, int nOrdinal) { ASSERT(m_hWnd); if (m_hCursor) { DestroyIcon(m_hCursor); m_hCursor = NULL; } HRSRC hrsrc = FindResource(NULL, MAKEINTRESOURCE(nOrdinal), RT_CURSOR); if (hrsrc) { HGLOBAL h = LoadResource(NULL, hrsrc); if (h) { BYTE* p = (BYTE*)LockResource(h); if (p) { LOCALHEADER* pLocalHeader = (LOCALHEADER*)p; p += sizeof(LOCALHEADER); BITMAPINFO* pInfo = (BITMAPINFO*)p; HBITMAP hbmColor = NULL; HBITMAP hbmMask = NULL; bool bBitmapSuccess = false; // calculate color table size int cbColorTable = 0; if (pInfo->bmiHeader.biCompression == BI_RGB && pInfo->bmiHeader.biBitCount <= 8) { // has color table if (pInfo->bmiHeader.biClrUsed) { cbColorTable = pInfo->bmiHeader.biClrUsed * (int)sizeof(RGBQUAD); } else { cbColorTable = (1 << pInfo->bmiHeader.biBitCount) * (int)sizeof(RGBQUAD); } } int cbImageDataOffset = sizeof(BITMAPINFO) - sizeof(RGBQUAD) + cbColorTable; BYTE* pImageData = (BYTE*)pInfo + cbImageDataOffset; int cx = pInfo->bmiHeader.biWidth; int cy = abs(pInfo->bmiHeader.biHeight); int stride = ((((cx * pInfo->bmiHeader.biBitCount) + 31) & ~31) >> 3); if (pInfo->bmiHeader.biBitCount == 1) { // monochrome cursor consisits of one double height bitmap void* pBits = nullptr; hbmMask = CreateDIBSection(NULL, pInfo, DIB_RGB_COLORS, &pBits, NULL, 0); if (pBits) { memcpy(pBits, pImageData, stride * cy); } hbmColor = NULL; if (hbmMask) { bBitmapSuccess = true; } } else { cy /= 2; BYTE* pMaskData = pImageData + stride * cy; // color cursor consists of one color bitmap and one monochrome mask bitmap // bits of both bitmaps are strangely packed in one color bitmap { // color bitmap // make a copy of BITMAPINFO including Color Table to correct bitmap size BITMAPINFO* pInfoCopied = (BITMAPINFO*)malloc(cbImageDataOffset); if (pInfoCopied) { memcpy(pInfoCopied, pInfo, cbImageDataOffset); pInfoCopied->bmiHeader.biHeight /= 2; pInfoCopied->bmiHeader.biSizeImage = stride * cy; void* pBits = nullptr; hbmColor = CreateDIBSection(NULL, pInfoCopied, cbColorTable ? DIB_RGB_COLORS : 0, &pBits, NULL, 0); if (pBits) { memcpy(pBits, pImageData, stride * cy); } free(pInfoCopied); } } { // monochrome mask bitmap // calculate stride and bitmapdata size int strideMask = ((((pInfo->bmiHeader.biWidth) + 31) & ~31) >> 3); // make a copy of BITMAPINFO to correct bitmap size, bitcount, etc size_t cbInfo = sizeof(BITMAPINFO) + sizeof(RGBQUAD) * (2 - 1); BITMAPINFO* pInfoCopied = (BITMAPINFO*)malloc(cbInfo); if (pInfoCopied) { memcpy(pInfoCopied, pInfo, sizeof(BITMAPINFOHEADER)); pInfoCopied->bmiHeader.biSize = (int)cbInfo; pInfoCopied->bmiHeader.biHeight /= 2; pInfoCopied->bmiHeader.biBitCount = 1; pInfoCopied->bmiHeader.biSizeImage = strideMask * cy; pInfoCopied->bmiColors[0] = RGBQUAD{}; pInfoCopied->bmiColors[1] = RGBQUAD{ 255, 255, 255, 0 }; void* pBits = nullptr; hbmMask = CreateDIBSection(NULL, pInfoCopied, DIB_RGB_COLORS, &pBits, NULL, 0); if (pBits) { // stride is rounded up to 32 bit boundary DWORD* pDst = (DWORD*)pBits; DWORD* pSrc = (DWORD*)pMaskData; int c = (strideMask * cy) / 4; for (int i = 0; i < c; i++) { *(pDst++) = ~*(pSrc++); } } free(pInfoCopied); } } if (hbmColor && hbmMask) { bBitmapSuccess = true; } } if (bBitmapSuccess) { ICONINFO ii = {}; ii.xHotspot = pLocalHeader->xHotSpot; ii.yHotspot = pLocalHeader->yHotSpot; ii.fIcon = FALSE; ii.hbmColor = hbmColor; ii.hbmMask = hbmMask; m_hCursor = CreateIconIndirect(&ii); } if (hbmColor) DeleteObject(hbmColor); if (hbmMask) DeleteObject(hbmMask); UnlockResource(h); } FreeResource(h); } } else { ASSERT(0); // something went wrong } if (m_hCursor) { m_nCursorSize = cxCursor; return TRUE; } else { return FALSE; } } BOOL CCursorDpi::FindCursorSize(int cxCursorWanted, int& cxCursor, int& nOrdinal) { if (m_mapImages.empty()) { cxCursor = 0; nOrdinal = 0; return FALSE; } // find cursor image that is smaller or equal to cursor size wanted auto iter = m_mapImages.begin(); cxCursor = iter->first; nOrdinal = iter->second; iter++; for (; iter != m_mapImages.end(); iter++) { if (iter->first > cxCursorWanted) { break; } cxCursor = iter->first; nOrdinal = iter->second; } return TRUE; }