UWP 照片编辑器示例应用 (C++/WinRT) 的 Windows App SDK 迁移

本主题是获取 C++/WinRT UWP 照片编辑器示例应用并将其迁移到 Windows App SDK 的案例研究。

重要

有关执行迁移过程的注意事项和策略,以及如何设置用于迁移的开发环境,请参阅总体迁移策略

安装适用于 Windows App SDK 的工具

要设置开发计算机,请参阅安装适用于 Windows App SDK 的工具

重要

你会发现发行说明主题以及 Windows App SDK 发行通道主题。 每个通道都有发行说明。 Be sure to check any limitations and known issues in those release notes, since those might affect the results of following along with this case study and/or running the migrated app.

创建新项目

  • 在 Visual Studio 中,通过“打包的空白应用(桌面版 WinUI 3)”项目模板创建一个新的 C++/WinRT 项目。 将项目命名为“PhotoEditor”,取消选中“将解决方案和项目置于同一目录中”。 你可以针对客户端操作系统的最新版本(非预览版)。

注意

我们将示例项目的 UWP 版本(你从其 repo 克隆的版本)称为解决方案/项目。 我们将 Windows App SDK 版本称为目标解决方案/项目。

我们迁移代码的顺序

MainPage 是应用中重要且突出的部分。 但如果我们首先迁移 MainPage,我们很快就会意识到“MainPage”依赖于“DetailPage”视图;以及,“DetailPage”依赖于“Photo”模型。 因此,对于本演练,我们将采用下面这种方法。

  • 我们首先复制资产文件。
  • 然后,我们将迁移 Photo 模型。
  • 接下来,我们将迁移 App 类(因为需要向它添加 DetailPageMainPage 所依赖的一些成员)。
  • 接下来,我们将开始迁移视图,首先从 DetailPage 开始。
  • 我们将通过迁移 MainPage 视图来完成此操作。

我们将复制整个源代码文件

在本演练中,我们将使用“文件资源管理器”复制源代码文件。 如果更希望复制文件内容,请参阅本主题末尾的附录:复制 Photo 模型文件的内容部分,了解有关如何为 Photo 执行该操作的示例(你随后可以将类似过程应用于项目中的其他类型)。 但是,该选项确实涉及许多更多步骤。

复制资产文件

  1. 在源项目的克隆中,在文件资源管理器中,找到文件夹 Windows-appsample-photo-editor>PhotoEditor>Assets。 你会在该文件夹中找到八个资产文件。 选择这八个文件,然后将它们复制到剪贴板。

  2. 同样在“文件资源管理器”中,现在在你创建的目标项目中找到相应的文件夹。 该文件夹的路径为 PhotoEditor>PhotoEditor>Assets。 将你刚刚复制的资产文件粘贴到该文件夹中,并接受提示以替换目标中已存在的七个文件。

  3. 在 Visual Studio 的目标项目中,在“解决方案资源管理器”中,展开 Assets 文件夹。 将刚粘贴的现有 bg1.png 资产文件添加到该文件夹。 可以将鼠标指针悬停在资产文件上。 将对每个资产文件显示缩略图预览,确认你已正确替换/添加资产文件。

迁移 Photo 模型

Photo 是表示照片的运行时类。 它是一个模型(在模型、视图和视图模型的意义上)。

复制 Photo 源代码文件

  1. 在源项目的克隆中,在文件资源管理器中,找到文件夹 Windows-appsample-photo-editor>PhotoEditor。 在该文件夹中,你会找到三个源代码文件 Photo.idlPhoto.hPhoto.cpp;这些文件一起实现 Photo 运行时类。 选择这三个文件,然后将它们复制到剪贴板。

  2. 在 Visual Studio 中,右键单击目标项目节点,然后单击“打开文件资源管理器中的文件夹”。 这将在文件资源管理器中打开目标项目文件夹。 将刚刚复制的三个文件粘贴到该文件夹中。

  3. 返回“解决方案资源管理器”,选中目标项目节点,确保将“显示所有文件”打开。 右键单击刚刚粘贴的三个文件,然后单击“包括在项目中”。 关闭“显示所有文件”。

  4. 在源项目中,在“解决方案资源管理器”中,Photo.h.cppPhoto.idl 下嵌套,以指示它们是通过它生成的(依赖于它)。 如果喜欢这种排列,则可以通过手动编辑 \PhotoEditor\PhotoEditor\PhotoEditor\PhotoEditor.vcxproj,在目标项目中执行相同操作(首先需要在 Visual Studio 选择“全部保存”)。 查找以下内容:

    <ClInclude Include="Photo.h" />
    

    将它替换为以下内容:

    <ClInclude Include="Photo.h">
      <DependentUpon>Photo.idl</DependentUpon>
    </ClInclude>
    

    Photo.cpp 重复该操作,然后保存并关闭项目文件。 将焦点设置回 Visual Studio 后,单击“重新加载”。

