使用新式 UWP 组件扩展桌面应用

某些 Windows 体验(例如支持触控的 UI 页面)必须在 AppContainer 内部运行。 如果要添加这些体验,请用 UWP 项目和 Windows 运行时组件扩展桌面应用。

在许多情况下,你可以直接从桌面应用程序中调用 Windows 运行时 API,因此在查看本指南前,请参阅面向 Windows 的增强

注意

本主题中所述的功能要求将应用打包(在运行时具有程序包标识符)。 这包括打包的应用(请参阅为打包的 WinUI 3 桌面应用创建新项目)和使用外部位置打包的应用(请参阅通过使用外部位置进行打包来授予程序包标识符)。 另请参阅需要程序包标识符的功能

首先,设置你的解决方案

向你的解决方案添加一个或多个 UWP 项目和运行时组件。

从包含 Windows 应用程序打包项目 (引用你的桌面应用程序)的解决方案开始。

下图显示了一个示例解决方案。

Extend start project

如果你的解决方案不包含打包项目,请参阅使用 Visual Studio 打包桌面应用程序

配置桌面应用程序

确保桌面应用程序引用了你调用 Windows 运行时 API 所需的文件。

要执行此操作,请参阅在桌面应用中调用 Windows 运行时 API

添加 UWP 项目

向你的解决方案中添加一个空白应用(通用 Windows) 。

这是你将构建新式 XAML UI 或使用只在 UWP 进程中运行的 API 的位置。

Add new project

在打包项目中,右键单击“应用程序”节点,然后单击“添加引用” 。

Add reference

然后,添加对该 UWP 项目的引用。

Select UWP project

你的解决方案将如下所示:

Solution with UWP project

(可选)添加 Windows 运行时组件

要实现某些方案,你需要向 Windows 运行时组件添加代码。

runtime component app service

然后,在 UWP 项目中添加对运行时组件的引用。 你的解决方案将如下所示:

Runtime Component Reference

生成解决方案

生成解决方案以确保无错误出现。 如果收到错误,请打开配置管理器并确保你的项目以同一平台为应用目标。

Config manager

以下是可以使用 UWP 项目和运行时组件执行的一些操作。

显示新式 XAML UI

作为应用程序流的一部分,你可以将基于 XAML 的新式用户界面并入桌面应用程序。 这些用户界面可自然地适应不同屏幕尺寸和分辨率,并支持新式交互模型,例如触摸和墨迹。

例如,借助少量 XAML 标记,你可以为用户提供与地图相关的强大可视化功能。

此图像显示 Windows 窗体应用程序,该应用程序可打开包含地图控件的、基于 XAML 的新式 UI。

adaptive-design

注意

本示例通过向解决方案中添加一个 UWP 项目来展示了一个 XAML UI。 这是支持在桌面应用程序中显示 XAML UI 的一种稳定方法。 此方法的另一种替代方法是使用 XAML 岛直接向桌面应用程序添加 UWP XAML 控件。 XAML 岛当前以开发者预览版的形式提供。 现在,尽管我们鼓励在自己的原型代码中试用,但不建议在生产代码中使用它们。 在未来的 Windows 版本中,这些 API 和控件将继续趋于成熟和稳定。 要详细了解 XAML 岛,请参阅桌面应用程序中的 UWP 控件

设计模式

要显示基于 XAML 的 UI,请执行以下操作:

1️⃣ 设置解决方案

2️⃣ 创建 XAML UI

3️⃣ 向 UWP 项目添加协议扩展

4️⃣ 从桌面应用启动 UWP 应用

5️⃣ 在 UWP 项目中,显示所需页面

设置解决方案

有关如何设置解决方案的一般指导,请参阅本指南开头的首先,设置你的解决方案部分。

解决方案将如下所示:

XAML UI Solution

在该示例中,Windows 窗体项目被命名为“Landmarks” ,包含 XAML UI 的 UWP 项目被命名为“MapUI” 。

创建 XAML UI

