使用 C 创建单实例 WinUI 应用#

本操作说明演示如何使用 C# 和 Windows 应用 SDK创建单实例 WinUI 3 应用。 单实例应用一次只允许运行一个应用的实例。 默认情况下,WinUI 应用是多实例的。 它们允许你同时启动同一应用的多个实例。 这引用了多个实例。 但是,你可能希望根据应用的用例实现单实例化。 尝试启动单实例应用的第二个实例只会导致第一个实例的主窗口被激活。 本教程演示如何在 WinUI 应用中实现单实例化。

本文介绍如何执行以下操作:

  • 关闭 XAML 生成的 Program 代码
  • 定义重定向的自定义 Main 方法
  • 在应用部署后测试单实例

先决条件

本教程使用 Visual Studio 并在 WinUI 空白应用模板上生成。 如果你不熟悉 WinUI 开发,可以按照 WinUI 入门中的说明进行设置。 你将安装 Visual Studio,将其配置为使用 WinUI 开发应用,同时确保具有最新版本的 WinUI 和 Windows 应用 SDK,并创建 Hello World 项目。

完成此操作后,请返回此处了解如何将“Hello World”项目转换为单实例应用。

注意

本操作方法基于 WinUI 3 上的 Windows 博客系列中的“制作应用单实例”(第 3 部分) 博客文章。 GitHub提供了这些文章的代码。

禁用自动生成的程序代码

在创建任何窗口之前,我们需要尽早检查重定向。 为此,必须在项目文件中定义符号“DISABLE_XAML_GENERATED_MAIN”。 按照以下步骤禁用自动生成的程序代码:

  1. 右键单击解决方案资源管理器中的项目名称,然后选择“编辑项目文件”。

  2. 为每个配置和平台定义DISABLE_XAML_GENERATED_MAIN符号。 将以下 XML 添加到项目文件:

    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
      <DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>
    </PropertyGroup>
    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x86'">
      <DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>
    </PropertyGroup>
    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x86'">
      <DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>
    </PropertyGroup>
    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
      <DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>
    </PropertyGroup>
    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|arm64'">
      <DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>
    </PropertyGroup>
    <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|arm64'">
      <DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>
    </PropertyGroup>
    

添加DISABLE_XAML_GENERATED_MAIN符号将禁用项目的自动生成的程序代码。

使用 Main 方法定义 Program 类