迁移 Photo 源代码

  1. Photo.idl 中,搜索命名空间名称 Windows.UI.Xaml(它是 UWP XAML 的命名空间),然后将该命名空间更改为 Microsoft.UI.Xaml(它是 WinUI XAML 的命名空间)。

注意

将 UWP API 映射到 Windows App SDK 主题提供了 UWP API 到它们的 Windows App SDK 等效项的映射。 我们上面所做的更改是迁移过程中必要的命名空间名称更改的示例。

  1. Photo.cpp 中,将 #include "Photo.g.cpp" 添加到现有 include 指令,紧接在 #include "Photo.h" 之后。 这是 UWP 与 Windows App SDK 项目之间需要注意的文件夹和文件名差异 (C++/WinRT) 之一。

  2. 在刚刚复制并粘贴的文件的所有源代码内容中,进行以下查找/替换(匹配大小写和整个单词)。

    • %>
  3. 从源项目中的 pch.h,复制以下包含项,并将其粘贴到目标项目的 pch.h 中。 这是源项目中包含的头文件的子集;这些只是支持到目前为止迁移的代码所需的头文件。

    #include <winrt/Microsoft.UI.Xaml.Media.Imaging.h>
    #include <winrt/Windows.Storage.h>
    #include <winrt/Windows.Storage.FileProperties.h>
    #include <winrt/Windows.Storage.Streams.h>
    
  4. 现在,确认可以生成目标解决方案(但暂时不要运行)。

迁移 App 类

无需对目标项目的 App.idlApp.xaml 进行更改。 但我们需要编辑 App.xaml.h 和 App.xaml.cpp,以将一些新成员添加到 App 类。 我们将以一种让我们可以在每个部分之后进行生成的方式来执行此操作(关于 App::OnLaunched 的最后一部分除外)。

使主窗口对象可用

在此步骤中,我们会进行更改(在将 Windows.UI.Xaml.Window.Current 更改为 App.Window 中进行了说明)。

在目标项目中,应用将主窗口对象存储在其私有数据成员 window 中。 在迁移过程的后期(当我们迁移源项目对 Window.Current 的使用时),如果该 window 数据成员是静态的,并且也通过访问器函数提供,会十分方便。 因此,我们接下来会进行这些更改。

  • 由于我们将 window 设置为静态,因此我们需要在 App.xaml.cpp 中初始化它,而不是通过代码当前使用的默认成员初始值设定项。 以下是这些更改在 App.xaml.hApp.xaml.cpp 中的内容。

    // App.xaml.h
    ...
    struct App : AppT<App>
    {
         ...
         static winrt::Microsoft::UI::Xaml::Window Window(){ return window; };
    
    private:
         static winrt::Microsoft::UI::Xaml::Window window;
    };
    ...
    
    // App.xaml.cpp
    ...
    winrt::Microsoft::UI::Xaml::Window App::window{ nullptr };
    ...
    

App::OnNavigationFailed

照片编辑器示例应用使用导航逻辑在 MainPageDetailPage 之间导航。 有关需要(以及不需要)导航的 Windows App SDK 应用的详细信息,请参阅是否需要实现页面导航?

