From 6f2f7f52e864fb66aa605212c42e3eff34ae03cb Mon Sep 17 00:00:00 2001 From: 0x5BFA <62196528+0x5bfa@users.noreply.github.com> Date: Mon, 17 Mar 2025 00:41:35 +0900 Subject: [PATCH 1/3] Initial commit --- src/Files.App.CsWin32/Extras.cs | 11 + src/Files.App.CsWin32/NativeMethods.txt | 5 +- .../FilePreviews/ShellPreview.xaml.cs | 23 +- .../Previews/ShellPreviewViewModel.cs | 211 ++++++++++-------- 4 files changed, 142 insertions(+), 108 deletions(-) diff --git a/src/Files.App.CsWin32/Extras.cs b/src/Files.App.CsWin32/Extras.cs index 2e3fbe69b4aa..e35fb4dc0f02 100644 --- a/src/Files.App.CsWin32/Extras.cs +++ b/src/Files.App.CsWin32/Extras.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Marshalling; using Windows.Win32.Foundation; using Windows.Win32.UI.WindowsAndMessaging; @@ -37,4 +38,14 @@ public static unsafe nint SetWindowLongPtr(HWND hWnd, WINDOW_LONG_PTR_INDEX nInd : _SetWindowLongPtr(hWnd, (int)nIndex, dwNewLong); } } + + namespace Extras + { + [GeneratedComInterface, Guid("EACDD04C-117E-4E17-88F4-D1B12B0E3D89"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public partial interface IDCompositionTarget + { + [PreserveSig] + int SetRoot(nint visual); + } + } } diff --git a/src/Files.App.CsWin32/NativeMethods.txt b/src/Files.App.CsWin32/NativeMethods.txt index aa4758a66497..655fad506192 100644 --- a/src/Files.App.CsWin32/NativeMethods.txt +++ b/src/Files.App.CsWin32/NativeMethods.txt @@ -28,9 +28,7 @@ HWND LRESULT WPARAM LPARAM -WM_LBUTTONUP -WM_RBUTTONUP -WM_DESTROY +WM_* SetForegroundWindow GetForegroundWindow GetCurrentThreadId @@ -173,3 +171,4 @@ FSCTL_LOCK_VOLUME FILE_FILE_COMPRESSION WM_WINDOWPOSCHANGING WINDOWPOS +UnregisterClass diff --git a/src/Files.App/UserControls/FilePreviews/ShellPreview.xaml.cs b/src/Files.App/UserControls/FilePreviews/ShellPreview.xaml.cs index 74a3a8f0c35b..d7aa144dfd11 100644 --- a/src/Files.App/UserControls/FilePreviews/ShellPreview.xaml.cs +++ b/src/Files.App/UserControls/FilePreviews/ShellPreview.xaml.cs @@ -1,8 +1,11 @@ +// Copyright (c) Files Community +// Licensed under the MIT License. + using Files.App.ViewModels.Previews; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; -using Vanara.PInvoke; using Windows.Foundation; +using Windows.Win32.Foundation; namespace Files.App.UserControls.FilePreviews { @@ -13,13 +16,15 @@ public sealed partial class ShellPreview : UserControl public ShellPreview(ShellPreviewViewModel model) { ViewModel = model; - this.InitializeComponent(); + + InitializeComponent(); } private void PreviewHost_Loaded(object sender, RoutedEventArgs e) { ViewModel.LoadPreview(contentPresenter); ViewModel.SizeChanged(GetPreviewSize()); + if (XamlRoot.Content is FrameworkElement element) { element.SizeChanged += PreviewHost_SizeChanged; @@ -38,13 +43,12 @@ private RECT GetPreviewSize() var physicalSize = contentPresenter.RenderSize; var physicalPos = source.TransformPoint(new Point(0, 0)); var scale = XamlRoot.RasterizationScale; - var result = new RECT - { - X = (int)(physicalPos.X * scale + 0.5), - Y = (int)(physicalPos.Y * scale + 0.5), - Width = (int)(physicalSize.Width * scale + 0.5), - Height = (int)(physicalSize.Height * scale + 0.5) - }; + var result = RECT.FromXYWH( + (int)(physicalPos.X * scale + 0.5), + (int)(physicalPos.Y * scale + 0.5), + (int)(physicalSize.Width * scale + 0.5), + (int)(physicalSize.Height * scale + 0.5)); + return result; } @@ -55,6 +59,7 @@ private void PreviewHost_Unloaded(object sender, RoutedEventArgs e) element.SizeChanged -= PreviewHost_SizeChanged; element.PointerEntered -= PreviewHost_PointerEntered; } + ViewModel.UnloadPreview(); } diff --git a/src/Files.App/ViewModels/UserControls/Previews/ShellPreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/ShellPreviewViewModel.cs index 16e2d1de4d3b..329e07e8d553 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/ShellPreviewViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/ShellPreviewViewModel.cs @@ -6,21 +6,17 @@ using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Hosting; using System.Runtime.InteropServices; -using System.Text; -using Vanara.PInvoke; using Windows.Win32; -using Windows.Win32.System.Com; +using Windows.Win32.Foundation; using Windows.Win32.Graphics.Direct3D; using Windows.Win32.Graphics.Direct3D11; using Windows.Win32.Graphics.DirectComposition; using Windows.Win32.Graphics.Dwm; using Windows.Win32.Graphics.Dxgi; +using Windows.Win32.System.Com; +using Windows.Win32.UI.Shell; using Windows.Win32.UI.WindowsAndMessaging; using WinRT; -using static Vanara.PInvoke.ShlwApi; -using static Vanara.PInvoke.User32; -using System.Runtime.InteropServices.Marshalling; - #pragma warning disable CS8305 // Type is for evaluation purposes only and is subject to change or removal in future updates. @@ -28,82 +24,78 @@ namespace Files.App.ViewModels.Previews { public sealed partial class ShellPreviewViewModel : BasePreviewModel { - private const string IPreviewHandlerIid = "{8895b1c6-b41f-4c1c-a562-0d564250836f}"; - private static readonly Guid QueryAssociationsClsid = new Guid(0xa07034fd, 0x6caa, 0x4954, 0xac, 0x3f, 0x97, 0xa2, 0x72, 0x16, 0xf9, 0x8a); - private static readonly Guid IQueryAssociationsIid = Guid.ParseExact("c46ca590-3c3f-11d2-bee6-0000f805ca57", "d"); - - PreviewHandler? currentHandler; - ContentExternalOutputLink? outputLink; - WindowClass? wCls; - HWND hwnd = HWND.NULL; - bool isOfficePreview = false; - - [GeneratedComInterface, Guid("EACDD04C-117E-4E17-88F4-D1B12B0E3D89"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - public partial interface IDCompositionTarget - { - [PreserveSig] - int SetRoot(nint visual); - } + // Fields + + ContentExternalOutputLink? _contentExternalOutputLink; + PreviewHandler? _previewHandler; + WNDCLASSEXW _windowClass; + WNDPROC _windProc = null!; + HWND _hWnd = HWND.Null; + bool _isOfficePreview = false; + + // Constructor public ShellPreviewViewModel(ListedItem item) : base(item) { } - public async override Task> LoadPreviewAndDetailsAsync() - => []; + // Methods + + public override Task> LoadPreviewAndDetailsAsync() + => Task.FromResult>([]); - public static Guid? FindPreviewHandlerFor(string extension, nint hwnd) + public static unsafe Guid? FindPreviewHandlerFor(string extension, nint hwnd) { if (string.IsNullOrEmpty(extension)) return null; - var hr = AssocCreate(QueryAssociationsClsid, IQueryAssociationsIid, out var queryAssoc); - if (!hr.Succeeded) - return null; - try { - if (queryAssoc == null) - return null; - - queryAssoc.Init(ASSOCF.ASSOCF_INIT_DEFAULTTOSTAR, extension, nint.Zero, hwnd); - - var sb = new StringBuilder(128); - uint cch = 64; - - queryAssoc.GetString(ASSOCF.ASSOCF_NOTRUNCATE, ASSOCSTR.ASSOCSTR_SHELLEXTENSION, IPreviewHandlerIid, sb, ref cch); - - Debug.WriteLine($"Preview handler for {extension}: {sb}"); - return Guid.Parse(sb.ToString()); + fixed (char* pszAssoc = extension, + pszExtra = "{8895b1c6-b41f-4c1c-a562-0d564250836f}", + pszOutput = new char[1024]) + { + PWSTR pwszAssoc = new(pszAssoc); + PWSTR pwszExtra = new(pszExtra); + PWSTR pwszOutput = new(pszOutput); + uint cchOutput = 2024; + + // Try to find registered preview handler associated with specified extension name + var res = PInvoke.AssocQueryString( + ASSOCF.ASSOCF_NOTRUNCATE, + ASSOCSTR.ASSOCSTR_SHELLEXTENSION, + pwszAssoc, + pwszExtra, + pwszOutput, + &cchOutput); + + return Guid.Parse(pwszOutput.ToString()); + } } catch { return null; } - finally - { - Marshal.ReleaseComObject(queryAssoc); - } } public void SizeChanged(RECT size) { - if (hwnd != HWND.NULL) - SetWindowPos(hwnd, HWND.HWND_TOP, size.Left, size.Top, size.Width, size.Height, SetWindowPosFlags.SWP_NOACTIVATE); + if (_hWnd != HWND.Null) + PInvoke.SetWindowPos(_hWnd, new(0), size.left, size.top, size.Width, size.Height, SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); - currentHandler?.ResetBounds(new(0, 0, size.Width, size.Height)); + _previewHandler?.ResetBounds(new(0, 0, size.Width, size.Height)); - if (outputLink is not null) - outputLink.PlacementVisual.Size = new(size.Width, size.Height); + if (_contentExternalOutputLink is not null) + _contentExternalOutputLink.PlacementVisual.Size = new(size.Width, size.Height); } - private nint WndProc(HWND hwnd, uint msg, nint wParam, nint lParam) + private unsafe LRESULT WndProc(HWND hwnd, uint msg, WPARAM wParam, LPARAM lParam) { - if (msg == (uint)WindowMessage.WM_CREATE) + if (msg is PInvoke.WM_CREATE) { - var clsid = FindPreviewHandlerFor(Item.FileExtension, hwnd.DangerousGetHandle()); + var clsid = FindPreviewHandlerFor(Item.FileExtension, hwnd); - isOfficePreview = new Guid?[] + _isOfficePreview = new Guid?[] { Guid.Parse("84F66100-FF7C-4fb4-B0C0-02CD7FB668FE"), // preview handler for Word files Guid.Parse("65235197-874B-4A07-BDC5-E65EA825B718"), // preview handler for PowerPoint files @@ -112,43 +104,71 @@ private nint WndProc(HWND hwnd, uint msg, nint wParam, nint lParam) try { - currentHandler = new PreviewHandler(clsid.Value, hwnd.DangerousGetHandle()); - currentHandler.InitWithFileWithEveryWay(Item.ItemPath); - currentHandler.DoPreview(); + _previewHandler = new PreviewHandler(clsid!.Value, hwnd); + _previewHandler.InitWithFileWithEveryWay(Item.ItemPath); + _previewHandler.DoPreview(); } catch { UnloadPreview(); } } - else if (msg == (uint)WindowMessage.WM_DESTROY) + else if (msg is PInvoke.WM_DESTROY) { - if (currentHandler is not null) + if (_previewHandler is not null) { - currentHandler.Dispose(); - currentHandler = null; + _previewHandler.Dispose(); + _previewHandler = null; } } - return DefWindowProc(hwnd, msg, wParam, lParam); + return PInvoke.DefWindowProc(hwnd, msg, wParam, lParam); } - public void LoadPreview(UIElement presenter) + public unsafe void LoadPreview(UIElement presenter) { var parent = MainWindow.Instance.WindowHandle; + var hInst = PInvoke.GetModuleHandle(default(PWSTR)); + var szClassName = $"{GetType().Name}-{Guid.NewGuid()}"; + var szWindowName = $"Preview"; - HINSTANCE hInst = Kernel32.GetModuleHandle(); - - wCls = new WindowClass($"{GetType().Name}{Guid.NewGuid()}", hInst, WndProc); + fixed (char* pszClassName = szClassName) + { + _windProc = new(WndProc); + var pWindProc = Marshal.GetFunctionPointerForDelegate(_windProc); + var pfnWndProc = (delegate* unmanaged[Stdcall])pWindProc; - hwnd = CreateWindowEx( - WindowStylesEx.WS_EX_LAYERED | WindowStylesEx.WS_EX_COMPOSITED, - wCls.ClassName, - "Preview", - WindowStyles.WS_CHILD | WindowStyles.WS_CLIPSIBLINGS | WindowStyles.WS_VISIBLE, - 0, 0, 0, 0, - hWndParent: parent, - hInstance: hInst); + _windowClass = new WNDCLASSEXW() + { + cbSize = (uint)sizeof(WNDCLASSEXW), + lpfnWndProc = pfnWndProc, + hInstance = hInst, + lpszClassName = pszClassName, + style = 0, + hIcon = default, + hIconSm = default, + hCursor = default, + hbrBackground = default, + lpszMenuName = null, + cbClsExtra = 0, + cbWndExtra = 0, + }; + + PInvoke.RegisterClassEx(_windowClass); + + fixed (char* pszWindowName = szWindowName) + { + _hWnd = PInvoke.CreateWindowEx( + WINDOW_EX_STYLE.WS_EX_LAYERED | WINDOW_EX_STYLE.WS_EX_COMPOSITED, + pszClassName, + pszWindowName, + WINDOW_STYLE.WS_CHILD | WINDOW_STYLE.WS_CLIPSIBLINGS | WINDOW_STYLE.WS_VISIBLE, + 0, 0, 0, 0, + new(parent), + HMENU.Null, + hInst); + } + } _ = ChildWindowToXaml(parent, presenter); } @@ -161,14 +181,14 @@ private unsafe bool ChildWindowToXaml(nint parent, UIElement presenter) D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_WARP, ]; - Windows.Win32.Foundation.HRESULT hr = default; + HRESULT hr = default; Guid IID_IDCompositionDevice = typeof(IDCompositionDevice).GUID; using ComPtr pD3D11Device = default; using ComPtr pD3D11DeviceContext = default; using ComPtr pDXGIDevice = default; using ComPtr pDCompositionDevice = default; using ComPtr pControlSurface = default; - ComPtr pChildVisual = default; // Don't dispose this one, it's used by the compositor + ComPtr pChildVisual = default; // This should not be disposed this; otherwise, it will crash // Create the D3D11 device foreach (var driverType in driverTypes) @@ -198,21 +218,21 @@ private unsafe bool ChildWindowToXaml(nint parent, UIElement presenter) // Create the visual hr = pDCompositionDevice.Get()->CreateVisual(pChildVisual.GetAddressOf()); - hr = pDCompositionDevice.Get()->CreateSurfaceFromHwnd(new(hwnd.DangerousGetHandle()), pControlSurface.GetAddressOf()); + hr = pDCompositionDevice.Get()->CreateSurfaceFromHwnd(_hWnd, pControlSurface.GetAddressOf()); hr = pChildVisual.Get()->SetContent(pControlSurface.Get()); if (pChildVisual.IsNull || pControlSurface.IsNull) return false; // Get the compositor and set the visual on it var compositor = ElementCompositionPreview.GetElementVisual(presenter).Compositor; - outputLink = ContentExternalOutputLink.Create(compositor); + _contentExternalOutputLink = ContentExternalOutputLink.Create(compositor); - var target = outputLink.As(); + var target = _contentExternalOutputLink.As(); target.SetRoot((nint)pChildVisual.Get()); - outputLink.PlacementVisual.Size = new(0, 0); - outputLink.PlacementVisual.Scale = new(1 / (float)presenter.XamlRoot.RasterizationScale); - ElementCompositionPreview.SetElementChildVisual(presenter, outputLink.PlacementVisual); + _contentExternalOutputLink.PlacementVisual.Size = new(0, 0); + _contentExternalOutputLink.PlacementVisual.Scale = new(1 / (float)presenter.XamlRoot.RasterizationScale); + ElementCompositionPreview.SetElementChildVisual(presenter, _contentExternalOutputLink.PlacementVisual); // Commit the all pending DComp commands pDCompositionDevice.Get()->Commit(); @@ -221,7 +241,7 @@ private unsafe bool ChildWindowToXaml(nint parent, UIElement presenter) return PInvoke.DwmSetWindowAttribute( - new((nint)hwnd), + new((nint)_hWnd), DWMWINDOWATTRIBUTE.DWMWA_CLOAK, &dwAttrib, (uint)Marshal.SizeOf(dwAttrib)) @@ -230,14 +250,13 @@ private unsafe bool ChildWindowToXaml(nint parent, UIElement presenter) public void UnloadPreview() { - if (hwnd != HWND.NULL) - DestroyWindow(hwnd); + if (_hWnd != HWND.Null) + PInvoke.DestroyWindow(_hWnd); - outputLink?.Dispose(); - outputLink = null; + _contentExternalOutputLink?.Dispose(); + _contentExternalOutputLink = null; - if (wCls is not null) - UnregisterClass(wCls.ClassName, Kernel32.GetModuleHandle()); + PInvoke.UnregisterClass(_windowClass.lpszClassName, PInvoke.GetModuleHandle(default(PWSTR))); } public unsafe void PointerEntered(bool onPreview) @@ -247,25 +266,25 @@ public unsafe void PointerEntered(bool onPreview) var dwAttrib = Convert.ToUInt32(false); PInvoke.DwmSetWindowAttribute( - new((nint)hwnd), + new((nint)_hWnd), DWMWINDOWATTRIBUTE.DWMWA_CLOAK, &dwAttrib, (uint)Marshal.SizeOf(dwAttrib)); - if (isOfficePreview) - PInvoke.SetWindowLongPtr(new((nint)hwnd), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, 0); + if (_isOfficePreview) + PInvoke.SetWindowLongPtr(new((nint)_hWnd), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, 0); } else { PInvoke.SetWindowLongPtr( - new((nint)hwnd), + new((nint)_hWnd), WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE, (nint)(WINDOW_EX_STYLE.WS_EX_LAYERED | WINDOW_EX_STYLE.WS_EX_COMPOSITED)); var dwAttrib = Convert.ToUInt32(true); PInvoke.DwmSetWindowAttribute( - new((nint)hwnd), + new((nint)_hWnd), DWMWINDOWATTRIBUTE.DWMWA_CLOAK, &dwAttrib, (uint)Marshal.SizeOf(dwAttrib)); @@ -274,4 +293,4 @@ public unsafe void PointerEntered(bool onPreview) } } -#pragma warning restore CS8305 // Type is for evaluation purposes only and is subject to change or removal in future updates. \ No newline at end of file +#pragma warning restore CS8305 // Type is for evaluation purposes only and is subject to change or removal in future updates. From 693ffab03d5b2a0d68793db11c96bf88890182bf Mon Sep 17 00:00:00 2001 From: 0x5BFA <62196528+0x5bfa@users.noreply.github.com> Date: Sat, 22 Mar 2025 00:53:12 +0900 Subject: [PATCH 2/3] Update ShellPreviewViewModel.cs --- .../ViewModels/UserControls/Previews/ShellPreviewViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Files.App/ViewModels/UserControls/Previews/ShellPreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/ShellPreviewViewModel.cs index 329e07e8d553..4c861148786e 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/ShellPreviewViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/ShellPreviewViewModel.cs @@ -188,7 +188,7 @@ private unsafe bool ChildWindowToXaml(nint parent, UIElement presenter) using ComPtr pDXGIDevice = default; using ComPtr pDCompositionDevice = default; using ComPtr pControlSurface = default; - ComPtr pChildVisual = default; // This should not be disposed this; otherwise, it will crash + ComPtr pChildVisual = default; // Don't dispose this one, it's used by the compositor // Create the D3D11 device foreach (var driverType in driverTypes) From 141bc824af6c1d838dd76b33006c6859dd4b94f0 Mon Sep 17 00:00:00 2001 From: 0x5BFA <62196528+0x5bfa@users.noreply.github.com> Date: Wed, 26 Mar 2025 02:59:00 +0900 Subject: [PATCH 3/3] Update src/Files.App/ViewModels/UserControls/Previews/ShellPreviewViewModel.cs Co-authored-by: Steve Signed-off-by: 0x5BFA <62196528+0x5bfa@users.noreply.github.com> --- .../ViewModels/UserControls/Previews/ShellPreviewViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Files.App/ViewModels/UserControls/Previews/ShellPreviewViewModel.cs b/src/Files.App/ViewModels/UserControls/Previews/ShellPreviewViewModel.cs index 4c861148786e..233b514b6996 100644 --- a/src/Files.App/ViewModels/UserControls/Previews/ShellPreviewViewModel.cs +++ b/src/Files.App/ViewModels/UserControls/Previews/ShellPreviewViewModel.cs @@ -129,7 +129,7 @@ public unsafe void LoadPreview(UIElement presenter) { var parent = MainWindow.Instance.WindowHandle; var hInst = PInvoke.GetModuleHandle(default(PWSTR)); - var szClassName = $"{GetType().Name}-{Guid.NewGuid()}"; + var szClassName = $"{nameof(ShellPreviewViewModel)}-{Guid.NewGuid()}"; var szWindowName = $"Preview"; fixed (char* pszClassName = szClassName)