From fe092bfc5735a9373df4a80939603469990a7944 Mon Sep 17 00:00:00 2001 From: dotMorten Date: Mon, 23 Sep 2024 22:30:45 -0700 Subject: [PATCH 01/13] Initial work on the new fast splash screen --- src/WinUIEx/NativeMethods.txt | 12 +++++++++++- src/WinUIEx/SplashScreen.cs | 5 +++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/WinUIEx/NativeMethods.txt b/src/WinUIEx/NativeMethods.txt index c2b8722..b2d485e 100644 --- a/src/WinUIEx/NativeMethods.txt +++ b/src/WinUIEx/NativeMethods.txt @@ -54,4 +54,14 @@ DwmExtendFrameIntoClientArea CreateRectRgn CreateSolidBrush FillRect -GetDC \ No newline at end of file +GetDC +ReleaseDC +GdiplusStartup +GdiplusShutdown +GetObject +GdipCreateBitmapFromFile +GdipCreateHBITMAPFromBitmap +WNDCLASSEXW +RegisterClassEx +CreateCompatibleDC +SelectObject \ No newline at end of file diff --git a/src/WinUIEx/SplashScreen.cs b/src/WinUIEx/SplashScreen.cs index 3cf1b6d..9df4879 100644 --- a/src/WinUIEx/SplashScreen.cs +++ b/src/WinUIEx/SplashScreen.cs @@ -7,9 +7,10 @@ namespace WinUIEx { /// - /// A splash screen window that shows with no chrome, and once has completed, - /// opens a new window + /// A splash screen window for rendering XAML that shows with no chrome, and once has completed, + /// opens a new window. /// + /// public class SplashScreen : Window { private Window? _window; From 61711ba717a9353c11309e62690247d7f865035e Mon Sep 17 00:00:00 2001 From: dotMorten Date: Mon, 23 Sep 2024 22:31:40 -0700 Subject: [PATCH 02/13] Initial work on the new fast splash screen --- src/WinUIEx/FastSplashScreen.cs | 379 ++++++++++++++++++++++++++++++++ 1 file changed, 379 insertions(+) create mode 100644 src/WinUIEx/FastSplashScreen.cs diff --git a/src/WinUIEx/FastSplashScreen.cs b/src/WinUIEx/FastSplashScreen.cs new file mode 100644 index 0000000..2583a85 --- /dev/null +++ b/src/WinUIEx/FastSplashScreen.cs @@ -0,0 +1,379 @@ +// Based on an implementation by Castorix: https://github.com/castorix/WinUI3_SplashScreen + +//#define MEDIAPLAYER +using Microsoft.UI.Xaml; +using System; +using System.Runtime.InteropServices; +using System.Xml.Linq; +using System.Xml.XPath; + +#if MEDIAPLAYER +using MFPlay; +#endif +using Windows.Win32; +using Windows.Win32.UI.WindowsAndMessaging; + +namespace WinUIEx +{ + /// + /// Simple and fast splash screen for display images during application startup. + /// + /// + /// Once your application window has launched/loaded, The splashscreen should be removed by either disposing this instance or calling . + /// + /// + public sealed class FastSplashScreen : IDisposable + { + private const nint COLOR_BACKGROUND = 1; + + private DispatcherTimer? dTimer; + private TimeSpan tsFadeoutDuration; + private DateTime tsFadeoutEnd; + Windows.Win32.Foundation.HWND hWndSplash = Windows.Win32.Foundation.HWND.Null; + // private Windows.Win32.UI.WindowsAndMessaging.WNDPROC delegateWndProc; + private Windows.Win32.Graphics.Gdi.HGDIOBJ hBitmap = Windows.Win32.Graphics.Gdi.HGDIOBJ.Null; + private nuint initToken = 0; +#if MEDIAPLAYER + private MFPlayer? mediaPlayer; +#endif + + /// + /// Shows the splashscreen image specified in the application manifest. + /// + /// + /// + public static FastSplashScreen ShowDefaultSplashScreen() + { + var image = GetManifestSplashScreen(); + if (image is null) + throw new InvalidOperationException("SplashScreen section not found in AppxManifest.xml"); + var manager = new Microsoft.Windows.ApplicationModel.Resources.ResourceManager(); + var context = manager.CreateResourceContext(); + + var dpi = PInvoke.GetDpiForWindow(PInvoke.GetDesktopWindow()); + var scale = (int)(dpi / 96d * 100); + context.QualifierValues["Scale"] = scale.ToString(); + var splashScreenImageResource = manager.MainResourceMap.TryGetValue("Files/" + image.Replace('\\','/'), context); + if (splashScreenImageResource is not null && splashScreenImageResource.Kind == Microsoft.Windows.ApplicationModel.Resources.ResourceCandidateKind.FilePath) + { + return FastSplashScreen.ShowSplashScreenImage(splashScreenImageResource.ValueAsString); + } + throw new InvalidOperationException("Splash screen image not found in resources"); + } + + private static string? GetManifestSplashScreen() + { + var rootFolder = Windows.ApplicationModel.Package.Current?.InstalledLocation.Path ?? AppContext.BaseDirectory; + var docPath = System.IO.Path.Combine(rootFolder, "AppxManifest.xml"); + if (!System.IO.File.Exists(docPath)) + throw new System.IO.FileNotFoundException("AppxManifest.xml not found"); + + var doc = XDocument.Load(docPath, LoadOptions.None); + var reader = doc.CreateReader(); + var namespaceManager = new System.Xml.XmlNamespaceManager(reader.NameTable); + namespaceManager.AddNamespace("x", "http://schemas.microsoft.com/appx/manifest/foundation/windows10"); + namespaceManager.AddNamespace("uap", "http://schemas.microsoft.com/appx/manifest/uap/windows10"); + + var element = doc.Root?.XPathSelectElement("/x:Package/x:Applications/x:Application/uap:VisualElements/uap:SplashScreen", namespaceManager); + return element?.Attribute(XName.Get("Image"))?.Value; + } + + /// + /// Shows a splash screen image at the center of the screen + /// + /// + /// + public static FastSplashScreen ShowSplashScreenImage(string image) + { + var s = new FastSplashScreen(); + s.Initialize(); + var hBitmap = s.GetBitmap(image); + s.DisplaySplash(Windows.Win32.Foundation.HWND.Null, hBitmap, null); + return s; + } +#if MEDIAPLAYER + public static FastSplashScreen ShowSplashScreenVideo(string video) + { + var s = new FastSplashScreen(); + s.Initialize(); + s.DisplaySplash(Windows.Win32.Foundation.HWND.Null, Windows.Win32.Graphics.Gdi.HBITMAP.Null, video); + return s; + } +#endif + private void Initialize() + { + var input = new Windows.Win32.Graphics.GdiPlus.GdiplusStartupInput() + { + GdiplusVersion = 1, + SuppressBackgroundThread = false, + SuppressExternalCodecs = false + }; + var output = new Windows.Win32.Graphics.GdiPlus.GdiplusStartupOutput(); + var nStatus = PInvoke.GdiplusStartup(ref initToken, input, ref output); + } + + /// + public void Dispose() + { + Dispose(true); + } + + private void Dispose(bool disposing) + { + CleanUp(); + if (disposing) + { +#if MEDIAPLAYER + mediaPlayer.Dispose(); +#endif + } + } + + private void CleanUp() + { + if (hWndSplash != IntPtr.Zero) + { + PInvoke.DestroyWindow(hWndSplash); + } + if (hBitmap != IntPtr.Zero) + { + PInvoke.DeleteObject(new Windows.Win32.Graphics.Gdi.HGDIOBJ(hBitmap)); + hBitmap = Windows.Win32.Graphics.Gdi.HGDIOBJ.Null; + } + PInvoke.GdiplusShutdown(initToken); + } + + /// + /// Finalizer + /// + ~FastSplashScreen() => Dispose(false); + + private unsafe void DisplaySplash(Windows.Win32.Foundation.HWND hWnd, Windows.Win32.Graphics.Gdi.HBITMAP bitmap, string? sVideo) + { + Windows.Win32.UI.WindowsAndMessaging.WNDCLASSEXW wcex; + this.hBitmap = bitmap; + wcex.cbSize = (uint)Marshal.SizeOf(typeof(WNDCLASSEXW)); + wcex.style = WNDCLASS_STYLES.CS_HREDRAW | WNDCLASS_STYLES.CS_VREDRAW | WNDCLASS_STYLES.CS_DBLCLKS; + wcex.hbrBackground = new Windows.Win32.Graphics.Gdi.HBRUSH(COLOR_BACKGROUND + 1); + wcex.cbClsExtra = 0; + wcex.cbWndExtra = 0; + wcex.hInstance = new Windows.Win32.Foundation.HINSTANCE(Marshal.GetHINSTANCE(this.GetType().Module)); + wcex.hIcon = HICON.Null; + wcex.hCursor = HCURSOR.Null; + wcex.lpszMenuName = null; + string sClassName = "Win32Class"; + fixed (char* name = sClassName) + wcex.lpszClassName = name; + wcex.lpfnWndProc = &Win32WndProc; + + wcex.hIconSm = HICON.Null; + ushort nRet = PInvoke.RegisterClassEx(wcex); + if (nRet == 0) + { + int nError = Marshal.GetLastWin32Error(); + if (nError != 1410) //0x582 ERROR_CLASS_ALREADY_EXISTS + return; + } + int nWidth = 0, nHeight = 0; + if (hBitmap != IntPtr.Zero) + { + BITMAP bm; + PInvoke.GetObject(new DeleteObjectSafeHandle(hBitmap), Marshal.SizeOf(typeof(BITMAP)), &bm); + nWidth = bm.bmWidth; + nHeight = bm.bmHeight; + } + + hWndSplash = PInvoke.CreateWindowEx(WINDOW_EX_STYLE.WS_EX_TOOLWINDOW | WINDOW_EX_STYLE.WS_EX_LAYERED | WINDOW_EX_STYLE.WS_EX_TRANSPARENT | WINDOW_EX_STYLE.WS_EX_TOPMOST, + sClassName, "Win32 window", WINDOW_STYLE.WS_POPUP | WINDOW_STYLE.WS_VISIBLE, 400, 400, nWidth, nHeight, hWnd, null, + new DestroyIconSafeHandle(wcex.hInstance), null); + if (hBitmap != IntPtr.Zero) + { + SetPictureToLayeredWindow(hWndSplash, hBitmap); + CenterToScreen(hWndSplash); + } +#if MEDIAPLAYER + if (sVideo != null) + { + mediaPlayer = new MFPlayer(this, hWndSplash, sVideo); + } +#endif + } + [StructLayoutAttribute(LayoutKind.Sequential)] + private struct BITMAP + { + public int bmType; + public int bmWidth; + public int bmHeight; + public int bmWidthBytes; + public short bmPlanes; + public short bmBitsPixel; + public IntPtr bmBits; + } + +#if MEDIAPLAYER + private class MFPlayer : IMFPMediaPlayerCallback, IDisposable + { + private readonly FastSplashScreen m_ss; + private readonly Windows.Win32.Foundation.HWND m_hWndParent; + private readonly IMFPMediaPlayer m_pMediaPlayer; + + internal unsafe MFPlayer(FastSplashScreen ss, Windows.Win32.Foundation.HWND hWnd, string sVideo) + { + GlobalStructures.HRESULT hr = MFPlayTools.MFPCreateMediaPlayer(sVideo, false, MFPlay.MFPlayTools.MFP_CREATION_OPTIONS.MFP_OPTION_NONE, this, hWnd, out m_pMediaPlayer); + m_hWndParent = hWnd; + m_ss = ss; + } + + void IMFPMediaPlayerCallback.OnMediaPlayerEvent(MFP_EVENT_HEADER pEventHeader) + { + switch (pEventHeader.eEventType) + { + case MFP_EVENT_TYPE.MFP_EVENT_TYPE_MEDIAITEM_SET: + { + GlobalStructures.SIZE szVideo, szARVideo; + GlobalStructures.HRESULT hr = m_pMediaPlayer.GetNativeVideoSize(out szVideo, out szARVideo); + if (hr == GlobalStructures.HRESULT.S_OK) + { + PInvoke.SetWindowPos(m_hWndParent, Windows.Win32.Foundation.HWND.Null, 0, 0, szVideo.cx, szVideo.cy, SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); + m_ss.CenterToScreen(m_hWndParent); + hr = m_pMediaPlayer.Play(); + } + } + break; + case MFP_EVENT_TYPE.MFP_EVENT_TYPE_PLAYBACK_ENDED: + { + PlaybackEnded?.Invoke(this, EventArgs.Empty); + } + break; + } + return; + } + + ~MFPlayer() => Dispose(false); + + public void Dispose() => Dispose(true); + + private void Dispose(bool disposing) => m_pMediaPlayer.Shutdown(); + + public event EventHandler PlaybackEnded; + } +#endif + + /// + /// Hides the splashscreen + /// + /// Time spend fading out the splash screen (defaults to no fade) + public void Hide(TimeSpan fadeTimout = default) + { + if (fadeTimout.Ticks <= 0) + CleanUp(); + else + { + dTimer = new DispatcherTimer(); + dTimer.Interval = TimeSpan.FromMilliseconds(16); + tsFadeoutDuration = fadeTimout; + tsFadeoutEnd = DateTime.UtcNow + tsFadeoutDuration; + dTimer.Tick += FadeTimer_Tick; + dTimer.Start(); + } + } + + private unsafe Windows.Win32.Graphics.Gdi.HBITMAP GetBitmap(string sBitmapFile) + { + var bitmap = new Windows.Win32.Graphics.GdiPlus.GpBitmap(); + var pBitmap = &bitmap; + + var nStatus = PInvoke.GdipCreateBitmapFromFile(sBitmapFile, ref pBitmap); + + if (nStatus == Windows.Win32.Graphics.GdiPlus.Status.Ok) + { + var hBitmap = new Windows.Win32.Graphics.Gdi.HBITMAP(); + PInvoke.GdipCreateHBITMAPFromBitmap(pBitmap, &hBitmap, (uint)System.Drawing.ColorTranslator.ToWin32(System.Drawing.Color.FromArgb(0))); + return hBitmap; + } + throw new InvalidOperationException("Failed to open bitmap: " + nStatus.ToString()); + } + + private void FadeTimer_Tick(object? sender, object e) + { + DateTime dtNow = DateTime.UtcNow; + if (dtNow >= tsFadeoutEnd) + { + if (dTimer != null) + { + dTimer.Stop(); + dTimer = null; + } + CleanUp(); + } + else + { + double nProgress = (tsFadeoutEnd - dtNow).TotalMilliseconds / tsFadeoutDuration.TotalMilliseconds; + // BLENDFUNCTION bf = new BLENDFUNCTION(); + // bf.BlendOp = AC_SRC_OVER; + // bf.AlphaFormat = AC_SRC_ALPHA; + // bf.SourceConstantAlpha = (byte)(255 * nProgress); + var bf = new Windows.Win32.Graphics.Gdi.BLENDFUNCTION() + { + BlendOp = AC_SRC_OVER, + SourceConstantAlpha = (byte)(255 * nProgress), + AlphaFormat = AC_SRC_ALPHA + }; + + PInvoke.UpdateLayeredWindow(hWndSplash, + new Windows.Win32.Graphics.Gdi.HDC(0), null, null, new Windows.Win32.Graphics.Gdi.HDC(0), (System.Drawing.Point?)null, new Windows.Win32.Foundation.COLORREF(0), bf, UPDATE_LAYERED_WINDOW_FLAGS.ULW_ALPHA); + } + } + + private const byte AC_SRC_OVER = 0x00; + private const byte AC_SRC_ALPHA = 0x01; + + private unsafe void SetPictureToLayeredWindow(Windows.Win32.Foundation.HWND hWnd, IntPtr hBitmap) + { + BITMAP bm; + Windows.Win32.Graphics.Gdi.HBITMAP bitmap = new Windows.Win32.Graphics.Gdi.HBITMAP(hBitmap); + PInvoke.GetObject(new Windows.Win32.DeleteObjectSafeHandle(hBitmap), Marshal.SizeOf(typeof(BITMAP)), &bm); + System.Drawing.Size sizeBitmap = new System.Drawing.Size(bm.bmWidth, bm.bmHeight); + + var hDCScreen = PInvoke.GetDC(Windows.Win32.Foundation.HWND.Null); + var hDCMem = PInvoke.CreateCompatibleDC(hDCScreen); + using var hBitmapOld = PInvoke.SelectObject(hDCMem, new DestroyIconSafeHandle(hBitmap)); + + var bf = new Windows.Win32.Graphics.Gdi.BLENDFUNCTION() + { + BlendOp = AC_SRC_OVER, + SourceConstantAlpha = 255, + AlphaFormat = AC_SRC_ALPHA + }; + PInvoke.GetWindowRect(hWnd, out var rectWnd); + + System.Drawing.Point ptSrc = new System.Drawing.Point(); + System.Drawing.Point ptDest = new System.Drawing.Point(rectWnd.left, rectWnd.top); + + bool bRet = PInvoke.UpdateLayeredWindow(hWnd, + hDCScreen, ptDest, new Windows.Win32.Foundation.SIZE(sizeBitmap.Width, sizeBitmap.Height), + hDCMem, ptSrc, new Windows.Win32.Foundation.COLORREF(0), bf, UPDATE_LAYERED_WINDOW_FLAGS.ULW_ALPHA); + + PInvoke.ReleaseDC(Windows.Win32.Foundation.HWND.Null, hDCScreen); + + } + + private unsafe void CenterToScreen(IntPtr hWnd) + { + var rcWorkArea = new Windows.Win32.Foundation.RECT(); + PInvoke.SystemParametersInfo(SYSTEM_PARAMETERS_INFO_ACTION.SPI_GETWORKAREA, 0, &rcWorkArea, 0); + var h = new Windows.Win32.Foundation.HWND(hWnd); + PInvoke.GetWindowRect(h, out Windows.Win32.Foundation.RECT rc); + int nX = System.Convert.ToInt32((rcWorkArea.left + rcWorkArea.right) / (double)2 - (rc.right - rc.left) / (double)2); + int nY = System.Convert.ToInt32((rcWorkArea.top + rcWorkArea.bottom) / (double)2 - (rc.bottom - rc.top) / (double)2); + PInvoke.SetWindowPos(h, Windows.Win32.Foundation.HWND.Null, nX, nY, -1, -1, SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER | SET_WINDOW_POS_FLAGS.SWP_FRAMECHANGED | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE); + } + + [UnmanagedCallersOnly(CallConvs = new Type[] { typeof(System.Runtime.CompilerServices.CallConvStdcall) })] + private static Windows.Win32.Foundation.LRESULT Win32WndProc(Windows.Win32.Foundation.HWND hwnd, + uint msg, Windows.Win32.Foundation.WPARAM wParam, Windows.Win32.Foundation.LPARAM lParam) + { + return PInvoke.DefWindowProc(hwnd, msg, wParam, lParam); + } + } +} From 666db1542d7114307bf28c5ac88cc1cfed3d3caf Mon Sep 17 00:00:00 2001 From: dotMorten Date: Mon, 23 Sep 2024 22:33:09 -0700 Subject: [PATCH 03/13] Clean up --- src/WinUIEx/FastSplashScreen.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/WinUIEx/FastSplashScreen.cs b/src/WinUIEx/FastSplashScreen.cs index 2583a85..ef03bdb 100644 --- a/src/WinUIEx/FastSplashScreen.cs +++ b/src/WinUIEx/FastSplashScreen.cs @@ -30,7 +30,6 @@ public sealed class FastSplashScreen : IDisposable private TimeSpan tsFadeoutDuration; private DateTime tsFadeoutEnd; Windows.Win32.Foundation.HWND hWndSplash = Windows.Win32.Foundation.HWND.Null; - // private Windows.Win32.UI.WindowsAndMessaging.WNDPROC delegateWndProc; private Windows.Win32.Graphics.Gdi.HGDIOBJ hBitmap = Windows.Win32.Graphics.Gdi.HGDIOBJ.Null; private nuint initToken = 0; #if MEDIAPLAYER @@ -309,10 +308,6 @@ private void FadeTimer_Tick(object? sender, object e) else { double nProgress = (tsFadeoutEnd - dtNow).TotalMilliseconds / tsFadeoutDuration.TotalMilliseconds; - // BLENDFUNCTION bf = new BLENDFUNCTION(); - // bf.BlendOp = AC_SRC_OVER; - // bf.AlphaFormat = AC_SRC_ALPHA; - // bf.SourceConstantAlpha = (byte)(255 * nProgress); var bf = new Windows.Win32.Graphics.Gdi.BLENDFUNCTION() { BlendOp = AC_SRC_OVER, @@ -355,7 +350,6 @@ private unsafe void SetPictureToLayeredWindow(Windows.Win32.Foundation.HWND hWnd hDCMem, ptSrc, new Windows.Win32.Foundation.COLORREF(0), bf, UPDATE_LAYERED_WINDOW_FLAGS.ULW_ALPHA); PInvoke.ReleaseDC(Windows.Win32.Foundation.HWND.Null, hDCScreen); - } private unsafe void CenterToScreen(IntPtr hWnd) From dfb40be69b4da740c2dbcaa3514ba8e00d7ec68e Mon Sep 17 00:00:00 2001 From: dotMorten Date: Mon, 23 Sep 2024 22:55:41 -0700 Subject: [PATCH 04/13] Add fastsplashscreen to startup sequence --- src/WinUIExSample/App.xaml.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/WinUIExSample/App.xaml.cs b/src/WinUIExSample/App.xaml.cs index 5ba3758..1c6e6f6 100644 --- a/src/WinUIExSample/App.xaml.cs +++ b/src/WinUIExSample/App.xaml.cs @@ -25,6 +25,7 @@ public App() { if (WebAuthenticator.CheckOAuthRedirectionActivation()) return; + fss = FastSplashScreen.ShowDefaultSplashScreen(); this.InitializeComponent(); int length = 0; var sb = new System.Text.StringBuilder(0); @@ -34,8 +35,9 @@ public App() // Not a packaged app. Configure file-based persistence instead WinUIEx.WindowManager.PersistenceStorage = new FilePersistence("WinUIExPersistence.json"); } + } - + FastSplashScreen fss; /// /// Invoked when the application is launched normally by the end user. Other entry points /// will be used such as when the application is launched to open a specific file. @@ -45,7 +47,18 @@ protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs ar { var window = new MainWindow(); var splash = new SplashScreen(window); - splash.Completed += (s, e) => m_window = (WindowEx)e; + splash.Completed += (s, e) => + { + m_window = (WindowEx)e; + }; + splash.Activated += Splash_Activated; + } + + private void Splash_Activated(object sender, WindowActivatedEventArgs args) + { + ((Window)sender).Activated -= Splash_Activated; + fss?.Hide(TimeSpan.FromSeconds(1)); + fss = null; } private WindowEx m_window; From a73cd3727ef2f2ec41240ff8ed6a2a122e804012 Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 24 Sep 2024 11:28:35 -0700 Subject: [PATCH 05/13] Add code for custom main for faster splashscreen --- src/WinUIExSample/App.xaml.cs | 26 +++++++++++++++++++++++++- src/WinUIExSample/WinUIExSample.csproj | 1 + 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/WinUIExSample/App.xaml.cs b/src/WinUIExSample/App.xaml.cs index 1c6e6f6..30eb1a2 100644 --- a/src/WinUIExSample/App.xaml.cs +++ b/src/WinUIExSample/App.xaml.cs @@ -23,9 +23,11 @@ public partial class App : Application /// public App() { +#if !DISABLE_XAML_GENERATED_MAIN // With custom main, we'd rather want to do this code in main if (WebAuthenticator.CheckOAuthRedirectionActivation()) return; fss = FastSplashScreen.ShowDefaultSplashScreen(); +#endif this.InitializeComponent(); int length = 0; var sb = new System.Text.StringBuilder(0); @@ -37,7 +39,7 @@ public App() } } - FastSplashScreen fss; + internal FastSplashScreen fss { get; set; } /// /// Invoked when the application is launched normally by the end user. Other entry points /// will be used such as when the application is launched to open a specific file. @@ -142,4 +144,26 @@ public void Clear() IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException(); // TODO } } + +#if DISABLE_XAML_GENERATED_MAIN + /// + /// Program class + /// + public static class Program + { + [global::System.STAThreadAttribute] + static void Main(string[] args) + { + if (WebAuthenticator.CheckOAuthRedirectionActivation(true)) + return; + var fss = FastSplashScreen.ShowDefaultSplashScreen(); + global::WinRT.ComWrappersSupport.InitializeComWrappers(); + global::Microsoft.UI.Xaml.Application.Start((p) => { + var context = new global::Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext(global::Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread()); + global::System.Threading.SynchronizationContext.SetSynchronizationContext(context); + new App() { fss = fss }; + }); + } + } +#endif } diff --git a/src/WinUIExSample/WinUIExSample.csproj b/src/WinUIExSample/WinUIExSample.csproj index 5b2bd76..258cacb 100644 --- a/src/WinUIExSample/WinUIExSample.csproj +++ b/src/WinUIExSample/WinUIExSample.csproj @@ -17,6 +17,7 @@ 10.0.22621.38 + DISABLE_XAML_GENERATED_MAIN;$(DefineConstants) true From 921dd3b554cac9bf536e4b3d0f49dda99d5f6610 Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 24 Sep 2024 16:26:58 -0700 Subject: [PATCH 06/13] Update doc --- docs/concepts/Splashscreen.md | 124 ++++++++++++++++++++++++++-------- 1 file changed, 95 insertions(+), 29 deletions(-) diff --git a/docs/concepts/Splashscreen.md b/docs/concepts/Splashscreen.md index 33c8228..8802828 100644 --- a/docs/concepts/Splashscreen.md +++ b/docs/concepts/Splashscreen.md @@ -1,7 +1,73 @@ ## Splash Screen +WinUIEx provides two kinds of splash screens: + - `FastSplashScreen`: A simple splash screen that shows just an image and can be launched before any UI loads. + - `SplashScreen`: A more advanced splash screen that can show any XAML including a progress bar and status text. + + +### FastSplashScreen + +`FastSplashScreen` will show an image while the app is loading. To use it, create a new `FastSplashScreen` in App.xaml.cs: + +```cs +private FastSplashScreen vss { get; set; } + +public App() +{ + fss = FastSplashScreen.ShowDefaultSplashScreen(); // Shows the splash screen you already defined in your app manifest. For unpackaged apps use .ShowSplashScreenImage(imagepath): + // fss = FastSplashScreen.ShowSplashScreenImage(full_path_to_image_); // Shows a custom splash screen image. Must be a full-path (no relative paths) + this.InitializeComponent(); +} +``` + +Once your window is activated, you can remove the splash screen by either calling `.Dispose()` or `Hide()`. + +```cs +protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) +{ + m_window = new MainWindow(); + m_window.Activate(); + Window_Activated += Window_Activated; + +} + +private void Window_Activated(object sender, WindowActivatedEventArgs args) +{ + ((Window)sender).Activated -= Window_Activated; + fss?.Hide(); + fss = null; +} +``` + +For even faster splash screen, disable the XAML generated main method by defining the `DISABLE_XAML_GENERATED_MAIN` preprocessor directive +and instead defining your own start method. You'll then be able to display the splash screen as the very first thing before the application is created. For example: + +```cs +#if DISABLE_XAML_GENERATED_MAIN + public static class Program + { + [System.STAThreadAttribute] + static void Main(string[] args) + { + // If you're using the WebAuthenticator, make sure you call this method first before the splashscreen shows + if (WebAuthenticator.CheckOAuthRedirectionActivation(true)) + return; + var fss = FastSplashScreen.ShowDefaultSplashScreen(); + WinRT.ComWrappersSupport.InitializeComWrappers(); + Microsoft.UI.Xaml.Application.Start((p) => { + var context = new Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext(Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread()); + System.Threading.SynchronizationContext.SetSynchronizationContext(context); + new App(fss); // Pass the splash screen to your app so it can close it on activation + }); + } + } +#endif +``` + +### SplashScreen + To create a new splash screen, first create a new WinUI Window. -Next change the baseclass from "Window" to SplashScreen: +Next change the baseclass from `Window` to `SplashScreen`: Before: ```xml @@ -21,52 +87,52 @@ Before: ```cs public sealed partial class SplashScreen : Window { - public SplashScreen() - { - this.InitializeComponent(); - } +public SplashScreen() +{ + this.InitializeComponent(); +} } ``` After: ```cs public sealed partial class SplashScreen : WinUIEx.SplashScreen { - public SplashScreen(Type window) : base(window) - { - this.InitializeComponent(); - } +public SplashScreen(Type window) : base(window) +{ + this.InitializeComponent(); +} } ``` Next in App.xaml.cs, change OnLaunched from: ```cs - protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) - { - m_window = new MainWindow(); - m_window.Activate(); - } +protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) +{ + m_window = new MainWindow(); + m_window.Activate(); +} ``` To: ```cs - protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) - { - var splash = new SplashScreen(typeof(MainWindow)); - splash.Completed += (s, e) => m_window = e; - } +protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) +{ + var splash = new SplashScreen(typeof(MainWindow)); + splash.Completed += (s, e) => m_window = e; +} ``` Lastly, override OnLoading, to create some long-running setup work - once this method completes, the splash screen will close and the window in the type parameter will launch. Example: ``` - protected override async Task OnLoading() - { - //TODO: Do some actual work - for (int i = 0; i < 100; i+=5) - { - statusText.Text = $"Loading {i}%..."; - progressBar.Value = i; - await Task.Delay(50); - } - } +protected override async Task OnLoading() +{ + //TODO: Do some actual work + for (int i = 0; i < 100; i+=5) + { + statusText.Text = $"Loading {i}%..."; + progressBar.Value = i; + await Task.Delay(50); + } +} ``` \ No newline at end of file From 0aa460ad97b5f32cc0b15417b5b8e3993ad787c9 Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 24 Sep 2024 16:28:30 -0700 Subject: [PATCH 07/13] Fix indents --- docs/concepts/Splashscreen.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/concepts/Splashscreen.md b/docs/concepts/Splashscreen.md index 8802828..2691b6c 100644 --- a/docs/concepts/Splashscreen.md +++ b/docs/concepts/Splashscreen.md @@ -87,20 +87,20 @@ Before: ```cs public sealed partial class SplashScreen : Window { -public SplashScreen() -{ + public SplashScreen() + { this.InitializeComponent(); -} + } } ``` After: ```cs public sealed partial class SplashScreen : WinUIEx.SplashScreen { -public SplashScreen(Type window) : base(window) -{ + public SplashScreen(Type window) : base(window) + { this.InitializeComponent(); -} + } } ``` @@ -108,16 +108,16 @@ Next in App.xaml.cs, change OnLaunched from: ```cs protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) { - m_window = new MainWindow(); - m_window.Activate(); + m_window = new MainWindow(); + m_window.Activate(); } ``` To: ```cs protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) { - var splash = new SplashScreen(typeof(MainWindow)); - splash.Completed += (s, e) => m_window = e; + var splash = new SplashScreen(typeof(MainWindow)); + splash.Completed += (s, e) => m_window = e; } ``` @@ -127,12 +127,12 @@ Example: ``` protected override async Task OnLoading() { - //TODO: Do some actual work - for (int i = 0; i < 100; i+=5) - { - statusText.Text = $"Loading {i}%..."; - progressBar.Value = i; - await Task.Delay(50); - } + //TODO: Do some actual work + for (int i = 0; i < 100; i+=5) + { + statusText.Text = $"Loading {i}%..."; + progressBar.Value = i; + await Task.Delay(50); + } } ``` \ No newline at end of file From e475407b125295bb5e2c2561af7da87006b13dc5 Mon Sep 17 00:00:00 2001 From: Morten Nielsen Date: Tue, 24 Sep 2024 16:29:01 -0700 Subject: [PATCH 08/13] set lang --- docs/concepts/Splashscreen.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/concepts/Splashscreen.md b/docs/concepts/Splashscreen.md index 2691b6c..bcfa0fc 100644 --- a/docs/concepts/Splashscreen.md +++ b/docs/concepts/Splashscreen.md @@ -124,7 +124,7 @@ protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs ar Lastly, override OnLoading, to create some long-running setup work - once this method completes, the splash screen will close and the window in the type parameter will launch. Example: -``` +```cs protected override async Task OnLoading() { //TODO: Do some actual work From 45957760611fadc7448e60ecfb7b6a5400521bff Mon Sep 17 00:00:00 2001 From: dotMorten Date: Mon, 18 Nov 2024 19:20:12 -0800 Subject: [PATCH 09/13] Rename FastSplashScreen to SimpleSplashScreen --- docs/concepts/Splashscreen.md | 14 ++++++------- ...tSplashScreen.cs => SimpleSplashScreen.cs} | 20 +++++++++---------- src/WinUIEx/SplashScreen.cs | 2 +- src/WinUIExSample/App.xaml.cs | 6 +++--- 4 files changed, 21 insertions(+), 21 deletions(-) rename src/WinUIEx/{FastSplashScreen.cs => SimpleSplashScreen.cs} (95%) diff --git a/docs/concepts/Splashscreen.md b/docs/concepts/Splashscreen.md index bcfa0fc..965239f 100644 --- a/docs/concepts/Splashscreen.md +++ b/docs/concepts/Splashscreen.md @@ -1,21 +1,21 @@ ## Splash Screen WinUIEx provides two kinds of splash screens: - - `FastSplashScreen`: A simple splash screen that shows just an image and can be launched before any UI loads. + - `SimpleSplashScreen`: A simple splash screen that shows just an image and can be launched before any UI loads. - `SplashScreen`: A more advanced splash screen that can show any XAML including a progress bar and status text. -### FastSplashScreen +### SimpleSplashScreen -`FastSplashScreen` will show an image while the app is loading. To use it, create a new `FastSplashScreen` in App.xaml.cs: +`SimpleSplashScreen` will show an image while the app is loading. To use it, create a new `SimpleSplashScreen` in App.xaml.cs: ```cs -private FastSplashScreen vss { get; set; } +private SimpleSplashScreen vss { get; set; } public App() { - fss = FastSplashScreen.ShowDefaultSplashScreen(); // Shows the splash screen you already defined in your app manifest. For unpackaged apps use .ShowSplashScreenImage(imagepath): - // fss = FastSplashScreen.ShowSplashScreenImage(full_path_to_image_); // Shows a custom splash screen image. Must be a full-path (no relative paths) + fss = SimpleSplashScreen.ShowDefaultSplashScreen(); // Shows the splash screen you already defined in your app manifest. For unpackaged apps use .ShowSplashScreenImage(imagepath): + // fss = SimpleSplashScreen.ShowSplashScreenImage(full_path_to_image_); // Shows a custom splash screen image. Must be a full-path (no relative paths) this.InitializeComponent(); } ``` @@ -52,7 +52,7 @@ and instead defining your own start method. You'll then be able to display the s // If you're using the WebAuthenticator, make sure you call this method first before the splashscreen shows if (WebAuthenticator.CheckOAuthRedirectionActivation(true)) return; - var fss = FastSplashScreen.ShowDefaultSplashScreen(); + var fss = SimpleSplashScreen.ShowDefaultSplashScreen(); WinRT.ComWrappersSupport.InitializeComWrappers(); Microsoft.UI.Xaml.Application.Start((p) => { var context = new Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext(Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread()); diff --git a/src/WinUIEx/FastSplashScreen.cs b/src/WinUIEx/SimpleSplashScreen.cs similarity index 95% rename from src/WinUIEx/FastSplashScreen.cs rename to src/WinUIEx/SimpleSplashScreen.cs index ef03bdb..0d9f244 100644 --- a/src/WinUIEx/FastSplashScreen.cs +++ b/src/WinUIEx/SimpleSplashScreen.cs @@ -22,7 +22,7 @@ namespace WinUIEx /// Once your application window has launched/loaded, The splashscreen should be removed by either disposing this instance or calling . /// /// - public sealed class FastSplashScreen : IDisposable + public sealed class SimpleSplashScreen : IDisposable { private const nint COLOR_BACKGROUND = 1; @@ -41,7 +41,7 @@ public sealed class FastSplashScreen : IDisposable /// /// /// - public static FastSplashScreen ShowDefaultSplashScreen() + public static SimpleSplashScreen ShowDefaultSplashScreen() { var image = GetManifestSplashScreen(); if (image is null) @@ -55,7 +55,7 @@ public static FastSplashScreen ShowDefaultSplashScreen() var splashScreenImageResource = manager.MainResourceMap.TryGetValue("Files/" + image.Replace('\\','/'), context); if (splashScreenImageResource is not null && splashScreenImageResource.Kind == Microsoft.Windows.ApplicationModel.Resources.ResourceCandidateKind.FilePath) { - return FastSplashScreen.ShowSplashScreenImage(splashScreenImageResource.ValueAsString); + return SimpleSplashScreen.ShowSplashScreenImage(splashScreenImageResource.ValueAsString); } throw new InvalidOperationException("Splash screen image not found in resources"); } @@ -82,18 +82,18 @@ public static FastSplashScreen ShowDefaultSplashScreen() /// /// /// - public static FastSplashScreen ShowSplashScreenImage(string image) + public static SimpleSplashScreen ShowSplashScreenImage(string image) { - var s = new FastSplashScreen(); + var s = new SimpleSplashScreen(); s.Initialize(); var hBitmap = s.GetBitmap(image); s.DisplaySplash(Windows.Win32.Foundation.HWND.Null, hBitmap, null); return s; } #if MEDIAPLAYER - public static FastSplashScreen ShowSplashScreenVideo(string video) + public static SimpleSplashScreen ShowSplashScreenVideo(string video) { - var s = new FastSplashScreen(); + var s = new SimpleSplashScreen(); s.Initialize(); s.DisplaySplash(Windows.Win32.Foundation.HWND.Null, Windows.Win32.Graphics.Gdi.HBITMAP.Null, video); return s; @@ -145,7 +145,7 @@ private void CleanUp() /// /// Finalizer /// - ~FastSplashScreen() => Dispose(false); + ~SimpleSplashScreen() => Dispose(false); private unsafe void DisplaySplash(Windows.Win32.Foundation.HWND hWnd, Windows.Win32.Graphics.Gdi.HBITMAP bitmap, string? sVideo) { @@ -212,11 +212,11 @@ private struct BITMAP #if MEDIAPLAYER private class MFPlayer : IMFPMediaPlayerCallback, IDisposable { - private readonly FastSplashScreen m_ss; + private readonly SimpleSplashScreen m_ss; private readonly Windows.Win32.Foundation.HWND m_hWndParent; private readonly IMFPMediaPlayer m_pMediaPlayer; - internal unsafe MFPlayer(FastSplashScreen ss, Windows.Win32.Foundation.HWND hWnd, string sVideo) + internal unsafe MFPlayer(SimpleSplashScreen ss, Windows.Win32.Foundation.HWND hWnd, string sVideo) { GlobalStructures.HRESULT hr = MFPlayTools.MFPCreateMediaPlayer(sVideo, false, MFPlay.MFPlayTools.MFP_CREATION_OPTIONS.MFP_OPTION_NONE, this, hWnd, out m_pMediaPlayer); m_hWndParent = hWnd; diff --git a/src/WinUIEx/SplashScreen.cs b/src/WinUIEx/SplashScreen.cs index 9df4879..5caa33a 100644 --- a/src/WinUIEx/SplashScreen.cs +++ b/src/WinUIEx/SplashScreen.cs @@ -10,7 +10,7 @@ namespace WinUIEx /// A splash screen window for rendering XAML that shows with no chrome, and once has completed, /// opens a new window. /// - /// + /// public class SplashScreen : Window { private Window? _window; diff --git a/src/WinUIExSample/App.xaml.cs b/src/WinUIExSample/App.xaml.cs index 30eb1a2..db5a56b 100644 --- a/src/WinUIExSample/App.xaml.cs +++ b/src/WinUIExSample/App.xaml.cs @@ -26,7 +26,7 @@ public App() #if !DISABLE_XAML_GENERATED_MAIN // With custom main, we'd rather want to do this code in main if (WebAuthenticator.CheckOAuthRedirectionActivation()) return; - fss = FastSplashScreen.ShowDefaultSplashScreen(); + fss = SimpleSplashScreen.ShowDefaultSplashScreen(); #endif this.InitializeComponent(); int length = 0; @@ -39,7 +39,7 @@ public App() } } - internal FastSplashScreen fss { get; set; } + internal SimpleSplashScreen fss { get; set; } /// /// Invoked when the application is launched normally by the end user. Other entry points /// will be used such as when the application is launched to open a specific file. @@ -156,7 +156,7 @@ static void Main(string[] args) { if (WebAuthenticator.CheckOAuthRedirectionActivation(true)) return; - var fss = FastSplashScreen.ShowDefaultSplashScreen(); + var fss = SimpleSplashScreen.ShowDefaultSplashScreen(); global::WinRT.ComWrappersSupport.InitializeComWrappers(); global::Microsoft.UI.Xaml.Application.Start((p) => { var context = new global::Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext(global::Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread()); From 29d69fa08bb857660691b08679f8d15f6045502d Mon Sep 17 00:00:00 2001 From: dotMorten Date: Mon, 18 Nov 2024 19:39:12 -0800 Subject: [PATCH 10/13] Fix splashscreen scale calculation --- src/WinUIEx/NativeMethods.txt | 3 ++- src/WinUIEx/SimpleSplashScreen.cs | 10 +++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/WinUIEx/NativeMethods.txt b/src/WinUIEx/NativeMethods.txt index b2d485e..39ed06f 100644 --- a/src/WinUIEx/NativeMethods.txt +++ b/src/WinUIEx/NativeMethods.txt @@ -64,4 +64,5 @@ GdipCreateHBITMAPFromBitmap WNDCLASSEXW RegisterClassEx CreateCompatibleDC -SelectObject \ No newline at end of file +SelectObject +MonitorFromPoint diff --git a/src/WinUIEx/SimpleSplashScreen.cs b/src/WinUIEx/SimpleSplashScreen.cs index 0d9f244..e87cb13 100644 --- a/src/WinUIEx/SimpleSplashScreen.cs +++ b/src/WinUIEx/SimpleSplashScreen.cs @@ -49,8 +49,16 @@ public static SimpleSplashScreen ShowDefaultSplashScreen() var manager = new Microsoft.Windows.ApplicationModel.Resources.ResourceManager(); var context = manager.CreateResourceContext(); - var dpi = PInvoke.GetDpiForWindow(PInvoke.GetDesktopWindow()); + uint dpi = 96; + var monitor = PInvoke.MonitorFromPoint(new System.Drawing.Point(0, 0), Windows.Win32.Graphics.Gdi.MONITOR_FROM_FLAGS.MONITOR_DEFAULTTOPRIMARY); + + if(monitor != IntPtr.Zero) + { + PInvoke.GetDpiForMonitor(monitor, Windows.Win32.UI.HiDpi.MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var dpiX, out var dpiy); + dpi = dpiX; + } var scale = (int)(dpi / 96d * 100); + if (scale == 0) scale = 100; context.QualifierValues["Scale"] = scale.ToString(); var splashScreenImageResource = manager.MainResourceMap.TryGetValue("Files/" + image.Replace('\\','/'), context); if (splashScreenImageResource is not null && splashScreenImageResource.Kind == Microsoft.Windows.ApplicationModel.Resources.ResourceCandidateKind.FilePath) From 6097cd3adc91de93f5b6a8dcf8b845772128b76e Mon Sep 17 00:00:00 2001 From: dotMorten Date: Mon, 18 Nov 2024 19:39:42 -0800 Subject: [PATCH 11/13] Set version to v2.5.0 --- src/Directory.Build.targets | 2 +- src/WinUIEx/WinUIEx.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index cc23f14..9a1a617 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -17,7 +17,7 @@ Morten Nielsen - https://xaml.dev Morten Nielsen - https://xaml.dev logo.png - 2.4.2 + 2.5.0 diff --git a/src/WinUIEx/WinUIEx.csproj b/src/WinUIEx/WinUIEx.csproj index 28e6d66..51350d9 100644 --- a/src/WinUIEx/WinUIEx.csproj +++ b/src/WinUIEx/WinUIEx.csproj @@ -25,7 +25,7 @@ WinUIEx WinUI Extensions - - Workaround package bug in WebView2 nuget package. (Issue #188) + - Added `SimpleSplashScreen` for display application launch splash screens @@ -37,7 +37,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 15648113207cb0dff99b0736569ec20571741118 Mon Sep 17 00:00:00 2001 From: dotMorten Date: Mon, 18 Nov 2024 19:42:14 -0800 Subject: [PATCH 12/13] Center first before displaying splashscreen --- src/WinUIEx/SimpleSplashScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WinUIEx/SimpleSplashScreen.cs b/src/WinUIEx/SimpleSplashScreen.cs index e87cb13..148a8e9 100644 --- a/src/WinUIEx/SimpleSplashScreen.cs +++ b/src/WinUIEx/SimpleSplashScreen.cs @@ -195,8 +195,8 @@ private unsafe void DisplaySplash(Windows.Win32.Foundation.HWND hWnd, Windows.Wi new DestroyIconSafeHandle(wcex.hInstance), null); if (hBitmap != IntPtr.Zero) { - SetPictureToLayeredWindow(hWndSplash, hBitmap); CenterToScreen(hWndSplash); + SetPictureToLayeredWindow(hWndSplash, hBitmap); } #if MEDIAPLAYER if (sVideo != null) From 5cf1f26eb0a61dc238a5434a836e630f6d9656c6 Mon Sep 17 00:00:00 2001 From: dotMorten Date: Wed, 20 Nov 2024 22:51:58 -0800 Subject: [PATCH 13/13] WebAuthenticator: Add support for unpackaged apps --- docs/concepts/WebAuthenticator.md | 23 +++++++++++++++- src/Directory.Build.targets | 4 +-- src/WinUIEx/WebAuthenticator.cs | 38 +++++++++++++++++++++++--- src/WinUIEx/WinUIEx.csproj | 2 +- src/WinUIExSample/App.xaml.cs | 25 ++++++++--------- src/WinUIExSample/Pages/OAuth.xaml.cs | 10 +++++++ src/WinUIExSample/WinUIExSample.csproj | 1 + 7 files changed, 82 insertions(+), 21 deletions(-) diff --git a/docs/concepts/WebAuthenticator.md b/docs/concepts/WebAuthenticator.md index d10415b..9162b8c 100644 --- a/docs/concepts/WebAuthenticator.md +++ b/docs/concepts/WebAuthenticator.md @@ -17,8 +17,29 @@ Next you can make a make a call to authenticate using your default browser: WebAuthenticatorResult result = await WinUIEx.WebAuthenticator.AuthenticateAsync(authorizeUrl, callbackUri); ``` -Your callback uri must use a custom scheme, and you must define this scheme in your application manifest. For example if your callback uri is "myscheme://loggedin", your manifest dialog should look like this: +### Configuration + +Your app must be configured for OAuth with schema activation. The scheme must be the first part of the url, ie if your oauth redirection url starts with for instance `myscheme://signin/`, the scheme would be `myscheme`. Note that http(s) schemes are not supported here. + +#### Packaged Apps + +If your app is packaged, in your app's `Package.appxmanifest` under `Declarations`, add a Protocol declaration and add the scheme you registered for your application's oauth redirect url under "Name". +For example if your callback uri is "myscheme://loggedin", your manifest dialog should look like this: ![image](https://user-images.githubusercontent.com/1378165/166501267-1da07930-ab4d-431e-87cf-a7b183cc3c87.png) +#### Unpackaged Apps + +If your app is unpackaged, instead of relying on the app manifest to handle this for you, make sure you register the application for protocol activation. For example: +``` cs +try +{ + Microsoft.Windows.AppLifecycle.ActivationRegistrationManager.RegisterForProtocolActivation("myscheme", "Assets\\Square150x150Logo.scale-100", "My App Name", null); + var result = await WebAuthenticator.AuthenticateAsync(authorizeUri, callbackUri, cancellationToken); +} +finally +{ + Microsoft.Windows.AppLifecycle.ActivationRegistrationManager.UnregisterForProtocolActivation("myscheme", null); +} +``` \ No newline at end of file diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 9a1a617..f4a053d 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -17,8 +17,8 @@ Morten Nielsen - https://xaml.dev Morten Nielsen - https://xaml.dev logo.png - 2.5.0 - + 2.5.1 + 2.5.0 diff --git a/src/WinUIEx/WebAuthenticator.cs b/src/WinUIEx/WebAuthenticator.cs index 4243499..48798e5 100644 --- a/src/WinUIEx/WebAuthenticator.cs +++ b/src/WinUIEx/WebAuthenticator.cs @@ -19,9 +19,30 @@ namespace WinUIEx /// /// /// - /// Your app must be configured for OAuth. In you app package's Package.appxmanifest under Declarations, add a + /// Your app must be configured for OAuth with schema activation. The scheme must be the first part of the url, ie if your + /// oauth redirection url starts with for instance myappscheme://signin/, the scheme would be myappscheme. + /// Note that http(s) schemes are not supported here. + /// + /// + /// If your app is packaged, in your app's Package.appxmanifest under Declarations, add a /// Protocol declaration and add the scheme you registered for your application's oauth redirect url under "Name". /// + /// + /// If your app is unpackaged, make sure you register the application for protocol activation. + /// + /// + /// + /// try + /// { + /// Microsoft.Windows.AppLifecycle.ActivationRegistrationManager.RegisterForProtocolActivation("myappscheme", "Assets\\Square150x150Logo.scale-100", "My App Name", null); + /// var result = await WebAuthenticator.AuthenticateAsync(authorizeUri, callbackUri, cancellationToken); + /// } + /// finally + /// { + /// Microsoft.Windows.AppLifecycle.ActivationRegistrationManager.UnregisterForProtocolActivation("myappscheme", null); + /// } + /// + /// /// public sealed class WebAuthenticator { @@ -218,11 +239,20 @@ private async Task Authenticate(Uri authorizeUri, Uri ca } if (!Helpers.IsAppPackaged) { - throw new InvalidOperationException("The WebAuthenticator requires a packaged app with an AppxManifest"); + if(callbackUri.Scheme == "http" || callbackUri.Scheme == "https") + throw new InvalidOperationException($"{callbackUri.Scheme}:// schemes are not allowed for callbackUri. Use a custom scheme like 'myapp' instead."); + var value = Microsoft.Win32.Registry.ClassesRoot.OpenSubKey(callbackUri.Scheme); + if(value is null || value.GetValue("URL Protocol") is null) + { + throw new InvalidOperationException($"The URI Scheme '{callbackUri.Scheme}' is not registered. Call ActivationRegistrationManager.RegisterForProtocolActivation to register protocol activation."); + } } - if (!IsUriProtocolDeclared(callbackUri.Scheme)) + else { - throw new InvalidOperationException($"The URI Scheme {callbackUri.Scheme} is not declared in AppxManifest.xml"); + if (!IsUriProtocolDeclared(callbackUri.Scheme)) + { + throw new InvalidOperationException($"The URI Scheme {callbackUri.Scheme} is not declared in AppxManifest.xml"); + } } var g = Guid.NewGuid(); var taskId = g.ToString(); diff --git a/src/WinUIEx/WinUIEx.csproj b/src/WinUIEx/WinUIEx.csproj index 51350d9..80180ca 100644 --- a/src/WinUIEx/WinUIEx.csproj +++ b/src/WinUIEx/WinUIEx.csproj @@ -25,7 +25,7 @@ WinUIEx WinUI Extensions - - Added `SimpleSplashScreen` for display application launch splash screens + - WebAuthenticator: Added support for unpackaged apps. diff --git a/src/WinUIExSample/App.xaml.cs b/src/WinUIExSample/App.xaml.cs index db5a56b..8338058 100644 --- a/src/WinUIExSample/App.xaml.cs +++ b/src/WinUIExSample/App.xaml.cs @@ -29,16 +29,12 @@ public App() fss = SimpleSplashScreen.ShowDefaultSplashScreen(); #endif this.InitializeComponent(); - int length = 0; - var sb = new System.Text.StringBuilder(0); - int result = GetCurrentPackageFullName(ref length, sb); - if(result == 15700L) - { - // Not a packaged app. Configure file-based persistence instead - WinUIEx.WindowManager.PersistenceStorage = new FilePersistence("WinUIExPersistence.json"); - } - +#if UNPACKAGED + // Use file-based persistence since we can't rely on default storage for window persistence when unpackaged + WinUIEx.WindowManager.PersistenceStorage = new FilePersistence("WinUIExPersistence.json"); +#endif } + internal SimpleSplashScreen fss { get; set; } /// /// Invoked when the application is launched normally by the end user. Other entry points @@ -67,9 +63,7 @@ private void Splash_Activated(object sender, WindowActivatedEventArgs args) public WindowEx MainWindow => m_window; - [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - private static extern int GetCurrentPackageFullName(ref int packageFullNameLength, System.Text.StringBuilder packageFullName); - +#if UNPACKAGED private class FilePersistence : IDictionary { private readonly Dictionary _data = new Dictionary(); @@ -143,6 +137,7 @@ public void Clear() IEnumerator IEnumerable.GetEnumerator() => throw new NotImplementedException(); // TODO } +#endif } #if DISABLE_XAML_GENERATED_MAIN @@ -156,7 +151,11 @@ static void Main(string[] args) { if (WebAuthenticator.CheckOAuthRedirectionActivation(true)) return; +#if UNPACKAGED + var fss = SimpleSplashScreen.ShowSplashScreenImage("Assets\\SplashScreen.scale-100.png"); +#else var fss = SimpleSplashScreen.ShowDefaultSplashScreen(); +#endif global::WinRT.ComWrappersSupport.InitializeComWrappers(); global::Microsoft.UI.Xaml.Application.Start((p) => { var context = new global::Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext(global::Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread()); @@ -166,4 +165,4 @@ static void Main(string[] args) } } #endif -} + } diff --git a/src/WinUIExSample/Pages/OAuth.xaml.cs b/src/WinUIExSample/Pages/OAuth.xaml.cs index 725c0c9..3e28de5 100644 --- a/src/WinUIExSample/Pages/OAuth.xaml.cs +++ b/src/WinUIExSample/Pages/OAuth.xaml.cs @@ -65,6 +65,10 @@ private async void DoOAuth(string responseType) OAuthWindow.Visibility = Visibility.Visible; try { +#if UNPACKAGED + // Packaged app uses appxmanifest for protocol activation. Unpackaged apps must manually register + Microsoft.Windows.AppLifecycle.ActivationRegistrationManager.RegisterForProtocolActivation("winuiex", "Assets\\Square150x150Logo.scale-100", "WinUI EX", null); +#endif var result = await WebAuthenticator.AuthenticateAsync(new Uri(authorizeUri), new Uri(callbackUri), oauthCancellationSource.Token); MainWindow.BringToFront(); OAuthWindow.Visibility = Visibility.Collapsed; @@ -75,6 +79,12 @@ private async void DoOAuth(string responseType) catch (TaskCanceledException) { Result.Text = "Sign in cancelled"; } + finally + { +#if UNPACKAGED + Microsoft.Windows.AppLifecycle.ActivationRegistrationManager.UnregisterForProtocolActivation("winuiex", null); +#endif + } } private void OAuthCancel_Click(object sender, RoutedEventArgs e) diff --git a/src/WinUIExSample/WinUIExSample.csproj b/src/WinUIExSample/WinUIExSample.csproj index 258cacb..3a674d9 100644 --- a/src/WinUIExSample/WinUIExSample.csproj +++ b/src/WinUIExSample/WinUIExSample.csproj @@ -18,6 +18,7 @@ 10.0.22621.38 DISABLE_XAML_GENERATED_MAIN;$(DefineConstants) + UNPACKAGED;$(DefineConstants) true