C ++体系结构指南
尽管NoesisGUI基于WPF(一种C#框架),但该API中公开的许多核心概念并未直接转换为C ++。例如,反射是数据绑定的关键概念,在C ++中不可用。在C ++中也没有垃圾回收的概念。为了创建更易于使用的C ++ API,NoesisGUI提供了一些帮助程序,这些帮助程序使从C#的过渡变得轻松而高效。如果要以C ++语言使用Noesis,请仔细阅读以下部分。
头文件和命名空间
NoesisGUI标头按模块分组(以Ns为前缀)。整个API在名称空间Noesis中公开。
#include
#include
#include
#include
#include
#include
#include
#include
using namespace Noesis;
如果您不想手动选择每个头文件,我们还提供了一个集合'Noesis_pch.h',公开了所有NoesisGUI API。当使用预编译头文件时,这特别有用。
#include
using namespace Noesis;
注意
Noesis 应用程序框架Application Framework (不是内核的一部分)公开在另一个命名空间NoesisApp中,并且其头文件不包含在Noesis_pch.h中。
参考计数(Reference Counting)
在Noesis中,每个对象都包含一个控制其生命周期的参考计数器。使用new运算符创建实例时,参考计数器将初始化为1 。每次调用AddReference()或Release()时,此计数器都会增加或减少。如果对象的引用计数达到零,则使用运算符delete将其自动销毁。
// Create a new instance. Reference counter is '1' at this point
Brush* color = new SolidColorBrush(Color::Red);
//...
// Release instance and delete it if reference counter reaches zero
color->Release();
永远不要使用运算符delete删除Noesis实例,因为可能会有很多引用此实例的对象,而您可能忽略它们。您应该改用Release()。这种情况下有BaseRefCounted检测。
可以使用AddReference()和Release()手动处理对组件的引用。例如:
Brush* color0 = new SolidColorBrush(Color::Red); // Ref '1' - Created
Brush* color1 = color0;
color0->AddReference(); // Ref '1' -> '2'
//...
color0->Release(); // Ref '2' -> '1'
color1->Release(); // Ref '1' -> '0' - Destroyed
为了避免手动处理引用计数器,Noesis提供了Ptr <>,它是一个智能指针,可以通过执行AddReference()和Release()来自动处理对象的生存期。Ptr <>重载某些运算符以使其表现类似于指针,但会确保在不再需要对象时将其删除,以避免内存泄漏。
Ptr color0 = *new SolidColorBrush(Color::Red);
Ptr color1(color0);
//...
// color0 and color1 are automatically destroyed when they go out of scope
BaseRefCounted对象以设置为1的参考计数器开始。当它们存储在Ptr <>中时,计数器再次增加1。如您在上面的示例中看到的那样,为了避免必须手动执行Release()来取消该额外的引用,Ptr <>支持基于引用构造。在这种情况下,参考计数器不会再次递增。我们还使用MakePtr提供了更好,更方便的替代方法:
Ptr color0 = MakePtr(Color::Red);
Ptr color1(color0);
//...
// color0 and color1 are automatically destroyed when they go out of scope
Ptr <>隐式转换为原始指针,因此大多数情况下,其用法是完全透明的,您无需关心细节。
// Read XAMLs from current working directory
GUI::SetXamlProvider(MakePtr("."));
// Renderer initialization with an OpenGL device
Ptr device = GLFactory::CreateDevice();
_view->GetRenderer()->Init(device);
注意
为避免总是在堆中创建引用对象,还支持在堆栈中创建的实例。在这种情况下,确保销毁时没有多余的参考是非常重要的。如果检测到该方案,则BaseRefCounted将断言。
装箱(Boxing)
有时需要将基本类型或结构转换为BaseComponent。这会将多态性的开销添加到类型,引用计数器和堆中的分配中,但是在某些情况下,例如在实现值转换器时,这是必需的。
将堆栈类型实例转换为组件的机制称为装箱,并使用Boxing :: Box()执行:
Ptr boxed = Boxing::Box(50.0f);
反向操作称为拆箱。使用Boxing :: Unbox()将装箱的值拆箱:
float val = Boxing::CanUnbox(boxed) ? Boxing::Unbox(boxed) : 0.0f;
尽管装箱在内部进行了优化(例如,使用池来避免内存分配),但应尽可能避免装箱,因为它不能被视为“高效”操作。
以下代码片段实现了一个千位转换器。请注意输入值是如何作为需要取消装箱的BaseComponent实例给出的,以及如何将结果装箱到BaseComponent实例。
bool ThousandConverter::TryConvert(BaseComponent* value, const Type*, BaseComponent*, Ptr& result)
{
if (Boxing::CanUnbox(value))
{
char str[16];
int v = Boxing::Unbox(value);
snprintf(str, sizeof(str), "%.2f K", (float)v / 1000.0f);
result = Boxing::Box(str);
return true;
}
return false;
}
可空值(Nullables)
可空类型是Noesis :: Nullable
Nullable a(nullptr);
assert(!a.HasValue());
assert(a == nullptr);
Nullable b(false);
assert(b.HasValue());
assert(b == false);
assert(b.GetValue() == false);
注意
您可以分配一个值,并与具有可为空值的值进行比较,就像对普通值类型一样。
仅当对象为非空时,才将基于可为空类型的对象装箱。如果HasValue为false,则将对象引用分配为null而不是装箱:
Ptr obj = Boxing::Box(Nullable(nullptr));
assert(obj == nullptr);
委托(Delegates)
委托是回调的通用实现。Noesis中的委托的实现与.NET委托非常相似。委托确保回调方法是类型安全的。委托还集成了顺序调用多个方法的功能,并支持静态方法和实例方法的调用。
声明委托(Declaring Delegates)
使用函数签名声明委托。例如:
/// A delegate with void return and two parameters: int and float
Delegate d;
非成员方法(Non-member Methods)
以下代码将静态方法添加到委托中并调用它:
void Print(int size, float value)
{
printf("%d %4.3f", size, value);
}
void main()
{
// Create and Bind the delegate
Delegate d = &Print;
// Invoke
d(500, 10.0f);
}
成员方法(Member Methods)
以类似的方式,实例方法可以绑定到委托:
struct Printer
{
void Print(const char* string) const
{
printf("%s", string);
}
};
void main()
{
// Create the instance
Printer printer;
// Create and Bind the delegate
Delegate d = MakeDelegate(&printer, &Printer::Print);
// Invoke
d("hi :)");
}
Lambdas
也可以使用C ++ 11 lambda表达式:
void main()
{
Delegate d = [](uint32_t x, uint32_t y) { return x + y; };
assert(d(123, 456) == 579);
}
多播委托(MultiDelegates)
可以使用重载的运算符+ =和-=将委托绑定到多个回调:
struct Printer
{
void Print(const char* string) const
{
printf("Printer: %s", string);
}
};
struct Screen
{
void Print(const char* string) const
{
printf("Screen: %s", string);
}
};
void main()
{
// Create the instances
Printer printer;
Screen screen;
// Create and Bind the delegate
Delegate delegate;
delegate += MakeDelegate(&printer, &Printer::Print);
delegate += MakeDelegate(&screen, &Screen::Print);
// Invoke. This line will call all the callbacks
delegate("hi :)");
}
使用MultiDelegates时,从委托调用返回的值是从上一次调用获得的值。
注意
与C#相反,委托不增加目标实例的引用计数器。这样做是为了避免创建在引用指向包含它的对象时出现的循环引用。这意味着在销毁作为委托目标的实例之前,必须将其从委托中删除。
反射(Reflection)
反射是程序在运行时检查数据的结构和状态并可能对其进行修改的能力。默认情况下,Java或C#语言包含此类功能。但是,在C ++语言中,不能直接获得这种信息。
类
Noesis提供了一些宏,可以轻松地将反射信息合并到类和结构中。这通常用于将反射信息从客户端代码公开给Noesis,例如,在使用连接视图和模型的数据绑定时。
宏有两种,通常在标头中使用的声明宏(NS_DECLARE_REFLECTION):
struct Quest: public BaseComponent
{
bool completed;
NsString title;
NsString description;
Ptr image;
NS_DECLARE_REFLECTION(Quest, BaseComponent)
};
class ViewModel final: public NotifyPropertyChangedBase
{
public:
void SetSelectedQuest(Quest* value);
Quest* GetSelectedQuest() const;
private:
Ptr> _quests;
Ptr _selectedQuest;
NS_DECLARE_REFLECTION(ViewModel, NotifyPropertyChangedBase)
};
以及将在.cpp文件中使用的实现宏(NS_IMPLEMENT_REFLECTION):
NS_IMPLEMENT_REFLECTION(Quest)
{
NsProp("Title", &Quest::title);
NsProp("Image", &Quest::image);
NsProp("Description", &Quest::description);
NsProp("Completed", &Quest::completed);
}
NS_IMPLEMENT_REFLECTION(ViewModel)
{
NsProp("Quests", &ViewModel::_quests);
NsProp("SelectedQuest", &ViewModel::GetSelectedQuest, &ViewModel::SetSelectedQuest);
}
不能同时使用两个宏,而只能使用一个,但是不建议这样做,因为这会在头文件中增加额外的膨胀,从而稍微增加了构建时间。如果可能,请避免使用它,尽管有时是强制性的,例如使用模板。
template struct Vector2
{
T x;
T y;
NS_IMPLEMENT_INLINE_REFLECTION(Vector2, NoParent)
{
NsProp("x", &Vector2::x);
NsProp("y", &Vector2::y);
}
}
注意如何使用NsProp直接公开成员变量,getters和setters或仅公开getters(用于只读属性)。例如:
class Game final: public NotifyPropertyChangedBase
{
public:
void SetSelectedTeam(int selectedTeam)
{
if (_selectedTeam != selectedTeam)
{
_selectedTeam = selectedTeam;
OnPropertyChanged("SelectedTeam");
}
}
int GetSelectedTeam() const
{
return _selectedTeam;
}
Collection* GetVisibleTeams() const
{
return _visibleTeams;
}
private:
int _selectedTeam;
Ptr> _visibleTeams;
NS_IMPLEMENT_INLINE_REFLECTION(Game, NotifyPropertyChangedBase)
{
NsProp("SelectedTeam", &Game::GetSelectedTeam, &Game::SetSelectedTeam);
NsProp("VisibleTeams", &Game::GetVisibleTeams);
}
};
枚举
枚举需要一组不同的宏。对于头文件,必须在全局名称空间中使用NS_DECLARE_REFLECTION_ENUM。
namespace Scoreboard
{
enum class Team
{
Alliance,
Horde,
};
enum class Class
{
Fighter,
Rogue,
Hunter,
Mage,
Cleric,
};
}
NS_DECLARE_REFLECTION_ENUM(Scoreboard::Team)
NS_DECLARE_REFLECTION_ENUM(Scoreboard::Class)
而NS_IMPLEMENT_REFLECTION_ENUM为执行文件。
NS_IMPLEMENT_REFLECTION_ENUM(Scoreboard::Team)
{
NsMeta("Team");
NsVal("Alliance", Team::Alliance);
NsVal("Horde", Team::Horde);
}
NS_IMPLEMENT_REFLECTION_ENUM(Scoreboard::Class)
{
NsMeta("Class");
NsVal("Fighter", Scoreboard::Class::Fighter);
NsVal("Rogue", Scoreboard::Class::Rogue);
NsVal("Hunter", Scoreboard::Class::Hunter);
NsVal("Mage", Scoreboard::Class::Mage);
NsVal("Cleric", Scoreboard::Class::Cleric);
}
注意
与类类似,您可以只使用一个宏NS_IMPLEMENT_INLINE_REFLECTION_ENUM
TypeId
如果需要从XAML实例化类,例如转换器或用户控件,则必须在组件工厂中注册该类。这可以通过将TypeId元数据添加到反射中来实现。
NS_IMPLEMENT_REFLECTION(Scoreboard::ThousandConverter)
{
NsMeta("Scoreboard.ThousandConverter");
}
您还需要在组件工厂中注册该类,然后才能在XAML中使用它。我们的应用程序框架为此目的公开了一个虚拟函数RegisterComponents。在扩展NoesisGUI教程中找到有关的更多信息。
class AppLauncher final: public ApplicationLauncher
{
private:
void RegisterComponents() const override
{
NsRegisterComponent();
NsRegisterComponent();
NsRegisterComponent();
NsRegisterComponent>();
NsRegisterComponent>();
}
};
注意
枚举没有直接注册。EnumConverter必须用于此目的。
一旦在工厂中注册了一个类,就可以在XAML中使用它。例如:
接口(Interfaces)
在极少数情况下,您需要实现接口,必须在相应的反射部分中使用NsImpl helper。
注意
还必须使用NS_IMPLEMENT_INTERFACE_FIXUP来自动实现Noesis :: Interface所需的一些内部功能
class NotifyPropertyChangedBase: public BaseComponent, public INotifyPropertyChanged
{
public:
/// From INotifyPropertyChanged
//@{
PropertyChangedEventHandler& PropertyChanged() override final;
//@}
NS_IMPLEMENT_INTERFACE_FIXUP
protected:
void OnPropertyChanged(const char* name);
private:
PropertyChangedEventHandler _propertyChanged;
NS_DECLARE_REFLECTION(NotifyPropertyChangedBase, BaseComponent)
};
NS_IMPLEMENT_REFLECTION(NotifyPropertyChangedBase)
{
NsImpl();
}
RTTI
在类中具有反射宏还可以在运行时安全地进行强制转换。这与标准dynamic_cast非常相似,但是使用DynamicCast。例如:
Freezable* IsFreezableValue(BaseComponent* value)
{
Freezable* freezable = DynamicCast(value);
if (freezable != 0 && !freezable->IsFrozen())
{
return freezable;
}
else
{
return nullptr;
}
}
DynamicPtrCast也可的情况下,从动态铸造PTR <>到PTR <>是必要的。请注意,这比使用GetPtr()手动获取指针并使用DynamicCast进行强制转换更为有效。
Ptr value = GetLocalValue();
Ptr expr = DynamicPtrCast(value);
BaseBindingExpression* expr_ = DynamicCast(value.GetPtr());