因此,我们将在接下来的几个部分中迁移的成员都是为了支持应用中的导航而存在。

  1. 我们首先迁移 OnNavigationFailed 事件处理程序。 从源项目中复制该成员函数的声明和定义,并将其粘贴到目标中(在 App.xaml.hApp.xaml.cpp 中)。

  2. 在粘贴到 App.xaml.h 的代码中,将 Windows::UI::Xaml 更改为 Microsoft::UI::Xaml

App::CreateRootFrame

  1. 源项目包含名为 App::CreateRootFrame 的帮助程序函数。 从源项目中复制该帮助程序函数的声明和定义,并将其粘贴到目标中(在 App.xaml.hApp.xaml.cpp 中)。

  2. 在粘贴到 App.xaml.h 的代码中,将 Windows::UI::Xaml 更改为 Microsoft::UI::Xaml

  3. 在粘贴到 App.xaml.cpp 的代码中,将出现的两个 Window::Current() 更改为 window(这是前面看到的 App 类的数据成员名称)。

App::OnLaunched

目标项目已包含 OnLaunched 事件处理程序的实现。 它的参数是对 Microsoft::UI::Xaml::LaunchActivatedEventArgs 的常量引用,这对于 Windows App SDK 是正确的(与源项目相反,源项目使用 Windows::ApplicationModel::Activation::LaunchActivatedEventArgs,这对于 UWP 是正确的)。

  • 只需合并 OnLaunched 的两个定义(源和目标),以便目标项目的 App.xaml.cpp 中的 App::OnLaunched 类似于下面的清单。 请注意,它使用 window(而不是 Window::Current(),这类似于 UWP 版本)。

    void App::OnLaunched(LaunchActivatedEventArgs const&)
    {
         window = make<MainWindow>();
    
         Frame rootFrame = CreateRootFrame();
         if (!rootFrame.Content())
         {
             rootFrame.Navigate(xaml_typename<PhotoEditor::MainPage>());
         }
    
         window.Activate();
    }
    

上面的代码使 App 依赖于 MainPage,因此在迁移 DetailPage,然后迁移 MainPage 之前,无法从此时开始生成。 当我们能够再次生成时,我们会指出来。

迁移 DetailPage 视图

DetailPage 是代表照片编辑器页面的类,其中 Win2D 效果被切换、设置和链接在一起。 你可以通过在 MainPage 上选择照片缩略图进入照片编辑器页面。 DetailPage 是一个视图(在模型、视图和视图模型的意义上)。

引用 Win2D NuGet 包

为了支持 DetailPage 中的代码,源项目依赖于 Microsoft.Graphics.Win2D。 因此,我们还需要在目标项目中依赖 Win2D。

  • 在 Visual Studio 的目标解决方案中,单击“工具”>“NuGet 包管理器”>“管理解决方案 NuGet 程序包...”>“浏览”。 请确保未选中“包括预发行版”,并在搜索框中键入或粘贴 Microsoft.Graphics.Win2D。 在搜索结果中选择正确的项目,检查 PhotoEditor 项目,然后单击“安装”以安装包。

复制 DetailPage 源代码文件

  1. 在源项目的克隆中,在文件资源管理器中,找到文件夹 Windows-appsample-photo-editor>PhotoEditor。 在该文件夹中,你会找到四个源代码文件 DetailPage.idlDetailPage.xamlDetailPage.hDetailPage.cpp;这些文件一起实现 DetailPage 视图。 选择这四个文件,然后将它们复制到剪贴板。

  2. 在 Visual Studio 中,右键单击目标项目节点,然后单击“打开文件资源管理器中的文件夹”。 这将在文件资源管理器中打开目标项目文件夹。 将刚刚复制的四个文件粘贴到该文件夹中。

  3. 仍在“文件资源管理器”中,将 DetailPage.hDetailPage.cpp 的名称分别更改为 DetailPage.xaml.hDetailPage.xaml.cpp。 这是 UWP 与 Windows App SDK 项目之间需要注意的文件夹和文件名差异 (C++/WinRT) 之一。

  4. 返回“解决方案资源管理器”,选中目标项目节点,确保将“显示所有文件”打开。 右键单击刚刚粘贴(并重命名)的四个文件,然后单击“包括在项目中”。 关闭“显示所有文件”。

  5. 在源项目中,在“解决方案资源管理器”中,DetailPage.idlDetailPage.xaml 下嵌套。 如果喜欢这种排列,则可以通过手动编辑 \PhotoEditor\PhotoEditor\PhotoEditor\PhotoEditor.vcxproj,在目标项目中执行相同操作(首先需要在 Visual Studio 选择“全部保存”)。 查找以下内容:

    <Midl Include="DetailPage.idl" />
    

    将它替换为以下内容:

    <Midl Include="DetailPage.idl">
      <DependentUpon>DetailPage.xaml</DependentUpon>
    </Midl>
    