向 UWP 项目添加 XAML UI。 以下是一个基本地图的 XAML。

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" Margin="12,20,12,14">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <maps:MapControl x:Name="myMap" Grid.Column="0" Width="500" Height="500"
                     ZoomLevel="{Binding ElementName=zoomSlider,Path=Value, Mode=TwoWay}"
                     Heading="{Binding ElementName=headingSlider,Path=Value, Mode=TwoWay}"
                     DesiredPitch="{Binding ElementName=desiredPitchSlider,Path=Value, Mode=TwoWay}"
                     HorizontalAlignment="Left"
                     MapServiceToken="<Your Key Goes Here" />
    <Grid Grid.Column="1" Margin="12">
        <StackPanel>
            <Slider Minimum="1" Maximum="20" Header="ZoomLevel" Name="zoomSlider" Value="17.5"/>
            <Slider Minimum="0" Maximum="360" Header="Heading" Name="headingSlider" Value="0"/>
            <Slider Minimum="0" Maximum="64" Header=" DesiredPitch" Name="desiredPitchSlider" Value="32"/>
        </StackPanel>
    </Grid>
</Grid>

添加协议扩展

在“解决方案资源管理器” 中,打开解决方案中打包项目的“package.appxmanifest”文件 并添加该扩展。

<Extensions>
  <uap:Extension Category="windows.protocol" Executable="MapUI.exe" EntryPoint="MapUI.App">
    <uap:Protocol Name="xamluidemo" />
  </uap:Extension>
</Extensions>

为协议命名,并提供 UWP 项目生成的可执行文件的名称以及入口点类的名称。

还可以在设计器中打开 package.appxmanifest ,选择“声明” 选项卡,然后在其中添加扩展。

declarations-tab

注意

地图控件可从 Internet 下载数据,因此如果你使用一个地图控件,则还必须要向清单中添加“Internet 客户端”功能。

启动 UWP 应用

首先,在桌面应用程序中,创建一个 URI,其中包含协议名称和要传入 UWP 应用的任何参数。 然后,调用 LaunchUriAsync 方法。


private void Statue_Of_Liberty_Click(object sender, EventArgs e)
{
    ShowMap(40.689247, -74.044502);
}

private async void ShowMap(double lat, double lon)
{
    string str = "xamluidemo://";

    Uri uri = new Uri(str + "location?lat=" +
        lat.ToString() + "&?lon=" + lon.ToString());

    var success = await Windows.System.Launcher.LaunchUriAsync(uri);

}

分析参数并显示页面

在 UWP 项目的“应用” 类中,覆盖 OnActivated 事件处理程序。 如果应用已通过协议激活,则分析参数并打开所需页面。

protected override void OnActivated(Windows.ApplicationModel.Activation.IActivatedEventArgs e)
{
    if (e.Kind == ActivationKind.Protocol)
    {
        ProtocolActivatedEventArgs protocolArgs = (ProtocolActivatedEventArgs)e;
        Uri uri = protocolArgs.Uri;
        if (uri.Scheme == "xamluidemo")
        {
            Frame rootFrame = new Frame();
            Window.Current.Content = rootFrame;
            rootFrame.Navigate(typeof(MainPage), uri.Query);
            Window.Current.Activate();
        }
    }
}

在 XAML 页面的代码中,覆盖 OnNavigatedTo 方法以使用传递到该页面的参数。 在本例中,我们将使用传递给该页面的纬度和经度来在地图中显示一个位置。

protected override void OnNavigatedTo(NavigationEventArgs e)
 {
     if (e.Parameter != null)
     {
         WwwFormUrlDecoder decoder = new WwwFormUrlDecoder(e.Parameter.ToString());

         double lat = Convert.ToDouble(decoder[0].Value);
         double lon = Convert.ToDouble(decoder[1].Value);

         BasicGeoposition pos = new BasicGeoposition();

         pos.Latitude = lat;
         pos.Longitude = lon;

         myMap.Center = new Geopoint(pos);

         myMap.Style = MapStyle.Aerial3D;

     }

     base.OnNavigatedTo(e);
 }

