Kommentar
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
Det här avsnittet visar hur du skapar C++/WinRT-API :er med hjälp av winrt::implements base struct, antingen direkt eller indirekt. Synonymer för skapa i det här sammanhanget är producera eller implementera. Det här avsnittet beskriver följande scenarier för att implementera API:er på en C++/WinRT-typ i den här ordningen.
Note
Det här avsnittet berör ämnet Windows Runtime komponenter, men bara i samband med C++/WinRT. Om du letar efter innehåll om Windows Runtime komponenter som omfattar alla Windows Runtime språk kan du läsa Windows Runtime komponenter.
- Du redigerar inte en Windows Runtime-klass (körningsklass). Du vill bara implementera ett eller flera Windows Runtime gränssnitt för lokal förbrukning i din app. Du härleder direkt från winrt::implements i det här fallet och implementerar funktioner.
- Du skriver en runtime-klass. Du kanske redigerar en komponent som ska användas från en app. Eller så kanske du redigerar en typ som ska användas från XAML-användargränssnittet (UI), och i så fall både implementerar och använder du en körningsklass i samma kompileringsenhet. I dessa fall låter du verktygen generera klasser åt dig som härleds från winrt::implements.
I båda fallen kallas den typ som implementerar dina C++/WinRT-API:er för implementeringstypen.
Viktigt!
Det är viktigt att skilja begreppet implementeringstyp från en projekterad typ. Den projicerade typen beskrivs i Använda API:er med C++/WinRT.
Om du inte skapar en körningsklass
Det enklaste scenariot är när din typ implementerar ett Windows Runtime-gränssnitt, och du kommer att använda den typen i samma app. I så fall behöver din typ inte vara en runtime-klass, utan bara en vanlig C++-klass. Du kanske till exempel skriver en WinUI 3-skrivbordsapp baserat på Microsoft::UI::Xaml::Application.
Om din typ refereras till av XAML-användargränssnittet måste den vara en körningsklass, även om den finns i samma projekt som XAML. I det fallet, se avsnittet Om du skapar en runtimeklass som ska refereras från ditt XAML-användargränssnitt.
Note
Information om hur du installerar och använder VSIX (C++/WinRT Visual Studio Extension) och NuGet-paketet (som tillsammans tillhandahåller projektmall och byggstöd) finns i Visual Studio support för C++/WinRT.
I Visual Studio illustrerar projektmallen Tom app, Paketerad (WinUI 3 i Desktop) för C++ WinUI 3-programmönstret. Klassen App härleds från Microsoft::UI::Xaml::Application, och startpunkten anropar startmetoden.
#include "App.xaml.h"
int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
winrt::init_apartment();
::winrt::Microsoft::UI::Xaml::Application::Start(
[](auto&&) { ::winrt::make<App>(); });
}
Klassen App skapar en Microsoft::UI::Xaml::Window och aktiverar den i 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 har basstrukturen winrt::implements, en strukturmallen, för att göra det enkelt att implementera ett gränssnitt (eller flera) utan att behöva ta till programmering i COM-stil. Du härleder bara din typ från implementeringar och implementerar sedan gränssnittets funktioner. Här är ett exempel som implementerar ett anpassat gränssnitt.
struct MyType : implements<MyType, IStringable>
{
hstring ToString()
{
return L"MyType";
}
};
Om du skapar en runtimeklass i en Windows Runtime-komponent
Om din typ paketeras i en Windows Runtime-komponent för användning från en annan binärfil (den andra binärfilen är vanligtvis en applikation) måste din typ vara en runtimeklass. Du deklarerar en runtimeklass i en Microsoft Interface Definition Language-fil (IDL) (.idl) (se Dela upp runtimeklasser i MIDL-filer (.idl)).
Varje IDL-fil resulterar i en .winmd fil och Visual Studio sammanfogar alla dessa till en enda fil med samma namn som rotnamnområdet. Den sista .winmd filen kommer att vara den som konsumenterna av komponenten refererar till.
Här är ett exempel på hur du deklarerar en körningsklass i en IDL-fil.
// 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;
}
}
Den här IDL-filen deklarerar en Windows Runtime-klass (runtime). En körningsklass är en typ som kan aktiveras och användas via moderna COM-gränssnitt, vanligtvis över körbara gränser. När du lägger till en IDL-fil i projektet och skapar genererar C++/WinRT-verktygskedjan (midl.exe och cppwinrt.exe) en implementeringstyp åt dig. Ett exempel på IDL-filarbetsflödet i praktiken finns i XAML-kontroller; binda till en C++/WinRT-egenskap.
Med hjälp av exemplet IDL ovan är implementeringstypen en C++ struct stub med namnet winrt::MyProject::implementation::MyRuntimeClass i källkodsfiler med namnet \MyProject\MyProject\Generated Files\sources\MyRuntimeClass.h och MyRuntimeClass.cpp.
Implementeringstypen ser ut så här.
// 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.
Observera att det F-bundna polymorfismmönstret som används (MyRuntimeClass använder sig själv som ett mallargument till sin bas, MyRuntimeClassT). Detta kallas även det märkligt återkommande mallmönstret (CRTP). Om du följer arvskedjan uppåt stöter du på MyRuntimeClass_base.
Du kan förenkla implementeringen av enkla egenskaper med hjälp av Windows Implementeringsbibliotek (WIL). Så här gör du:
// MyRuntimeClass.h
...
namespace winrt::MyProject::implementation
{
struct MyRuntimeClass : MyRuntimeClassT<MyRuntimeClass>
{
MyRuntimeClass() = default;
wil::single_threaded_rw_property<winrt::hstring> Name;
};
}
Se Enkla egenskaper.
template <typename D, typename... I>
struct MyRuntimeClass_base : implements<D, MyProject::IMyRuntimeClass, I...>
Så i det här scenariot är winrt::implements-basstruktursmallen återigen i roten av arvshierarkin.
Mer information, kod och en genomgång av redigerings-API:er i en Windows Runtime komponent finns i Windows Runtime komponenter med C++/WinRT- och Author-händelser i C++/WinRT.
Om du skapar en körningsklass som ska refereras till i ditt XAML-användargränssnitt
Om din typ refereras i ditt XAML-gränssnitt måste den vara en runtimeklass, även om den finns i samma projekt som XAML-koden. Även om de vanligtvis aktiveras över körbara gränser kan en körningsklass i stället användas i kompileringsenheten som implementerar den.
I det här scenariot utvecklar du både och använder API:erna. Proceduren för implementering av din runtime-klass är i stort sett densamma som för en Windows Runtime-komponent. Se därför föregående avsnitt – Om du skapar en körningsklass i en Windows Runtime-komponent. Den enda detalj som skiljer sig är att C++/WinRT-verktygskedjan från IDL genererar inte bara en implementeringstyp utan även en beräknad typ. Det är viktigt att inse att det i det här scenariot kan vara tvetydigt att bara säga "MyRuntimeClass"; det finns flera entiteter av olika slag med det namnet.
- MyRuntimeClass är namnet på en runtime-klass. Men det här är verkligen en abstraktion: deklareras i IDL och implementeras på något programmeringsspråk.
-
MyRuntimeClass är namnet på C++ struct winrt::MyProject::implementation::MyRuntimeClass, som är C++/WinRT-implementeringen av körningsklassen. Som vi har sett finns den här structen endast i implementeringsprojektet om det finns separata implementerings- och användningsprojekt. Det här är implementeringstypen eller implementeringen. Den här typen genereras (av
cppwinrt.exeverktyget) i filerna\MyProject\MyProject\Generated Files\sources\MyRuntimeClass.hochMyRuntimeClass.cpp. -
MyRuntimeClass är namnet på den projekterade typen i form av C++ struct winrt::MyProject::MyRuntimeClass. Om det finns separata implementerings- och användningsprojekt finns den här structen endast i det förbrukande projektet. Det här är den projekterade typen eller projektionen. Den här typen genereras (av
cppwinrt.exe) i filen\MyProject\MyProject\Generated Files\winrt\impl\MyProject.2.h.
Här är de delar av den projekterade typen som är relevanta för det här ämnet.
// MyProject.2.h
...
namespace winrt::MyProject
{
struct MyRuntimeClass : MyProject::IMyRuntimeClass
{
MyRuntimeClass(std::nullptr_t) noexcept {}
MyRuntimeClass();
};
}
En exempelgenomgång om hur du implementerar gränssnittet INotifyPropertyChanged i en körningsklass finns i XAML-kontroller; binda till en C++/WinRT-egenskap.
Proceduren för att använda körningsklassen i det här scenariot beskrivs i Använda API:er med C++/WinRT.
Separera runtime-klasser till MIDL-filer (.idl)
Visual Studios projekt- och objektmallar skapar en separat IDL-fil för varje runtimeklass. Det ger en logisk korrespondens mellan en IDL-fil och dess genererade källkodsfiler.
Men om du konsoliderar alla projektets körningsklasser till en enda IDL-fil kan det avsevärt förbättra byggtiden. Om du annars skulle ha komplexa (eller cirkulära) import beroenden bland dem, kan det faktiskt vara nödvändigt att konsolidera. Och det kan vara lättare att skriva och granska dina runtimeklasser om de finns samlade.
Konstruktorer för körningsklasser
Här är några punkter att ta bort från de listor som vi har sett ovan.
- Varje konstruktor som du deklarerar i din IDL gör att en konstruktor genereras både på din implementeringstyp och på din planerade typ. IDL-deklarerade konstruktorer används för att nyttja runtime-klassen från en annan kompileringsenhet.
- Oavsett om du har IDL-deklarerade konstruktorer eller inte genereras en konstruktoröverlagring som accepterar std::nullptr_t för din projekterade typ. Att anropa konstruktorn för std::nullptr_t är det första av två steg vid användning av körningsklassen i samma kompileringsenhet. Mer information och ett kodexempel finns i Använda API:er med C++/WinRT.
- Om du använder körningsklassen från samma kompileringsenhet kan du även implementera icke-standardkonstruktorer direkt på implementeringstypen (som, kom ihåg, finns i
MyRuntimeClass.h).
Note
Om du förväntar dig att din körningsklass ska användas från en annan kompileringsenhet (vilket är vanligt), ska du inkludera konstruktorer i din IDL (åtminstone en standardkonstruktor). Genom att göra det får du även en fabriksimplementering tillsammans med din implementeringstyp.
Om du bara vill skapa och använda körningsklassen inom samma kompileringsenhet ska du inte deklarera några konstruktorer i din IDL. Du behöver ingen fabriksimplementering och en kommer inte att genereras. Standardkonstruktorn för implementeringstypen tas bort, men du kan enkelt redigera den och standardredigera den i stället.
Om du bara vill skapa och använda körningsklassen inom samma kompileringsenhet och du behöver konstruktorparametrar skapar du de konstruktorer som du behöver direkt på din implementeringstyp.
Körningsklassmetoder, egenskaper och händelser
Vi har sett att arbetsflödet innebär att man använder IDL för att deklarera din runtime-klass och dess medlemmar, och att verktygen sedan genererar prototyper och stubbimplementeringar åt dig. När det gäller de autogenererade prototyperna för medlemmarna i din runtimeklass kan du redigera dem så att de använder andra typer än de typer som du deklarerar i din IDL. Men du kan bara göra det så länge den typ som du deklarerar i IDL kan vidarebefordras till den typ som du deklarerar i den implementerade versionen.
Här följer några exempel.
- Du kan lätta på parametertyperna. Om din metod till exempel använder en SomeClass i IDL kan du välja att ändra den till IInspectable i implementeringen. Detta fungerar eftersom alla SomeClass kan vidarebefordras till IInspectable (det omvända skulle naturligtvis inte fungera).
- Du kan acceptera en kopierbar parameter efter värde i stället för som referens. Ändra
SomeClass const&till exempel tillSomeClass. Det är nödvändigt när du behöver undvika att samla in en referens i en coroutine (se Parameter-passing). - Du kan lätta på returvärdet. Du kan till exempel ändra void till winrt::fire_and_forget.
De två sista är mycket användbara när du skriver en asynkron händelsehanterare.
Instansiera och returnera implementeringstyper och gränssnitt
I det här avsnittet tar vi som exempel en implementeringstyp med namnet MyType, som implementerar gränssnitten IStringable och IClosable .
Du kan härleda MyType direkt från winrt::implements (det är inte en körningsklass).
#include <winrt/Windows.Foundation.h>
using namespace winrt;
using namespace Windows::Foundation;
struct MyType : implements<MyType, IStringable, IClosable>
{
winrt::hstring ToString(){ ... }
void Close(){}
};
Eller så kan du generera den från IDL (det är en runtimeklass).
// MyType.idl
namespace MyProject
{
runtimeclass MyType: Windows.Foundation.IStringable, Windows.Foundation.IClosable
{
MyType();
}
}
Du kan inte allokera implementeringstypen direkt.
MyType myimpl; // error C2259: 'MyType': cannot instantiate abstract class
Men du kan gå från MyType till ett IStringable - eller IClosable-objekt som du kan använda eller returnera som en del av projektionen genom att anropa funktionsmallen winrt::make . gör returnerar implementeringstypens standardgränssnitt.
IStringable istringable = winrt::make<MyType>();
Note
Men om du refererar till din typ från ditt XAML-användargränssnitt finns det både en implementeringstyp och en projekterad typ i samma projekt. I så fall returnerar make en instans av den projicerade typen. Ett kodexempel på det scenariot finns i XAML-kontroller; binda till en C++/WinRT-egenskap.
Vi kan bara använda istringable (i kodexemplet ovan) för att anropa medlemmarna i IStringable-gränssnittet . Men ett C++/WinRT-gränssnitt (som är ett projicerat gränssnitt) härleds från winrt::Windows::Foundation::IUnknown. Därför kan du anropa IUnknown::as (eller IUnknown::try_as) på den för att fråga efter andra planerade typer eller gränssnitt, som du också kan använda eller returnera.
Tip
Ett scenario där du inte ska anropa as eller try_as är härledning av klasser vid körning ("komponerbara klasser"). När en implementeringstyp komponerar en annan klass ska du inte anropa as eller try_as för att utföra ett okontrollerat eller kontrollerat QueryInterface av den klass som komponerats. Använd i stället datamedlemmen (this->) m_inner och anropa as eller try_as på den. Mer information finns i Härledning av Runtime-klassen i den här artikeln.
istringable.ToString();
IClosable iclosable = istringable.as<IClosable>();
iclosable.Close();
Om du behöver komma åt alla implementeringsmedlemmar och sedan returnera ett gränssnitt till en anropare använder du funktionsmallen winrt::make_self . make_self returnerar en winrt::com_ptr som omsluter implementeringstypen. Du kan komma åt medlemmarna i alla dess gränssnitt (med hjälp av piloperatorn), du kan returnera den till en anropare as-is, eller så kan du anropa som på den och returnera det resulterande gränssnittsobjektet till en anropare.
winrt::com_ptr<MyType> myimpl = winrt::make_self<MyType>();
myimpl->ToString();
myimpl->Close();
IClosable iclosable = myimpl.as<IClosable>();
iclosable.Close();
Klassen MyType ingår inte i projektionen. det är implementeringen. Men på så sätt kan du anropa dess implementeringsmetoder direkt, utan att behöva använda ett virtuellt funktionsanrop. I exemplet ovan, även om MyType::ToString använder samma signatur som den planerade metoden på IStringable, anropar vi den icke-virtuella metoden direkt, utan att korsa det binära programgränssnittet (ABI).
Com_ptr innehåller bara en pekare till MyType-structen, så att du också kan komma åt annan intern information om MyType via variabeln och piloperatorn.myimpl
Om du har ett gränssnittsobjekt och du råkar veta att det är ett gränssnitt för implementeringen kan du återgå till implementeringen med hjälp av funktionsmallen winrt::get_self . Återigen är det en teknik som undviker virtuella funktionsanrop och låter dig komma direkt vid implementeringen.
Note
Om du inte har installerat Windows SDK version 10.0.17763.0 (Windows 10 version 1809) eller senare måste du anropa winrt::from_abi i stället för winrt::get_self.
Här följer ett exempel. Det finns ett annat exempel i Implementera den anpassade BgLabelControl-kontrollklassen.
void ImplFromIClosable(IClosable const& from)
{
MyType* myimpl = winrt::get_self<MyType>(from);
myimpl->ToString();
myimpl->Close();
}
Men bara det ursprungliga gränssnittsobjektet innehåller en referens. Om du vill behålla den kan du anropa 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.
Själva implementeringstypen härleds inte från winrt::Windows::Foundation::IUnknown, så den har ingen som funktion. Men som du ser i funktionen ImplFromIClosable ovan kan du komma åt medlemmarna i alla dess gränssnitt. Men om du gör det returnerar du inte instansen av råimplementeringstypen till anroparen. Använd i stället någon av de tekniker som redan visas och returnera ett projicerat gränssnitt eller en com_ptr.
Om du har en instans av din implementeringstyp och du behöver skicka den till en funktion som förväntar sig motsvarande projekttyp kan du göra det, som du ser i kodexemplet nedan. Det finns en konverteringsoperator för din implementeringstyp (förutsatt att implementeringstypen genererades av cppwinrt.exe verktyget) som gör detta möjligt. Du kan skicka ett implementeringstypvärde direkt till en metod som förväntar sig ett värde av motsvarande projekttyp. Från en medlemsfunktion av implementeringstyp kan du skicka *this till en metod som förväntar sig ett värde av motsvarande projekttyp.
// 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);
}
...
Körningsklasshärledning
Du kan skapa en runtimeklass som ärver från en annan runtimeklass, förutsatt att basklassen deklareras som "inte förseglad". Den Windows Runtime termen för klasshärledning är "sammansättningsbara klasser". Koden för att implementera en härledd klass beror på om basklassen tillhandahålls av en annan komponent eller av samma komponent. Som tur är behöver du inte lära dig de här reglerna– du kan bara kopiera exempelimplementeringarna från utdatamappen sourcescppwinrt.exe som kompilatorn har skapat.
Tänk på det här exemplet.
// 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();
}
}
I exemplet ovan härleds MyButton från XAML-knappkontrollen , som tillhandahålls av en annan komponent. I så fall ser implementeringen ut precis som implementeringen av en icke-komposterbar klass:
namespace winrt::MyProject::implementation
{
struct MyButton : MyButtonT<MyButton>
{
};
}
namespace winrt::MyProject::factory_implementation
{
struct MyButton : MyButtonT<MyButton, implementation::MyButton>
{
};
}
I exemplet ovan härleds å andra sidan MyDerived från en annan klass i samma komponent. I det här fallet kräver implementeringen ytterligare en mallparameter som anger implementeringsklassen för basklassen.
namespace winrt::MyProject::implementation
{
struct MyDerived : MyDerivedT<MyDerived, implementation::MyBase>
{ // ^^^^^^^^^^^^^^^^^^^^^^
};
}
namespace winrt::MyProject::factory_implementation
{
struct MyDerived : MyDerivedT<MyDerived, implementation::MyDerived>
{
};
}
I båda fallen kan implementeringen anropa en metod från basklassen genom att kvalificera den med typaliaset 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();
}
};
}
Tip
När en implementeringstyp komponerar en annan klass ska du inte anropa as eller try_as för att utföra ett okontrollerat eller kontrollerat QueryInterface av den klass som komponerats. Använd i stället datamedlemmen (this->) m_inner och anropa as eller try_as på den.
Härled från en typ som har en icke-standardkonstruktor
ToggleButtonAutomationPeer::ToggleButtonAutomationPeer(ToggleButton) är ett exempel på en konstruktor som inte är standard. Det finns ingen standardkonstruktor, så för att skapa en ToggleButtonAutomationPeer måste du skicka en ägare. Följaktligen måste du, om du ärver från ToggleButtonAutomationPeer, tillhandahålla en konstruktor som tar en owner och skickar den vidare till basklassen. Nu ska vi se hur det ser ut i praktiken.
// 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);
};
}
Den genererade konstruktorn för din implementeringstyp ser ut så här.
// MySpecializedToggleButtonAutomationPeer.cpp
...
MySpecializedToggleButtonAutomationPeer::MySpecializedToggleButtonAutomationPeer
(MyNamespace::MySpecializedToggleButton const& owner)
{
...
}
...
Det enda som saknas är att du behöver skicka konstruktorparametern till basklassen. Kommer du ihåg det F-bundna polymorfismmönstret som vi nämnde ovan? När du är bekant med informationen om det mönstret som används av C++/WinRT kan du ta reda på vad basklassen heter (eller så kan du bara titta i implementeringsklassens huvudfil). Så här anropar du basklasskonstruktorn i det här fallet.
// MySpecializedToggleButtonAutomationPeer.cpp
...
MySpecializedToggleButtonAutomationPeer::MySpecializedToggleButtonAutomationPeer
(MyNamespace::MySpecializedToggleButton const& owner) :
MySpecializedToggleButtonAutomationPeerT<MySpecializedToggleButtonAutomationPeer>(owner)
{
...
}
...
Basklasskonstruktorn förväntar sig en ToggleButton. Och MySpecializedToggleButtonär enToggleButton.
Tills du gör den redigering som beskrivs ovan (för att skicka konstruktorparametern till basklassen) flaggar kompilatorn konstruktorn och påpekar att det inte finns någon lämplig standardkonstruktor tillgänglig för en typ som heter (i det här fallet) MySpecializedToggleButtonAutomationPeer_base<MySpecializedToggleButtonAutomationPeer>. Det är faktiskt basklassen för basklassen för din implementeringstyp.
Namnrymder: projicerade typer, implementeringstyper och fabriker
Som du har sett tidigare i det här avsnittet finns det en C++/WinRT-körningsklass i form av mer än en C++-klass i mer än ett namnområde. Namnet MyRuntimeClass har alltså en betydelse i namnområdet winrt::MyProject och en annan betydelse i namnområdet winrt::MyProject::implementation . Tänk på vilket namnområde du för närvarande har i kontexten och använd sedan namnområdesprefix om du behöver ett namn från ett annat namnområde. Nu ska vi titta närmare på namnrymderna i fråga.
- winrt::MyProject. Det här namnområdet innehåller projekterade typer. Ett objekt av en projekterad typ är en proxy. Det är i princip en smart pekare till ett stödobjekt, där det stödobjektet kan implementeras här i projektet eller implementeras i en annan kompileringsenhet.
- winrt::MyProject::implementation. Det här namnområdet innehåller implementeringstyper. Ett objekt av en implementeringstyp är inte en pekare. det är ett värde – ett fullständigt C++-stackobjekt. Skapa inte en implementeringstyp direkt. anropa i stället winrt::make och skicka implementeringstypen som mallparameter. Vi har visat exempel på winrt::make in action tidigare i det här avsnittet, och det finns ett annat exempel i XAML-kontroller, bind till en C++/WinRT-egenskap. Se även Diagnostisera direktallokeringar.
- winrt::MyProject::factory_implementation. Det här namnområdet innehåller fabriker. Ett objekt i det här namnområdet stöder IActivationFactory.
Den här tabellen visar den minsta namnområdeskvalifikation som du behöver använda i olika kontexter.
| Namnområdet som finns i kontexten | Så här anger du den planerade typen | Så här anger du implementeringstypen |
|---|---|---|
| winrt::MyProject | MyRuntimeClass |
implementation::MyRuntimeClass |
| winrt::MyProject::implementation | MyProject::MyRuntimeClass |
MyRuntimeClass |
Viktigt!
När du vill returnera en projekterad typ från implementeringen bör du se till att du inte instansierar implementeringstypen genom att skriva MyRuntimeClass myRuntimeClass;. Rätt tekniker och kod för det scenariot visas tidigare i det här avsnittet i avsnittet Instansiera och returnera implementeringstyper och gränssnitt.
Problemet med MyRuntimeClass myRuntimeClass; i det scenariot är att det skapar ett winrt::MyProject::implementation::MyRuntimeClass-objekt på stacken. Det objektet (av implementeringstyp) fungerar som den planerade typen på vissa sätt– du kan anropa metoder på det på samma sätt. och konverteras till och med till en projekterad typ. Men objektet förstörs enligt vanliga C++-regler när scopet lämnas. Så om du returnerade en projicerad typ (en smart pekare) till det objektet, är den pekaren nu en hängande pekare.
Den här minnesskadade typen av bugg är svår att diagnostisera. För felsökningsversioner hjälper en C++/WinRT-försäkran dig därför att fånga upp det här misstaget med hjälp av en stackidentifiering. Men coroutines allokeras på högen, så du kommer inte att få hjälp med detta misstag om du gör det inuti en coroutine. Mer information finns i Diagnostisera direktallokeringar.
Använda planerade typer och implementeringstyper med olika C++/WinRT-funktioner
Här är olika platser där en C++/WinRT-funktion förväntar sig en typ och vilken typ den förväntar sig (beräknad typ, implementeringstyp eller båda).
| Feature | Accepterar | Notes |
|---|---|---|
T (representerar en smartpekare) |
Beräknat | Se varningen i Namnområden: beräknade typer, implementeringstyper och fabriker om att använda implementeringstypen av misstag. |
agile_ref<T> |
Both | Om du använder implementeringstypen måste konstruktorargumentet vara com_ptr<T>. |
com_ptr<T> |
Implementation | Om du använder den projicerade typen genereras felet: 'Release' is not a member of 'T'. |
default_interface<T> |
Both | Om du använder implementeringstypen returneras det första implementerade gränssnittet. |
get_self<T> |
Implementation | Om du använder den projicerade typen genereras felet: '_abi_TrustLevel': is not a member of 'T'. |
guid_of<T>() |
Both | Returnerar GUID för standardgränssnittet. |
IWinRTTemplateInterface<T> |
Projicerad | Kod som använder implementeringstypen kompileras, men det är ett misstag – se varningen i Namnområden: projicerade typer, implementeringstyper och fabriker. |
make<T> |
Implementation | Om du använder den projicerade typen genereras felet: 'implements_type': is not a member of any direct or indirect base class of 'T' |
make_agile(T const&) |
Both | Om du använder implementeringstypen måste argumentet vara com_ptr<T>. |
make_self<T> |
Implementation | Om du använder den projicerade typen genereras felet: 'Release': is not a member of any direct or indirect base class of 'T' |
name_of<T> |
Beräknat | Om du använder implementeringstypen får du det strängifierade GUID:et för standardgränssnittet. |
weak_ref<T> |
Both | Om du använder implementeringstypen måste konstruktorargumentet vara com_ptr<T>. |
Delta i enhetlig konstruktion och direkt implementeringsåtkomst
Det här avsnittet beskriver en valfri funktion i C++/WinRT 2.0, även om den är aktiverad som standard för nya projekt. För ett befintligt projekt behöver du aktivera detta genom att konfigurera verktyget cppwinrt.exe. I Visual Studio anger du projektegenskapen Gemensamma egenskaper>C++/WinRT>Optimerad till Ja. Det innebär att du lägger <CppWinRTOptimized>true</CppWinRTOptimized> till i projektfilen. Och det har samma effekt som att lägga till växeln när du anropar cppwinrt.exe från kommandoraden.
Växeln -opt[imize] möjliggör det som ofta kallas enhetlig konstruktion. Med enhetlig (eller enhetlig) konstruktion använder du själva C++/WinRT-språkprojektionen för att skapa och använda dina implementeringstyper (typer som implementeras av komponenten, för användning av program) effektivt och utan problem med lastaren.
Innan vi beskriver funktionen ska vi först visa situationen utan enhetlig konstruktion. För att illustrera börjar vi med det här exemplet Windows Runtime-klassen.
// MyClass.idl
namespace MyProject
{
runtimeclass MyClass
{
MyClass();
void Method();
static void StaticMethod();
}
}
Som C++-utvecklare som är bekant med att använda C++/WinRT-biblioteket kanske du vill använda klassen så här.
using namespace winrt::MyProject;
MyClass c;
c.Method();
MyClass::StaticMethod();
Och det skulle vara helt rimligt förutsatt att den använda koden som visas inte fanns inom samma komponent som implementerar den här klassen. Som språkprojektion skyddar C++/WinRT dig som utvecklare från ABI (det COM-baserade binärt programgränssnitt som Windows Runtime definierar). C++/WinRT anropar inte implementeringen direkt; det går via ABI:t.
På kodraden där du skapar ett MyClass-objekt (MyClass c;) anropar därför C++/WinRT-projektionen RoGetActivationFactory för att hämta klassen eller aktiveringsfabriken och använder sedan den fabriken för att skapa objektet. Den sista raden använder också fabriken för att göra vad som verkar vara ett statiskt metodanrop. Allt detta kräver att klassen registreras och att modulen implementerar startpunkten DllGetActivationFactory . C++/WinRT har en mycket snabb fabrikscache, så inget av detta orsakar ett problem för ett program som använder din komponent. Problemet är att du inom din komponent just har gjort något som är lite problematiskt.
För det första, oavsett hur snabbt C++/WinRT-fabrikscachen är, kommer anrop via RoGetActivationFactory (eller till och med efterföljande anrop via fabrikscachen) alltid att vara långsammare än att anropa direkt i implementeringen. Ett anrop till RoGetActivationFactory följt av IActivationFactory::ActivateInstance följt av QueryInterface kommer uppenbarligen inte att vara lika effektivt som att använda ett C++ new -uttryck för en lokalt definierad typ. Därför är erfarna C++/WinRT-utvecklare vana vid att använda winrt::make eller winrt::make_self hjälpfunktioner när du skapar objekt i en komponent.
// MyClass c;
MyProject::MyClass c{ winrt::make<implementation::MyClass>() };
Men som du kan se är det inte alls lika bekvämt eller koncist. Du måste använda en hjälpfunktion för att skapa objektet, och du måste också skilja mellan implementeringstypen och den planerade typen.
För det andra innebär användningen av projektionen för att skapa klassen att dess aktiveringsfabrik cachelagras. Normalt är det precis vad du vill, men om fabriken finns i samma modul (DLL) som utför anropet, har du i praktiken låst DLL:en i minnet och förhindrat att den någonsin avladdas. I många fall spelar det ingen roll; men vissa systemkomponenter måste ha stöd för avlastning.
Det är här termen enhetlig konstruktion kommer in. Oavsett om skapandekoden finns i ett projekt som bara förbrukar klassen, eller om den finns i projektet som faktiskt implementerar klassen, kan du fritt använda samma syntax för att skapa objektet.
// MyProject::MyClass c{ winrt::make<implementation::MyClass>() };
MyClass c;
När du skapar komponentprojektet med växeln -opt[imize] kompileras anropet via språkprojektionen ned till samma effektiva anrop till funktionen winrt::make som direkt skapar implementeringstypen. Det gör din syntax enkel och förutsägbar, undviker all prestandaförlust av att anropa via fabriken och undviker att låsa komponenten i processen. Förutom komponentprojekt är detta också användbart för XAML-program. Genom att kringgå RoGetActivationFactory för klasser som implementeras i samma program kan du konstruera dem (utan att behöva registreras) på samma sätt som om de var utanför komponenten.
Enhetlig uppbyggnad gäller för vilket som helst anrop som hanteras av fabriken bakom kulisserna. I praktiken innebär det att optimeringen hanterar både konstruktorer och statiska medlemmar. Här är det ursprungliga exemplet igen.
MyClass c;
c.Method();
MyClass::StaticMethod();
Utan -opt[imize]kräver den första och sista instruktionen anrop via fabriksobjektet.
Med-opt[imize] gör ingen av dem det. Och dessa anrop sammanställs direkt mot implementeringen och har till och med potential att infogas. Vilket talar till den andra termen som ofta används när man talar om -opt[imize], nämligen direkt implementeringsåtkomst .
Språkprojektioner är praktiska, men när du har direkt åtkomst till implementeringen kan och bör du dra nytta av det för att skapa den mest effektiva koden som möjligt. C++/WinRT kan göra det åt dig, utan att tvinga dig att lämna projektionens säkerhet och produktivitet.
Detta är en icke-bakåtkompatibel ändring eftersom komponenten måste samarbeta för att språkprojektionen ska kunna nå in och få direkt åtkomst till dess implementeringstyper. Eftersom C++/WinRT är ett bibliotek som bara består av headerfiler kan du titta i det och se vad som händer. Utan -opt[imize]definieras MyClass-konstruktorn och StaticMethod-medlemmen av projektionen så här.
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(); });
}
}
Det är inte nödvändigt att följa alla ovanstående; avsikten är att visa att båda anropen omfattar ett anrop till en funktion med namnet call_factory. Det är din ledtråd att dessa anrop omfattar fabrikscachen och att de inte har direkt åtkomst till implementeringen.
Med-opt[imize] definieras inte samma funktioner alls. I stället deklareras de av projektionen och deras definitioner lämnas upp till komponenten.
Komponenten kan sedan tillhandahålla definitioner som anropar direkt till implementeringen. Nu har vi kommit fram till den bakåtinkompatibla ändringen. Dessa definitioner genereras åt dig när du använder både -component och -opt[imize], och de visas i en fil med namnet Type.g.cpp, där Type är namnet på den körningsklass som implementeras. Det är därför du kan stöta på olika länkfel när du först aktiverar -opt[imize] i ett befintligt projekt. Du måste inkludera den genererade filen i implementeringen för att kunna sy ihop saker och ting.
I vårt exempel MyClass.h kan det se ut så här (oavsett om -opt[imize] används).
// 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>
{
};
}
Din MyClass.cpp är platsen där allt samlas.
#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()
{
}
}
Om du vill använda enhetlig konstruktion i ett befintligt projekt måste du redigera varje implementerings .cpp-fil så att du #include <Sub/Namespace/Type.g.cpp> efter inkluderingen (och definitionen) av implementeringsklassen. Filen innehåller definitionerna av de funktioner som projektionen lämnade odefinierat. Så här ser dessa definitioner ut i MyClass.g.cpp filen.
namespace winrt::MyProject
{
MyClass::MyClass() :
MyClass(make<MyProject::implementation::MyClass>())
{
}
void MyClass::StaticMethod()
{
return MyProject::implementation::MyClass::StaticMethod();
}
}
Och det slutför fint projektionen med effektiva anrop direkt till implementeringen, undviker anrop till fabrikscachen och tillfredsställer länkaren.
Det sista som -opt[imize] gör för dig är att ändra implementeringen av ditt projekts module.g.cpp (filen som hjälper dig att implementera DLL-exporterna DllGetActivationFactory och DllCanUnloadNow) på ett sådant sätt att inkrementella byggen tenderar att bli mycket snabbare genom att eliminera det starka typberoende som krävdes av C++/WinRT 1.0. Detta kallas ofta för typraderade fabriker. Utan -opt[imize] börjar module.g.cpp-filen som genereras för komponenten med att inkludera definitionerna av alla dina implementeringsklasser – MyClass.h, i det här exemplet. Därefter skapas implementeringsfabriken direkt för varje klass så här.
if (requal(name, L"MyProject.MyClass"))
{
return winrt::detach_abi(winrt::make<winrt::MyProject::factory_implementation::MyClass>());
}
Återigen behöver du inte hänga med i alla detaljer. Det som är användbart att se är att detta kräver en fullständig definition för alla klasser som implementeras av komponenten. Detta kan ha en dramatisk effekt på den inre loopen, eftersom alla ändringar i en enda implementering gör module.g.cpp att omkompileras. Med -opt[imize]är detta inte längre fallet. I stället händer två saker med den genererade module.g.cpp filen. Den första är att den inte längre innehåller några implementeringsklasser. I det här exemplet kommer MyClass.h inte att inkluderas alls. I stället skapas implementeringsfabrikerna utan någon kunskap om implementeringen.
void* winrt_make_MyProject_MyClass();
if (requal(name, L"MyProject.MyClass"))
{
return winrt_make_MyProject_MyClass();
}
Det är uppenbart att det inte finns någon anledning att inkludera deras definitioner, och det är upp till länkaren att lösa upp definitionen av funktionen winrt_make_Component_Class. Naturligtvis behöver du inte tänka på detta, eftersom den MyClass.g.cpp fil som genereras åt dig (och som du tidigare inkluderade för att stödja enhetlig konstruktion) också definierar den här funktionen. Här är hela MyClass.g.cpp filen som genereras för det här exemplet.
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();
}
}
Som du ser skapar funktionen winrt_make_MyProject_MyClass direkt implementeringens fabrik. Allt detta innebär att du gärna kan ändra en viss implementering och behöver module.g.cpp inte kompileras om alls. Det är bara när du lägger till eller tar bort Windows Runtime-klasser som module.g.cpp uppdateras och måste omkompileras.
Åsidoskriva virtuella metoder i basklassen
Din härledda klass kan ha problem med virtuella metoder om både basklassen och den härledda klassen är appdefinierade klasser, men den virtuella metoden definieras i en Windows Runtime-klass. I praktiken händer detta om du härleder från XAML-klasser. Resten av det här avsnittet fortsätter från exemplet i Härledda klasser.
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);
};
}
Hierarkin är Microsoft::UI::Xaml::Controls::Page<- BasePage<- DerivedPage. Metoden BasePage::OnNavigatedFrom åsidosätter korrekt Page::OnNavigatedFrom, men DerivedPage::OnNavigatedFrom åsidosätter inte BasePage::OnNavigatedFrom.
Här återanvänder DerivedPage den virtuella IPageOverrides-tabellen från BasePage, vilket innebär att den inte åsidosätter metoden IPageOverrides::OnNavigatedFrom . En potentiell lösning kräver att BasePage självt är en mallklass och att den implementeras helt i en rubrikfil, men det gör saker och ting oacceptabelt komplicerade.
Som en lösning deklarerar du metoden OnNavigatedFrom som explicit virtuell i basklassen. På så sätt, när vtable-posten för DerivedPage::IPageOverrides::OnNavigatedFrom anropar BasePage::IPageOverrides::OnNavigatedFrom, anropar producenten BasePage::OnNavigatedFrom, som (eftersom den är virtuell) slutar med att anropa 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);
};
}
Detta kräver att alla medlemmar i klasshierarkin är överens om returvärdet och parametertyperna för metoden OnNavigatedFrom . Om de skiljer sig åt bör du använda versionen ovan som virtuell metod och kapsla in de alternativa versionerna.
Note
Din IDL behöver inte deklarera den åsidosatta metoden. Mer information finns i Implementera åsidosättbara metoder.
Viktiga API:er
- winrt::com_ptr struct-mall
- winrt::com_ptr::copy_from funktion
- winrt::from_abi funktionsmall
- winrt::get_self funktionsmall
- winrt::implements struct template
- winrt::make-funktionsmall
- winrt::make_self funktionsmall
- winrt::Windows::Foundation::IUnknown::as function
- winrt::Windows::Foundation::IUnknown::try_as function
Relaterade ämnen
Windows developer