保存并关闭项目文件。 将焦点设置回 Visual Studio 后,单击“重新加载”。

迁移 DetailPage 源代码

  1. DetailPage.idl 中,搜索 Windows.UI.Xaml,然后将该项更改为 Microsoft.UI.Xaml

  2. DetailPage.xaml.cpp 中,将 #include "DetailPage.h" 更改为 #include "DetailPage.xaml.h"

  3. 紧接在该项下面,添加 #include "DetailPage.g.cpp"

  4. 要调用静态 App::Window 方法(我们即将添加)以进行编译,请仍在 DetailPage.xaml.cpp 中,紧接在 #include "App.xaml.h" 之前添加 #include "Photo.h"

  5. 在刚刚复制并粘贴的文件的源代码内容中,进行以下查找/替换(匹配大小写和整个单词)。

    • DetailPage.xaml.h.xaml.cpp 中,Windows::UI::Composition =>Microsoft::UI::Composition
    • DetailPage.xaml.h.xaml.cpp 中,Windows::UI::Xaml =>Microsoft::UI::Xaml
    • DetailPage.xaml.cpp 中,Window::Current() =>App::Window()
  6. 从源项目中的 pch.h,复制以下包含项,并将其粘贴到目标项目的 pch.h 中。

    #include <winrt/Windows.Graphics.Effects.h>
    #include <winrt/Microsoft.Graphics.Canvas.Effects.h>
    #include <winrt/Microsoft.Graphics.Canvas.UI.Xaml.h>
    #include <winrt/Microsoft.UI.Composition.h>
    #include <winrt/Microsoft.UI.Xaml.Input.h>
    #include <winrt/Windows.Graphics.Imaging.h>
    #include <winrt/Windows.Storage.Pickers.h>
    
  7. 此外,在 pch.h 顶部,紧接在 #pragma once 之后,添加以下内容:

    // This is required because we are using std::min and std::max, otherwise 
    // we have a collision with min and max macros being defined elsewhere.
    #define NOMINMAX
    

我们尚无法生成,但在迁移 MainPage(这在接下来进行)之后将能够生成。

迁移 MainPage 视图

应用的主页表示在运行应用时首先看到的视图。 该页面从图片库中加载照片,并显示平铺缩略图视图。

复制 MainPage 源代码文件

  1. 与对 DetailPage 执行的操作类似,现在复制 MainPage.idlMainPage.xamlMainPage.hMainPage.cpp

  2. .h.cpp 文件分别重命名为 .xaml.h.xaml.cpp

  3. 像以前一样,在目标项目中包含所有四个文件。

  4. 在源项目中,在“解决方案资源管理器”中,MainPage.idlMainPage.xaml 下嵌套。 如果喜欢这种排列,则可以通过手动编辑 \PhotoEditor\PhotoEditor\PhotoEditor\PhotoEditor.vcxproj,在目标项目中执行相同操作。 查找以下内容:

    <Midl Include="MainPage.idl" />
    

    并将其替换为:

    <Midl Include="MainPage.idl">
      <DependentUpon>MainPage.xaml</DependentUpon>
    </Midl>
    

迁移 MainPage 源代码

  1. MainPage.idl 中,搜索 Windows.UI.Xaml,然后将两次出现都更改为 Microsoft.UI.Xaml

  2. MainPage.xaml.cpp 中,将 #include "MainPage.h" 更改为 #include "MainPage.xaml.h"

  3. 紧接在该项下面,添加 #include "MainPage.g.cpp"

  4. 要调用静态 App::Window 方法(我们即将添加)以进行编译,请仍在 MainPage.xaml.cpp 中,紧接在 #include "App.xaml.h" 之前添加 #include "Photo.h"