使桌面应用程序成为共享目标

你可以使你的桌面应用程序成为共享目标,这样一来,用户就能够轻松地共享数据,例如来自支持共享的其他应用的图片。

例如,用户可以选择你的应用程序来共享来自 Microsoft Edge 的图片,即照片应用。 下面是具有该功能的 WPF 示例应用程序。

share target

有关完整示例,请参阅此处

设计模式

要使你的应用程序成为共享目标,请执行以下操作:

1️⃣ 添加共享目标扩展

2️⃣ 覆盖 OnShareTargetActivated 事件处理程序

3️⃣ 将桌面扩展添加到 UWP 项目

4️⃣ 添加完全信任的进程扩展

5️⃣ 修改桌面应用程序以获取共享文件

具体步骤如下

添加共享目标扩展

在“解决方案资源管理器” 中,打开解决方案中打包项目的“package.appxmanifest”文件 并添加此共享目标扩展。

<Extensions>
      <uap:Extension
          Category="windows.shareTarget"
          Executable="ShareTarget.exe"
          EntryPoint="App">
        <uap:ShareTarget>
          <uap:SupportedFileTypes>
            <uap:SupportsAnyFileType />
          </uap:SupportedFileTypes>
          <uap:DataFormat>Bitmap</uap:DataFormat>
        </uap:ShareTarget>
      </uap:Extension>
</Extensions>  

提供 UWP 项目生成的可执行文件的名称以及入口点类的名称。 此标记假设 UWP 应用的可执行文件的名称是 ShareTarget.exe

你还需要指定可与你的应用共享的文件类型。 在此示例中,我们将 WPF PhotoStoreDemo 桌面应用程序作为位图图像的共享目标,因此对于支持的文件类型,我们指定 Bitmap

覆盖 OnShareTargetActivated 事件处理程序

在 UWP 项目的“应用” 类中,覆盖 OnShareTargetActivated 事件处理程序。

此事件处理程序会在用户选择你的应用来共享他们的文件时被调用。


protected override void OnShareTargetActivated(ShareTargetActivatedEventArgs args)
{
    shareWithDesktopApplication(args.ShareOperation);
}

private async void shareWithDesktopApplication(ShareOperation shareOperation)
{
    if (shareOperation.Data.Contains(StandardDataFormats.StorageItems))
    {
        var items = await shareOperation.Data.GetStorageItemsAsync();
        StorageFile file = items[0] as StorageFile;
        IRandomAccessStreamWithContentType stream = await file.OpenReadAsync();

        await file.CopyAsync(ApplicationData.Current.LocalFolder);
            shareOperation.ReportCompleted();

        await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync();
    }
}

在本代码中,我们将用户正在共享的图像保存到应用的本地存储文件夹中。 稍后,我们将修改桌面应用程序以从同一文件夹中拉取图像。 由于桌面应用程序包含在 UWP 应用所在的同一包中,因此可以执行此操作。

将桌面扩展添加到 UWP 项目

将适用于 UWP 的 Windows 桌面扩展添加到 UWP 应用项目中 。 你将看到扩展的多种版本(例如 10.0.18362.0 和 10.0.19041.0)。 有关如何选择版本的信息,请参阅扩展 SDK 及其引用方式

desktop extension

添加完全信任的进程扩展

在“解决方案资源管理器” 中,打开解决方案中打包项目的 package.appxmanifest 文件,然后添加之前添加此文件的共享目标扩展旁的完全信任的进程扩展。

<Extensions>
  ...
      <desktop:Extension Category="windows.fullTrustProcess" Executable="PhotoStoreDemo\PhotoStoreDemo.exe" />
  ...
</Extensions>  

此扩展将允许 UWP 应用启动你要与之共享文件的桌面应用程序。 在本例中,我们引用 WPF PhotoStoreDemo 桌面应用程序的可执行文件。

修改桌面应用程序以获取共享文件

修改桌面应用程序以找到并处理共享文件。 在此示例中,UWP 应用将共享文件存储在本地应用数据文件夹中。 因此,我们将修改 WPF PhotoStoreDemo 桌面应用程序以从该文件夹拉取照片。

