Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
В этом разделе представлено пример переноса одного из примеров приложений Universal Windows Platform (UWP) из C# в C++/WinRT. Вы можете получить практические навыки и опыт переноса, следуя пошаговому руководству и самостоятельно перенося пример по ходу работы.
Note
Переносимый исходный код — это приложение UWP на C#. Целевой код C++/WinRT в этой статье написан для WinUI 3 (Windows App SDK). Где источник UWP использует API, которые отличаются в WinUI 3 (например, Windows.UI.Core.CoreDispatcher против Microsoft.UI.Dispatching.DispatcherQueue), эта статья явно показывает правильный эквивалент WinUI 3 в выходных данных C++/WinRT. Вы можете использовать шаблоны кода из столбца C++/WinRT непосредственно в приложении WinUI 3.
Полный перечень технических сведений по переносу с C# на C++/WinRT см. в сопутствующей теме Переход с C# на C++/WinRT.
Краткое описание файлов исходного кода C# и C++
В проекте C# файлы исходного кода являются в основном .cs файлами. При переходе на C++вы заметите, что существуют другие виды файлов исходного кода, к которым можно привыкнуть. Причина заключается в том, чтобы сделать разницу между компиляторами, способ повторного использования исходного кода C++ и понятия объявления и определения типа и его функций (его методов).
Объявление функции описывает только сигнатуру функции (тип возвращаемого значения, его имя и его типы параметров и имена). Определение функции включает тело функции (ее реализацию).
Это немного отличается, когда речь идет о типах. Вы определяете тип, указав его имя и (как минимум), просто объявив все его функции-члены (и другие члены). Это правильно, можно определить тип, даже если вы не определяете его функции-члены.
- Распространённые файлы исходного кода C++ — это файлы
.h(dot h) и.cpp..hФайл является файлом заголовка и определяет один или несколько типов. Хотя вы можете определить функции-члены в заголовке, обычно это то, для чего.cppпредназначен файл. Таким образом, для гипотетического типа C++ MyClass необходимо определить MyClass вMyClass.h, и вы определите его функции-члены вMyClass.cpp. Для других разработчиков для повторного использования классов вы будете предоставлять общий доступ только к файлам и коду.hобъектов. Вы бы держали свои.cppфайлы в секрете, поскольку реализация является вашей интеллектуальной собственностью. - Предварительно скомпилированные заголовки (
pch.h). Как правило, есть набор файлов заголовков, которые вы включаете в приложение, и эти файлы не изменяются очень часто. Таким образом, вместо обработки содержимого этого набора заголовков при каждой компиляции можно объединить эти заголовки в один файл, скомпилировать его один раз, а затем использовать выходные данные этого шага предварительной компиляции при каждой сборке. Это можно сделать с помощью предварительно скомпилированного файла заголовка (обычно именованногоpch.h). - Файлы
.idl. Эти файлы содержат язык определения интерфейса (IDL). IDL можно считать заголовочными файлами для типов среда выполнения Windows. Дополнительные сведения об IDL см. в разделе IDL для типа MainPage.
Скачайте и протестируйте пример буфера обмена
Посетите пример веб-страницы буфера обмена и нажмите кнопку "Скачать ZIP". Распакуйте скачанный файл и просмотрите структуру папок.
- Версия C# примера исходного кода содержится в папке с именем
cs. - Версия C++/WinRT примера исходного кода содержится в папке с именем
cppwinrt. - Другие файлы, используемые версией C# и версией C++/WinRT, можно найти в
sharedпапках иSharedContentпапках.
В этом разделе приведено пошаговое руководство, в котором показано, как можно воссоздать версию примера Clipboard для C++/WinRT, перенеся её из исходного кода на C#. Таким образом, можно увидеть, как перенести собственные проекты C# в C++/WinRT.
Чтобы получить представление о том, что делает пример, откройте решение C# (), измените конфигурацию соответствующим образом (\Clipboard_sample\cs\Clipboard.slnвозможно, на x64), выполните сборку и запуск. Собственный пользовательский интерфейс этого примера пошагово знакомит вас с его различными возможностями.
Подсказка
Корневая папка скачаемого примера может быть названа Clipboard , а не Clipboard_sample. Но мы по-прежнему будем называть эту папку Clipboard_sample, чтобы отличать её от версии C++/WinRT, которую вы создадите на одном из следующих шагов.
Создайте пустое приложение с именем Clipboard
Note
Сведения об установке и использовании расширения C++/WinRT Visual Studio (VSIX) и пакета NuGet (которые вместе предоставляют поддержку шаблона проекта и сборки), см. Visual Studio поддержку C++/WinRT.
Начните процесс переноса, создав проект C++/WinRT в Microsoft Visual Studio. Создайте новый проект, используя шаблон проекта C++ «Пустое приложение, упакованное (WinUI 3 в настольном приложении)». Задайте имя Clipboard и (чтобы структура папок соответствовала пошаговому руководству) убедитесь, что флажок Поместить решение и проект в один каталог снят.
Для начала убедитесь, что этот новый пустой проект собирается и запускается.
Package.appxmanifest и файлы ресурсов
Если версии C# и C++/WinRT этого примера не нужно устанавливать параллельно на одном и том же компьютере, тогда исходные файлы манифеста пакета приложения для этих двух проектов (Package.appxmanifest) могут быть идентичными. В этом случае можно просто скопировать Package.appxmanifest из проекта C# в проект C++/WinRT, и все готово.
Чтобы две версии примера могли сосуществовать, им нужны разные идентификаторы. В этом случае в проекте C++/WinRT откройте Package.appxmanifest файл в редакторе XML и запишите эти три значения.
- В элементе /Package/Identity обратите внимание на значение атрибута Name . Это имя пакета. Для вновь созданного проекта ему изначально присваивается уникальный GUID.
- В элементе /Package/Applications/Application обратите внимание на значение атрибута Id . Это идентификатор приложения.
- В элементе /Package/mp:PhoneIdentity обратите внимание на значение атрибута PhoneProductId . Опять же, для только что созданного проекта будет задан тот же ИДЕНТИФИКАТОР GUID, что и имя пакета.
Затем скопируйте Package.appxmanifest из проекта C# в проект C++/WinRT. Наконец, можно восстановить три значения, которые вы указали. Вы также можете изменить скопированные значения, чтобы сделать их уникальными и/или соответствующими для приложения и для вашей организации (как правило, для нового проекта). Например, в этом случае вместо восстановления значения имени пакета можно просто заменить скопированное значение с Microsoft.SDKSamples.Clipboard.CS на Microsoft.SDKSamples.Clipboard.CppWinRT. И мы можем оставить идентификатор приложения равным App. Если имя пакета или идентификатор приложения отличаются, то два приложения будут иметь разные идентификаторы модели пользователей приложений (AUMID). И это то, что необходимо для двух приложений, которые должны быть установлены параллельно на одном компьютере.
В этом пошаговом руководстве имеет смысл внести несколько других изменений.Package.appxmanifest Есть три вхождения строки Clipboard C# Sample. Измените это на пример C++/WinRT для буфера обмена.
В проекте Package.appxmanifest C++/WinRT файл и проект теперь не синхронизируются с файлами активов, на которые они ссылаются. Чтобы устранить это, сначала удалите ресурсы из проекта C++/WinRT, выбрав все файлы в Assets папке (в Обозреватель решений в Visual Studio) и удалив их (нажмите кнопку "Удалить в диалоговом окне").
Проект C# ссылается на файлы ресурсов из общей папки. Вы можете сделать то же самое в проекте C++/WinRT или скопировать файлы, как мы будем делать в этом пошаговом руководстве.
Перейдите в папку \Clipboard_sample\SharedContent\media. Выберите семь файлов, которые включает проект C# (microsoft-sdk.png, , smalltile-sdk.png, splash-sdk.png, squaretile-sdk.pngstorelogo-sdk.png, tile-sdk.pngи windows-sdk.png), скопируйте их и вставьте их в папку \Clipboard\Clipboard\Assets в новом проекте.
Щелкните правой кнопкой мыши папку Assets (в обозревателе решений в проекте C++/WinRT) >Добавить>Существующий элемент... и перейдите к \Clipboard\Clipboard\Assets. В средство выбора файлов выберите семь файлов и нажмите кнопку "Добавить".
Package.appxmanifest теперь снова синхронизирован с файлами ресурсов проекта.
MainPage, включая функциональность для настройки примера
Пример работы с буфером обмена — как и все примеры приложений Universal Windows Platform (UWP) — представляет собой набор сценариев, которые пользователь может проходить по одному. Коллекция сценариев в данном примере настраивается в исходном коде примера. Каждый сценарий в коллекции — это элемент данных, который хранит заголовок, а также тип класса в проекте, реализующего сценарий.
В версии C# примера при просмотре файла SampleConfiguration.cs исходного кода вы увидите два класса. Большая часть логики конфигурации находится в классе MainPage, который является частичным классом (он образует полный класс в сочетании с разметкой в MainPage.xaml и императивным кодом в MainPage.xaml.cs). Другой класс в этом файле исходного кода — "Сценарий" со своими свойствами Title и ClassType .
В следующих нескольких подразделах мы рассмотрим, как перенести MainPage и сценарий.
IDL для типа MainPage
Давайте начнем с этого раздела, кратко говоря о языке определения интерфейса (IDL) и о том, как это помогает нам при программировании с помощью C++/WinRT. IDL — это тип исходного кода, описывающий вызываемую поверхность типа среда выполнения Windows. Доступная для вызова (или общедоступная) часть типа выводится наружу, чтобы этот тип можно было использовать. Эта видимая часть типа отличается от фактической внутренней реализации типа, которая, конечно, не является ни вызываемой, ни общедоступной. Именно проецируемую часть мы и определяем в IDL.
Создав исходный .idl код IDL (в файле), вы можете скомпилировать IDL в файлы метаданных, доступные для машинного чтения (также известные как метаданные Windows). Эти файлы метаданных имеют расширение .winmd, и вот некоторые из их использования.
-
.winmdможет описывать типы среда выполнения Windows в компоненте. При ссылке на компонент среда выполнения Windows (WRC) из проекта приложения проект приложения считывает метаданные Windows, принадлежащие WRC (эти метаданные могут находиться в отдельном файле или могут быть упакованы в тот же файл, что и сам WRC), чтобы вы могли использовать типы WRC из приложения. -
.winmdможет описывать типы среда выполнения Windows в одной части вашего приложения, чтобы другая часть того же приложения могла использовать их. Например, тип среда выполнения Windows, используемый на странице XAML в том же приложении. - Чтобы упростить вам работу с типами среда выполнения Windows (встроенными или сторонними), система сборки C++/WinRT использует файлы
.winmdдля создания типов-обёрток, представляющих спроецированные части этих типов среда выполнения Windows. - Чтобы упростить вам реализацию собственных типов среда выполнения Windows, система сборки C++/WinRT преобразует ваш IDL в файл
.winmd, а затем использует этот файл для создания обёрток для вашей проекции, а также заглушек, на основе которых строится ваша реализация (позже в этом разделе мы подробнее поговорим об этих заглушках).
Определенная версия IDL, используемая с C++/WinRT, — это Microsoft язык определения интерфейса 3.0. В оставшейся части этого раздела мы рассмотрим тип MainPage C# подробно. Мы определим, какие части должны находиться в проекции типа C++/WinRT MainPage (т. е. в вызываемой или общедоступной, поверхности) и которые могут быть просто частью его реализации. Это различие важно, поскольку, когда мы будем писать наш IDL (что мы и сделаем в следующем разделе), мы определим в нём только вызываемые элементы.
Файлы исходного кода C#, которые вместе реализуют тип MainPage : MainPage.xaml (который мы скоро портим, копируя его), MainPage.xaml.csи SampleConfiguration.cs.
В версии C++/WinRT мы будем учитывать тип MainPage в файлы исходного кода аналогичным образом. Мы возьмём логику из MainPage.xaml.cs и по большей части перенесём её в MainPage.h и MainPage.cpp. И логику в SampleConfiguration.cs мы переведём в SampleConfiguration.h и SampleConfiguration.cpp.
Классы в приложении UWP, написанном на C#, разумеется, относятся к типам среда выполнения Windows. Но при создании типа в приложении C++/WinRT можно выбрать, является ли этот тип типом среда выполнения Windows или обычным классом C++ или структурой или перечислением.
Любая страница XAML в нашем проекте должна быть типом среда выполнения Windows, поэтому MainPage должен быть типом среда выполнения Windows. В проекте C++/WinRT MainPage уже является среда выполнения Windows типом, поэтому нам не нужно изменять этот аспект. В частности, это класс среды выполнения.
- Дополнительные сведения о том, следует ли создавать класс времени выполнения для конкретного типа, см. в разделе Разработка API с помощью C++/WinRT.
- В C++/WinRT внутренняя реализация класса среды выполнения и проецируемых (общедоступных) частей существуют в виде двух разных классов. Они называются типом реализации и проецируемым типом. Дополнительные сведения о них см. в разделе, упоминаемом в предыдущей точке маркера, а также в разделе "Использование API с помощью C++/WinRT".
- Дополнительные сведения о связи между классами среды выполнения и IDL-файлами (
.idl) можно найти в разделе Элементы управления XAML; привязка к свойству C++/WinRT и следовать приведенным в нем инструкциям. В этом разделе описывается процесс разработки нового класса среды выполнения, первого шага которого является добавление нового элемента Midl File (IDL) в проект.
Для MainPage у нас уже есть необходимый MainPage.idl файл в проекте C++/WinRT. Это потому, что шаблон проекта создал его за нас. Но далее в этом пошаговом руководстве мы добавим дополнительные .idl файлы в проект.
Вскоре мы увидим точный перечень того, какой именно IDL нужно добавить в существующий файл MainPage.idl. Перед этим нам нужно разобраться, что должно, а что не должно входить в IDL.
Чтобы определить, какие члены MainPage необходимо объявить в MainPage.idl (чтобы они стали частью класса времени выполнения MainPage), а какие могут быть просто членами типа реализации MainPage, давайте составим список членов класса C# MainPage. Мы находим этих членов, глядя в MainPage.xaml.cs и в SampleConfiguration.cs.
Мы обнаруживаем в общей сложности двенадцать protectedполейprivate и методов. И мы находим следующие public члены.
- Конструктор
MainPage()по умолчанию. - Статические поля Current и FEATURE_NAME.
- Свойства IsClipboardContentChangedEnabled и Scenarios.
- Методы BuildClipboardFormatsOutputString, DisplayToast, EnableClipboardContentChangedNotifications и NotifyUser.
Это те public члены, которые являются кандидатами на объявление в MainPage.idl. Итак, давайте рассмотрим каждый из них и посмотрим, должны ли они быть частью класса среды выполнения MainPage или должны ли они быть частью его реализации.
- Конструктор
MainPage()по умолчанию. Для XAML Page обычно объявляют конструктор по умолчанию в её IDL. Таким образом, платформа пользовательского интерфейса XAML может активировать тип. - Текущее статическое поле используется из отдельных страниц XAML сценария для доступа к экземпляру MainPage приложения. Так как Current не используется для взаимодействия с платформой XAML (и не используется в единицах компиляции), мы могли бы зарезервировать его исключительно в качестве члена типа реализации. Если речь идет о ваших собственных проектах, в подобных случаях вы можете поступить так. Но поскольку поле является экземпляром спроецированного типа, его логично объявить в IDL. Таким образом, это то, что мы будем делать здесь (и делать это также делает код немного более чистым).
- Это аналогичный случай для статического поля FEATURE_NAME , доступ к которому осуществляется в типе MainPage . И снова, если объявить это в IDL, наш код станет немного чище.
- Свойство IsClipboardContentChangedEnabled используется только в классе OtherScenarios . Поэтому при переносе мы немного упростим это и сделаем это закрытым полем класса времени выполнения OtherScenarios. Так что он не будет идти в IDL.
- Свойство Scenarios представляет собой коллекцию объектов типа Scenario (типа, который мы упоминали ранее). Мы поговорим о сценарии в следующем подразделе, поэтому давайте оставим свойство Scenarios до тех пор.
- Методы BuildClipboardFormatsOutputString, DisplayToast и EnableClipboardContentChangedNotifications являются служебными функциями, которые больше связаны с общим состоянием примера, чем о главной странице. Поэтому при переносе мы вынесем эти три метода в новый вспомогательный тип под названием SampleState (который не обязательно должен быть типом среда выполнения Windows). По этой причине эти три метода не будут включены в IDL.
- Метод NotifyUser вызывается из отдельных XAML-страниц сценария на экземпляре MainPage, возвращаемом статическим полем Current. Так как (как уже отмечалось) Current является экземпляром проецируемого типа, необходимо объявить NotifyUser в IDL. NotifyUser принимает параметр типа NotifyType. Мы поговорим об этом в следующем подразделе.
Любой член, который вы хотите использовать для привязки данных, также должен быть объявлен в IDL (независимо от того, используете ли вы {x:Bind} или {Binding}). Дополнительные сведения см. в разделе "Привязка данных".
Мы продвигаемся: мы составляем список того, какие элементы следует добавлять в файл MainPage.idl, а какие — нет. Но мы все еще должны обсудить свойство Scenarios и тип NotifyType . Итак, давайте сделаем это дальше.
IDL для типов Scenario и NotifyType
Класс "Сценарий " определен в SampleConfiguration.cs. У нас есть решение о переносе этого класса в C++/WinRT. По умолчанию мы, вероятно, сделаем его обычным C++ struct. Но если сценарий используется в двоичных файлах или взаимодействует с платформой XAML, то его необходимо объявить в IDL как тип среда выполнения Windows.
Изучая исходный код C#, мы находим, что сценарий используется в этом контексте.
<ListBox x:Name="ScenarioControl" ... >
var itemCollection = new List<Scenario>();
int i = 1;
foreach (Scenario s in scenarios)
{
itemCollection.Add(new Scenario { Title = $"{i++}) {s.Title}", ClassType = s.ClassType });
}
ScenarioControl.ItemsSource = itemCollection;
Коллекция объектов Scenario назначается свойству ItemsSourceобъекта ListBox (который является элементом управления элементами). Так как сценарийдолжен взаимодействовать с XAML, он должен быть типом среда выполнения Windows. Поэтому его необходимо определить в IDL. Определение типа Scenario в IDL приводит к тому, что система сборки C++/WinRT автоматически создает для вас определение Scenario в исходном коде в служебном файле заголовка (имя и расположение этого файла не важны для этого пошагового руководства).
И вы помните, что MainPage.Scenarios — это коллекция объектов сценариев , которые мы только что сказали, что должны находиться в IDL. По этой причине сам MainPage.Scenarios также необходимо объявить в IDL.
NotifyType — это enum, объявленный в MainPage.xaml.cs языка C#. Так как мы передаем NotifyType методу, относящемуся к классу среды выполнения MainPage, notifyType тоже должен быть типом среда выполнения Windows; и его необходимо определить в MainPage.idl.
Теперь добавим в файл MainPage.idl новые типы и новый член Mainpage, которые мы решили объявить в IDL. В то же время мы удалим из IDL элементы заполнителя Mainpage, которые предоставил нам шаблон проекта Visual Studio.
Таким образом, в проекте C++/WinRT откройте MainPage.idlи измените его таким образом, чтобы он выглядел как приведенный ниже список. Обратите внимание, что одно из изменений состоит в том, чтобы изменить имя пространства имён с Clipboard на SDKTemplate. Если хотите, можете просто заменить всё содержимое MainPage.idl следующим кодом. Ещё одно изменение, на которое стоит обратить внимание: мы переименовываем Scenario::ClassType в Scenario::ClassName.
// MainPage.idl
namespace SDKTemplate
{
struct Scenario
{
String Title;
Microsoft.UI.Xaml.Interop.TypeName ClassName;
};
enum NotifyType
{
StatusMessage,
ErrorMessage
};
[default_interface]
runtimeclass MainPage : Microsoft.UI.Xaml.Controls.Page
{
MainPage();
static MainPage Current{ get; };
static String FEATURE_NAME{ get; };
static Windows.Foundation.Collections.IVector<Scenario> scenarios{ get; };
void NotifyUser(String strMessage, NotifyType type);
};
}
Note
Дополнительные сведения о содержимом .idl файла в проекте C++/WinRT см. в Microsoft языке определения интерфейса 3.0.
При выполнении собственного портирования вы, возможно, не захотите или не будете нуждаться в изменении имени пространства имен, как мы сделали выше. Мы делаем это здесь только потому, что пространство имён по умолчанию в проекте C#, который мы переносим, — SDKTemplate, тогда как имя проекта и сборки — Clipboard.
Но по мере продолжения переноса в этом пошаговом руководстве мы будем заменять каждое вхождение имени пространства имён Clipboard в исходном коде на SDKTemplate. Также есть место в свойствах проекта C++/WinRT, где указано имя пространства имён Clipboard, поэтому давайте сразу изменим его.
В Visual Studio для проекта C++/WinRT задайте для свойства проекта Общие свойства>C++/WinRT>Корневое пространство имен значение SDKTemplate.
Сохраните IDL и повторно создайте файлы-заглушки
Тема Элементы управления XAML; привязка к свойству C++/WinRT вводит понятие файлов-заглушек и показывает, как они используются на практике. Мы также уже упоминали заглушки ранее в этом разделе, когда говорили, что система сборки C++/WinRT преобразует содержимое файлов .idl в метаданные Windows, а затем на основе этих метаданных инструмент с именем cppwinrt.exe создает заглушки, на которых можно основывать реализацию.
Каждый раз, когда вы добавляете, удаляете или изменяете что-либо в IDL и выполняете сборку, система сборки обновляет реализации заглушек в этих файлах заглушек. Поэтому каждый раз, когда вы изменяете IDL и выполняете сборку, мы рекомендуем просматривать эти файлы заглушек, копировать все изменённые сигнатуры и вставлять их в свой проект. Мы предоставим более точные сведения и примеры того, как это сделать в данный момент. Но преимущество такого подхода в том, что он позволяет вам всегда точно и без ошибок знать, какой должна быть структура вашего типа реализации и какими должны быть сигнатуры его методов.
На этом этапе в этом пошаговом руководстве мы пока закончили редактировать файл MainPage.idl, поэтому сейчас его следует сохранить. Сейчас проект не удастся собрать полностью, но тем не менее полезно выполнить сборку, поскольку при этом заново создаются файлы-заглушки для MainPage. Поэтому создайте проект сейчас и игнорируйте ошибки сборки.
Для этого проекта C++/WinRT файлы-заглушки создаются в папке \Clipboard\Clipboard\Generated Files\sources. Вы найдете их там после завершения частичной сборки (опять же, как и ожидалось, сборка не завершится полностью. Но шаг, который нас интересует — генерация заглушек, — будет успешно выполнен). Файлы, которые нас интересуют, — это MainPage.h и MainPage.cpp.
В этих двух файлах заглушки вы увидите новые реализации заглушки членов MainPage , которые мы добавили в IDL (Current и FEATURE_NAME, например). Вам нужно скопировать эти реализации-заглушки в файлы MainPage.h и MainPage.cpp, которые уже есть в проекте. В то же время, как мы сделали с IDL, мы удалим из этих существующих файлов фиктивные члены Mainpage, добавленные шаблоном проекта Visual Studio (фиктивное свойство с именем MyProperty и обработчик события с именем ClickHandler).
На самом деле, единственным членом текущей версии MainPage , которую мы хотим сохранить, является конструктор.
После того как вы скопировали новые члены из файлов-заглушек, удалили ненужные члены и обновили пространство имён, файлы MainPage.h и MainPage.cpp в вашем проекте должны выглядеть так, как показано в приведённых ниже листингах кода. Обратите внимание, что существует два типа MainPage . Один — в пространстве имен implementation, а второй — в пространстве имен factory_implementation. Единственное изменение, которое мы внесли в factory_implementation, — это добавление SDKTemplate в пространство имён.
// MainPage.h
#pragma once
#include "MainPage.g.h"
namespace winrt::SDKTemplate::implementation
{
struct MainPage : MainPageT<MainPage>
{
MainPage();
static SDKTemplate::MainPage Current();
static hstring FEATURE_NAME();
static Windows::Foundation::Collections::IVector<SDKTemplate::Scenario> scenarios();
void NotifyUser(hstring const& strMessage, SDKTemplate::NotifyType const& type);
};
}
namespace winrt::SDKTemplate::factory_implementation
{
struct MainPage : MainPageT<MainPage, implementation::MainPage>
{
};
}
// MainPage.cpp
#include "pch.h"
#include "MainPage.h"
#include "MainPage.g.cpp"
namespace winrt::SDKTemplate::implementation
{
MainPage::MainPage()
{
InitializeComponent();
}
SDKTemplate::MainPage MainPage::Current()
{
throw hresult_not_implemented();
}
hstring MainPage::FEATURE_NAME()
{
throw hresult_not_implemented();
}
Windows::Foundation::Collections::IVector<SDKTemplate::Scenario> MainPage::scenarios()
{
throw hresult_not_implemented();
}
void MainPage::NotifyUser(hstring const& strMessage, SDKTemplate::NotifyType const& type)
{
throw hresult_not_implemented();
}
}
Для строк C# использует System.String. Пример см. в методе MainPage.NotifyUser . В нашем IDL мы объявляем строковый тип с помощью String, и когда средство cppwinrt.exe генерирует для нас код C++/WinRT, оно использует тип winrt::hstring. В любой момент, когда мы сталкиваемся со строкой в коде C#, мы переносим его в winrt::hstring. Дополнительные сведения см. в разделе "Обработка строк" в C++/WinRT.
const& Описание параметров в сигнатурах метода см. в разделе "Передача параметров".
Обновите все оставшиеся объявления и ссылки пространства имён и выполните сборку.
Прежде чем собирать проект C++/WinRT, найдите все объявления пространства имён Clipboard (и ссылки на него) и измените их на SDKTemplate.
-
MainPage.xamlиApp.xaml. Пространство имён присутствует в значениях атрибутовx:Classиxmlns:local. -
App.idl. -
App.h. -
App.cpp. Есть две директивыusing namespace(поиск подстрокиusing namespace Clipboard) и две квалификации типа MainPage (поискClipboard::MainPage). Они нуждаются в изменении.
Так как мы удалили обработчик событий из MainPage, также перейдите MainPage.xaml к элементу Button и удалите его из разметки.
Сохраните все файлы. Очистите решение (Build>Clean Solution), а затем выполните сборку. Если вы выполнили все описанные выше изменения в точности как указано, сборка должна пройти успешно.
Реализация элементов MainPage , объявленных в IDL
Конструктор, Current и FEATURE_NAME
Ниже приведен соответствующий код (из проекта C#), который необходимо перенести.
<!-- MainPage.xaml -->
...
<TextBlock x:Name="SampleTitle" ... />
...
// MainPage.xaml.cs
...
public sealed partial class MainPage : Page
{
public static MainPage Current;
public MainPage()
{
InitializeComponent();
Current = this;
SampleTitle.Text = FEATURE_NAME;
}
...
}
...
// SampleConfiguration.cs
...
public partial class MainPage : Page
{
public const string FEATURE_NAME = "Clipboard C# sample";
...
}
...
Скоро мы будем повторно использовать MainPage.xaml целиком, скопировав его. Теперь (ниже) мы временно добавим элемент TextBlock с соответствующим именем в MainPage.xaml проект C++/WinRT.
FEATURE_NAME является статическим полем MainPage (поле C# const по сути статично в его поведении), определенное в SampleConfiguration.cs. Для C++/WinRT вместо (статического) поля мы сделаем его выражением C++/WinRT свойства только для чтения. Метод получения свойства C++/WinRT является функцией, возвращающей значение свойства, и не принимает параметров (метод доступа). Поэтому статическое поле C# FEATURE_NAME становится статической функцией доступа C++/WinRT FEATURE_NAME (которая в данном случае возвращает строковый литерал).
Кстати, мы сделали бы то же самое, если бы переносили свойство C# только для чтения. Для свойства C# с возможностью записи в C++/WinRT сеттер свойства представляется в виде функции void, которая принимает значение свойства в качестве параметра (мутатор). В любом случае, если поле или свойство C# является статическим, то статическим будет и аксессор и/или мьютатор C++/WinRT.
Текущий — это статическое (а не константное) поле MainPage. Опять же, мы сделаем его (выражение C++/WinRT) свойством только для чтения и снова сделаем его статическим. Где FEATURE_NAME является константой, Current не является. Итак, в C++/WinRT нам потребуется закрытое поле, и наш аксессор будет возвращать его. Таким образом, в проекте C++/WinRT мы объявим в MainPage.h закрытое статическое поле с именем current, определим и инициализируем current в MainPage.cpp (так как оно имеет статическую продолжительность хранения) и будем обращаться к нему через общедоступную статическую функцию доступа с именем Current.
Конструктор выполняет несколько назначений, которые являются простыми для порта.
В проекте C++/WinRT добавьте новый элемент Visual C++>Code>C++ File (.cpp) с именем SampleConfiguration.cpp.
ИзменитеMainPage.xaml, MainPage.hMainPage.cppи SampleConfiguration.cpp в соответствии с приведенными ниже списками.
<!-- MainPage.xaml -->
...
<StackPanel ...>
<TextBlock x:Name="SampleTitle" />
</StackPanel>
...
// MainPage.h
...
namespace winrt::SDKTemplate::implementation
{
struct MainPage : MainPageT<MainPage>
{
...
static SDKTemplate::MainPage Current() { return current; }
...
private:
static SDKTemplate::MainPage current;
...
};
...
}
// MainPage.cpp
...
namespace winrt::SDKTemplate::implementation
{
SDKTemplate::MainPage MainPage::current{ nullptr };
MainPage::MainPage()
{
InitializeComponent();
MainPage::current = *this;
SampleTitle().Text(FEATURE_NAME());
}
...
}
// SampleConfiguration.cpp
#include "pch.h"
#include "MainPage.h"
using namespace winrt;
using namespace SDKTemplate;
hstring implementation::MainPage::FEATURE_NAME()
{
return L"Clipboard C++/WinRT Sample";
}
Кроме того, не забудьте удалить из MainPage.cpp существующие тела функций для MainPage::Current() и MainPage::FEATURE_NAME(), поскольку теперь эти методы определяются в другом месте.
Как видно, MainPage::current объявляется как тип SDKTemplate::MainPage, который является проецируемым типом. Это не объект типа SDKTemplate::implementation::MainPage, который является типом реализации. Проецируемый тип — это тип, который предназначен для использования в проекте для взаимодействия XAML или между двоичными файлами. Тип реализации используется для реализации функциональных возможностей, предоставляемых вашим проецируемым типом. Поскольку объявление MainPage::current (in MainPage.h) находится в пространстве имён реализации (winrt::SDKTemplate::implementation), неквалифицированное имя MainPage обозначало бы тип реализации. Поэтому мы указываем квалификатор SDKTemplate::, чтобы было ясно, что MainPage::current должен быть экземпляром проецируемого типа winrt::SDKTemplate::MainPage.
В конструкторе есть несколько моментов, связанных с MainPage::current = *this;, которые стоит объяснить.
- Когда вы используете указатель
thisвнутри члена типа реализации, указательthis, конечно, является указателем на тип реализации. - Чтобы преобразовать указатель
thisв соответствующий проецируемый тип, разыменуйте его. Если вы создаете тип реализации из IDL (как и здесь), тип реализации имеет оператор преобразования, который преобразуется в проецируемый тип. Вот почему назначение здесь работает.
Дополнительные сведения об этих подробностях см. в статье Создание экземпляров и возврат типов реализации и интерфейсов.
Кроме того, в конструкторе есть SampleTitle().Text(FEATURE_NAME());. Часть SampleTitle() — это вызов простой функции доступа с именем SampleTitle, которая возвращает TextBlock , которую мы добавили в XAML. Каждый раз, когда вы x:Name элемент XAML, компилятор XAML создает для него аксессор, имя которого образовано от имени элемента. Часть .Text(...) вызывает функцию-мутатор Text для объекта TextBlock, который был возвращён методом доступа SampleTitle. И FEATURE_NAME() вызывает нашу статическую функцию доступа MainPage::FEATURE_NAME, чтобы вернуть строковый литерал. В целом эта строка кода задает свойство Text элемента TextBlock с именем SampleTitle.
Обратите внимание, что, поскольку в среда выполнения Windows используются широкие строки, при переносе строкового литерала мы добавляем к нему префикс кодировки wide-char L. Поэтому мы заменяем (например) "a string literal" на L"a string literal". См. также широкие строковые литералы.
Сценарии
Ниже приведен соответствующий код C#, который нам нужно перенести.
// MainPage.xaml.cs
...
public sealed partial class MainPage : Page
{
...
public List<Scenario> Scenarios
{
get { return this.scenarios; }
}
...
}
...
// SampleConfiguration.cs
...
public partial class MainPage : Page
{
...
List<Scenario> scenarios = new List<Scenario>
{
new Scenario() { Title = "Copy and paste text", ClassType = typeof(CopyText) },
new Scenario() { Title = "Copy and paste an image", ClassType = typeof(CopyImage) },
new Scenario() { Title = "Copy and paste files", ClassType = typeof(CopyFiles) },
new Scenario() { Title = "Other Clipboard operations", ClassType = typeof(OtherScenarios) }
};
...
}
...
Из предыдущего анализа мы знаем, что эта коллекция объектов Scenario отображается в элементе ListBox. В C++/WinRT существуют ограничения на тип коллекции , которую можно назначить свойству ItemsSource элемента управления items. Коллекция должна быть вектором или наблюдаемым вектором, а его элементы должны быть одним из следующих элементов:
- Классы среды выполнения, или
- IInspectable.
В случае IInspectable, если элементы сами по себе не являются классами времени выполнения, то они должны относиться к типу, который можно упаковывать в IInspectable и распаковывать из него. А это означает, что они должны быть типами среда выполнения Windows (см. Упаковка и распаковка значений в IInspectable).
В этом примере мы не сделали сценарий классом среды выполнения. Это по-прежнему разумный вариант, хотя. И в вашей работе по портированию будут случаи, когда класс времени выполнения определённо окажется подходящим решением. Например, если необходимо сделать тип элемента наблюдаемым (см. элементы управления XAML; привязка к свойству C++/WinRT), или если элемент должен иметь методы по любой другой причине, и это больше, чем просто набор элементов данных.
Так как в этом пошаговом руководстве мы не будем использовать класс среды выполнения для типа сценария , то нам нужно подумать о боксе. Если бы мы сделали Scenario обычным объектом C++ struct, то не смогли бы упаковать его. Но мы объявили Scenario как struct в IDL, и поэтому мы можем поместить его в оболочку.
Нам остаётся выбрать: упаковать Scenario заранее или подождать до момента, когда мы будем готовы присвоить значение свойству ItemsSource, и выполнять упаковку по мере необходимости. Ниже приведены некоторые соображения по поводу этих двух вариантов.
- Бокс впереди. Для этого варианта наш элемент данных — это коллекция IInspectable, которую можно сразу привязать к пользовательскому интерфейсу. При инициализации мы помещаем объекты Scenario в это поле данных. Нам нужна только одна копия этой коллекции, но мы должны разблокировать элемент каждый раз, когда нам нужно считывать его поля.
- Бокс просто вовремя. Для этого параметра наше поле данных является коллекцией объектов типа Scenario. Когда приходит время передать данные пользовательскому интерфейсу, мы упаковываем объекты Scenario из члена данных в новую коллекцию объектов IInspectable. Поля элементов в элементе данных можно считывать без распаковки, но требуется две копии коллекции.
Как видите, для небольшой коллекции вроде этой плюсы и минусы в целом уравновешивают друг друга. Итак, в этом исследовании мы остановимся на варианте just-in-time.
Член сценариев — это поле MainPage, определенное и инициализированное в SampleConfiguration.cs. И сценарии — это свойство mainPage только для чтения, определенное в MainPage.xaml.cs (и реализованное для простого возврата поля сценариев ). Мы сделаем что-то подобное в проекте C++/WinRT; но мы сделаем два члена статическими (так как нам нужен только один экземпляр в приложении, и поэтому мы можем получить доступ к ним без необходимости в экземпляре класса). Их мы назовем scenariosInner и scenarios соответственно. Мы объявим scenariosInner в MainPage.h. И, поскольку он имеет статическую длительность хранения, мы определим и инициализировать его в .cpp файле (SampleConfiguration.cppв данном случае).
Измените MainPage.h и SampleConfiguration.cpp так, чтобы они соответствовали приведённым ниже спискам.
// MainPage.h
...
struct MainPage : MainPageT<MainPage>
{
...
static Windows::Foundation::Collections::IVector<Scenario> scenarios() { return scenariosInner; }
...
private:
static winrt::Windows::Foundation::Collections::IVector<Scenario> scenariosInner;
...
};
// SampleConfiguration.cpp
...
using namespace Windows::Foundation::Collections;
...
IVector<Scenario> implementation::MainPage::scenariosInner = winrt::single_threaded_observable_vector<Scenario>(
{
Scenario{ L"Copy and paste text", xaml_typename<SDKTemplate::CopyText>() },
Scenario{ L"Copy and paste an image", xaml_typename<SDKTemplate::CopyImage>() },
Scenario{ L"Copy and paste files", xaml_typename<SDKTemplate::CopyFiles>() },
Scenario{ L"History and roaming", xaml_typename<SDKTemplate::HistoryAndRoaming>() },
Scenario{ L"Other Clipboard operations", xaml_typename<SDKTemplate::OtherScenarios>() },
});
Кроме того, не забудьте удалить существующий текст функции из MainPage.cppMainPage::scenarios(), так как теперь мы определяем этот метод в файле заголовка.
Как видно, в SampleConfiguration.cpp мы инициализируем статический член данных scenariosInner, вызывая вспомогательную функцию C++/WinRT под названием winrt::single_threaded_observable_vector. Эта функция создает для нас новый объект коллекции среда выполнения Windows и возвращает его в виде интерфейса IObservableVector. Так как в этом примере коллекция не является наблюдаемой (в этом нет необходимости, поскольку после инициализации в неё не добавляются и из неё не удаляются элементы), мы могли бы вместо этого вызвать winrt::single_threaded_vector. Эта функция возвращает коллекцию в виде интерфейса IVector .
Дополнительные сведения о коллекциях и привязке данных к ним см. в статьях Элементы управления элементами XAML; привязка к коллекции C++/WinRT и Коллекции с помощью C++/WinRT.
Код инициализации, который вы только что добавили, ссылается на типы, которых еще нет в проекте (например, winrt::SDKTemplate::CopyText. Чтобы исправить это, давайте добавим пять новых пустых страниц XAML в проект.
Добавление пяти новых пустых страниц XAML
Добавьте в проект новый элемент Visual C++>Blank Page (C++/WinRT) (убедитесь, что это именно шаблон элемента Blank Page (C++/WinRT), а не Blank Page). Назовите его CopyText. Новая страница XAML определяется в пространстве имен SDKTemplate , которое мы хотим.
Повторите описанный выше процесс еще четыре раза и назовите страницы XAML CopyImage, CopyFiles, HistoryAndRoaming и OtherScenarios.
Теперь вы сможете создать еще раз, если вы хотите.
NotifyUser
В проекте C# вы найдете реализацию метода MainPage.NotifyUser в MainPage.xaml.cs.
MainPage.NotifyUser имеет зависимость от MainPage.UpdateStatus, и этот метод, в свою очередь, имеет зависимости от элементов XAML, которые мы еще не переносили. Теперь мы просто заглушим метод UpdateStatus в проекте C++/WinRT, и мы будем переносить его позже.
Ниже приведен соответствующий код C#, который нам нужно перенести.
// MainPage.xaml.cs
...
public void NotifyUser(string strMessage, NotifyType type)
if (Dispatcher.HasThreadAccess)
{
UpdateStatus(strMessage, type);
}
else
{
var task = Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () => UpdateStatus(strMessage, type));
}
private void UpdateStatus(string strMessage, NotifyType type) { ... }{
...
NotifyUser отправляет обновления пользовательского интерфейса в основной поток. В WinUI 3 здесь используется Microsoft.UI.Dispatching.DispatcherQueue вместо более старого CoreDispatcher. В C++/WinRT каждый раз, когда вы хотите использовать тип из пространства имен Windows или Microsoft, необходимо включить соответствующий файл заголовка пространства имен C++/WinRT (дополнительные сведения об этом см. в статье "Начало работы с C++/WinRT"). В этом случае, как вы увидите в приведённом ниже листинге кода, заголовок — winrt/Microsoft.UI.Dispatching.h, и мы включим его в pch.h.
UpdateStatus является частным. Поэтому мы создадим частный метод в нашем типе реализации MainPage . UpdateStatus не предназначен для вызова в классе среды выполнения, поэтому мы не будем объявлять его в IDL.
После переноса MainPage.NotifyUser и отрезания mainPage.UpdateStatus это то, что мы имеем в проекте C++/WinRT. После этого описания кода мы рассмотрим некоторые сведения.
// pch.h
...
#include <winrt/Microsoft.UI.Dispatching.h>
...
// MainPage.h
...
struct MainPage : MainPageT<MainPage>
{
...
void NotifyUser(hstring const& strMessage, SDKTemplate::NotifyType const& type);
...
private:
void UpdateStatus(hstring const& strMessage, SDKTemplate::NotifyType const& type);
...
};
// MainPage.cpp
...
void MainPage::NotifyUser(hstring const& strMessage, SDKTemplate::NotifyType const& type)
{
if (DispatcherQueue().HasThreadAccess())
{
UpdateStatus(strMessage, type);
}
else
{
DispatcherQueue().TryEnqueue([strMessage, type, this]()
{
UpdateStatus(strMessage, type);
});
}
}
void MainPage::UpdateStatus(hstring const& strMessage, SDKTemplate::NotifyType const& type)
{
throw hresult_not_implemented();
}
...
В C# можно использовать точечную нотацию, чтобы обращаться к вложенным свойствам. Таким образом, тип MainPage C# может получить доступ к собственному свойству диспетчера с синтаксисом Dispatcher. И C# может далее указать это значение с помощью синтаксиса, например Dispatcher.HasThreadAccess. В C++/WinRT свойства реализуются как функции доступа, поэтому синтаксис отличается только в том, что для каждого вызова функции добавляются скобки.
| C# | C++/WinRT |
|---|---|
Dispatcher.HasThreadAccess |
DispatcherQueue().HasThreadAccess() |
Когда версия C# notifyUser вызывает Dispatcher.RunAsync, эквивалент WinUI 3 использует DispatcherQueue.TryEnqueue. Версия C++/WinRT использует делегат обратного вызова в виде лямбда-функции. В C++/WinRT мы фиксируем два параметра, которые мы будем использовать, а также this указатель (так как мы будем вызывать функцию-член). Дополнительные сведения о реализации делегатов в виде лямбда-примеров и примеров кода см. в разделе "Обработка событий с помощью делегатов в C++/WinRT".
Реализация оставшихся элементов MainPage
Давайте составим полный список элементов MainPage (реализованных в MainPage.xaml.cs и SampleConfiguration.cs), чтобы увидеть, какие из них мы уже перенесли, а какие ещё предстоит перенести.
| Член | Access | Status |
|---|---|---|
| Конструктор MainPage | public |
Перенесено |
| Текущее свойство | public |
Перенесено |
| свойство FEATURE_NAME | public |
Перенесено |
| Свойство IsClipboardContentChangedEnabled | public |
Не начато |
| Свойство "Сценарии" | public |
Перенесено |
| Метод BuildClipboardFormatsOutputString | public |
Не начато |
| Метод DisplayToast | public |
Не начато |
| Метод EnableClipboardContentChangedNotifications | public |
Не начато |
| Метод NotifyUser | public |
Портировано |
| Метод OnNavigatedTo | protected |
Не начато |
| Поле isApplicationWindowActive | private |
Не начато |
| поле needToPrintClipboardFormat | private |
Не начато |
| Поле сценариев | private |
Перенесено |
| метод Button_Click | private |
Не начато |
| Метод DisplayChangedFormats | private |
Не начато |
| метод Footer_Click | private |
Не начато |
| Метод HandleClipboardChanged | private |
Не начато |
| Метод OnClipboardChanged | private |
Не начато |
| Метод OnWindowActivated | private |
Не начато |
| метод ScenarioControl_SelectionChanged | private |
Не начато |
| Метод UpdateStatus | private |
Стукнулся |
Мы поговорим о еще непортированных членах в следующих нескольких подразделах, а затем.
Note
Время от времени мы будем сталкиваться со ссылками в исходном коде на элементы пользовательского интерфейса в разметке XAML (в MainPage.xaml). Когда мы дойдем до этих ссылок, мы временно обойдем их, добавив в XAML простые элементы-заполнители. Таким образом, проект будет продолжать создаваться после каждого подраздела. Альтернативой является разрешение ссылок путем копирования всего содержимого MainPage.xaml проекта C# в проект C++/WinRT. Но если мы так сделаем, то пройдёт много времени, прежде чем мы сможем сделать остановку и снова выполнить сборку (из-за чего мы можем не заметить опечатки или другие ошибки, которые допустим по пути).
После переноса императивного кода для класса MainPageмы скопируйм содержимое XAML-файла и убедимся, что проект по-прежнему будет построен.
IsClipboardContentChangedEnabled
Это свойство get-set C#, которое по умолчанию используется false. Это член MainPage и определен в SampleConfiguration.cs.
Для C++/WinRT нам потребуется функция доступа, функция мутатора и элемент резервных данных в качестве поля. Так как IsClipboardContentChangedEnabled представляет состояние одного из сценариев в примере, а не состояние MainPage , мы создадим новые члены в новом типе служебной программы с именем SampleState. И мы реализуем это в нашем SampleConfiguration.cpp файле исходного кода, и мы создадим элементы static (так как нам нужен только один экземпляр в приложении; и чтобы мы могли получить к ним доступ без необходимости экземпляра класса).
В дополнение к нашему SampleConfiguration.cpp в проекте C++/WinRT добавьте новый элемент Visual C++>Code>Header File (.h) с именем SampleConfiguration.h. Измените SampleConfiguration.h и SampleConfiguration.cpp так, чтобы они соответствовали приведённым ниже спискам.
// SampleConfiguration.h
#pragma once
#include "pch.h"
namespace winrt::SDKTemplate
{
struct SampleState
{
static bool IsClipboardContentChangedEnabled();
static void IsClipboardContentChangedEnabled(bool checked);
private:
static bool isClipboardContentChangedEnabled;
};
}
// SampleConfiguration.cpp
...
#include "SampleConfiguration.h"
...
bool SampleState::isClipboardContentChangedEnabled = false;
...
bool SampleState::IsClipboardContentChangedEnabled()
{
return isClipboardContentChangedEnabled;
}
void SampleState::IsClipboardContentChangedEnabled(bool checked)
{
if (isClipboardContentChangedEnabled != checked)
{
isClipboardContentChangedEnabled = checked;
}
}
Опять же, поле с static хранилищем (например, SampleState::isClipboardContentChangedEnabled) должно быть определено один раз в приложении, и .cpp файл является хорошим местом для этого (SampleConfiguration.cpp в данном случае).
BuildClipboardFormatsOutputString
Этот метод является общедоступным членом MainPage, и он определен в SampleConfiguration.cs.
// SampleConfiguration.cs
...
public string BuildClipboardFormatsOutputString()
{
DataPackageView clipboardContent = Windows.ApplicationModel.DataTransfer.Clipboard.GetContent();
StringBuilder output = new StringBuilder();
if (clipboardContent != null && clipboardContent.AvailableFormats.Count > 0)
{
output.Append("Available formats in the clipboard:");
foreach (var format in clipboardContent.AvailableFormats)
{
output.Append(Environment.NewLine + " * " + format);
}
}
else
{
output.Append("The clipboard is empty");
}
return output.ToString();
}
...
В C++/WinRT мы создадим BuildClipboardFormatsOutputString общедоступный статический метод SampleState. Мы можем сделать его static, так как он не обращается к членам экземпляра.
Чтобы использовать типы Clipboard и DataPackageView в C++/WinRT, необходимо включить файл заголовка пространства имён Windows для C++/WinRT winrt/Windows.ApplicationModel.DataTransfer.h.
В C#свойство DataPackageView.AvailableFormats является IReadOnlyList, поэтому мы можем получить доступ к свойству Count этого. В C++/WinRT метод доступа DataPackageView::AvailableFormats возвращает объект IVectorView, у которого можно вызвать метод доступа Size.
Чтобы перенести использование типа C# System.Text.StringBuilder , мы будем использовать стандартный тип C++ std::wostringstream. Этот тип представляет собой выходной поток для широких строк (и для его использования нам потребуется подключить заголовочный файл sstream). Вместо использования метода Добавления , как и в StringBuilder, вы используете оператор вставки (<<) с выходным потоком, таким как wostringstream. Дополнительные сведения см. в статье о программировании iostream и форматировании строк C++/WinRT.
Код C# создает StringBuilder с ключевым словом new . В C# объекты по умолчанию являются ссылочными типами и объявляются в куче с помощью new. В современном стандарте C++объекты являются типами значений по умолчанию, объявленными в стеке (без использования new). Поэтому мы переносим StringBuilder output = new StringBuilder(); в C++/WinRT просто как std::wostringstream output;.
Ключевое слово C# var запрашивает компилятору вывод типа. Вы переносите var на auto с помощью C++/WinRT. Но в C++/WinRT бывают случаи, когда, чтобы избежать копирования, нужна ссылка на выводимый (или выведенный) тип, а lvalue-ссылку на выведенный тип обозначают с помощью auto&. Существуют также случаи, когда требуется особый вид ссылки, которая корректно привязывается независимо от того, инициализируется ли она значением lvalue или значением rvalue. И вы выражаете это с помощью auto&&. Это форма, которую вы видите в цикле for в перенесенном коде ниже. Общие сведения о lvalues и rvalue см. в статьях "Категории значений" и ссылки на них.
Измените pch.h, SampleConfiguration.h и SampleConfiguration.cpp, чтобы они соответствовали приведённым ниже спискам.
// pch.h
...
#include <sstream>
#include "winrt/Windows.ApplicationModel.DataTransfer.h"
...
// SampleConfiguration.h
...
struct SampleState
{
static hstring BuildClipboardFormatsOutputString();
...
}
...
// SampleConfiguration.cpp
...
using namespace Windows::ApplicationModel::DataTransfer;
...
hstring SampleState::BuildClipboardFormatsOutputString()
{
DataPackageView clipboardContent{ Clipboard::GetContent() };
std::wostringstream output;
if (clipboardContent && clipboardContent.AvailableFormats().Size() > 0)
{
output << L"Available formats in the clipboard:";
for (auto&& format : clipboardContent.AvailableFormats())
{
output << std::endl << L" * " << std::wstring_view(format);
}
}
else
{
output << L"The clipboard is empty";
}
return hstring{ output.str() };
}
Note
Синтаксис в строке кода DataPackageView clipboardContent{ Clipboard::GetContent() }; использует функцию современной стандартной C++ под названием универсальной инициализации, с характерным использованием фигурных квадратных скобок вместо = знака. Этот синтаксис дает понять, что инициализация, а не назначение, происходит. Если вы предпочитаете форму синтаксиса, который выглядит как назначение (но на самом деле нет), можно заменить синтаксис выше эквивалентным DataPackageView clipboardContent = Clipboard::GetContent();. Полезно освоиться с обоими способами записи инициализации, поскольку вы, скорее всего, будете часто встречать оба варианта в коде, с которым вам предстоит работать.
DisplayToast
DisplayToast — это общедоступный статический метод класса MainPage C#, в котором он определен.SampleConfiguration.cs В C++/WinRT мы сделаем его общедоступным статическим методом SampleState.
Мы уже рассмотрели большинство деталей и приёмов, необходимых для переноса этого метода. Следует отметить один новый момент: вы переносите дословный строковый литерал C# (@) в стандартный необработанный строковый литерал C++ raw string literal (LR).
Кроме того, при ссылке на типы ToastNotification и XmlDocument в C++/WinRT их можно квалифицировать по имени пространства имен или изменять SampleConfiguration.cpp и добавлять using namespace директивы, такие как следующий пример.
using namespace Windows::UI::Notifications;
При ссылке на тип XmlDocument и при ссылке на любой другой тип среда выполнения Windows имеется тот же выбор.
Помимо этих элементов, просто выполните те же инструкции, которые вы сделали ранее, чтобы выполнить следующие действия.
- Объявите метод в
SampleConfiguration.h, а затем определите его вSampleConfiguration.cpp. - Измените
pch.h, добавив все необходимые файлы заголовков пространства имён Windows для C++/WinRT. - Создайте объекты C++/WinRT в стеке, а не в куче.
- Замените вызовы методов доступа получения свойства на синтаксис вызова функции (
()).
Очень распространённой причиной ошибок компиляции или компоновки является то, что вы забываете включить необходимые вам заголовочные файлы пространства имён Windows для C++/WinRT. Дополнительные сведения об одной из возможных ошибок см. в статье C3779: Почему компилятор выдает сообщение об ошибке "consume_Something: функцию, возвращающую 'auto', нельзя использовать до ее определения"?
Если вы хотите следовать пошаговому руководству и самостоятельно перенести DisplayToast, то можете сравнить свой результат с кодом версии C++/WinRT в ZIP-архиве исходного кода примера Clipboard sample, который вы скачали.
Включить уведомления об изменении содержимого буфера обмена
EnableClipboardContentChangedNotifications — это общедоступный статический метод класса MainPage C# и определен в SampleConfiguration.cs.
// SampleConfiguration.cs
...
public bool EnableClipboardContentChangedNotifications(bool enable)
{
if (IsClipboardContentChangedEnabled == enable)
{
return false;
}
IsClipboardContentChangedEnabled = enable;
if (enable)
{
Clipboard.ContentChanged += OnClipboardChanged;
Window.Current.Activated += OnWindowActivated;
}
else
{
Clipboard.ContentChanged -= OnClipboardChanged;
Window.Current.Activated -= OnWindowActivated;
}
return true;
}
...
private void OnClipboardChanged(object sender, object e) { ... }
private void OnWindowActivated(object sender, WindowActivatedEventArgs e) { ... }
...
В C++/WinRT мы сделаем его общедоступным статическим методом SampleState.
В C# для регистрации и отмены регистрации делегатов обработки событий используется синтаксис операторов += и -=. В C++/WinRT есть несколько синтаксических параметров для регистрации и отзыва делегата, как описано в разделе "Обработка событий с помощью делегатов в C++/WinRT". Но в общем случае схема такова: вы регистрируете и отменяете регистрацию, вызывая пару функций, названных в соответствии с событием. Чтобы зарегистрироваться, вы передаёте свой делегат регистрирующей функции и в ответ получаете маркер отзыва (winrt::event_token). Чтобы аннулировать, передайте этот токен в функцию аннулирования. В этом случае обработчик статический и, как видно из следующего листинга кода, синтаксис вызова функции прост.
Похожие токены действительно используются, за кулисами, в C#. Но язык делает эти детали неявными. C++/WinRT делает его явным.
Тип объекта отображается в подписях обработчика событий C#. На языке C# объект является псевдонимом для типа system.Object .NET. Эквивалентом в C++/WinRT является winrt::Windows::Foundation::IInspectable. Таким образом, вы увидите IInspectable в обработчиках событий C++/WinRT.
Измените SampleConfiguration.h и SampleConfiguration.cpp так, чтобы они соответствовали приведённым ниже спискам.
// SampleConfiguration.h
...
static bool EnableClipboardContentChangedNotifications(bool enable);
...
private:
...
static event_token clipboardContentChangedToken;
static event_token activatedToken;
static void OnClipboardChanged(Windows::Foundation::IInspectable const& sender, Windows::Foundation::IInspectable const& e);
static void OnWindowActivated(Windows::Foundation::IInspectable const& sender, Microsoft::UI::Xaml::WindowActivatedEventArgs const& e);
...
// SampleConfiguration.cpp
...
using namespace Windows::Foundation;
using namespace Microsoft::UI;
using namespace Microsoft::UI::Xaml;
...
event_token SampleState::clipboardContentChangedToken;
event_token SampleState::activatedToken;
...
bool SampleState::EnableClipboardContentChangedNotifications(bool enable)
{
if (isClipboardContentChangedEnabled == enable)
{
return false;
}
IsClipboardContentChangedEnabled(enable);
if (enable)
{
clipboardContentChangedToken = Clipboard::ContentChanged(OnClipboardChanged);
activatedToken = Window::Current().Activated(OnWindowActivated);
}
else
{
Clipboard::ContentChanged(clipboardContentChangedToken);
Window::Current().Activated(activatedToken);
}
return true;
}
void SampleState::OnClipboardChanged(IInspectable const&, IInspectable const&){}
void SampleState::OnWindowActivated(IInspectable const&, WindowActivatedEventArgs const& e){}
Пока оставьте сами делегаты-обработчики событий (OnClipboardChanged и OnWindowActivated) в виде заглушек. Они уже есть в нашем списке элементов, которые нужно перенести, поэтому мы рассмотрим их в следующих подразделах.
OnNavigatedTo
OnNavigatedTo — это защищенный метод класса MainPage C# и он определен в MainPage.xaml.cs. Здесь он содержится вместе со списком XAML ListBox , на который он ссылается.
<!-- MainPage.xaml -->
...
<ListBox x:Name="ScenarioControl" ... />
...
// MainPage.xaml.cs
protected override void OnNavigatedTo(NavigationEventArgs e)
{
// Populate the scenario list from the SampleConfiguration.cs file
var itemCollection = new List<Scenario>();
int i = 1;
foreach (Scenario s in scenarios)
{
itemCollection.Add(new Scenario { Title = $"{i++}) {s.Title}", ClassType = s.ClassType });
}
ScenarioControl.ItemsSource = itemCollection;
if (Window.Current.Bounds.Width < 640)
{
ScenarioControl.SelectedIndex = -1;
}
else
{
ScenarioControl.SelectedIndex = 0;
}
}
Это важный и интересный метод, поскольку именно здесь наша коллекция объектов Scenario передаётся пользовательскому интерфейсу. Код C# создает список объектов Scenario типа System.Collections.Generic.List и присваивает его свойству ItemsSource элемента управления ListBox (который является элементом управления элементами). В C#мы используем интерполяцию строк для создания заголовка для каждого объекта сценария (обратите внимание на использование специального символа $ ).
В C++/WinRT мы сделаем OnNavigatedTo общедоступным методом MainPage. И мы добавим элемент ListBox в XAML, чтобы сборка была успешной. После перечисления кода мы рассмотрим некоторые сведения.
<!-- MainPage.xaml -->
...
<StackPanel ...>
...
<ListBox x:Name="ScenarioControl" />
</StackPanel>
...
// MainPage.h
...
void OnNavigatedTo(Microsoft::UI::Xaml::Navigation::NavigationEventArgs const& e);
...
// MainPage.cpp
...
using namespace winrt::Microsoft::UI::Xaml;
using namespace winrt::Microsoft::UI::Xaml::Navigation;
...
void MainPage::OnNavigatedTo(NavigationEventArgs const& /* e */)
{
auto itemCollection = winrt::single_threaded_observable_vector<IInspectable>();
int i = 1;
for (auto s : MainPage::scenarios())
{
s.Title = winrt::to_hstring(i++) + L") " + s.Title;
itemCollection.Append(winrt::box_value(s));
}
ScenarioControl().ItemsSource(itemCollection);
if (Window::Current().Bounds().Width < 640)
{
ScenarioControl().SelectedIndex(-1);
}
else
{
ScenarioControl().SelectedIndex(0);
}
}
...
Опять же, мы вызываем функцию winrt::single_threaded_observable_vector , но на этот раз для создания коллекции IInspectable. Это было частью нашего решения выполнять упаковку объектов Scenario непосредственно по мере необходимости.
И вместо используемой здесь в C# интерполяции строк мы применяем сочетание функции to_hstring и оператора конкатенации для winrt::hstring.
isApplicationWindowActive
В C# isApplicationWindowActive — это простое частное bool поле, принадлежащее классу MainPage , и оно определено в SampleConfiguration.cs. По умолчанию имеет значение false. В C++/WinRT мы сделаем его частным статическим полем SampleState (по причинам, которые мы уже описали) в SampleConfiguration.h и SampleConfiguration.cpp файлах с тем же значением по умолчанию.
Мы уже видели, как объявить, определить и инициализировать статическое поле. Чтобы освежить в памяти, вспомните, что мы делали с полем isClipboardContentChangedEnabled, и сделайте то же самое с isApplicationWindowActive.
needToPrintClipboardFormat
Тот же шаблон, что и у isApplicationWindowActive (см. заголовок непосредственно перед этим).
Button_Click
Button_Click — это частный метод (обработка событий) класса MainPage C# и он определен в MainPage.xaml.cs. Здесь он содержится вместе с XAML SplitView , на который он ссылается, и ToggleButton , который регистрирует его.
<!-- MainPage.xaml -->
...
<SplitView x:Name="Splitter" ... />
...
<ToggleButton Click="Button_Click" .../>
...
private void Button_Click(object sender, RoutedEventArgs e)
{
Splitter.IsPaneOpen = !Splitter.IsPaneOpen;
}
И вот эквивалент, перенесенный в C++/WinRT. Обратите внимание, что в версии на C++/WinRT обработчик событий записывается как public (как видите, вы объявляете его перед объявлениями private:). Это связано с тем, что обработчик событий, зарегистрированный в разметке XAML, например этот, должен находиться public в C++/WinRT, чтобы разметка XAML могла получить к ней доступ. С другой стороны, если вы регистрируете обработчик событий в императивном коде (например, в MainPage::EnableClipboardContentChangedNotifications ранее), обработчик событий не должен быть public.
<!-- MainPage.xaml -->
...
<StackPanel ...>
...
<SplitView x:Name="Splitter" />
</StackPanel>
...
// MainPage.h
...
void Button_Click(Windows::Foundation::IInspectable const& sender, Microsoft::UI::Xaml::RoutedEventArgs const& e);
private:
...
// MainPage.cpp
void MainPage::Button_Click(Windows::Foundation::IInspectable const& /* sender */, Microsoft::UI::Xaml::RoutedEventArgs const& /* e */)
{
Splitter().IsPaneOpen(!Splitter().IsPaneOpen());
}
DisplayChangedFormats
В C# DisplayChangedFormats является частным методом, принадлежащим к классу MainPage , и он определен в SampleConfiguration.cs.
private void DisplayChangedFormats()
{
string output = "Clipboard content has changed!" + Environment.NewLine;
output += BuildClipboardFormatsOutputString();
NotifyUser(output, NotifyType.StatusMessage);
}
В C++/WinRT мы сделаем его закрытым статическим полем SampleState (поскольку оно не обращается к каким-либо членам экземпляра) в файлах SampleConfiguration.h и SampleConfiguration.cpp. Код этого метода на C# не использует System.Text.StringBuilder; но в нём выполняется достаточно операций форматирования строк, поэтому в версии C++/WinRT это ещё один удачный случай для использования std::wostringstream.
Вместо статического свойства System.Environment.NewLine , используемого в коде C#, мы вставим стандартный C++ std::endl (новый символ) в выходной поток.
// SampleConfiguration.h
...
private:
static void DisplayChangedFormats();
...
// SampleConfiguration.cpp
void SampleState::DisplayChangedFormats()
{
std::wostringstream output;
output << L"Clipboard content has changed!" << std::endl;
output << BuildClipboardFormatsOutputString().c_str();
MainPage::Current().NotifyUser(output.str(), NotifyType::StatusMessage);
}
Существует небольшая неэффективность в проектировании версии C++/WinRT выше. Сначала мы создадим std::wostringstream. Но мы также вызываем метод BuildClipboardFormatsOutputString (который мы переносили ранее). Этот метод создает собственный std::wostringstream. И он преобразует этот поток в winrt::hstring и возвращает его. Мы вызываем функцию hstring::c_str , чтобы превратить возвращаемую строку hstring обратно в строку стиля C, а затем вставить ее в наш поток. Было бы эффективнее создать всего один std::wostringstream и передавать ссылку на него дальше, чтобы методы могли записывать строки непосредственно в него.
Именно это мы делаем в версии C++/WinRT образца исходного кода Clipboard sample (в ZIP-архиве, который вы скачали). В этом исходном коде есть новый частный статический метод с именем SampleState::AddClipboardFormatsOutputString, который принимает и работает со ссылкой на выходной поток. А затем методы SampleState::DisplayChangedFormats и SampleState::BuildClipboardFormatsOutputString рефакторятся так, чтобы вызывать этот новый метод. Это функционально эквивалентно описаниям кода в этом разделе, но это более эффективно.
Footer_Click
Footer_Click — это асинхронный обработчик событий, принадлежащий классу MainPage C#, и он определен в MainPage.xaml.cs. Приведенный ниже код функционально эквивалентен методу в скачанном исходном коде. Но здесь я развернул это из одной строки в четыре, чтобы было проще увидеть, что он делает, и, следовательно, как нам следует его портировать.
async void Footer_Click(object sender, RoutedEventArgs e)
{
var hyperlinkButton = (HyperlinkButton)sender;
string tagUrl = hyperlinkButton.Tag.ToString();
Uri uri = new Uri(tagUrl);
await Windows.System.Launcher.LaunchUriAsync(uri);
}
Хотя технически метод является асинхронным, после await он ничего не делает, поэтому ему не требуется await (как и ключевое слово async). Он, вероятно, использует их, чтобы избежать сообщения IntelliSense в Visual Studio.
Эквивалентный метод C++/WinRT также будет асинхронным (так как вызывает Launcher.LaunchUriAsync). Но ему не нужно ни co_await, ни возвращать асинхронный объект. Сведения о co_await и асинхронных объектах см. в разделе Параллелизм и асинхронные операции с C++/WinRT.
Теперь давайте поговорим о том, что делает метод. Поскольку это обработчик события Click элемента HyperlinkButton, объект с именем sender на самом деле является объектом HyperlinkButton. Таким образом, преобразование типов безопасно (мы могли бы также выразить это преобразование как sender as HyperlinkButton). Затем мы извлекаем значение свойства Тега (если посмотреть разметку XAML в проекте C#, вы увидите, что это строка, представляющая веб-URL-адрес). Хотя свойство FrameworkElement.Tag (HyperlinkButton — это FrameworkElement) имеет тип object, в C# мы можем преобразовать его в строку с помощью Object.ToString. Из результирующей строки мы создадим объект URI . И, наконец, (с помощью оболочки) мы запускаем браузер и переходим по URL-адресу.
Ниже приведен метод, перенесенный в C++/WinRT (опять же, развернутый для ясности), после чего будет описано описание сведений.
// pch.h
...
#include "winrt/Windows.System.h"
...
// MainPage.h
...
void Footer_Click(Windows::Foundation::IInspectable const& sender, Microsoft::UI::Xaml::RoutedEventArgs const& e);
private:
...
// MainPage.cpp
...
using namespace winrt::Windows::Foundation;
using namespace winrt::Microsoft::UI::Xaml::Controls;
...
void MainPage::Footer_Click(Windows::Foundation::IInspectable const& sender, Microsoft::UI::Xaml::RoutedEventArgs const&)
{
auto hyperlinkButton{ sender.as<HyperlinkButton>() };
hstring tagUrl{ winrt::unbox_value<hstring>(hyperlinkButton.Tag()) };
Uri uri{ tagUrl };
Windows::System::Launcher::LaunchUriAsync(uri);
}
Как всегда, мы создаём обработчик событий public. Мы используем функцию as для объекта sender, чтобы преобразовать его в HyperlinkButton. В C++/WinRT свойство Tag имеет тип IInspectable (аналог Object). Но у IInspectable нет Tostring. Вместо этого нам нужно распаковать объект IInspectable в скалярное значение (в данном случае — строку). Опять же, дополнительные сведения о боксе и распаковки см. в разделе "Бокс" и "Распаковка значений" в IInspectable.
Последние две строки повторяют шаблоны переноса, которые мы видели раньше, и они довольно сильно отражают версию C#.
HandleClipboardChanged
Нет ничего нового в переносе этого метода. Вы можете сравнить версии C# и C++/WinRT в ZIP-архиве с исходным кодом примера Clipboard sample, который вы скачали.
OnClipboardChanged и OnWindowActivated
Пока у нас имеются лишь пустые заглушки для этих двух обработчиков событий. Но перенести их несложно, и это не даёт ничего нового для обсуждения.
ScenarioControl_SelectionChanged
Это еще один обработчик частного события, принадлежащий классу MainPage C# и определенный в MainPage.xaml.cs. В C++/WinRT мы сделаем его общедоступным, и реализуем его в MainPage.h и MainPage.cpp.
Для этого метода нам потребуется MainPage::navigating — закрытое логическое поле, инициализированное значением false. И вам потребуется фрейм с MainPage.xamlименем ScenarioFrame. Но, помимо этих подробностей, перенос этого метода не показывает новых методов.
Если вместо переноса вручную вы копируете код из версии C++/WinRT в ZIP-архиве с исходным кодом примера Clipboard sample, который вы скачали, то увидите, что там используется MainPage::NavigateTo. Пока просто вынесите содержимое NavigateTo в ScenarioControl_SelectionChanged при рефакторинге.
UpdateStatus
Пока у нас есть только заглушка для MainPage.UpdateStatus. Перенос его реализации, опять же, во многом касается уже пройденного материала. Одним из новых моментов является то, что в C# можно сравнить строку со String.Empty, в C++/WinRT мы вместо этого вызываем функцию winrt::hstring::empty . Ещё один момент заключается в том, что nullptr является стандартным эквивалентом null из C# в C++.
Вы можете выполнить оставшуюся часть портирования, используя методы, которые мы уже рассмотрели. Ниже приведен список типов действий, которые необходимо выполнить до компиляции переносной версии этого метода.
- Чтобы
MainPage.xamlдобавить границу с именем StatusBorder. - Чтобы
MainPage.xaml, добавьте элемент TextBlock с именем StatusBlock. - Чтобы
MainPage.xamlдобавить StackPanel с именем StatusPanel. - Чтобы добавить
pch.h, добавьте#include "winrt/Microsoft.UI.Xaml.Media.h". - Чтобы добавить
pch.h, добавьте#include "winrt/Microsoft.UI.Xaml.Automation.Peers.h". - Добавить
MainPage.cppusing namespace winrt::Microsoft::UI::Xaml::Media;. - Добавить
MainPage.cppusing namespace winrt::Microsoft::UI::Xaml::Automation::Peers;.
Скопируйте XAML и стили, необходимые для завершения переноса MainPage
Для XAML идеальный вариант заключается в том, что вы можете использовать одну и ту же разметку XAML в C# и проекте C++/WinRT. И пример с буфером обмена — один из таких случаев.
В файле Styles.xaml примера Clipboard содержится XAML-словарь ресурсов ResourceDictionary со стилями, который применяется к кнопкам, меню и другим элементам пользовательского интерфейса во всём интерфейсе приложения. Страница Styles.xaml объединяется в App.xaml. А затем есть стандартная отправная MainPage.xaml точка для пользовательского интерфейса, которую мы уже видели кратко. Теперь мы можем повторно использовать эти три .xaml файла без изменений в версии проекта C++/WinRT.
Как и в случае с файлами ресурсов, вы можете ссылаться на одни и те же общие файлы XAML из нескольких версий приложения. В этом пошаговом руководстве мы будем копировать файлы в проект C++/WinRT и добавлять их таким образом.
Перейдите в папку \Clipboard_sample\SharedContent\xaml, выберите и скопируйте App.xaml и MainPage.xaml, а затем вставьте эти два файла в папку \Clipboard\Clipboard в проекте C++/WinRT, согласившись на замену файлов при появлении запроса.
В проекте C++/WinRT в Visual Studio нажмите кнопку "Показать все файлы", чтобы включить его. Теперь добавьте новую папку сразу же под узлом проекта и присвойте ей Stylesимя. В проводнике перейдите к папке \Clipboard_sample\SharedContent\xaml , выберите и скопируйте Styles.xamlее и вставьте ее в только что созданную папку \Clipboard\Clipboard\Styles . Вернувшись в Обозреватель решений в проекте C++/WinRT, щелкните правой кнопкой мыши папку Styles, выберите >Добавить>Существующий элемент... и перейдите к \Clipboard\Clipboard\Styles. В средство выбора файлов выберите Styles и нажмите кнопку "Добавить".
Добавьте новую папку в проект C++/WinRT сразу же под узлом проекта и с именем Styles. Перейдите к папке \Clipboard_sample\SharedContent\xaml , выберите и скопируйте Styles.xamlее и вставьте ее в папку \Clipboard\Clipboard\Styles в проекте C++/WinRT. Щелкните правой кнопкой мыши папку Styles (в обозревателе решений в проекте C++/WinRT) >Добавить>Существующий элемент... и перейдите к \Clipboard\Clipboard\Styles. В средство выбора файлов выберите Styles и нажмите кнопку "Добавить".
Нажмите кнопку "Показать все файлы ", чтобы снова отключить его.
Теперь мы закончили перенос MainPage, и если вы выполнили действия, то проект C++/WinRT теперь будет создавать и запускаться.
Объедините свои файлы .idl
Помимо стандартной отправной точки для пользовательского интерфейса MainPage.xaml, пример Clipboard содержит еще пять страниц XAML для отдельных сценариев, а также соответствующие им файлы кода. Мы будем повторно использовать фактическую разметку XAML всех этих страниц, без изменений в версии C++/WinRT проекта. И мы рассмотрим, как перенести code-behind в нескольких следующих крупных разделах. Но перед этим давайте поговорим об IDL.
Существует значение консолидации IDL для классов среды выполнения в один файл IDL. Чтобы узнать об этом значении, см. Вынесение классов среды выполнения в файлы MIDL (.idl). Далее мы консолидируем содержимое CopyFiles.idl, CopyImage.idlCopyText.idlHistoryAndRoaming.idlи OtherScenarios.idl путем перемещения IDL в один файл с именем Project.idl (а затем удаление исходных файлов).
Пока мы делаем это, давайте также удалим автоматически созданное фиктивное свойство (Int32 MyProperty;и его реализацию) из каждого из этих пяти типов страниц XAML.
Сначала добавьте новый элемент Midl File (IDL) в проект C++/WinRT. Назовите его Project.idl. Замените все содержимое Project.idl следующим кодом.
// Project.idl
namespace SDKTemplate
{
[default_interface]
runtimeclass CopyFiles : Microsoft.UI.Xaml.Controls.Page
{
CopyFiles();
}
[default_interface]
runtimeclass CopyImage : Microsoft.UI.Xaml.Controls.Page
{
CopyImage();
}
[default_interface]
runtimeclass CopyText : Microsoft.UI.Xaml.Controls.Page
{
CopyText();
}
[default_interface]
runtimeclass HistoryAndRoaming : Microsoft.UI.Xaml.Controls.Page
{
HistoryAndRoaming();
}
[default_interface]
runtimeclass OtherScenarios : Microsoft.UI.Xaml.Controls.Page
{
OtherScenarios();
}
}
Как видите, это просто копия содержимого отдельных файлов .idl, всё в одном пространстве имён, а MyProperty удалён из каждого класса времени выполнения.
В Обозреватель решений в Visual Studio выделите все исходные IDL-файлы (CopyFiles.idl, CopyImage.idl, CopyText.idl, HistoryAndRoaming.idl и OtherScenarios.idl), а затем выберите команду Правка>Удалить для них (в диалоговом окне выберите Удалить).
Наконец, чтобы завершить удаление MyProperty, в файлах .h и .cpp для каждого из тех же пяти типов страниц XAML удалите объявления и определения функций доступа int32_t MyProperty() и изменения void MyProperty(int32_t).
Кстати, всегда рекомендуется иметь имя файлов XAML в соответствии с именем класса, который они представляют. Например, если у вас есть x:Class="MyNamespace.MyPage" файл разметки XAML, этот файл должен быть назван MyPage.xaml. Хотя это не техническое требование, не нужно переключать разные имена для одного артефакта, это сделает проект более понятным и доступным для обслуживания, а также упрощает работу с ним.
CopyFiles
В проекте C# тип страницы XAML CopyFiles реализован в файлах исходного кода CopyFiles.xaml и CopyFiles.xaml.cs. Давайте рассмотрим каждый из членов CopyFiles в свою очередь.
rootPage
Это частное поле.
// CopyFiles.xaml.cs
...
public sealed partial class CopyFiles : Page
{
MainPage rootPage = MainPage.Current;
...
}
...
В C++/WinRT мы можем определить и инициализировать его следующим образом.
// CopyFiles.h
...
struct CopyFiles : CopyFilesT<CopyFiles>
{
...
private:
SDKTemplate::MainPage rootPage{ MainPage::Current() };
};
...
Опять же (как и с MainPage::current), CopyFiles::rootPage объявляется как тип SDKTemplate::MainPage, который является проецируемым типом, а не типом реализации.
CopyFiles (конструктор)
В проекте C++/WinRT тип CopyFiles уже имеет конструктор, содержащий нужный код (он просто вызывает InitializeComponent).
CopyButton_Click
Метод C# CopyButton_Click — это обработчик событий, и по ключевому слову async в его сигнатуре можно понять, что метод выполняет асинхронные операции. В C++/WinRT мы реализуем асинхронный метод как корутину. Сведения о параллельном выполнении в C++/WinRT, а также описание того, что такое сопрограмма, см. в разделе Параллельное выполнение и асинхронные операции с C++/WinRT.
Обычно требуется запланировать дальнейшую работу после завершения корутины, и в таких случаях корутин возвратит некоторый асинхронный тип объекта, который можно ожидать, и что при необходимости сообщает о ходе выполнения. Но эти рекомендации обычно не применяются к обработчику событий. Таким образом, если у вас есть обработчик событий, выполняющий асинхронные операции, вы можете реализовать его в виде корутины, возвращающей winrt::fire_and_forget. Дополнительные сведения см. в разделе Fire and forget.
Хотя смысл корутины типа fire-and-forget в том, что вам не важно, когда она завершится, работа всё равно продолжается в фоновом режиме (или остаётся приостановленной в ожидании возобновления). Из реализации на C# видно, что CopyButton_Click зависит от указателя this (он обращается к члену данных экземпляра rootPage). Поэтому необходимо убедиться, что указатель this (указатель на объект типа CopyFiles) существует дольше, чем сопрограмма CopyButton_Click. В такой ситуации, как это пример приложения, где пользователь перемещается между страницами пользовательского интерфейса, мы не можем напрямую управлять временем существования этих страниц. Если страница CopyFiles будет уничтожена (при переходе с неё), пока CopyButton_Click всё ещё выполняется в фоновом потоке, доступ к rootPage будет небезопасен. Чтобы корутина работала корректно, она должна получить строгую ссылку на указатель this и сохранять эту ссылку на всё время выполнения корутины. Дополнительные сведения см. в разделе "Сильные и слабые ссылки" в C++/WinRT.
Если вы посмотрите в версии примера на C++/WinRT, то в методе CopyFiles::CopyButton_Click увидите, что это делается с помощью простого объявления локальной переменной в стеке.
fire_and_forget CopyFiles::CopyButton_Click(IInspectable const&, RoutedEventArgs const&)
{
auto lifetime{ get_strong() };
...
}
Давайте рассмотрим другие аспекты перенесенного кода, которые следует отметить.
В коде мы создадим экземпляр объекта FileOpenPicker , а две строки позже мы имеем доступ к свойству FileTypeFilter этого объекта. Тип, возвращаемый этим свойством, реализует IVector строк. И для этого IVector мы вызываем метод IVector<T>.ReplaceAll(T[]). Интересным аспектом является значение, которое мы передаваем в этот метод, где ожидается массив. Ниже приведена строка кода.
filePicker.FileTypeFilter().ReplaceAll({ L"*" });
Значение, которое мы передаваем ({ L"*" }) является стандартным списком инициализаторов C++. Он содержит один объект, в данном случае, но список инициализатора может содержать любое количество разделенных запятыми объектов. Компоненты C++/WinRT, которые позволяют передавать список инициализации в такой метод, описаны в разделе Стандартные списки инициализации.
Мы переносим ключевое слово await из C# в co_await в C++/WinRT. Ниже приведен пример кода.
auto storageItems{ co_await filePicker.PickMultipleFilesAsync() };
Далее рассмотрим эту строку кода C#.
dataPackage.SetStorageItems(storageItems);
C# может неявно преобразовать IReadOnlyList<StorageFile>, представленный переменной storageItems, в IEnumerable<IStorageItem>, который ожидает DataPackage.SetStorageItems. Но в C++/WinRT необходимо явно преобразовать IVectorView<StorageFile> в IIterable<IStorageItem>. Итак, у нас есть ещё один пример работы функции as.
dataPackage.SetStorageItems(storageItems.as<IVectorView<IStorageItem>>());
Где мы используем ключевое null слово в C# (например, Clipboard.SetContentWithOptions(dataPackage, null)), мы используем nullptr в C++/WinRT (например, Clipboard::SetContentWithOptions(dataPackage, nullptr)).
PasteButton_Click
Это ещё один обработчик событий в форме корутины fire-and-forget. Давайте рассмотрим аспекты перенесенного кода, которые следует отметить.
В версии примера на C# мы перехватываем исключения с помощью catch (Exception ex). В перенесенном коде C++/WinRT вы увидите выражение catch (winrt::hresult_error const& ex). Дополнительные сведения о winrt::hresult_error и о работе с ним см. в статье об обработке ошибок с помощью C++/WinRT.
Пример проверки того, является ли объект C# null или нет, приведён в if (storageItems != null). В C++/WinRT можно полагаться на оператор преобразования в bool, который выполняет внутреннюю проверку на nullptr.
Ниже приведена немного упрощенная версия фрагмента кода из перенесенной версии C++/WinRT примера.
std::wostringstream output;
output << std::wstring_view(ApplicationData::Current().LocalFolder().Path());
Создание std::wstring_view из winrt::hstring таким образом демонстрирует альтернативу вызову функции hstring::c_str (чтобы преобразовать winrt::hstring в C-строку). Эта альтернатива работает благодаря оператору преобразования hstringв std::wstring_view.
Рассмотрим этот фрагмент C#.
var file = storageItem as StorageFile;
if (file != null)
...
Чтобы перенести ключевое слово C# as в C++/WinRT, мы уже несколько раз использовали функцию as. Эта функция вызывает исключение, если преобразование типов завершается ошибкой. Но если мы хотим, чтобы при сбое преобразование возвращало nullptr (чтобы мы могли обработать это условие в коде), то вместо этого используем функцию try_as.
auto file{ storageItem.try_as<StorageFile>() };
if (file)
...
Скопируйте КОД XAML, необходимый для завершения переноса CopyFiles
Теперь можно выбрать все содержимое CopyFiles.xaml файла из shared папки скачивания исходного исходного кода и вставить его CopyFiles.xaml в файл в проекте C++/WinRT (заменив существующее содержимое этого файла в проекте C++/WinRT).
Наконец, измените CopyFiles.h и .cpp, а затем удалите фиктивную функцию ClickHandler, поскольку мы только что перезаписали соответствующую разметку XAML.
Теперь мы завершили перенос CopyFiles, и если вы следовали всем шагам, то ваш проект C++/WinRT теперь будет собираться и запускаться, а сценарий CopyFiles будет работать.
CopyImage
Чтобы перенести тип страницы CopyImage XAML, выполните тот же процесс, что и для CopyFiles. При переносе CopyImage вы столкнетесь с использованием инструкции C#, которая гарантирует правильность удаления объектов, реализующих интерфейс IDisposable .
if (imageReceived != null)
{
using (var imageStream = await imageReceived.OpenReadAsync())
{
... // Pass imageStream to other APIs, and do other work.
}
}
Эквивалентный интерфейс в C++/WinRT — IClosable с одним методом Close . Вот эквивалент кода C# выше на C++/WinRT.
if (imageReceived)
{
auto imageStream{ co_await imageReceived.OpenReadAsync() };
... // Pass imageStream to other APIs, and do other work.
imageStream.Close();
}
Объекты C++/WinRT реализуют IClosable в первую очередь для преимуществ языков, которые не имеют детерминированной финализации. C++/WinRT имеет детерминированную финализацию, поэтому часто не нужно вызывать IClosable::Close при написании C++/WinRT. Но бывают моменты, когда лучше остановиться, и это как раз один из таких случаев. Здесь идентификатор imageStream представляет собой оболочку с подсчётом ссылок для базового объекта среда выполнения Windows (в данном случае — объекта, реализующего интерфейс IRandomAccessStreamWithContentType). Хотя мы можем определить, что финализатор imageStream (его деструктор) будет вызван в конце внешней области видимости (в фигурных скобках), мы не можем быть уверены, что этот финализатор вызовет Close. Это связано с тем, что мы передали imageStream другим API, и они по-прежнему могут способствовать подсчету ссылок базового объекта среда выполнения Windows. Поэтому это случай, когда рекомендуется явно вызывать Close . Дополнительные сведения см. в статье " Нужно ли вызывать IClosable::Close on runtime classes, которые я использую?".
Затем рассмотрим выражение (uint)(imageDecoder.OrientedPixelWidth * 0.5)C#, которое вы найдете в обработчике событий OnDeferredImageRequestedHandler . Это выражение умножает uint на double, в результате получается double. Затем это приводится к типу uint. В C++/WinRT мы могли бы использовать аналогичное приведение в стиле C ((uint32_t)(imageDecoder.OrientedPixelWidth() * 0.5)), но предпочтительнее явно указать, какое именно приведение мы имеем в виду, и в данном случае для этого мы бы использовали static_cast<uint32_t>(imageDecoder.OrientedPixelWidth() * 0.5).
В версии C# метода CopyImage.OnDeferredImageRequestedHandler есть предложение finally, но нет предложения catch. Мы пошли немного дальше в варианте C++/WinRT и добавили конструкцию catch, чтобы можно было сообщать, была ли отложенная отрисовка успешной.
Перенос остальной части этой страницы XAML не дает ничего нового для обсуждения. Не забудьте удалить фиктивную функцию ClickHandler . И, как и в copyFiles, последний шаг в порту — выбрать все содержимое CopyImage.xamlи вставить его в один и тот же файл в проекте C++/WinRT.
CopyText
Вы можете перенести CopyText.xaml и CopyText.xaml.cs, используя методы, которые мы уже рассмотрели.
HistoryAndRoaming
При переносе типа XAML-страницы HistoryAndRoaming возникает ряд важных моментов.
Сначала просмотрите исходный код C# и проследите поток управления от OnNavigatedTo через обработчик события OnHistoryEnabledChanged и, наконец, к асинхронной функции CheckHistoryAndRoaming (результат которой не ожидается, то есть она фактически запускается без ожидания завершения). Поскольку CheckHistoryAndRoaming выполняется асинхронно, в C++/WinRT нужно внимательно следить за временем жизни указателя this. Результат можно увидеть при просмотре реализации в файле исходного HistoryAndRoaming.cpp кода. Во-первых, когда мы подключаем делегаты к событиям Clipboard::HistoryEnabledChanged и Clipboard::RoamingEnabledChanged, мы сохраняем только слабую ссылку на объект страницы HistoryAndRoaming. Мы делаем это, создавая делегат с зависимостью от значения, которое возвращает winrt::get_weak, а не с зависимостью от указателя this. Это означает, что сам делегат, который в конечном итоге вызывает асинхронный код, не сохраняет страницу HistoryAndRoaming в живых, если мы отойдем от нее.
И во-вторых, когда мы наконец доходим до нашей корутины CheckHistoryAndRoaming типа fire-and-forget, первое, что мы делаем, — это берем сильную ссылку на this, чтобы гарантировать, что страница HistoryAndRoaming будет существовать по крайней мере до тех пор, пока корутина окончательно не завершится. Дополнительные сведения об обоих аспектах, которые только что описаны, см. в разделе "Сильные и слабые ссылки" в C++/WinRT.
Мы находим еще одну точку интереса при переносе CheckHistoryAndRoaming. Он содержит код для обновления пользовательского интерфейса; Поэтому мы должны быть уверены, что мы делаем это в основном потоке пользовательского интерфейса. Поток, который изначально вызывает обработчик событий, является основным потоком пользовательского интерфейса. Но обычно асинхронный метод может выполняться и /или возобновлять работу в любом произвольном потоке. В C#решение предназначено для отправки работы в поток пользовательского интерфейса. В C++/WinRT мы можем использовать функцию winrt::resume_foreground вместе со свойством DispatcherQueue указателя this, чтобы приостановить выполнение корутины и немедленно возобновить его в основном потоке пользовательского интерфейса.
Соответствующее выражение — co_await winrt::resume_foreground(DispatcherQueue());. Более короткий вариант достигается благодаря оператору преобразования, который предоставляет C++/WinRT.
Перенос остальной части этой страницы XAML не дает ничего нового для обсуждения. Не забудьте удалить фиктивную функцию ClickHandler и скопировать разметку XAML.
OtherScenarios
Вы можете перенести OtherScenarios.xaml и OtherScenarios.xaml.cs, используя методы, которые мы уже рассмотрели.
Conclusion
Надеюсь, это пошаговое руководство дало вам достаточно сведений и приемов для переноса, и теперь вы можете приступить к переносу собственных приложений C# на C++/WinRT. Чтобы освежить в памяти материал, вы по-прежнему можете обращаться к версиям исходного кода до (C#) и после (C++/WinRT) в примере Clipboard и сравнивать их рядом, чтобы увидеть соответствие между ними.
Связанные темы
Windows developer