对于下一步,我们将进行 ContentDialog 和 Popup 中解释的更改。

  1. 因此,仍然在 MainPage.xaml.cpp 中,在 MainPage::GetItemsAsync 方法中,在紧接在 ContentDialog unsupportedFilesDialog{}; 行的后面,添加此代码行。

    unsupportedFilesDialog.XamlRoot(this->Content().XamlRoot());
    
  2. 在刚刚复制并粘贴的文件的源代码内容中,进行以下查找/替换(匹配大小写和整个单词)。

    • MainPage.xaml.h.xaml.cpp 中,Windows::UI::Composition =>Microsoft::UI::Composition
    • MainPage.xaml.h.xaml.cpp 中,Windows::UI::Xaml =>Microsoft::UI::Xaml
    • MainPage.xaml.cpp 中,Window::Current() =>App::Window()
  3. 从源项目中的 pch.h,复制以下包含项,并将其粘贴到目标项目的 pch.h 中。

    #include <winrt/Microsoft.UI.Xaml.Hosting.h>
    #include <winrt/Microsoft.UI.Xaml.Media.Animation.h>
    #include <winrt/Windows.Storage.Search.h>
    

确认可以生成目标解决方案(但暂时不要运行)。

更新 MainWindow

  1. MainWindow.xaml 中,删除 StackPanel 及其内容,因为我们在 MainWindow 中不需要任何 UI。 这仅保留空的 Window 元素。

  2. MainWindow.idl 中,删除占位符 Int32 MyProperty;,仅保留构造函数。

  3. MainWindow.xaml.hMainWindow.xaml.cpp 中,删除占位符 MyPropertymyButton_Click 的声明和定义,仅保留构造函数。

线程模型差异所需的迁移更改

由于 UWP 与 Windows App SDK 之间的线程模型差异,需要进行此部分中的两个更改,如 ASTA 到 STA 线程模型中所述。 下面是问题原因的简要说明,以及解决每个问题的方法。

MainPage

MainPagePictures 文件夹加载图像文件,调用 StorageItemContentProperties.GetImagePropertiesAsync 以获取图像文件的属性,为每个图像文件创建 Photo 模型对象(在数据成员中保存这些相同属性),并将该 Photo 对象添加到集合。 Photo 对象的集合将数据绑定到 UI 中的 GridViewMainPage 代表该 GridView 处理 ContainerContentChanging 事件,对于阶段 1,该处理程序会调入一个协同例程,后者调用 StorageFile.GetThumbnailAsync。 对 GetThumbnailAsync 的此调用会导致抽取消息(不会立即返回,会异步执行其所有工作),而这会导致重新进入。 结果是 GridView 在进行布局时更改了其 Items 集合,这会导致崩溃。

如果我们注释掉对 StorageItemContentProperties::GetImagePropertiesAsync 的调用,则我们不会遇到崩溃。 但真正的解决方法是,就在调用 GetThumbnailAsync 之前,协同等待 wil::resume_foreground,从而使 StorageFile.GetThumbnailAsync 调用显式异步。 此方法会起作用,因为 wil::resume_foreground 会将紧跟它的代码调度为 DispatcherQueue 上的任务。

下面是要更改的代码:

// MainPage.xaml.cpp
IAsyncAction MainPage::OnContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
{
    ...
    if (args.Phase() == 1)
    {
        ...
        try
        {
            co_await wil::resume_foreground(this->DispatcherQueue());
            auto thumbnail = co_await impleType->GetImageThumbnailAsync(this->DispatcherQueue());
            image.Source(thumbnail);
        }
        ...
    }
}

照片

Photo::ImageTitle 属性的数据绑定到 UI,因此每当需要其值时,UI 都会调入该属性的访问器函数。 但是,当我们尝试在 UI 线程中从该访问器函数访问 ImageProperties.Title 时,我们会收到访问冲突。

