Loading Visual Styles Per-Application
Mar 22, 2024 10:08:17 GMT -8
Post by ephemeralViolette on Mar 22, 2024 10:08:17 GMT -8
It is possible for an application to load a style on a per-application basis using an undocumented API. The most well-known example is probably StartIsBack, but just any application can do it fairly easily.
Note: This will not override the way default controls are drawn. The window decorations (drawn by DWM and/or UxTheme directly) and default controls (ComCtl32) will not be overridden without additional hooking. A Windhawk mod provided at the bottom of this post serves as a proof of concept for overriding all of these (except DWM server-side frames).
The method described in this post applies to modern versions of Windows. Specifically, Windows 10 and 11, but it probably applies to any NT 6 as well. For XP, undocumented ordinal export #2 (OpenThemeFile) could be used for a similar purpose, but I have not tried it personally. Here is a link to the ReactOS implementation of this API, but I am not sure if the signatures will match between ReactOS and Windows XP.
Themes are loaded globally and then shared to all applications by default. This reduces the memory usage of individual programs, and also provides some speed advantages. In order to load a theme on your own, you'll need to import a few functions from UxTheme (including undocumented functions), as well as define a few undocumented structures.
The key function for loading a theme is LoaderLoadTheme (ordinal export #92 in UxTheme). If you call this function correctly, you can load any theme (including unsigned ones) into your program. Additionally, GetThemeDefaults (ordinal #7) is very useful for being able to load any arbitrary theme file, and OpenThemeDataFromFile (ordinal #16) is useful for loading theme data in question.
Here are the C signatures for each of these functions:
typedef HRESULT (*GetThemeDefaults_t)(
LPCWSTR pszThemeFileName,
LPWSTR pszColorName,
DWORD dwColorNameLen,
LPWSTR pszSizeName,
DWORD dwSizeNameLen
);
GetThemeDefaults_t GetThemeDefaults;
typedef HRESULT (*LoaderLoadTheme_t)(
HANDLE hThemeFile,
HINSTANCE hThemeLibrary,
LPCWSTR pszThemeFileName,
LPCWSTR pszColorParam,
LPCWSTR pszSizeParam,
OUT HANDLE *hSharableSection,
LPWSTR pszSharableSectionName,
int cchSharableSectionName,
OUT HANDLE *hNonsharableSection,
LPWSTR pszNonsharableSectionName,
int cchNonsharableSectionName,
PVOID pfnCustomLoadHandler,
OUT HANDLE *hReuseSection,
int a,
int b,
BOOL fEmulateGlobal
);
LoaderLoadTheme_t LoaderLoadTheme;
typedef HTHEME (*OpenThemeDataFromFile_t)(
HANDLE hThemeFile,
HWND hWnd,
LPCWSTR pszClassList,
DWORD dwFlags
// DWORD unknown,
// bool a
);
OpenThemeDataFromFile_t OpenThemeDataFromFile;
To call LoaderLoadTheme for any arbitrary theme, you must first call GetThemeDefaults with the path of the theme file. This will return the default colour and size names of the theme. I think that these have been deprecated since Vista (the GUI hasn't been able to configure them since then), but I think that they're still technically supported by the theme engine. If you only need to load a specific theme that you authored, then you can skip this step since you'll already know which colour and size variants to load.
Additionally, you need to map a theme file into memory so that you can pass it to OpenThemeDataFromFile. In order to do this, you will need to have a structure laid out like this:
typedef struct _UXTHEMEFILE
{
char header[7]; // must be "thmfile"
LPVOID sharableSectionView;
HANDLE hSharableSection;
LPVOID nsSectionView;
HANDLE hNsSection;
char end[3]; // must be "end"
} UXTHEMEFILE, *LPUXTHEMEFILE;
In this structure, the views must be mapped views of the sections (MapViewOfFile), and the handles (prefixed with "h") must be the handles themselves. "nsSection" refers to the non-sharable section returned by LoaderLoadTheme.
And with that, you should be able to load any theme that you want into your program.
Here is the source code for an example Windhawk mod which allows you to implement per-application theming. You should get the best results with basic theme, because DWM window frames are not drawn by the application itself, nor does the application have control over them.
// ==WindhawkMod==
// @id uxtheme-per-application-theming
// @name Per-Application Theming
// @description The best mod ever that does great things
// @version 0.1
// @author ephemeralViolette
// @github https://github.com/ephemeralViolette
// @include calc.exe
// @include notepad.exe
// @compilerOptions -luxtheme
// ==/WindhawkMod==
// ==WindhawkModReadme==
/*
# a
*/
// ==/WindhawkModReadme==
// ==WindhawkModSettings==
/*
- programs:
- - name: notepad.exe
$name: Program name or path
- theme: "C:\\Windows\\Resources\\Themes\\Aero\\Aero.msstyles"
$name: The full file path to a given msstyles theme.
$name: Per-program configuration.
*/
// ==/WindhawkModSettings==
#include <processenv.h>
#include <windhawk_utils.h>
#include <libloaderapi.h>
#include <memoryapi.h>
#include <uxtheme.h>
#include <winnt.h>
typedef HRESULT (*GetThemeDefaults_t)(
LPCWSTR pszThemeFileName,
LPWSTR pszColorName,
DWORD dwColorNameLen,
LPWSTR pszSizeName,
DWORD dwSizeNameLen
);
GetThemeDefaults_t GetThemeDefaults;
typedef HRESULT (*LoaderLoadTheme_t)(
HANDLE hThemeFile,
HINSTANCE hInstance,
LPCWSTR pszThemeFileName,
LPCWSTR pszColorParam,
LPCWSTR pszSizeParam,
OUT HANDLE *hSharableSection,
LPWSTR pszSharableSectionName,
int cchSharableSectionName,
OUT HANDLE *hNonsharableSection,
LPWSTR pszNonsharableSectionName,
int cchNonsharableSectionName,
PVOID pfnCustomLoadHandler,
OUT HANDLE *hReuseSection,
int a,
int b,
BOOL fEmulateGlobal
);
LoaderLoadTheme_t LoaderLoadTheme;
typedef HRESULT (*LoaderLoadTheme_t_win11)(
HANDLE hThemeFile,
HINSTANCE hInstance,
LPCWSTR pszThemeFileName,
LPCWSTR pszColorParam,
LPCWSTR pszSizeParam,
OUT HANDLE *hSharableSection,
LPWSTR pszSharableSectionName,
int cchSharableSectionName,
OUT HANDLE *hNonsharableSection,
LPWSTR pszNonsharableSectionName,
int cchNonsharableSectionName,
PVOID pfnCustomLoadHandler,
OUT HANDLE *hReuseSection,
int a,
int b
);
typedef HTHEME (*OpenThemeDataFromFile_t)(
HANDLE hThemeFile,
HWND hWnd,
LPCWSTR pszClassList,
DWORD dwFlags
// DWORD unknown,
// bool a
);
OpenThemeDataFromFile_t OpenThemeDataFromFile;
typedef struct _LocalThemeFile
{
char header[7]; // must be "thmfile"
LPVOID sharableSectionView;
HANDLE hSharableSection;
LPVOID nsSectionView;
HANDLE hNsSection;
char end[3]; // must be "end"
} LocalThemeFile;
//=================================================================================================
HANDLE g_hLocalTheme;
using OpenThemeData_t = decltype(&OpenThemeData);
OpenThemeData_t OpenThemeData_orig;
HTHEME OpenThemeData_hook(HWND hwnd, LPCWSTR pszClassList)
{
HTHEME fromFile = OpenThemeDataFromFile(g_hLocalTheme, hwnd, pszClassList, 0);
if (fromFile)
{
return fromFile;
}
return OpenThemeData_orig(hwnd, pszClassList);
}
using OpenThemeDataEx_t = decltype(&OpenThemeDataEx);
OpenThemeDataEx_t OpenThemeDataEx_orig;
HTHEME OpenThemeDataEx_hook(HWND hwnd, LPCWSTR pszClassList, DWORD dwFlags)
{
HTHEME fromFile = OpenThemeDataFromFile(g_hLocalTheme, hwnd, pszClassList, dwFlags);
if (fromFile)
{
return fromFile;
}
return OpenThemeDataEx_orig(hwnd, pszClassList, dwFlags);
}
using OpenNcThemeData_t = decltype(&OpenThemeData);
OpenNcThemeData_t OpenNcThemeData_orig;
HTHEME OpenNcThemeData_hook(HWND hwnd, LPCWSTR pszClassList)
{
HTHEME fromFile = OpenThemeDataFromFile(g_hLocalTheme, hwnd, pszClassList, 0);
if (fromFile)
{
return fromFile;
}
return OpenNcThemeData_orig(hwnd, pszClassList);
}
typedef HTHEME (*_OpenThemeData_t)(HWND hwnd, PCWSTR pszClassList, DWORD dwFlags, int unk1, bool unk2);
_OpenThemeData_t _OpenThemeData_orig;
HTHEME _OpenThemeData_hook(HWND hwnd, PCWSTR pszClassList, DWORD dwFlags, int unk1, bool unk2)
{
HTHEME fromFile = OpenThemeDataFromFile(g_hLocalTheme, hwnd, pszClassList, 0);
if (fromFile)
{
return fromFile;
}
return _OpenThemeData_orig(hwnd, pszClassList, dwFlags, unk1, unk2);
}
/*
* LoadThemeFromFilePath: Load a visual style from the provided file path.
*
* This relies on a few internal functions from uxtheme in order to work. It's
* basically the same approach as StartIsBack.
*/
HRESULT LoadThemeFromFilePath(PCWSTR szThemeFileName)
{
HRESULT hr = S_OK;
HMODULE hUxtheme = GetModuleHandleW(L"uxtheme.dll");
if (!hUxtheme)
{
return E_FAIL;
}
GetThemeDefaults = (GetThemeDefaults_t)GetProcAddress(hUxtheme, (LPCSTR)7);
LoaderLoadTheme = (LoaderLoadTheme_t)GetProcAddress(hUxtheme, (LPCSTR)92);
OpenThemeDataFromFile = (OpenThemeDataFromFile_t)GetProcAddress(hUxtheme, (LPCSTR)16);
if (!GetThemeDefaults || !LoaderLoadTheme || !OpenThemeDataFromFile)
{
return E_FAIL;
}
OSVERSIONINFOW verInfo = { 0 };
hr = GetVersionExW(&verInfo) ? S_OK : E_FAIL;
WCHAR defColor[MAX_PATH];
WCHAR defSize[MAX_PATH];
hr = GetThemeDefaults(
szThemeFileName,
defColor,
ARRAYSIZE(defColor),
defSize,
ARRAYSIZE(defSize)
);
HANDLE hSharableSection;
HANDLE hNonsharableSection;
if (verInfo.dwBuildNumber < 20000)
{
hr = LoaderLoadTheme(
NULL,
NULL,
szThemeFileName,
defColor,
defSize,
&hSharableSection,
NULL,
0,
&hNonsharableSection,
NULL,
0,
NULL,
NULL,
NULL,
NULL,
FALSE
);
}
else
{
hr = ((LoaderLoadTheme_t_win11)LoaderLoadTheme)(
NULL,
NULL,
szThemeFileName,
defColor,
defSize,
&hSharableSection,
NULL,
0,
&hNonsharableSection,
NULL,
0,
NULL,
NULL,
NULL,
NULL
);
}
g_hLocalTheme = malloc(sizeof(LocalThemeFile));
if (g_hLocalTheme)
{
LocalThemeFile *ltf = (LocalThemeFile *)g_hLocalTheme;
lstrcpyA(ltf->header, "thmfile");
lstrcpyA(ltf->header, "end");
ltf->sharableSectionView = MapViewOfFile(hSharableSection, 4, 0, 0, 0);
ltf->hSharableSection = hSharableSection;
ltf->nsSectionView = MapViewOfFile(hNonsharableSection, 4, 0, 0, 0);
ltf->hNsSection = hNonsharableSection;
}
else
{
hr = E_FAIL;
}
return S_OK;
}
/*
* LoadSettings: Load the Windhawk mod settings for the current program. If the
* user enabled a different theme for the program, then this will
* return true. Otherwise, it will return false.
*
* This implementation is heavily copied from the "Text Replace" mod.
*/
bool LoadSettings()
{
WCHAR programPath[1024];
DWORD dwSize = ARRAYSIZE(programPath);
if (!QueryFullProcessImageNameW(GetCurrentProcess(), 0, programPath, &dwSize))
{
*programPath = L'\0';
}
PCWSTR programFileName = wcsrchr(programPath, L'\\');
if (programFileName)
{
programFileName++;
if (!*programFileName)
{
programFileName = nullptr;
}
}
for (int i = 0; ; i++)
{
bool matched = false;
PCWSTR applicationName = Wh_GetStringSetting(L"programs[%d].name", i);
bool hasName = *applicationName;
if (hasName)
{
if (programFileName && wcsicmp(programFileName, applicationName) == 0)
{
matched = true;
}
else if (wcsicmp(programPath, applicationName) == 0)
{
matched = true;
}
}
Wh_FreeStringSetting(applicationName);
if (!hasName)
{
break;
}
else if (matched)
{
PCWSTR themePath = Wh_GetStringSetting(L"programs[%d].theme", i);
bool hasAppliedTheme = false;
if (*themePath)
{
LoadThemeFromFilePath(themePath);
}
Wh_FreeStringSetting(themePath);
return hasAppliedTheme;
}
}
return false;
}
// The mod is being initialized, load settings, hook functions, and do other
// initialization stuff if required.
BOOL Wh_ModInit()
{
Wh_Log(L"Init " WH_MOD_ID L" version " WH_MOD_VERSION);
HMODULE hUxtheme = GetModuleHandleW(L"uxtheme.dll");
GetThemeDefaults = (GetThemeDefaults_t)GetProcAddress(hUxtheme, (LPCSTR)7);
LoaderLoadTheme = (LoaderLoadTheme_t)GetProcAddress(hUxtheme, (LPCSTR)92);
OpenThemeDataFromFile = (OpenThemeDataFromFile_t)GetProcAddress(hUxtheme, (LPCSTR)16);
FARPROC OpenNcThemeData = GetProcAddress(hUxtheme, (LPCSTR)49);
LoadSettings();
Wh_SetFunctionHook(
(void *)OpenThemeData,
(void *)OpenThemeData_hook,
(void **)&OpenThemeData_orig
);
Wh_SetFunctionHook(
(void *)OpenThemeDataEx,
(void *)OpenThemeDataEx_hook,
(void **)&OpenThemeDataEx_orig
);
Wh_SetFunctionHook(
(void *)OpenNcThemeData,
(void *)OpenNcThemeData_hook,
(void **)&OpenNcThemeData_orig
);
WindhawkUtils::SYMBOL_HOOK hook = {
{ L"void * __cdecl _OpenThemeData(struct HWND__ *,unsigned short const *,int,unsigned long,bool)" },
(void **)&_OpenThemeData_orig,
(void *)_OpenThemeData_hook
};
WindhawkUtils::HookSymbols(hUxtheme, &hook, 1);
return TRUE;
}
// The mod is being unloaded, free all allocated resources.
void Wh_ModUninit()
{
Wh_Log(L"Uninit");
}