Photos.Path = Windows.Storage.ApplicationData.Current.LocalFolder.Path;

对于用户已打开的桌面应用程序的实例,我们还可以处理 FileSystemWatcher 事件,并传入文件位置的路径。 这样,任何打开的桌面应用程序实例都会显示共享照片。

...

   FileSystemWatcher watcher = new FileSystemWatcher(Photos.Path);

...

private void Watcher_Created(object sender, FileSystemEventArgs e)
{
    // new file got created, adding it to the list
    Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal, new Action(() =>
    {
        if (File.Exists(e.FullPath))
        {
            ImageFile item = new ImageFile(e.FullPath);
            Photos.Insert(0, item);
            PhotoListBox.SelectedIndex = 0;
            CurrentPhoto.Source = (BitmapSource)item.Image;
        }
    }));
}

创建后台任务

你可以添加后台任务来运行代码,即使应用已挂起时也是如此。 后台任务对于不需要用户交互的小任务非常有用。 例如,你的任务可以下载邮件,显示有关传入聊天消息的 Toast 通知,或对系统状况中的更改做出响应。

下面是一个注册后台任务的 WPF 示例应用程序。

background task

该任务发出 http 请求并测量从请求到返回响应所用的时间。 你的任务可能会更有趣,但该示例对了解后台任务的基本机制非常有用。

有关完整示例,请参阅此处

设计模式

要创建后台服务,请执行以下操作:

1️⃣ 实现后台任务

2️⃣ 配置后台任务

3️⃣ 注册后台任务

实施后台任务

通过向 Windows 运行时组件项目添加以下代码来实现该后台任务。

public sealed class SiteVerifier : IBackgroundTask
{
    public async void Run(IBackgroundTaskInstance taskInstance)
    {

        taskInstance.Canceled += TaskInstance_Canceled;
        BackgroundTaskDeferral deferral = taskInstance.GetDeferral();
        var msg = await MeasureRequestTime();
        ShowToast(msg);
        deferral.Complete();
    }

    private async Task<string> MeasureRequestTime()
    {
        string msg;
        try
        {
            var url = ApplicationData.Current.LocalSettings.Values["UrlToVerify"] as string;
            var http = new HttpClient();
            Stopwatch clock = Stopwatch.StartNew();
            var response = await http.GetAsync(new Uri(url));
            response.EnsureSuccessStatusCode();
            var elapsed = clock.ElapsedMilliseconds;
            clock.Stop();
            msg = $"{url} took {elapsed.ToString()} ms";
        }
        catch (Exception ex)
        {
            msg = ex.Message;
        }
        return msg;
    }

配置后台任务

在清单设计器中,打开解决方案中打包项目的 package.appxmanifest 文件。

在“声明” 选项卡中,添加一个“后台任务” 声明。

Background task option

然后,选择所需属性。 我们的示例使用 Timer 属性。

Timer property

在 Windows 运行时组件中提供实现后台任务的类的完全限定名称。

Specify entry point

注册后台任务

向注册后台任务的桌面应用程序项目添加以下代码。

public void RegisterBackgroundTask(String triggerName)
{
    var current = BackgroundTaskRegistration.AllTasks
        .Where(b => b.Value.Name == triggerName).FirstOrDefault().Value;

    if (current is null)
    {
        BackgroundTaskBuilder builder = new BackgroundTaskBuilder();
        builder.Name = triggerName;
        builder.SetTrigger(new MaintenanceTrigger(15, false));
        builder.TaskEntryPoint = "HttpPing.SiteVerifier";
        builder.Register();
        System.Diagnostics.Debug.WriteLine("BGTask registered:" + triggerName);
    }
    else
    {
        System.Diagnostics.Debug.WriteLine("Task already:" + triggerName);
    }
}

查找问题的答案

有问题? 请在 Stack Overflow 上向我们提问。 我们的团队会监视这些标记。 你还可以在此处提问。