因此,我们只能从 Photo 的构造函数访问该 Title 一次,如果它不为空,则将它存储在 m_imageName 数据成员中。 然后在 Photo::ImageTitle 访问器函数中,我们只需访问 m_imageName 数据成员。

下面是要更改的代码:

// Photo.h
...
Photo(Photo(Windows::Storage::FileProperties::ImageProperties const& props,
    ...
    ) : ...
{
    if (m_imageProperties.Title() != L"")
    {
        m_imageName = m_imageProperties.Title();
    }
}
...
hstring ImageTitle() const
{
    return m_imageName;
}
...

这是迁移照片编辑器示例应用所需进行的最后一个更改。 在“测试已迁移的应用”部分中,我们将确认已正确执行了步骤。

已知问题

应用类型问题(仅影响预览版 3)

如果使用适用于 Windows App SDK 版本 1.0 预览版 3 的 VSIX 中的项目模板执行此案例研究,则需要对 PhotoEditor.vcxproj 进行一个小更正。 下面是操作方法。

在 Visual Studio 中的“解决方案资源管理器”中,右键单击项目节点,然后单击“卸载项目”。 现在 PhotoEditor.vcxproj 处于打开状态,可进行编辑。 添加 PropertyGroup 元素作为 Project 的第一个子级,如下所示:

<Project ... >
    <PropertyGroup>
        <EnableWin32Codegen>true</EnableWin32Codegen>
    </PropertyGroup>
    <Import ... />
...

保存并关闭 PhotoEditor.vcxproj。 右键单击项目节点,然后单击“重新加载项目”。 现在重新生成项目。

测试已迁移的应用

现在生成项目,并运行应用来进行测试。 选择图像,设置缩放级别,选择效果,并对其进行配置。

附录:复制 Photo 模型文件的内容

如前面所讨论的那样,你可以选择复制源代码文件本身或源代码文件的内容。 我们已演示了如何复制源代码文件本身。 此部分提供了复制文件内容的示例。

在 Visual Studio 的源项目中,找到文件夹 PhotoEditor(通用 Windows)>Models。 该文件夹包含文件 Photo.idlPhoto.hPhoto.cpp,它们一起实现 Photo 运行时类。

添加 IDL 并生成存根

在 Visual Studio 的目标项目中,向项目中添加新的“Midl 文件(.idl)”项。 对新项 Photo.idl 命名。 删除 Photo.idl 的默认内容。

在 Visual Studio 的源项目中,复制 Models>Photo.idl 的内容,然后将其粘贴到刚添加到目标项目的 Photo.idl 文件中。 在粘贴的代码中,搜索 Windows.UI.Xaml 并将其更改为 Microsoft.UI.Xaml

保存文件。

重要

我们即将执行目标解决方案的生成。 生成在此时不会运行到完成,但它将足以为我们完成必要的工作。

现在生成目标解决方案。 尽管它不会完成,但现在需要生成,因为它会生成在开始实现 Photo 模型时所需的源代码文件(存根)。

在 Visual Studio 中,右键单击目标项目节点,然后单击“打开文件资源管理器中的文件夹”。 这将在文件资源管理器中打开目标项目文件夹。 在其中导航到 Generated Files\sources 文件夹(因此你会处于 \PhotoEditor\PhotoEditor\PhotoEditor\Generated Files\sources 中)。 复制存根文件 Photo.h.cpp,并将它们粘贴到项目文件夹中,该文件夹现在处于 \PhotoEditor\PhotoEditor\PhotoEditor 中的前两个文件夹级别上。

返回“解决方案资源管理器”,选中目标项目节点,确保将“显示所有文件”打开。 右键单击刚刚粘贴的存根文件(Photo.h.cpp),然后单击“包括在项目中”。 关闭“显示所有文件”。

你将在 Photo.h.cpp 的内容顶部看到 static_assert(需要删除)。

确认你可以再次生成(但尚未运行)。

将代码迁移到存根中

Photo.h.cpp 的内容从源项目复制到目标项目中。

在这里,迁移你复制的代码的其余步骤与迁移 Photo 源代码部分中提供的步骤相同。