必须创建自定义Program.cs文件,而不是运行默认 Main 方法。 添加到 Program 类的代码使应用能够检查重定向,这不是 WinUI 应用的默认行为。

  1. 导航到解决方案资源管理器,右键单击项目名称,然后选择“添加” |类

  2. 为新类 Program.cs 命名,然后选择“ 添加”。

  3. 将以下命名空间添加到 Program 类,替换任何现有命名空间:

    using System;
    using System.Diagnostics;
    using System.Runtime.InteropServices;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.UI.Dispatching;
    using Microsoft.UI.Xaml;
    using Microsoft.Windows.AppLifecycle;
    
  4. 将空 Program 类替换为以下内容:

    public class Program
    {
        [STAThread]
        static int Main(string[] args)
        {
            WinRT.ComWrappersSupport.InitializeComWrappers();
            bool isRedirect = DecideRedirection();
    
            if (!isRedirect)
            {
                Application.Start((p) =>
                {
                    var context = new DispatcherQueueSynchronizationContext(
                        DispatcherQueue.GetForCurrentThread());
                    SynchronizationContext.SetSynchronizationContext(context);
                    _ = new App();
                });
            }
    
            return 0;
        }
    }
    

    Main 方法确定应用是应重定向到第一个实例,还是调用 DecideRedirection启动一个新实例,接下来我们将定义该实例。

  5. Main 方法下面定义 DecideRedirection 方法:

    private static bool DecideRedirection()
    {
        bool isRedirect = false;
        AppActivationArguments args = AppInstance.GetCurrent().GetActivatedEventArgs();
        ExtendedActivationKind kind = args.Kind;
        AppInstance keyInstance = AppInstance.FindOrRegisterForKey("MySingleInstanceApp");
    
        if (keyInstance.IsCurrent)
        {
            keyInstance.Activated += OnActivated;
        }
        else
        {
            isRedirect = true;
            RedirectActivationTo(args, keyInstance);
        }
    
        return isRedirect;
    }
    

    DetermineRedirection 通过注册表示应用实例的唯一密钥来确定应用是否已注册。 根据密钥注册的结果,它可以确定是否存在正在运行的应用的当前实例。 确定后,该方法知道是重定向还是允许应用继续启动新实例。 如果需要重定向,将调用 RedirectActivationTo 方法。

  6. 接下来,让我们在 DecideRedirection 方法下创建 RedirectActivationTo 方法,以及所需的 DllImport 语句。 将以下代码添加到 Program 类:

    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
    private static extern IntPtr CreateEvent(
        IntPtr lpEventAttributes, bool bManualReset,
        bool bInitialState, string lpName);
    
    [DllImport("kernel32.dll")]
    private static extern bool SetEvent(IntPtr hEvent);
    
    [DllImport("ole32.dll")]
    private static extern uint CoWaitForMultipleObjects(
        uint dwFlags, uint dwMilliseconds, ulong nHandles,
        IntPtr[] pHandles, out uint dwIndex);
    
    [DllImport("user32.dll")]
    static extern bool SetForegroundWindow(IntPtr hWnd);
    
    private static IntPtr redirectEventHandle = IntPtr.Zero;
    
    // Do the redirection on another thread, and use a non-blocking
    // wait method to wait for the redirection to complete.
    public static void RedirectActivationTo(AppActivationArguments args,
                                            AppInstance keyInstance)
    {
        redirectEventHandle = CreateEvent(IntPtr.Zero, true, false, null);
        Task.Run(() =>
        {
            keyInstance.RedirectActivationToAsync(args).AsTask().Wait();
            SetEvent(redirectEventHandle);
        });
    
        uint CWMO_DEFAULT = 0;
        uint INFINITE = 0xFFFFFFFF;
        _ = CoWaitForMultipleObjects(
           CWMO_DEFAULT, INFINITE, 1,
           [redirectEventHandle], out uint handleIndex);
    
        // Bring the window to the foreground
        Process process = Process.GetProcessById((int)keyInstance.ProcessId);
        SetForegroundWindow(process.MainWindowHandle);
    }
    

    RedirectActivationTo 方法负责将激活重定向到应用的第一个实例。 它创建事件句柄,启动一个新线程来重定向激活,并等待重定向完成。 重定向完成后,该方法会将窗口引入前台。

  7. 最后,在 DecideRedirection 方法下面定义 Helper 方法 OnActivated

    private static void OnActivated(object sender, AppActivationArguments args)
    {
        ExtendedActivationKind kind = args.Kind;
    }
    

通过应用部署测试单实例

在此之前,我们一直在 Visual Studio 中进行调试来测试应用。 但是,我们一次只能运行一个调试器。 此限制可防止我们知道应用是否是单实例的,因为我们不能同时调试同一个项目两次。 为了进行准确的测试,我们将应用程序部署到本地 Windows 客户端。 部署后,我们可以像在 Windows 上安装任何应用一样从桌面启动应用。

  1. 导航到解决方案资源管理器,右键单击项目名称,然后选择“部署”。

  2. 打开开始菜单并单击搜索字段。

  3. 在搜索字段中键入应用的名称。

  4. 单击搜索结果中的应用图标以启动应用。

    注意

    如果在发布模式下遇到应用崩溃,则Windows 应用 SDK中剪裁的应用存在一些已知问题。 可以通过将 PublishTrimmed 属性设置为 false 来禁用项目中所有生成配置的.pubxml剪裁。 有关详细信息,请参阅 GitHub 上的此问题

  5. 重复步骤 2 到 4 以再次启动同一应用,并查看另一个实例是否打开。 如果应用是单实例的,则会激活第一个实例,而不是打开新的实例。

    提示

    可以选择性地将一些日志记录代码添加到 OnActivated 方法,以验证现有实例是否已激活。 请尝试向 Copilot 请求有关将 ILogger 实现添加到 WinUI 应用的帮助。

总结

此处介绍的所有代码都位于 GitHub,其中包含原始 Windows 博客系列中不同步骤的分支。 有关特定于此操作方法的代码,请参阅单实例分支。 该 main 分支是最全面的。 其他分支旨在向你展示应用体系结构是如何演变的。

应用实例化与应用生命周期 API

使应用单实例化(第 3 部分)

GitHub 上的 WinAppSDK-DrumPad 示例