Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
В этом разделе описано, как реализовывать API C++/WinRT с помощью базовой структуры winrt::implements как напрямую, так и косвенно. Синонимами author в этом контексте являются produce или implement. В этом разделе рассматриваются следующие сценарии реализации API-интерфейсов в типе C++/WinRT в этом порядке.
Note
В этом разделе затрагивается тема компонентов среда выполнения Windows, но только в контексте C++/WinRT. Если вы ищете содержимое о компонентах среда выполнения Windows, охватывающих все языки среда выполнения Windows, см. среда выполнения Windows компоненты.
- Вы не создаете класс среда выполнения Windows (класс среды выполнения); вы просто хотите реализовать один или несколько интерфейсов среда выполнения Windows для локального потребления в приложении. В этом случае вы наследуетесь напрямую от winrt::implements и реализуете функции.
- Вы создаете класс среды выполнения. Возможно, вы разрабатываете компонент, который будет использоваться в приложении. Или вы можете создавать тип, который будет использоваться из пользовательского интерфейса XAML, и в этом случае вы реализуете и используете класс среды выполнения в одном модуле компиляции. В таких случаях вы позволяете средствам генерировать за вас классы, производные от winrt::implements.
В обоих случаях тип, реализующий API C++/WinRT, называется типом реализации.
Это важно
Важно различать концепцию типа реализации от проецируемого типа. Проецируемый тип описан в разделе "Использование API с помощью C++/WinRT".
Если вы не создаете класс среды выполнения
Самый простой сценарий — когда ваш тип реализует интерфейс среда выполнения Windows, и вы будете использовать этот тип в рамках того же приложения. В этом случае тип не должен быть классом среды выполнения; просто обычный класс C++. Например, вы можете разрабатывать настольное приложение WinUI 3 на базе Microsoft::UI::Xaml::Application.
Если тип ссылается на пользовательский интерфейс XAML , то он должен быть классом среды выполнения, даже если он находится в том же проекте , что и XAML. В этом случае см. раздел " Если вы создаете класс среды выполнения, на который следует ссылаться в пользовательском интерфейсе XAML".
Note
Сведения об установке и использовании расширения C++/WinRT Visual Studio (VSIX) и пакета NuGet (которые вместе предоставляют поддержку шаблона проекта и сборки), см. Visual Studio поддержку C++/WinRT.
В Visual Studio шаблон проекта "Пустое приложение" (WinUI 3 в классическом режиме) для C++ иллюстрирует шаблон приложения WinUI 3. Класс App является производным от Microsoft::UI::Xaml::Application, а точка входа вызывает метод Start.
#include "App.xaml.h"
int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
winrt::init_apartment();
::winrt::Microsoft::UI::Xaml::Application::Start(
[](auto&&) { ::winrt::make<App>(); });
}
Класс App создает Microsoft::UI::Xaml::Window и активирует его в OnLaunched.
// App.xaml.h
struct App : AppT<App>
{
App();
void OnLaunched(Microsoft::UI::Xaml::LaunchActivatedEventArgs const&);
private:
winrt::Microsoft::UI::Xaml::Window window{ nullptr };
};
// App.xaml.cpp
void App::OnLaunched(LaunchActivatedEventArgs const&)
{
window = make<MainWindow>();
window.Activate();
}
C++/WinRT имеет базовый шаблон структуры winrt::реализует , чтобы упростить реализацию интерфейса (или нескольких) без использования программирования в стиле COM. Вы просто производите свой тип от implements, а затем реализуете функции интерфейса. Ниже приведен пример, реализующий пользовательский интерфейс.
struct MyType : implements<MyType, IStringable>
{
hstring ToString()
{
return L"MyType";
}
};
Если вы создаете класс среды выполнения в компоненте среда выполнения Windows
Если тип упаковается в компонент среда выполнения Windows для использования из другого двоичного файла (другой двоичный файл обычно является приложением), то тип должен быть классом среды выполнения. Класс среды выполнения объявляется в файле языка определения интерфейсов Microsoft (IDL) (.idl) (см. Разбиение классов среды выполнения на файлы Midl (.idl)).
Для каждого IDL-файла создаётся файл .winmd, а Visual Studio объединяет их все в один файл с тем же именем, что и ваше корневое пространство имён. Этот окончательный .winmd файл будет тем, на который будут ссылаться потребители компонента.
Ниже приведен пример объявления класса среды выполнения в файле IDL.
// MyRuntimeClass.idl
namespace MyProject
{
runtimeclass MyRuntimeClass
{
// Declaring a constructor (or constructors) in the IDL causes the runtime class to be
// activatable from outside the compilation unit.
MyRuntimeClass();
String Name;
}
}
Этот IDL объявляет класс среды выполнения среда выполнения Windows. Класс среды выполнения — это тип, который можно активировать и использовать с помощью современных интерфейсов COM, как правило, в пределах исполняемого файла. При добавлении IDL-файла в проект и сборке цепочка инструментов C++/WinRT (midl.exe и cppwinrt.exe) создает для вас тип реализации. Пример работы с файлом IDL см. в разделе Элементы управления XAML; привязка к свойству в C++/WinRT.
Используя приведенный выше пример IDL, тип реализации — это заглушка структуры C++ с именем winrt::MyProject::implementation::MyRuntimeClass в файлах исходного кода с именем \MyProject\MyProject\Generated Files\sources\MyRuntimeClass.h и MyRuntimeClass.cpp.
Тип реализации выглядит следующим образом.
// MyRuntimeClass.h
...
namespace winrt::MyProject::implementation
{
struct MyRuntimeClass : MyRuntimeClassT<MyRuntimeClass>
{
MyRuntimeClass() = default;
winrt::hstring Name();
void Name(winrt::hstring const& value);
};
}
// winrt::MyProject::factory_implementation::MyRuntimeClass is here, too.
Обратите внимание на используемый шаблон F-ограниченного полиморфизма (MyRuntimeClass использует сам себя в качестве аргумента шаблона для своего базового класса MyRuntimeClassT). Это также называется паттерном курьёзно повторяющегося шаблона (CRTP). Если вы следуете за цепочкой наследования вверх, вы увидите MyRuntimeClass_base.
Вы можете упростить реализацию простых свойств с помощью библиотек реализации Windows (WIL). Это делается следующим образом:
// MyRuntimeClass.h
...
namespace winrt::MyProject::implementation
{
struct MyRuntimeClass : MyRuntimeClassT<MyRuntimeClass>
{
MyRuntimeClass() = default;
wil::single_threaded_rw_property<winrt::hstring> Name;
};
}
См. простые свойства.
template <typename D, typename... I>
struct MyRuntimeClass_base : implements<D, MyProject::IMyRuntimeClass, I...>
Таким образом, в этом сценарии в основе иерархии наследования снова находится базовый шаблон структуры winrt::implements.
Дополнительные сведения, код и пошаговое руководство по созданию API в компоненте среда выполнения Windows см. в разделах среда выполнения Windows components with C++/WinRT и Author events in C++/WinRT.
Если вы создаете класс среды выполнения, на который можно ссылаться в пользовательском интерфейсе XAML
Если тип ссылается на пользовательский интерфейс XAML, он должен быть классом среды выполнения, даже если он находится в том же проекте, что и XAML. Хотя они обычно активируются в пределах исполняемого файла, класс среды выполнения можно использовать в модуле компиляции, реализующей его.
В этом сценарии вы создаете и используете API. Процедура реализации класса среды выполнения по сути аналогична процедуре реализации среда выполнения Windows компонента. Итак, см. предыдущий раздел. Если вы создаете класс среды выполнения в компоненте среда выполнения Windows. Единственное отличие состоит в том, что на основе IDL цепочка инструментов C++/WinRT создаёт не только тип реализации, но и проекционный тип. Важно понимать, что если в этом случае указать только "MyRuntimeClass", это может вызвать неоднозначность; существует несколько сущностей с таким именем, причём разного рода.
- MyRuntimeClass — это имя класса среды выполнения. Но это действительно абстракция: объявлена в IDL и реализована на некоторых языках программирования.
-
MyRuntimeClass — это имя структуры C++/WinRT winrt::MyProject::implementation::MyRuntimeClass, которая представляет собой реализацию класса времени выполнения на C++/WinRT. Как мы видели, если существуют отдельные проекты реализации и использования, эта структура существует только в реализуемом проекте. Это тип реализации или реализация. Этот тип генерируется (с помощью средства
cppwinrt.exe) в файлах\MyProject\MyProject\Generated Files\sources\MyRuntimeClass.hиMyRuntimeClass.cpp. -
MyRuntimeClass — это имя спроецированного типа в форме структуры C++ winrt::MyProject::MyRuntimeClass. Если существуют отдельные проекты реализации и использования, эта структура существует только в используемом проекте. Это проецируемый тип или проекция. Этот тип создается (по
cppwinrt.exe) в файле\MyProject\MyProject\Generated Files\winrt\impl\MyProject.2.h.
Ниже приведены части проецируемого типа, относящиеся к этому разделу.
// MyProject.2.h
...
namespace winrt::MyProject
{
struct MyRuntimeClass : MyProject::IMyRuntimeClass
{
MyRuntimeClass(std::nullptr_t) noexcept {}
MyRuntimeClass();
};
}
Пошаговый пример реализации интерфейса INotifyPropertyChanged в классе среды выполнения см. в статье Элементы управления XAML; привязка к свойству C++/WinRT.
Процедура использования класса среды выполнения в этом сценарии описана в разделе "Использование API с помощью C++/WinRT".
Факторирование классов среды выполнения в midl-файлах (IDL)
Шаблоны проектов и элементов Visual Studio создают отдельный IDL-файл для каждого класса среды выполнения. Это дает логическую связь между IDL-файлом и созданными файлами исходного кода.
Однако если объединить все классы среды выполнения проекта в один файл IDL, это может значительно улучшить время сборки. Если иначе между ними возникали бы сложные (или циклические) import взаимозависимости, то их консолидация действительно может оказаться необходимой. И вам может быть проще создавать и проверять классы времени выполнения, если они находятся в одном месте.
Конструкторы классов среды выполнения
Вот несколько выводов, которые можно сделать на основе списков, приведённых выше.
- Каждый конструктор, объявляемый вами в вашем IDL, приводит к созданию конструктора как в типе реализации, так и в проецируемом типе. Конструкторы, объявленные IDL, используются для использования класса среды выполнения из другой единицы компиляции.
- Независимо от того, есть ли у вас конструкторы, объявленные в IDL, или нет, для вашего проецируемого типа создается перегрузка конструктора, принимающая std::nullptr_t. Вызов конструктора std::nullptr_t является первым из двух шагов при использовании класса среды выполнения из одной единицы компиляции. Дополнительные сведения и пример кода см. в разделе "Использование API" с помощью C++/WinRT.
- Если вы используете класс среды выполнения из того же модуля компиляции, то можете также реализовать конструкторы, отличные от конструктора по умолчанию, непосредственно в типе реализации (который, напомним, находится в
MyRuntimeClass.h).
Note
Если вы ожидаете, что класс среды выполнения будет использоваться из другого блока компиляции (который является общим), включите конструкторы в IDL (по крайней мере конструктор по умолчанию). Таким образом вы также получите фабричную реализацию наряду с вашим типом реализации.
Если вы хотите создать и использовать класс среды выполнения только в одном модуле компиляции, не объявляйте конструкторы в IDL. Вам не потребуется реализация фабрики, и она не будет создаваться. Конструктор типа реализации по умолчанию будет удален, но его можно легко изменить и по умолчанию.
Если вы хотите создать и использовать класс среды выполнения только в пределах одной единицы компиляции, и вам нужны параметры конструктора, создайте конструкторы, необходимые непосредственно в типе реализации.
Методы класса среды выполнения, свойства и события
Мы видели, что рабочий процесс состоит в том, чтобы с помощью IDL объявить класс времени выполнения и его члены, а затем инструментарий генерирует для вас прототипы и реализации-заглушки. Что касается этих автоматически созданных прототипов для членов вашего класса среды выполнения, их можно изменить так, чтобы они передавали типы, отличные от тех, которые вы объявили в IDL. Но это можно сделать только до тех пор, пока тип, объявленный в IDL, можно перенаправить в тип, объявленный в реализованной версии.
Ниже приведены некоторые примеры.
- Вы можете ослабить ограничения на типы параметров. Например, если в IDL метод принимает SomeClass, можно изменить его на IInspectable в реализации. Это работает, так как любой класс SomeClass можно перенаправить в IInspectable (обратный, конечно, не будет работать).
- Можно принять скопированный параметр по значению, а не по ссылке. Например, измените
SomeClass const&наSomeClass. Это необходимо, если необходимо избежать записи ссылки в корутин (см. раздел " Передача параметров"). - Вы можете ослабить требования к возвращаемому значению. Например, можно изменить void на winrt::fire_and_forget.
Последние два очень полезны при написании асинхронного обработчика событий.
Создание экземпляров и возврат типов реализаций и интерфейсов
В этом разделе мы рассмотрим в качестве примера тип реализации MyType, реализующий интерфейсы IStringable и IClosable .
Вы можете наследить MyType непосредственно из winrt::implements (это не класс среды выполнения).
#include <winrt/Windows.Foundation.h>
using namespace winrt;
using namespace Windows::Foundation;
struct MyType : implements<MyType, IStringable, IClosable>
{
winrt::hstring ToString(){ ... }
void Close(){}
};
Или вы можете создать его из IDL (это класс среды выполнения).
// MyType.idl
namespace MyProject
{
runtimeclass MyType: Windows.Foundation.IStringable, Windows.Foundation.IClosable
{
MyType();
}
}
Вы не можете напрямую выделить тип реализации.
MyType myimpl; // error C2259: 'MyType': cannot instantiate abstract class
Но вы можете перейти из MyType в объект IStringable или IClosable , который можно использовать или возвращать в рамках проекции, вызвав шаблон функции winrt::make . make возвращает интерфейс по умолчанию для типа реализации.
IStringable istringable = winrt::make<MyType>();
Note
Однако если вы ссылаетесь на тип из пользовательского интерфейса XAML, то в одном проекте будет как тип реализации, так и проецируемый тип. В этом случае make возвращает экземпляр спроецированного типа. Пример кода для этого сценария см. в Элементы управления XAML; привязка к свойству C++/WinRT.
Мы можем использовать istringable (в приведенном выше примере кода) только для вызова членов интерфейса IStringable . Но интерфейс C++/WinRT (который является проецируемым интерфейсом) является производным от winrt::Windows::Foundation::IUnknown. Таким образом, можно вызвать IUnknown::as (или IUnknown::try_as), чтобы запросить другие проецируемые типы или интерфейсы, которые также можно использовать или возвращать.
Подсказка
Сценарий, в котором не следует вызывать as или try_as, — это наследование классов во время выполнения ("компонуемые классы"). Если тип реализации включает в себя другой класс, не вызывайте as или try_as для выполнения непроверенного или проверенного вызова QueryInterface для компонуемого класса. Вместо этого обратитесь к члену данных (this->) m_inner и вызовите для него as или try_as. Дополнительные сведения см. в разделе Наследование классов среды выполнения в этом разделе.
istringable.ToString();
IClosable iclosable = istringable.as<IClosable>();
iclosable.Close();
Если вам нужно получить доступ ко всем членам реализации, а затем вернуть интерфейс вызывающему объекту, используйте шаблон функции winrt::make_self . make_self возвращает объект winrt::com_ptr, оборачивающий тип реализации. Вы можете получить доступ к членам всех реализуемых им интерфейсов (с помощью оператора стрелки), вернуть его вызывающему как есть или вызвать для него as и вернуть вызывающему полученный объект интерфейса.
winrt::com_ptr<MyType> myimpl = winrt::make_self<MyType>();
myimpl->ToString();
myimpl->Close();
IClosable iclosable = myimpl.as<IClosable>();
iclosable.Close();
Класс MyType не является частью проекции; это реализация. Но таким образом можно напрямую вызывать методы реализации без дополнительных затрат на вызов виртуальной функции. В приведенном выше примере, даже если MyType::ToString использует ту же сигнатуру, что и проецируемый метод в IStringable, мы вызываем не виртуальный метод напрямую, не пересекая двоичный интерфейс приложения (ABI).
Com_ptr просто содержит указатель на структуру MyType, поэтому вы также можете получить доступ к любым другим внутренним сведениям MyType с помощью myimpl переменной и оператора стрелки.
В случае, когда у вас есть объект интерфейса, и вы знаете, что это интерфейс в реализации, вы можете вернуться к реализации с помощью шаблона функции winrt::get_self . И снова, это приём, который позволяет избежать вызовов виртуальных функций и получать прямой доступ к реализации.
Note
Если вы не установили пакет SDK для Windows версии 10.0.17763.0 (Windows 10 версии 1809) или более поздней версии, необходимо вызвать winrt::from_abi вместо winrt::get_self.
Приведем пример. Существует еще один пример реализации пользовательского класса управления BgLabelControl.
void ImplFromIClosable(IClosable const& from)
{
MyType* myimpl = winrt::get_self<MyType>(from);
myimpl->ToString();
myimpl->Close();
}
Но только исходный объект интерфейса содержит ссылку. Если вы хотите сохранить его у себя, то можете вызвать com_ptr::copy_from.
winrt::com_ptr<MyType> impl;
impl.copy_from(winrt::get_self<MyType>(from));
// com_ptr::copy_from ensures that AddRef is called.
Сам тип реализации не наследуется от winrt::Windows::Foundation::IUnknown, поэтому у него нет функции as. Тем не менее, как видно из показанной выше функции ImplFromIClosable, вы можете получить доступ к членам всех его интерфейсов. Но если вы так поступаете, не возвращайте вызывающей стороне необработанный экземпляр типа реализации. Вместо этого используйте один из уже описанных приёмов, и возвращайте проецируемый интерфейс или com_ptr.
Если у вас есть экземпляр типа реализации, и его необходимо передать в функцию, которая ожидает соответствующий проецируемый тип, то это можно сделать, как показано в приведенном ниже примере кода. В вашем типе реализации предусмотрен оператор преобразования (при условии, что тип реализации был сгенерирован с помощью средства cppwinrt.exe), который делает это возможным. Значение типа реализации можно передать непосредственно методу, который ожидает значение соответствующего проецируемого типа. Из функции-члена типа реализации можно передать *this методу, который ожидает значение соответствующего проецируемого типа.
// MyClass.idl
import "MyOtherClass.idl";
namespace MyProject
{
runtimeclass MyClass
{
MyClass();
void MemberFunction(MyOtherClass oc);
}
}
// MyClass.h
...
namespace winrt::MyProject::implementation
{
struct MyClass : MyClassT<MyClass>
{
MyClass() = default;
void MemberFunction(MyProject::MyOtherClass const& oc) { oc.DoWork(*this); }
};
}
...
// MyOtherClass.idl
import "MyClass.idl";
namespace MyProject
{
runtimeclass MyOtherClass
{
MyOtherClass();
void DoWork(MyClass c);
}
}
// MyOtherClass.h
...
namespace winrt::MyProject::implementation
{
struct MyOtherClass : MyOtherClassT<MyOtherClass>
{
MyOtherClass() = default;
void DoWork(MyProject::MyClass const& c){ /* ... */ }
};
}
...
//main.cpp
#include "pch.h"
#include <winrt/base.h>
#include "MyClass.h"
#include "MyOtherClass.h"
using namespace winrt;
// MyProject::MyClass is the projected type; the implementation type would be MyProject::implementation::MyClass.
void FreeFunction(MyProject::MyOtherClass const& oc)
{
auto defaultInterface = winrt::make<MyProject::implementation::MyClass>();
MyProject::implementation::MyClass* myimpl = winrt::get_self<MyProject::implementation::MyClass>(defaultInterface);
oc.DoWork(*myimpl);
}
...
Производный класс среды выполнения
Можно создать класс среды выполнения, производный от другого класса среды выполнения, при условии, что базовый класс объявлен как "неуправляемый". Термин среда выполнения Windows для производных классов — это "составные классы". Код для реализации производного класса зависит от того, предоставляется ли базовый класс другим компонентом или тем же компонентом. К счастью, вам не нужно изучать эти правила. Вы можете просто скопировать примеры реализаций из выходной sources папки, созданной компилятором cppwinrt.exe .
Рассмотрим этот пример.
// MyProject.idl
namespace MyProject
{
[default_interface]
runtimeclass MyButton : Microsoft.UI.Xaml.Controls.Button
{
MyButton();
}
unsealed runtimeclass MyBase
{
MyBase();
overridable Int32 MethodOverride();
}
[default_interface]
runtimeclass MyDerived : MyBase
{
MyDerived();
}
}
В приведенном выше примере MyButton является производным от элемента управления XAML Button , предоставляемого другим компонентом. В этом случае реализация выглядит так же, как реализация не компонуемого класса:
namespace winrt::MyProject::implementation
{
struct MyButton : MyButtonT<MyButton>
{
};
}
namespace winrt::MyProject::factory_implementation
{
struct MyButton : MyButtonT<MyButton, implementation::MyButton>
{
};
}
С другой стороны, в приведенном выше примере MyDerived является производным от другого класса в том же компоненте. В этом случае для реализации требуется дополнительный параметр шаблона, указывающий класс реализации базового класса.
namespace winrt::MyProject::implementation
{
struct MyDerived : MyDerivedT<MyDerived, implementation::MyBase>
{ // ^^^^^^^^^^^^^^^^^^^^^^
};
}
namespace winrt::MyProject::factory_implementation
{
struct MyDerived : MyDerivedT<MyDerived, implementation::MyDerived>
{
};
}
В любом случае ваша реализация может вызвать метод базового класса, указав перед ним псевдоним типа base_type:
namespace winrt::MyProject::implementation
{
struct MyButton : MyButtonT<MyButton>
{
void OnApplyTemplate()
{
// Call base class method
base_type::OnApplyTemplate();
// Do more work after the base class method is done
DoAdditionalWork();
}
};
struct MyDerived : MyDerivedT<MyDerived, implementation::MyBase>
{
int MethodOverride()
{
// Return double what the base class returns
return 2 * base_type::MethodOverride();
}
};
}
Подсказка
Если тип реализации включает в себя другой класс, не вызывайте as или try_as для выполнения непроверенного или проверенного вызова QueryInterface для компонуемого класса. Вместо этого обратитесь к члену данных (this->) m_inner и вызовите для него as или try_as.
Производный от типа с конструктором, не используемым по умолчанию
ToggleButtonAutomationPeer::ToggleButtonAutomationPeer(ToggleButton) является примером конструктора, отличного от конструктора по умолчанию. Конструктор по умолчанию отсутствует, поэтому, чтобы создать экземпляр ToggleButtonAutomationPeer, необходимо передать owner. Следовательно, если вы наследуете от ToggleButtonAutomationPeer, необходимо предоставить конструктор, который принимает владельца и передает его в базу. Давайте посмотрим, как это выглядит на практике.
// MySpecializedToggleButton.idl
namespace MyNamespace
{
runtimeclass MySpecializedToggleButton :
Microsoft.UI.Xaml.Controls.Primitives.ToggleButton
{
...
};
}
// MySpecializedToggleButtonAutomationPeer.idl
namespace MyNamespace
{
runtimeclass MySpecializedToggleButtonAutomationPeer :
Microsoft.UI.Xaml.Automation.Peers.ToggleButtonAutomationPeer
{
MySpecializedToggleButtonAutomationPeer(MySpecializedToggleButton owner);
};
}
Созданный конструктор для типа реализации выглядит следующим образом.
// MySpecializedToggleButtonAutomationPeer.cpp
...
MySpecializedToggleButtonAutomationPeer::MySpecializedToggleButtonAutomationPeer
(MyNamespace::MySpecializedToggleButton const& owner)
{
...
}
...
Единственный элемент, который отсутствует, заключается в том, что необходимо передать этот параметр конструктора в базовый класс. Помните шаблон полиморфизма с привязкой F, который мы упомянули выше? Когда вы разберётесь в деталях этого шаблона в том виде, в котором он используется в C++/WinRT, вы сможете понять, как называется ваш базовый класс (или просто посмотреть в заголовочном файле вашего класса реализации). Вот как вызвать конструктор базового класса в данном случае.
// MySpecializedToggleButtonAutomationPeer.cpp
...
MySpecializedToggleButtonAutomationPeer::MySpecializedToggleButtonAutomationPeer
(MyNamespace::MySpecializedToggleButton const& owner) :
MySpecializedToggleButtonAutomationPeerT<MySpecializedToggleButtonAutomationPeer>(owner)
{
...
}
...
Конструктор базового класса ожидает ToggleButton. И MySpecializedToggleButton — ToggleButton.
Пока вы не внесёте описанное выше изменение (чтобы передать этот параметр конструктора базовому классу), компилятор будет помечать ваш конструктор как ошибочный и укажет, что у типа с именем (в данном случае) MySpecializedToggleButtonAutomationPeer_base<MySpecializedToggleButtonAutomationPeer> нет подходящего конструктора по умолчанию. На самом деле это базовый класс «бас-класса» вашего типа реализации.
Пространства имен: проецируемые типы, типы реализации и фабрики
Как вы видели ранее в этом разделе, класс среды выполнения C++/WinRT существует в виде нескольких классов C++ в нескольких пространствах имен. Таким образом, имя MyRuntimeClass имеет одно значение в пространстве имен winrt::MyProject , а другое значение в пространстве имен winrt::MyProject::implementation . Помните, какое пространство имен в настоящее время имеется в контексте, а затем используйте префиксы пространства имен, если вам нужно имя из другого пространства имен. Давайте подробнее рассмотрим пространства имён, о которых идёт речь.
- winrt::MyProject. Это пространство имен содержит спроецированные типы. Объект проецируемого типа — это прокси-объект; по сути, это умный указатель на базовый объект, при этом сам базовый объект может быть реализован здесь, в вашем проекте, или в другой единице компиляции.
- winrt::MyProject::implementation. Это пространство имен содержит типы реализаций. Объект типа реализации не является указателем; это значение — полный объект стека C++. Не создавайте тип реализации напрямую; вместо этого вызовите winrt::make, передав тип реализации в качестве параметра шаблона. Ранее в этом разделе мы уже показывали примеры использования winrt::make, и еще один пример приведен в разделе Элементы управления XAML; привязка к свойству C++/WinRT. См. также раздел Диагностика прямых выделений.
- winrt::MyProject::factory_implementation. Это пространство имен содержит фабрики. Объект в этом пространстве имен поддерживает IActivationFactory.
В этой таблице показана минимальная квалификация пространства имен, используемая в разных контекстах.
| Пространство имен, которое находится в контексте | Указать проецируемый тип | Указание типа реализации |
|---|---|---|
| winrt::MyProject | MyRuntimeClass |
implementation::MyRuntimeClass |
| winrt::MyProject::implementation | MyProject::MyRuntimeClass |
MyRuntimeClass |
Это важно
Если вы хотите вернуть спроецированный тип в своей реализации, будьте осторожны и не создавайте экземпляр типа реализации, записывая MyRuntimeClass myRuntimeClass;. Правильные методы и код для этого сценария показаны ранее в этом разделе в разделе "Создание экземпляров и возврат типов реализаций и интерфейсов".
Проблема в MyRuntimeClass myRuntimeClass; этом сценарии заключается в создании объекта winrt::MyProject::implementation::MyRuntimeClass в стеке. Этот объект (типа реализации) ведет себя так же, как проецируемый тип, — можно вызывать методы на нем таким же образом; и он даже преобразуется в проецируемый тип. Но объект уничтожается в соответствии с обычными правилами C++ при выходе из области видимости. Таким образом, если вы вернули проецируемый тип данных (умный указатель) для этого объекта, то этот указатель теперь является висячим.
Этот тип повреждения памяти сложно диагностировать. Итак, в отладочных сборках проверка C++/WinRT помогает выявить эту ошибку с помощью средства обнаружения стека. Но корутины выделяются в куче, поэтому эта ошибка не будет обнаружена, если вы допустите её внутри корутины. Дополнительные сведения см. в разделе Диагностика прямых выделений.
Использование проецируемых типов и типов реализаций с различными функциями C++/WinRT
Ниже приведены различные случаи, в которых компонентам C++/WinRT требуется тип, а также указано, какой именно тип ожидается: проецируемый тип, тип реализации или оба.
| Функция | Принимает | Notes |
|---|---|---|
T (представляющий умный указатель) |
Прогнозируемый | Обратите внимание на предупреждение в Пространства имён: проецируемые типы, типы реализации и фабрики об ошибочном использовании типа реализации. |
agile_ref<T> |
Both | Если используется тип реализации, аргумент конструктора должен быть com_ptr<T>. |
com_ptr<T> |
Implementation | При использовании проецируемого типа возникает ошибка: 'Release' is not a member of 'T' |
default_interface<T> |
Both | При использовании типа реализации возвращается первый реализованный интерфейс. |
get_self<T> |
Implementation | При использовании проецируемого типа возникает ошибка: '_abi_TrustLevel': is not a member of 'T' |
guid_of<T>() |
Both | Возвращает GUID интерфейса по умолчанию. |
IWinRTTemplateInterface<T> |
Спроецированный | Использование типа реализации компилируется, но это ошибка — см. предупреждение в Пространства имён: проецируемые типы, типы реализации и фабрики. |
make<T> |
Implementation | При использовании проецируемого типа возникает ошибка: 'implements_type': is not a member of any direct or indirect base class of 'T' |
make_agile(T const&) |
Both | Если используется тип реализации, аргумент должен быть com_ptr<T>. |
make_self<T> |
Implementation | При использовании проецируемого типа возникает ошибка: 'Release': is not a member of any direct or indirect base class of 'T' |
name_of<T> |
Прогнозируемый | При использовании типа реализации вы получите строковый GUID интерфейса по умолчанию. |
weak_ref<T> |
Both | Если используется тип реализации, аргумент конструктора должен быть com_ptr<T>. |
Выберите унифицированную конструкцию и прямой доступ к реализации
В этом разделе описывается возможность C++/WinRT 2.0, которая является необязательной, хотя по умолчанию включена для новых проектов. Для существующего проекта вам потребуется включить эту возможность, настроив инструмент cppwinrt.exe. В Visual Studio установите для свойства проекта Общие свойства>C++/WinRT>Оптимизировано значение Да. Это приводит к добавлению <CppWinRTOptimized>true</CppWinRTOptimized> в ваш файл проекта. И он имеет тот же эффект, что и добавление переключателя при вызове cppwinrt.exe из командной строки.
Параметр -opt[imize] включает то, что часто называется единообразной конструкцией. При универсальной (или унифицированной) конструкции вы используете саму проекцию языка C++/WinRT для создания и использования типов реализаций (типов, реализованных компонентом, для эффективного использования приложениями) и без каких-либо трудностей с загрузчиком.
Прежде чем описывать эту функцию, давайте сначала покажем ситуацию без единообразной структуры. Чтобы проиллюстрировать, мы начнем с этого примера среда выполнения Windows класса.
// MyClass.idl
namespace MyProject
{
runtimeclass MyClass
{
MyClass();
void Method();
static void StaticMethod();
}
}
Если вы разработчик C++, знакомый с использованием библиотеки C++/WinRT, возможно, вы захотите использовать этот класс следующим образом.
using namespace winrt::MyProject;
MyClass c;
c.Method();
MyClass::StaticMethod();
И это было бы совершенно разумно при условии, что показанный использующий код не находился в том же компоненте, который реализует этот класс. Как языковая проекция, C++/WinRT скрывает от вас как разработчика ABI (COM-ориентированный двоичный интерфейс приложений, определяемый среда выполнения Windows). C++/WinRT не вызывает реализацию напрямую; он обращается через ABI.
Следовательно, в строке кода, в которой создается объект MyClass (MyClass c;), проекция C++/WinRT вызывает RoGetActivationFactory для получения класса или фабрики активации, а затем использует эту фабрику для создания объекта. Последняя строка аналогичным образом использует фабрику для создания того, что выглядит как статический вызов метода. Для всего этого требуется, чтобы класс был зарегистрирован, и что модуль реализует точку входа DllGetActivationFactory . C++/WinRT имеет очень быстрый кэш фабрик, поэтому это никак не создает проблем для приложения, использующего ваш компонент. Проблема в том, что внутри вашего компонента вы только что сделали кое-что не совсем корректное.
Во-первых, каким бы быстрым ни был кэш фабрик C++/WinRT, вызов через RoGetActivationFactory (или даже последующие вызовы через кэш фабрик) всегда будет медленнее, чем прямой вызов реализации. Вызов RoGetActivationFactory , за которым следует IActivationFactory::ActivateInstance , за которым следует QueryInterface , очевидно, не будет столь эффективным, как использование выражения C++ new для локально определенного типа. В результате опытные разработчики C++/WinRT привыкли использовать winrt::make или winrt::make_self вспомогательные функции при создании объектов в компоненте.
// MyClass c;
MyProject::MyClass c{ winrt::make<implementation::MyClass>() };
Но, как видите, это далеко не так удобно и лаконично. Для создания объекта необходимо использовать вспомогательную функцию, а также явно различать тип реализации и проецируемый тип.
Во-вторых, если для создания класса используется проекция, его фабрика активации будет помещена в кэш. Обычно это именно то, что нужно, но если фабрика расположена в том же модуле (DLL), который делает этот вызов, то вы фактически фиксируете DLL в памяти и не даёте ей выгрузиться. Во многих случаях это не имеет значения; но некоторые системные компоненты должны поддерживать выгрузку.
Здесь-то и появляется термин унифицированная конструкция. Независимо от того, находится ли код создания в проекте, который просто потребляет класс, или находится ли он в проекте, который фактически реализует класс, можно свободно использовать тот же синтаксис для создания объекта.
// MyProject::MyClass c{ winrt::make<implementation::MyClass>() };
MyClass c;
При сборке проекта компонента с помощью ключа -opt[imize] вызов через языковую проекцию сводится к тому же эффективному вызову функции winrt::make, которая напрямую создаёт тип реализации. Это делает синтаксис простым и предсказуемым, позволяет избежать потерь производительности при вызове через фабрику и исключает фиксацию компонента в памяти в процессе. Помимо проектов компонентов, это также полезно для приложений XAML. Обход RoGetActivationFactory для классов, реализованных в том же приложении, позволяет создавать их всеми теми же способами, что и если бы они находились вне вашего компонента, без необходимости регистрации.
Единообразное создание используется для любого вызова, который обрабатывается фабрикой под капотом. Практически это означает, что оптимизация служит как конструкторам, так и статическим элементам. Вот этот исходный пример еще раз.
MyClass c;
c.Method();
MyClass::StaticMethod();
Без -opt[imize]первых и последних инструкций требуются вызовы через объект фабрики.
С-opt[imize], ни один из них этого не делает. И эти вызовы компилируются непосредственно с реализацией и даже могут быть встроены. Это связано с другим термином, часто используемым, когда говорят о -opt[imize], а именно прямом доступе к реализации.
Языковые проекции удобны, но, когда вы можете напрямую получить доступ к реализации, вы можете и должны воспользоваться преимуществами этого для создания наиболее эффективного кода. C++/WinRT может сделать это для вас, не заставляя вас оставить безопасность и производительность проекции.
Это критическое изменение, поскольку компонент должен сотрудничать, чтобы позволить проекции языка достичь и напрямую получить доступ к его типам реализации. Поскольку C++/WinRT — это библиотека, состоящая только из заголовочных файлов, вы можете заглянуть внутрь и посмотреть, что происходит. Без -opt[imize] конструктор MyClass и член StaticMethod задаются проекцией следующим образом.
namespace winrt::MyProject
{
inline MyClass::MyClass() :
MyClass(impl::call_factory<MyClass>([](auto&& f){
return f.template ActivateInstance<MyClass>(); }))
{
}
inline void MyClass::StaticMethod()
{
impl::call_factory<MyClass, MyProject::IClassStatics>([&](auto&& f) {
return f.StaticMethod(); });
}
}
Не обязательно следовать всем приведенным выше. Цель состоит в том, чтобы показать, что оба вызова включают вызов функции с именем call_factory. Это подсказка о том, что эти вызовы связаны с кэшем фабрики и не получают прямой доступ к реализации.
При этом-opt[imize] эти же функции не определены вообще. Вместо этого они объявляются в проекции, а их определения оставляются на усмотрение компонента.
Затем компонент может предоставить определения, которые напрямую обращаются к реализации. Теперь мы пришли к критическому изменению. Эти определения создаются при использовании обоих -component и -opt[imize], и они отображаются в файле с именем Type.g.cpp, где Тип — имя реализуемого класса среды выполнения. Именно поэтому при первом включении -opt[imize] в существующем проекте вы можете столкнуться с различными ошибками компоновщика. Вам нужно подключить этот сгенерированный файл в свою реализацию, чтобы всё связать воедино.
В нашем примере MyClass.h может выглядеть так (независимо от того, используется ли -opt[imize]).
// MyClass.h
#pragma once
#include "MyClass.g.h"
namespace winrt::MyProject::implementation
{
struct MyClass : ClassT<MyClass>
{
MyClass() = default;
static void StaticMethod();
void Method();
};
}
namespace winrt::MyProject::factory_implementation
{
struct MyClass : ClassT<MyClass, implementation::MyClass>
{
};
}
Ваше MyClass.cpp — это место, где все объединяется.
#include "pch.h"
#include "MyClass.h"
#include "MyClass.g.cpp" // !!It's important that you add this line!!
namespace winrt::MyProject::implementation
{
void MyClass::StaticMethod()
{
}
void MyClass::Method()
{
}
}
Поэтому, чтобы использовать унифицированную конструкцию в существующем проекте, необходимо изменить файл .cpp каждой реализации так, чтобы после включения (и определения) класса реализации вы #include <Sub/Namespace/Type.g.cpp>. Этот файл содержит определения тех функций, которые левая проекция оставила неопределёнными. Вот как выглядят эти определения внутри MyClass.g.cpp файла.
namespace winrt::MyProject
{
MyClass::MyClass() :
MyClass(make<MyProject::implementation::MyClass>())
{
}
void MyClass::StaticMethod()
{
return MyProject::implementation::MyClass::StaticMethod();
}
}
И это удачно завершает проекцию, обеспечивая эффективные вызовы напрямую к реализации, без обращений к кэшу фабрики и удовлетворяя требованиям компоновщика.
И последнее, что -opt[imize] делает для вас, — это изменяет реализацию module.g.cpp вашего проекта (файла, который помогает реализовать экспорты вашей DLL: DllGetActivationFactory и DllCanUnloadNow) таким образом, чтобы инкрементальные сборки, как правило, выполнялись гораздо быстрее за счёт устранения жёсткой связанности типов, требовавшейся в C++/WinRT 1.0. Это часто называют фабриками со стиранием типов. Без -opt[imize] файл module.g.cpp, создаваемый для вашего компонента, сначала включает в себя определения всех ваших классов реализации — MyClass.h в этом примере. Затем он напрямую создает фабрику реализации для каждого класса следующим образом.
if (requal(name, L"MyProject.MyClass"))
{
return winrt::detach_abi(winrt::make<winrt::MyProject::factory_implementation::MyClass>());
}
Опять же, вам не нужно следовать всем деталям. Полезно увидеть, что для этого требуется полное определение для всех классов, реализованных компонентом. Это может оказать резкое влияние на внутренний цикл, так как любое изменение в одной реализации приведет module.g.cpp к повторной компиляции. Благодаря -opt[imize] это уже не так. Вместо этого с созданным module.g.cpp файлом происходят две вещи. Первым является то, что он больше не включает в себя классы реализации. В этом примере он не будет включать MyClass.h вообще. Вместо этого он создает фабрики реализации без каких-либо знаний о их реализации.
void* winrt_make_MyProject_MyClass();
if (requal(name, L"MyProject.MyClass"))
{
return winrt_make_MyProject_MyClass();
}
Очевидно, нет необходимости включать их определения, а определение функции winrt_make_Component_Class должен разрешить компоновщик. Конечно, вам не нужно думать об этом, так как MyClass.g.cpp файл, который создается для вас (и который вы ранее включили для поддержки единообразного строительства), также определяет эту функцию. Вот полное содержимое файла MyClass.g.cpp, созданного для этого примера.
void* winrt_make_MyProject_MyClass()
{
return winrt::detach_abi(winrt::make<winrt::MyProject::factory_implementation::MyClass>());
}
namespace winrt::MyProject
{
MyClass::MyClass() :
MyClass(make<MyProject::implementation::MyClass>())
{
}
void MyClass::StaticMethod()
{
return MyProject::implementation::MyClass::StaticMethod();
}
}
Как видно, функция winrt_make_MyProject_MyClass напрямую создает фабрику вашей реализации. Все это означает, что вы можете спокойно менять любую конкретную реализацию, и module.g.cpp при этом совсем не нужно перекомпилировать. Только при добавлении или удалении классов среда выполнения Windows module.g.cpp будет обновлён, и его потребуется перекомпилировать.
Переопределение виртуальных методов базового класса
У производного класса могут возникать проблемы с виртуальными методами, если и базовый, и производный классы определены в приложении, а виртуальный метод определён в классе-прародителе среда выполнения Windows. На практике такое происходит, если вы наследуете классы XAML. Остальная часть этого раздела продолжается из примера в производных классах.
namespace winrt::MyNamespace::implementation
{
struct BasePage : BasePageT<BasePage>
{
void OnNavigatedFrom(Microsoft::UI::Xaml::Navigation::NavigationEventArgs const& e);
};
struct DerivedPage : DerivedPageT<DerivedPage>
{
void OnNavigatedFrom(Microsoft::UI::Xaml::Navigation::NavigationEventArgs const& e);
};
}
Иерархия: Microsoft::UI::Xaml::Controls::Page<- BasePage<- DerivedPage. Метод BasePage::OnNavigatedFrom правильно переопределяет Page::OnNavigatedFrom, но DerivedPage::OnNavigatedFrom не переопределяет BasePage::OnNavigatedFrom.
Здесь DerivedPage повторно использует таблицу виртуальных методов (vtable) IPageOverrides из BasePage, а значит, не переопределяет метод IPageOverrides::OnNavigatedFrom. Одно из возможных решений предполагает, что BasePage сам является шаблонным классом, а его реализация должна целиком находиться в заголовочном файле, но это неприемлемо усложняет всё.
В качестве обходного решения объявите метод OnNavigatedFrom явно виртуальным в базовом классе. Таким образом, когда запись vtable для DerivedPage::IPageOverrides::OnNavigatedFrom вызывает BasePage::IPageOverrides::OnNavigatedFrom, производитель вызывает BasePage::OnNavigatedFrom, который (из-за ее виртуальности), в конечном итоге вызывает DerivedPage::OnNavigatedFrom.
namespace winrt::MyNamespace::implementation
{
struct BasePage : BasePageT<BasePage>
{
// Note the `virtual` keyword here.
virtual void OnNavigatedFrom(Microsoft::UI::Xaml::Navigation::NavigationEventArgs const& e);
};
struct DerivedPage : DerivedPageT<DerivedPage>
{
void OnNavigatedFrom(Microsoft::UI::Xaml::Navigation::NavigationEventArgs const& e);
};
}
Для этого требуется, чтобы все члены иерархии классов согласовали возвращаемое значение и типы параметров метода OnNavigatedFrom . Если они не согласны, то следует использовать указанную выше версию в качестве виртуального метода и упаковать альтернативные варианты.
Note
Идентификатор IDL не должен объявлять переопределенный метод. Дополнительные сведения см. в разделе "Реализация переопределиемых методов".
Важные API
- шаблон структуры winrt::com_ptr
- функция winrt::com_ptr::copy_from
- Шаблон функции winrt::from_abi
- Шаблон функции winrt::get_self
- winrt::реализует шаблон структуры
- шаблон функции winrt::make
- Шаблон функции winrt::make_self
- функция winrt::Windows::Foundation::IUnknown::as
- функция winrt::Windows::Foundation::IUnknown::try_as
Связанные темы
Windows developer