使用 C++/CX 创建 Windows 运行时组件

注意

本主题旨在帮助你维护 C++/CX 应用程序。 不过,我们建议你使用 C++/WinRT 编写新应用程序。 C++/WinRT 是 Windows 运行时 (WinRT) API 的完全标准新式 C++17 语言投影,以基于标头文件的库的形式实现,旨在为你提供对新式 Windows API 的一流访问。 若要了解如何使用 C++/WinRT 创建 Windows 运行时组件,请参阅使用 C++/WinRT 创建 Windows 运行时组件

本主题演示如何使用 C++/CX 创建Windows 运行时组件,该组件可从使用任何Windows 运行时语言(C#、Visual Basic、C++ 或 JavaScript)生成的通用 Windows 应用调用)。

用 C++ 生成 Windows 运行时组件有多种原因。

  • 获取复杂或计算密集型操作中C++的性能优势。
  • 重复使用已编写和测试的代码。

生成包含 JavaScript 或 .NET 项目的解决方案以及一个Windows 运行时组件项目时,JavaScript 项目文件和已编译的 DLL 将合并到一个包中,你可以在模拟器中本地调试或远程在受限设备上进行调试。 还可以仅将组件项目作为扩展 SDK 分发。 有关详细信息,请参阅创建软件开发工具包

一般情况下,在编写 C++/CX 组件代码时,请使用常规 C++ 库和内置类型,抽象二进制接口 (ABI) 边界除外,在该边界内,你将在另一个 .winmd 程序包中与代码传递数据。 因此,请使用 Windows 运行时类型和 C++/CX 支持用于创建和处理这些类型的特殊语法。 此外,在 C++/CX 代码中,使用诸如委派和事件的类型实现可从组件引发并在 JavaScript、Visual Basic、C++ 或 C# 中处理的事件。 有关 C++/CX 语法的详细信息,请参阅 Visual C++ 语言参考 (C++/CX)

大小写和命名规则

JavaScript

JavaScript 区分大小写。 因此,必须遵循以下大小写约定:

  • 引用C++命名空间和类时,请使用C++端使用的相同大小写。
  • 调用方法时,即使方法名称在C++端大写,也可以使用 camel 大小写。 例如,必须从 JavaScript 调用C++方法 GetDate() 作为 getDate()。
  • 可激活的类名和命名空间名称不能包含 UNICODE 字符。

.NET

.NET 语言遵循其普通大小写规则。

实例化对象

只有Windows 运行时类型才能跨 ABI 边界传递。 如果组件在公共方法中将 std::wstring 等类型作为返回类型或参数,编译器将引发错误。 Visual C++ 组件扩展(C++/CX)内置类型包括常规标量(如 int 和 double),以及其 typedef 等效项 int32、float64 等。 有关详细信息,请参阅类型系统(C++/CX)。

// ref class definition in C++
public ref class SampleRefClass sealed
{
    // Class members...

    // #include <valarray>
public:
    double LogCalc(double input)
    {
        // Use C++ standard library as usual.
        return std::log(input);
    }

};
//Instantiation in JavaScript (requires "Add reference > Project reference")
var nativeObject = new CppComponent.SampleRefClass();
//Call a method and display result in a XAML TextBlock
var num = nativeObject.LogCalc(21.5);
ResultText.Text = num.ToString();

C++/CX 内置类型、库类型和 Windows 运行时类型

可激活类(也称为 ref 类)是从另一种语言(如 JavaScript、C# 或 Visual Basic)实例化的类。 若要从另一种语言使用,组件必须至少包含一个可激活类。

Windows 运行时组件可以包含多个公共可激活类,以及仅在组件内部已知的其他类。 将 WebHostHidden 属性应用到旨在对 JavaScript 不可见的 C++/CX 类型。

所有公共类必须位于与组件元数据文件同名的同一根命名空间中。 例如,名为 A.B.C.MyClass 的类只有在名为 A.winmd 或 A.B.winmd 或 A.B.C.winmd 的元数据文件中定义后才能实例化。 DLL 的名称不必与 .winmd 文件名匹配。

客户端代码使用新的(Visual Basic 中的新增)关键字(与任何类一样)创建组件的实例。

必须将可激活类声明为 已密封的公共 ref 类。 ref 类关键字告知编译器将类创建为Windows 运行时兼容类型,并且密封关键字指定无法继承该类。 Windows 运行时目前不支持通用继承模型;有限的继承模型支持创建自定义 XAML 控件。 有关详细信息,请参阅 Ref 类和结构(C++/CX)。

对于 C++/CX,所有数字基元均在默认命名空间中定义。 Platform 命名空间包含特定于 Windows 运行时类型系统的 C++/CX 类。 其中包括 Platform::String 类和 Platform::Object 类。 Platform::Collections::Map 类和 Platform::Collections::Vector 类等具体集合类型在 Platform::Collections 命名空间中定义。 这些类型实现的公共接口在 Windows::Foundation::Collections Namespace(C++/CX)定义。 它是 JavaScript、C# 和 Visual Basic 使用的这些接口类型。 有关详细信息,请参阅类型系统(C++/CX)。

返回内置类型的值的方法

    // #include <valarray>
public:
    double LogCalc(double input)
    {
        // Use C++ standard library as usual.
        return std::log(input);
    }
//Call a method
var nativeObject = new CppComponent.SampleRefClass;
var num = nativeObject.logCalc(21.5);
document.getElementById('P2').innerHTML = num;

返回自定义值结构的方法

namespace CppComponent
{
    // Custom struct
    public value struct PlayerData
    {
        Platform::String^ Name;
        int Number;
        double ScoringAverage;
    };

    public ref class Player sealed
    {
    private:
        PlayerData m_player;
    public:
        property PlayerData PlayerStats
        {
            PlayerData get(){ return m_player; }
            void set(PlayerData data) {m_player = data;}
        }
    };
}

若要跨 ABI 传递用户定义的值结构,定义与使用 C++/CX 定义的值结构具有相同成员的 JavaScript 对象。 然后你可以将该对象作为参数传递到 C++/CX 方法,以便该对象隐式转换为 C++/CX 类型。

// Get and set the value struct
function GetAndSetPlayerData() {
    // Create an object to pass to C++
    var myData =
        { name: "Bob Homer", number: 12, scoringAverage: .357 };
    var nativeObject = new CppComponent.Player();
    nativeObject.playerStats = myData;

    // Retrieve C++ value struct into new JavaScript object
    var myData2 = nativeObject.playerStats;
    document.getElementById('P3').innerHTML = myData.name + " , " + myData.number + " , " + myData.scoringAverage.toPrecision(3);
}

另一种方法是定义实现 IPropertySet 的类(未显示)。

在 .NET 语言中,只需创建在 C++/CX 组件中定义的类型的变量。

private void GetAndSetPlayerData()
{
    // Create a ref class
    var player = new CppComponent.Player();

    // Create a variable of a value struct
    // type that is defined in C++
    CppComponent.PlayerData myPlayer;
    myPlayer.Name = "Babe Ruth";
    myPlayer.Number = 12;
    myPlayer.ScoringAverage = .398;

    // Set the property
    player.PlayerStats = myPlayer;

    // Get the property and store it in a new variable
    CppComponent.PlayerData myPlayer2 = player.PlayerStats;
    ResultText.Text += myPlayer.Name + " , " + myPlayer.Number.ToString() +
        " , " + myPlayer.ScoringAverage.ToString();
}

重载方法

C++/CX 公共 ref 类可以包含重载方法,而 JavaScript 区分重载方法的功能受限。 例如,它可以区分这些签名之间的差异:

public ref class NumberClass sealed
{
public:
    int GetNumber(int i);
    int GetNumber(int i, Platform::String^ str);
    double GetNumber(int i, MyData^ d);
};

但是它无法区分以下内容之间的区别:

int GetNumber(int i);
double GetNumber(double d);

在不明确的情况下,可以通过将 Windows::Foundation::Metadata::D efaultOverload 属性应用于 头文件中的方法签名,确保 JavaScript 始终调用特定的重载

此 JavaScript 始终调用特性化重载:

var nativeObject = new CppComponent.NumberClass();
var num = nativeObject.getNumber(9);
document.getElementById('P4').innerHTML = num;

.NET

.NET 语言与在任意 .NET 类中一样,在 C++/CX ref 类中识别重载。

DateTime

在Windows 运行时中,Windows::Foundation::D ateTime 对象只是一个 64 位有符号整数,表示 1601 年 1 月 1 日或之后的 100 纳秒间隔数。 Windows:Foundation::D ateTime 对象上没有方法。 相反,每种语言按照源于该语言的方法投射 DateTime:JavaScript 中的 Date 对象和 .NET 中的 System.DateTime 和 System.DateTimeOffset 类型。

public  ref class MyDateClass sealed
{
public:
    property Windows::Foundation::DateTime TimeStamp;
    void SetTime(Windows::Foundation::DateTime dt)
    {
        auto cal = ref new Windows::Globalization::Calendar();
        cal->SetDateTime(dt);
        TimeStamp = cal->GetDateTime(); // or TimeStamp = dt;
    }
};

在将 DateTime 值从 C++/CX 传递到 JavaScript 时,JavaScript 将其作为 Date 对象接受,并且默认将其显示为长格式日期字符串。

function SetAndGetDate() {
    var nativeObject = new CppComponent.MyDateClass();

    var myDate = new Date(1956, 4, 21);
    nativeObject.setTime(myDate);

    var myDate2 = nativeObject.timeStamp;

    //prints long form date string
    document.getElementById('P5').innerHTML = myDate2;

}

在 .NET 语言将 System.DateTime 传递到 C++/CX 组件时,该方法将其作为 Windows::Foundation::DateTime 接受。 在组件将 Windows::Foundation::DateTime 传递到 .NET 方法时,Framework 方法将其作为 DateTimeOffset 接受。

private void DateTimeExample()
{
    // Pass a System.DateTime to a C++ method
    // that takes a Windows::Foundation::DateTime
    DateTime dt = DateTime.Now;
    var nativeObject = new CppComponent.MyDateClass();
    nativeObject.SetTime(dt);

    // Retrieve a Windows::Foundation::DateTime as a
    // System.DateTimeOffset
    DateTimeOffset myDate = nativeObject.TimeStamp;

    // Print the long-form date string
    ResultText.Text += myDate.ToString();
}

集合和数组

集合始终作为句柄传递到 ABI 边界,以Windows 运行时类型(如 Windows::Foundation::Collections::IVector^ 和 Windows::Foundation::Collections::IMap^)。 例如,如果将句柄返回到 Platform::Collections::Map,它将隐式转换为 Windows::Foundation::Collections::IMap^。 集合接口在与提供具体实现的 C++/CX 类分开的命名空间中定义。 JavaScript 和 .NET 语言使用接口。 有关详细信息,请参阅集合(C++/CX)Array 和 WriteOnlyArray(C++/CX)。

传递 IVector

// Windows::Foundation::Collections::IVector across the ABI.
//#include <algorithm>
//#include <collection.h>
Windows::Foundation::Collections::IVector<int>^ SortVector(Windows::Foundation::Collections::IVector<int>^ vec)
{
    std::sort(begin(vec), end(vec));
    return vec;
}
var nativeObject = new CppComponent.CollectionExample();
// Call the method to sort an integer array
var inVector = [14, 12, 45, 89, 23];
var outVector = nativeObject.sortVector(inVector);
var result = "Sorted vector to array:";
for (var i = 0; i < outVector.length; i++)
{
    outVector[i];
    result += outVector[i].toString() + ",";
}
document.getElementById('P6').innerHTML = result;

.NET 语言将 IVector<T> 视为 IList<T>。

private void SortListItems()
{
    IList<int> myList = new List<int>();
    myList.Add(5);
    myList.Add(9);
    myList.Add(17);
    myList.Add(2);

    var nativeObject = new CppComponent.CollectionExample();
    IList<int> mySortedList = nativeObject.SortVector(myList);

    foreach (var item in mySortedList)
    {
        ResultText.Text += " " + item.ToString();
    }
}

传递 IMap

// #include <map>
//#include <collection.h>
Windows::Foundation::Collections::IMap<int, Platform::String^> ^GetMap(void)
{    
    Windows::Foundation::Collections::IMap<int, Platform::String^> ^ret =
        ref new Platform::Collections::Map<int, Platform::String^>;
    ret->Insert(1, "One ");
    ret->Insert(2, "Two ");
    ret->Insert(3, "Three ");
    ret->Insert(4, "Four ");
    ret->Insert(5, "Five ");
    return ret;
}
// Call the method to get the map
var outputMap = nativeObject.getMap();
var mStr = "Map result:" + outputMap.lookup(1) + outputMap.lookup(2)
    + outputMap.lookup(3) + outputMap.lookup(4) + outputMap.lookup(5);
document.getElementById('P7').innerHTML = mStr;

.NET 语言请参阅 IMap 和 IDictionary<K、V>。

private void GetDictionary()
{
    var nativeObject = new CppComponent.CollectionExample();
    IDictionary<int, string> d = nativeObject.GetMap();
    ResultText.Text += d[2].ToString();
}

属性

C++/CX 组件扩展中的公共 ref 类使用属性关键字将公共数据成员作为属性公开。 此概念与 .NET 属性一致。 普通属性类似于数据成员,因为它的功能是隐式的。 非普通属性具有显式获取和设置访问器和命名私有变量,该变量是值的“后盾存储”。 在此示例中,私有成员变量 _propertyAValue 是 PropertyA 的备份存储。 当属性的值发生更改时,属性可能会触发事件,客户端应用可以注册以接收该事件。

//Properties
public delegate void PropertyChangedHandler(Platform::Object^ sender, int arg);
public ref class PropertyExample  sealed
{
public:
    PropertyExample(){}

    // Event that is fired when PropertyA changes
    event PropertyChangedHandler^ PropertyChangedEvent;

    // Property that has custom setter/getter
    property int PropertyA
    {
        int get() { return m_propertyAValue; }
        void set(int propertyAValue)
        {
            if (propertyAValue != m_propertyAValue)
            {
                m_propertyAValue = propertyAValue;
                // Fire event. (See event example below.)
                PropertyChangedEvent(this, propertyAValue);
            }
        }
    }

    // Trivial get/set property that has a compiler-generated backing store.
    property Platform::String^ PropertyB;

private:
    // Backing store for propertyA.
    int m_propertyAValue;
};
var nativeObject = new CppComponent.PropertyExample();
var propValue = nativeObject.propertyA;
document.getElementById('P8').innerHTML = propValue;

//Set the string property
nativeObject.propertyB = "What is the meaning of the universe?";
document.getElementById('P9').innerHTML += nativeObject.propertyB;

.NET 语言在本机 C++/CX 对象上访问属性,就像它们在 .NET 对象上访问一样。

private void GetAProperty()
{
    // Get the value of the integer property
    // Instantiate the C++ object
    var obj = new CppComponent.PropertyExample();

    // Get an integer property
    var propValue = obj.PropertyA;
    ResultText.Text += propValue.ToString();

    // Set a string property
    obj.PropertyB = " What is the meaning of the universe?";
    ResultText.Text += obj.PropertyB;

}

委托和事件

委托是表示函数对象的Windows 运行时类型。 可以使用与事件、回调和异步方法调用相关的委托来指定稍后要执行的操作。 与函数对象一样,委托通过使编译器能够验证函数的返回类型和参数类型来提供类型安全性。 委托的声明类似于函数签名,实现类似于类定义,调用类似于函数调用。

添加事件侦听器

可以使用事件关键字声明指定委托类型的公共成员。 客户端代码使用特定语言提供的标准机制订阅事件。

public:
    event SomeHandler^ someEvent;

此示例使用与上一个属性部分相同的C++代码。

function Button_Click() {
    var nativeObj = new CppComponent.PropertyExample();
    // Define an event handler method
    var singlecasthandler = function (ev) {
        document.getElementById('P10').innerHTML = "The button was clicked and the value is " + ev;
    };

    // Subscribe to the event
    nativeObj.onpropertychangedevent = singlecasthandler;

    // Set the value of the property and fire the event
    var propValue = 21;
    nativeObj.propertyA = 2 * propValue;

}

在 .NET 语言中,在 C++ 组件中订阅事件与在 .NET 类中订阅事件一样:

//Subscribe to event and call method that causes it to be fired.
private void TestMethod()
{
    var objWithEvent = new CppComponent.PropertyExample();
    objWithEvent.PropertyChangedEvent += objWithEvent_PropertyChangedEvent;

    objWithEvent.PropertyA = 42;
}

//Event handler method
private void objWithEvent_PropertyChangedEvent(object __param0, int __param1)
{
    ResultText.Text = "the event was fired and the result is " +
         __param1.ToString();
}

为一个事件添加多个事件侦听器

JavaScript 具有 addEventListener 方法,使多个处理程序能够订阅单个事件。

public delegate void SomeHandler(Platform::String^ str);

public ref class LangSample sealed
{
public:
    event SomeHandler^ someEvent;
    property Platform::String^ PropertyA;

    // Method that fires an event
    void FireEvent(Platform::String^ str)
    {
        someEvent(Platform::String::Concat(str, PropertyA->ToString()));
    }
    //...
};
// Add two event handlers
var multicast1 = function (ev) {
    document.getElementById('P11').innerHTML = "Handler 1: " + ev.target;
};
var multicast2 = function (ev) {
    document.getElementById('P12').innerHTML = "Handler 2: " + ev.target;
};

var nativeObject = new CppComponent.LangSample();
//Subscribe to the same event
nativeObject.addEventListener("someevent", multicast1);
nativeObject.addEventListener("someevent", multicast2);

nativeObject.propertyA = "42";

// This method should fire an event
nativeObject.fireEvent("The answer is ");

在 C# 中,任意数量的事件处理程序都可以使用 += 运算符订阅事件,如前面的示例所示。

枚举

使用公共类枚举声明 C++/CX 中的 Windows 运行时枚举,它在标准 C++ 中类似于作用域枚举。

public enum class Direction {North, South, East, West};

public ref class EnumExampleClass sealed
{
public:
    property Direction CurrentDirection
    {
        Direction  get(){return m_direction; }
    }

private:
    Direction m_direction;
};

枚举值以整数形式在 C++/CX 和 JavaScript 之间传递。 你可以选择声明包含与 C++/CX 枚举相同的命名值的 JavaScript 对象,并按照如下方式使用它。

var Direction = { 0: "North", 1: "South", 2: "East", 3: "West" };
//. . .

var nativeObject = new CppComponent.EnumExampleClass();
var curDirection = nativeObject.currentDirection;
document.getElementById('P13').innerHTML =
Direction[curDirection];

C# 和 Visual Basic 都支持枚举。 这些语言将 C++ 公共枚举类视为 .NET 枚举一样。

异步方法

若要使用其他Windows 运行时对象公开的异步方法,请使用任务类(并发运行时)。 有关详细信息,请参阅和任务并行度(并发运行时)。

若要使用 C++/CX 实现异步方法,请使用在 ppltasks.h 中定义的 create_async 函数。 有关详细信息,请参阅使用 C++/CX 为 UWP 应用创建异步操作。 有关示例,请参阅创建 C++/CX Windows 运行时组件并通过 JavaScript 或 C# 调用此组件的演练。 .NET 语言会像使用任何在 .NET 中定义的异步方法一样使用 C++/CX 异步方法。

异常

可以引发Windows 运行时定义的任何异常类型。 不能从任何Windows 运行时异常类型派生自定义类型。 但是,可以引发 COMException 并提供可由捕获异常的代码访问的自定义 HRESULT。 无法在 COMException 中指定自定义消息。

调试提示

调试具有组件 DLL 的 JavaScript 解决方案时,可以将调试器设置为启用单步执行脚本或单步执行组件中的本机代码,但不能同时同时启用两者。 若要更改设置,请在解决方案资源管理器中选择 JavaScript 项目节点,然后选择“属性”、“调试”、“调试器类型”。

请确保在程序包设计器中选择相应的功能。 例如,如果尝试使用Windows 运行时 API 在用户的图片库中打开图像文件,请确保在清单设计器的功能窗格中选中“图片库”复选框。

如果 JavaScript 代码似乎无法识别组件中的公共属性或方法,请确保在 JavaScript 中使用 camel 大小写。 例如,LogCalc C++/CX 方法在 JavaScript 中必须引用为 logCalc。

如果你从某个解决方案中删除 C++/CX Windows 运行时组件项目,还必须从 JavaScript 项目中手动删除项目引用。 如果此操作无法完成,将阻止后续调试或生成操作。 如有必要,你可以稍后向 DLL 添加程序集引用。