I was exploring XAML’s Handoff Visual implementation and how it’s created, clipped, wrapped, etc… and found out that the function responsible for creating that visual is CUIElement::EnsureHandOffVisual and when I looked at this function I noticed that it has a bool createLayerVisual parameter:So I looked at the xrefs of this function to see if and when this parameter gets used, and noticed there’s a function called CUIElement::GetHandOffLayerVisual in the xrefs:So I was curious and checked the xrefs of it, and found a very interesting function, DirectUI::UIElementFactory::GetElementLayerVisual Stuff under the DirectUI namespace are usually exposed via interfaces, and that was the case here too!Xrefs showed that it’s exposed through the Windows::UI::Xaml::IUIElementStaticsPrivate interface:I was curious when this was introduced since I have never seen it before, and after checking multiple Windows.UI.Xaml.dll versions, it turns out that it was introduced in Windows 11 Build 22000.
LayerVisual is a visual type that allows you to apply effects and attach shadows to its child visuals.
Now that we have the interface and the function signature, the only thing missing is the interface IID, checking the UIElementFactory constructor reveals that the interface is stored at offset 0x50 of the factory class object:And if we look at the QueryInterface implementation, we can see that it wraps/calls another QueryInterface function but with a pointer to the object at offset 0x10 passed as a this parameter:This means that we are looking for the IID of object returned at offset 0x40 ( 0x50 - 0x10 ) in that wrapped QueryInterface function.If we look at that function we see that IDA mistyped the this parameter and also wasn’t able to detect that it’s the this parameter and showed it as a regular register variable:To fix this mess, we will simply change the type of this variable to void*, this way we can see the offsets more clearly, and here we go, we found our IID!We are using UWP .NET 9 for this experiment so we have to use Source Generated COM to declare this interface definition (technically we can use a projection project or handroll the interface projection codegen too but this is simpler, well, kinda)
private void playButton_Click(object sender, RoutedEventArgs e){ // In CsWinRT 3, this probably needs to be changed to something like: // using var objRef = WindowsRuntimeActivationFactory.GetActivationFactory(typeof(UIElement).FullName, typeof(IUIElementStaticsPrivate).GUID); // var staticsPrivate = ComInterfaceMarshaller<IUIElementStaticsPrivate>.ConvertToManaged(objRef.GetThisPtrUnsafe()); var staticsPrivate = UIElement.As<IUIElementStaticsPrivate>(); // GetElementLayerVisual expects an IUIElement* but CsWinRT won't give us an IUIElement from a composed class (such as ContentControl) unless // it's passed to a projected method that takes an UIElement, so we have to QI manually Marshal.QueryInterface(((IWinRTObject)contentControl).NativeObject.ThisPtr, new("676d0be9-b65c-41c6-ba40-58cf87f201c1") /* IUIElement */, out nint ppv); var layerVisualPtr = staticsPrivate.GetElementLayerVisual((void*)ppv); Marshal.Release(ppv); var layerVisual = LayerVisual.FromAbi((nint)layerVisualPtr); var compositor = layerVisual.Compositor; var blur = new GaussianBlurEffect { Name = "Blur", BlurAmount = 0.0f, BorderMode = EffectBorderMode.Soft, Optimization = EffectOptimization.Balanced, Source = new CompositionEffectSourceParameter("source") }; var effectFactory = compositor.CreateEffectFactory(blur, new[] { "Blur.BlurAmount" }); var effectBrush = effectFactory.CreateBrush(); layerVisual.Effect = effectBrush; var easing = CompositionEasingFunction.CreateExponentialEasingFunction(compositor, CompositionEasingFunctionMode.InOut, 6); var animation = compositor.CreateScalarKeyFrameAnimation(); animation.InsertKeyFrame(0.0f, 0.0f, easing); animation.InsertKeyFrame(1.0f, 10.0f, easing); animation.Duration = TimeSpan.FromSeconds(2); animation.IterationBehavior = AnimationIterationBehavior.Forever; animation.Direction = AnimationDirection.Alternate; effectBrush.StartAnimation("Blur.BlurAmount", animation); Marshal.QueryInterface(((IWinRTObject)hiButton).NativeObject.ThisPtr, new("676d0be9-b65c-41c6-ba40-58cf87f201c1") /* IUIElement */, out nint ppv2); var layerVisual2Ptr = staticsPrivate.GetElementLayerVisual((void*)ppv2); Marshal.Release(ppv2); var layerVisual2 = LayerVisual.FromAbi((nint)layerVisual2Ptr); var alphaMask = new AlphaMaskEffect { Name = "AlphaMask", Source = new CompositionEffectSourceParameter("source"), AlphaMask = new CompositionEffectSourceParameter("mask") }; var gradientBrush = compositor.CreateLinearGradientBrush(); gradientBrush.StartPoint = new(0, 0); gradientBrush.EndPoint = new(1, 0); gradientBrush.MappingMode = CompositionMappingMode.Relative; CompositionColorGradientStop stop; gradientBrush.ColorStops.Add(compositor.CreateColorGradientStop(-1.0f, Colors.Black)); gradientBrush.ColorStops.Add(stop = compositor.CreateColorGradientStop(1.0f, Colors.Black)); gradientBrush.ColorStops.Add(compositor.CreateColorGradientStop(1.0f, Colors.Transparent)); var easing2 = CompositionEasingFunction.CreatePowerEasingFunction(compositor, CompositionEasingFunctionMode.InOut, 3); var animation2 = compositor.CreateScalarKeyFrameAnimation(); animation2.InsertKeyFrame(0.0f, 1.0f, easing2); animation2.InsertKeyFrame(1.0f, -1.0f, easing2); animation2.Duration = TimeSpan.FromSeconds(2); animation2.IterationBehavior = AnimationIterationBehavior.Forever; animation2.Direction = AnimationDirection.Alternate; stop.StartAnimation("Offset", animation2); var effectFactory2 = compositor.CreateEffectFactory(alphaMask); var effectBrush2 = effectFactory2.CreateBrush(); effectBrush2.SetSourceParameter("mask", gradientBrush); layerVisual2.Effect = effectBrush2;}
And voila!
Warning
GetElementLayerVisual has to be called before any handoff visual was created for the element, so if ElementCompositionPreview.GetElementVisual was called on the element prior it, the function will fail.
If you are calling that method on an element that has any Popup children, you have to call ElementCompositionPreview.GetElementVisual on the element after calling GetElementLayerVisual or otherwise the Popup will break.
This function we just used doesn’t exist in the Windows 10 version of that interface, but looking at how createLayerVisual is used inside CUIElement::EnsureHandOffVisual we can see that the LayerVisual and the normal visual paths are very similar, so similar that we can hook IDCompositionDevice2::CreateVisual and redirect it to Windows.UI.Compositor.CreateLayerVisual and it will just work. We will be using a C++/WinRT Runtime Component for this since we are gonna need Detours, and using Detours in C# is a bit harder than using it from C++ directly.So let’s create a simple WinRT helper static class for this, lets start with the IDL:
namespace winrt::XamlCompositionHelpers::implementation{ bool ElementCompositionPreviewEx::s_Hooked = false; DWORD ElementCompositionPreviewEx::s_ThreadId = NULL; std::mutex ElementCompositionPreviewEx::s_Mutex = { }; decltype(ElementCompositionPreviewEx::s_CreateVisual) ElementCompositionPreviewEx::s_CreateVisual = nullptr; decltype(ElementCompositionPreviewEx::s_QueryInterface) ElementCompositionPreviewEx::s_QueryInterface = nullptr; static bool IsOnWindows11OrHigher() { bool isWin11 = ApiInformation::IsApiContractPresent(L"Windows.Foundation.UniversalApiContract", 14); return isWin11; } LayerVisual ElementCompositionPreviewEx::GetElementLayerVisual(UIElement const& element) { // On Windows 11 b22000 and higher, we can use the IUIElementStaticsPrivate interface to get the LayerVisual directly com_ptr<IUIElementStaticsPrivate> uiElementPrivate; if (IsOnWindows11OrHigher() && (uiElementPrivate = try_get_activation_factory<UIElement, IUIElementStaticsPrivate>())) { LayerVisual layerVisual { nullptr }; check_hresult(uiElementPrivate->GetElementLayerVisual(winrt::get_abi(element), put_abi(layerVisual))); return layerVisual; } // We are using the thread ID to verify and ensure that we aren't hoking any other ElementCompositionPreview::GetElementVisual call // that happened to be going in another thread at the same time we are hooking the function to return a LayerVisual, // and we use a lock to ensure that only one thread can be hooking at a time so that thread ID doesn't get changed mid-hook. std::scoped_lock lock(s_Mutex); EnsureHooked(); s_ThreadId = GetCurrentThreadId(); auto visual = ElementCompositionPreview::GetElementVisual(element); s_ThreadId = NULL; return visual.as<LayerVisual>(); } void ElementCompositionPreviewEx::EnsureHooked() { if (!s_Hooked) { // assuming we are on the UI thread, it wouldn't work otherwise anyway auto compositor = Window::Current().Compositor(); auto device3 = compositor.as<IDCompositionDevice3>(); auto vtbl = *reinterpret_cast<void***>(device3.get()); s_CreateVisual = reinterpret_cast<decltype(s_CreateVisual)>(vtbl[6]); DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourAttach(&(PVOID&)s_CreateVisual, &ElementCompositionPreviewEx::CreateVisualHook); check_win32(DetourTransactionCommit()); // some older Windows builds don't have the createLayerVisual parameter // and on these builds XAML actually tries to use the visual as IDCompsitionVisual* // which fails because LayerVisual is not an InteropVisual, we work around this // by creating a dummy InteropVisual and hooking QI calls on the LayerVisual // that query for IDCompsitionVisual* and redirect them to our dummy InteropVisual check_hresult(device3->CreateVisual(&s_dummyVisual)); auto layerVisual = compositor.CreateLayerVisual().as<Visual>(); auto layerVtbl = *reinterpret_cast<void***>(winrt::get_abi(layerVisual)); s_QueryInterface = reinterpret_cast<decltype(s_QueryInterface)>(layerVtbl[0]); DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourAttach(&(PVOID&)s_QueryInterface, &ElementCompositionPreviewEx::QueryInterfaceHook); check_win32(DetourTransactionCommit()); s_Hooked = true; } } HRESULT WINAPI ElementCompositionPreviewEx::CreateVisualHook(IDCompositionDevice2* pThis, IDCompositionVisual2** ppVisual) { // Ensure that we are only hooking our own calls to ElementCompositionPreview::GetElementVisual / IDCompositionDevice2::CreateVisual if (s_ThreadId != GetCurrentThreadId()) return s_CreateVisual(pThis, ppVisual); Compositor compositor { nullptr }; copy_from_abi(compositor, pThis); LayerVisual layerVisual = compositor.CreateLayerVisual(); copy_to_abi(layerVisual, *(void**&)ppVisual); return S_OK; } HRESULT WINAPI ElementCompositionPreviewEx::QueryInterfaceHook(IUnknown* pThis, REFIID riid, void** ppv) { const static auto xamlModule = GetModuleHandleW(L"Windows.UI.Xaml.dll"); static const constexpr auto IID1 = std::bit_cast<GUID>(guid("1CD4B256-A5ED-4DED-A0C1-82A7A20B8755")); static const constexpr auto IID2 = std::bit_cast<GUID>(guid("86C82676-5D2A-4E6A-A89F-50A22FF86263")); static const constexpr auto IID3 = std::bit_cast<GUID>(guid("A0AD01B5-9C60-4A6F-A7E2-04E18CF5E011")); LayerVisual layerVisual { nullptr }; HMODULE module = NULL; HRESULT result = s_QueryInterface(pThis, riid, ppv); if (FAILED(result) && s_dummyVisual && (riid == IID1 || riid == IID2 || riid == IID3) && GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (LPCWSTR)_ReturnAddress(), &module) && module == xamlModule && SUCCEEDED(s_QueryInterface(pThis, winrt::guid_of<ILayerVisual>(), winrt::put_abi(layerVisual)))) { result = s_dummyVisual->QueryInterface(riid, ppv); } return result; }}
And with this we can then replace our C# code with this:
Copy
private void playButton_Click(object sender, RoutedEventArgs e){ var layerVisual = ElementCompositionPreviewEx.GetElementLayerVisual(contentControl); var compositor = layerVisual.Compositor; var blur = new GaussianBlurEffect { Name = "Blur", BlurAmount = 0.0f, BorderMode = EffectBorderMode.Soft, Optimization = EffectOptimization.Balanced, Source = new CompositionEffectSourceParameter("source") }; var effectFactory = compositor.CreateEffectFactory(blur, new[] { "Blur.BlurAmount" }); var effectBrush = effectFactory.CreateBrush(); layerVisual.Effect = effectBrush; var easing = CompositionEasingFunctionEx.CreateExponentialEasingFunction(compositor, CompositionEasingFunctionMode.InOut, 6); var animation = compositor.CreateScalarKeyFrameAnimation(); animation.InsertKeyFrame(0.0f, 0.0f, easing); animation.InsertKeyFrame(1.0f, 10.0f, easing); animation.Duration = TimeSpan.FromSeconds(2); animation.IterationBehavior = AnimationIterationBehavior.Forever; animation.Direction = AnimationDirection.Alternate; effectBrush.StartAnimation("Blur.BlurAmount", animation); var layerVisual2 = ElementCompositionPreviewEx.GetElementLayerVisual(hiButton); var alphaMask = new AlphaMaskEffect { Name = "AlphaMask", Source = new CompositionEffectSourceParameter("source"), AlphaMask = new CompositionEffectSourceParameter("mask") }; var gradientBrush = compositor.CreateLinearGradientBrush(); gradientBrush.StartPoint = new(0, 0); gradientBrush.EndPoint = new(1, 0); gradientBrush.MappingMode = CompositionMappingMode.Relative; CompositionColorGradientStop stop; gradientBrush.ColorStops.Add(compositor.CreateColorGradientStop(-1.0f, Colors.Black)); gradientBrush.ColorStops.Add(stop = compositor.CreateColorGradientStop(1.0f, Colors.Black)); gradientBrush.ColorStops.Add(compositor.CreateColorGradientStop(1.0f, Colors.Transparent)); var easing2 = CompositionEasingFunctionEx.CreatePowerEasingFunction(compositor, CompositionEasingFunctionMode.InOut, 3); var animation2 = compositor.CreateScalarKeyFrameAnimation(); animation2.InsertKeyFrame(0.0f, 1.0f, easing2); animation2.InsertKeyFrame(1.0f, -1.0f, easing2); animation2.Duration = TimeSpan.FromSeconds(2); animation2.IterationBehavior = AnimationIterationBehavior.Forever; animation2.Direction = AnimationDirection.Alternate; stop.StartAnimation("Offset", animation2); var effectFactory2 = compositor.CreateEffectFactory(alphaMask); var effectBrush2 = effectFactory2.CreateBrush(); effectBrush2.SetSourceParameter("mask", gradientBrush); layerVisual2.Effect = effectBrush2;}
And voila!
As you might have noticed I’m using CompositionEasingFunctionEx instead of CompositionEasingFunction to create easing functions in the **Windows 10 **code, that’s because the latter only got the support for creating easing functions in Windows 11, so I created a CompositionEasingFunctionEx helper class to allow creating such easing functions under Windows 10, but that’s a story for another article.
Unfortunately WinUI 3 doesn’t have that interface method at all, neither in latest version or older ones, **BUT **we can use the same trick we used for Windows 10, so lets take a look at the CUIElement::EnsureHandOffVisual function in WinUI 3: Interesting, so it uses the WinRT Microsoft.UI.Composition API for both cases, so we need to copy the C++ implementation code we wrote before and make few changes to accommodate that (previous comments are omitted, so check them in the UWP/WUX code if you haven’t)
I accidentally found a better and easier way to achieve this without hooking or any complex stuff at all while working on another thing.So basically I found out that CompositionVisualSurface completely ignores the Opacity property of the source Visual, so you can just get the Visual of the target UIElement, make its Opacity0, then create a CompositionSurfaceBrush over a CompositionVisualSurface of the Visual then use that to create a SpriteVisual with CompositionEffectBrush or CompositionMaskBrush (with the source brush set to the CompositionSurfaceBrush) on top of it!Special thanks toDonglefor proofreading the article