[Qt开发探幽(二)]浅谈关于元对象,宏和Q_ENUM

news2024/9/23 9:31:42

[Qt开发探幽(二)]深入浅出关于元对象,宏和Q_ENUM

  • [Qt开发探幽(二)]深入浅出关于元对象,宏和Q_ENUM
    • 前言
    • 一、元对象
      • 但是
    • 二、关于Q_OBJECT等宏属性
      • 1.元对象系统
      • 2.信号与槽
      • 3.属性系统
    • 三、关于Q_ENUMS
      • 1.将其注册到Q_NAMESPACE下
      • 2.类内注册

[Qt开发探幽(二)]深入浅出关于元对象,宏和Q_ENUM

前言

最近在开发的时候,我自己写了一套虚函数。这也是我第一次写这么大一个框架,遇到了一些有点莫名其妙的问题(也不能算莫名奇妙,只能说有点玩不明白),详情可以见

[Qt开发思想探幽]QObject、模板继承和多继承

前两天我写了一些demo验证了一些我的想法,算是在元对象编程里简单的游了一游。

一、元对象

Qt的元对象是一个让人又爱又恨的东西。让人爱是因为它确实功能强大,可以允许我们从类、枚举类型、获得一些我们在正常C++开发中可能无法正常获取到的东西。比如最简单的:在正常C++开发中,枚举类型的类型名称对于C++而言只是一个有一个的十六进制码,而不是字符串的形式,也不可能获得字符串,那么可能就有如下的奇技淫巧:

在这里插入图片描述
在这里插入图片描述
没错,以上就是通过 Qt的元对象类型将一个枚举类型的成员转换成字符串,或者将字符串转回枚举类型的值

更变态的是什么?

更变态的是,通过元对象类型我们可以实现一个更夸张的功能:让一个类和一个Json字符串之间做转换:

在这里插入图片描述
当然了,做转换的前提是使用Q_PROPERTY宏包裹着属性,这样这个属性就被注册进了这个类的元对象系统内,然后就可以通过一些奇技淫巧,来实现类成员变量和字符串之间的转换了,以下是一个例子:

#pragma region Lev_Json
/// <summary>
/// name:Lev_Json
/// 说明:此类用作辅助参数类与json字符串之间的转换,使用此类请使用Q_PROPERTY声明所有的类成员变量
/// </summary>
class Lev_Json : QObject {

public:
	template<class T1>
	static bool ValidateJsonKeys(const QString& jsonString, const T1* T_Class) {
		QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonString.toUtf8());
		if (!jsonDoc.isObject()) {
			return false;
		}

		QJsonObject jsonObject = jsonDoc.object();
		const QMetaObject* metaObject = T_Class->metaObject();

		for (int i = 0; i < metaObject->propertyCount(); ++i) {
			QMetaProperty property = metaObject->property(i);
			QString propName = property.name();
			if (propName.contains("objectName"))
				continue;
			if (!jsonObject.contains(propName)) {
				return false;
			}
		}

		return true;
	}
	/// <summary>
	/// 判断这个Json字符串对于这个Object而言是否合法
	/// </summary>
	/// <typeparam name="T1"></typeparam>
	/// <param name="jsonString"></param>
	/// <returns></returns>
	template<class T1>
	static bool ValidateJsonKeys(const QString& jsonString, QSharedPointer<T1> T_Class_1) {
		QObject* T_Class = dynamic_cast<QObject*>(T_Class_1.data());
		QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonString.toUtf8());
		if (!jsonDoc.isObject()) {
			return false; // Return false if JSON is not an object
		}

		QJsonObject jsonObject = jsonDoc.object();
		const QMetaObject* metaObject = T_Class->metaObject();

		for (int i = 0; i < metaObject->propertyCount(); ++i) {
			QMetaProperty property = metaObject->property(i);
			QString propName = property.name();

			if (!jsonObject.contains(propName)) {
				return false;
			}
		}

		return true;
	}
	/// <summary>
	/// 推荐,序列化Qt对象,请用Q_PROPERTY包裹成员变量,使用内存安全的QSharedPointer
	/// </summary>
	/// <typeparam name="T1">模板对象,可以不声明,会自动识别</typeparam>
	/// <param name="T_Class_1">输入的对象</param>
	/// <returns></returns>
	template<class T1>
	static QString JsonSerialization(QSharedPointer<T1> T_Class_1) {
		QJsonObject ret;
		QObject* T_Class = dynamic_cast<QObject*>(T_Class_1.data());
		const QMetaObject* metaObject = T_Class->metaObject();

		for (int i = 0; i < metaObject->propertyCount(); ++i) {
			QMetaProperty property_ = metaObject->property(i);
			QVariant propValue = property_.read(T_Class);

			if (!QString(property_.name()).contains("objectName")) {
				ret.insert(property_.name(), variantToJsonValue(propValue));
			}
		}

		QJsonDocument jsonDoc(ret);
		return jsonDoc.toJson(QJsonDocument::Compact);
	}
	/// <summary>
	/// 推荐,反序列化Qt对象,请用Q_PROPERTY包裹成员变量,会返回一个内存安全的QSharedPointer
	/// </summary>
	/// <typeparam name="T1"></typeparam>
	/// <param name="jsonString"></param>
	/// <returns></returns>
	template<class T1>
	static QSharedPointer<T1> JsonDeserialization(const QString& jsonString) {
		QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonString.toUtf8());
		if (!jsonDoc.isObject()) {
			return QSharedPointer<T1>();
		}

		QJsonObject jsonObject = jsonDoc.object();
		QSharedPointer<T1> result = QSharedPointer<T1>::create();

		const QMetaObject* metaObject = result->metaObject();
		for (int i = 0; i < metaObject->propertyCount(); ++i) {
			QMetaProperty property = metaObject->property(i);
			QString propName = property.name();

			if (jsonObject.contains(propName)) {
				QJsonValue propJsonValue = jsonObject[propName];
				QVariant propValue = jsonValueToVariant(propJsonValue, property.userType());

				if (propValue.isValid()) {
					property.write(result.data(), propValue);
				}
			}
		}

		return result;
	}
	/// <summary>
	/// 可以用,序列化Qt对象,请用Q_PROPERTY包裹成员变量
	/// </summary>
	/// <typeparam name="T1">模板对象,可以不声明,会自动识别</typeparam>
	/// <param name="T_Class_1">输入的对象</param>
	/// <returns></returns>
	template<class T1>
	static QString JsonSerialization(const T1* T_Class) {
		QJsonObject ret;

		const QMetaObject* metaObject = T_Class->metaObject();

		for (int i = 0; i < metaObject->propertyCount(); ++i) {
			QMetaProperty property_ = metaObject->property(i);
			QVariant propValue = property_.read(T_Class);

			if (!QString(property_.name()).contains("objectName")) {
				ret.insert(property_.name(), variantToJsonValue(propValue));
			}
		}

		QJsonDocument jsonDoc(ret);
		return jsonDoc.toJson(QJsonDocument::Compact);
	}
	/// <summary>
	/// 不推荐使用,不安全的内存方案
	/// </summary>
	/// <typeparam name="T1"></typeparam>
	/// <param name="result"></param>
	/// <param name="jsonString"></param>
	/// <returns></returns>
	template<class T1>
	static QSharedPointer<T1> JsonDeserialization(T1* result, const QString& jsonString) {
		QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonString.toUtf8());
		if (!jsonDoc.isObject()) {
			return QSharedPointer<T1>();
		}

		QJsonObject jsonObject = jsonDoc.object();
		const QMetaObject* metaObject = result->metaObject();
		for (int i = 0; i < metaObject->propertyCount(); ++i) {
			QMetaProperty property = metaObject->property(i);
			QString propName = property.name();

			if (jsonObject.contains(propName)) {
				QJsonValue propJsonValue = jsonObject[propName];
				QVariant propValue = jsonValueToVariant(propJsonValue, property.userType());

				if (propValue.isValid()) {
					property.write(result.data(), propValue);
				}
			}
		}

		return result;
	}

private:
	static QJsonValue variantToJsonValue(const QVariant& variant) {
		if (variant.canConvert<QString>()) {
			return QJsonValue::fromVariant(variant.toString());
		}
		else if (variant.canConvert<int>()) {
			return QJsonValue::fromVariant(variant.toInt());
		}
		else if (variant.canConvert<double>()) {
			return QJsonValue::fromVariant(variant.toDouble());
		}
		else if (variant.canConvert<bool>()) {
			return QJsonValue::fromVariant(variant.toBool());
		}
		else if (variant.userType() == qMetaTypeId<QList<int>>()) {
			return listToJsonArray<int>(variant.value<QList<int>>());
		}
		else if (variant.userType() == qMetaTypeId<QList<QString>>()) {
			return listToJsonArray<QString>(variant.value<QList<QString>>());
		}
		else if (variant.userType() == qMetaTypeId<QList<bool>>()) {
			return listToJsonArray<bool>(variant.value<QList<bool>>());
		}
		return QJsonValue::Null;
	}

	template<typename T>
	static QJsonArray listToJsonArray(const QList<T>& list) {
		QJsonArray jsonArray;
		for (const T& value : list) {
			jsonArray.append(QJsonValue::fromVariant(value));
		}
		return jsonArray;
	}
	static QVariant jsonValueToVariant(const QJsonValue& jsonValue, int userType) {
		QVariant result;
		if (jsonValue.isString()) {
			result = jsonValue.toString();
		}
		else if (jsonValue.isDouble()) {
			if (userType == QMetaType::Int) {
				result = jsonValue.toInt();
			}
			else if (userType == QMetaType::Double) {
				result = jsonValue.toDouble();
			}
		}
		else if (jsonValue.isBool()) {
			if (userType == QMetaType::Bool) {
				result = jsonValue.toBool();
			}
		}
		else if (jsonValue.isArray()) {
			QJsonArray jsonArray = jsonValue.toArray();
			if (userType == qMetaTypeId<QList<int>>()) {
				QList<int> intList;
				for (const QJsonValue& element : jsonArray) {
					intList.append(element.toInt());
				}
				result = QVariant::fromValue(intList);
			}
			else if (userType == qMetaTypeId<QList<QString>>()) {
				QList<QString> stringList;
				for (const QJsonValue& element : jsonArray) {
					stringList.append(element.toString());
				}
				result = QVariant::fromValue(stringList);
			}
			// Add more cases for other QList types if needed
		}
		return result;
	}
};
#pragma endregion

当然了,Qt的元对象类型还有很多很强大的功能,比如对象名称等等,各种各样的功能,可以拿着Qt当C#来用了(笑)

但是

Qt的元对象类型也有很多局限性。正如我在前言中提到的,正因为Q_OBJECT宏的存在,QObject的对象是不能使用模板类继承的,也不能使用模板类多继承。这个实际上相当限制了Qt程序员的开发能力。模板类作为功能非常强大的一个功能,也正是C++能如此蓬勃发展的一个重要原因,结果在Qt上用不了,这是令人扼腕叹息的。

另外,值得一提的是,我们可以看到,在自己写继承的时候,从一个继承了QObject类和声明了Q_OBJECT宏的类中继承下来的子类仍然带有Q_OBJECT宏 这件事经常会通不过编译,我不知道自己是触犯了哪个规则,但是之后我的底层框架中最底层的部分都不会使用Q_OBJECT宏,直到我搞懂这件事,因为真的为了这个问题做了太多的妥协了。

二、关于Q_OBJECT等宏属性

如果要聊这个宏,我们得看一下这个宏做了什么,找到Qt Document:

Q_OBJECT宏必须出现在类定义的私有部分中,该类定义声明自己的信号和槽,或者使用Qt的元对象系统提供的其他服务。
#include <QObject>

class Counter : public QObject
{
    Q_OBJECT

public:
    Counter() { m_value = 0; }

    int value() const { return m_value; }

public slots:
    void setValue(int value);

signals:
    void valueChanged(int newValue);

private:
    int m_value;
};
注意:这个宏要求类是QObject的子类。使用Q_GADGET或Q_GADGET_EXPORT而不是Q_OBJECT来启用元对象系统对非QObject子类中的枚举的支持。

Q_OBJECT宏我们可以看到,主要是做了三件事:
1.将指定的类注册进入到元对象系统内,至于什么是元对象系统,我们接下来会说,你先知道是注册进元对象系统就行了
2.添加信号与槽函数的注册
3.注册Qt的属性系统

这三个功能其实也构成了Qt这套框架的全部,可以说Qt整套系统都是围绕着Q_OBJECT宏来做的。

1.元对象系统

元对象系统
Qt的元对象系统(Meta-Object System)为对象间通信、运行时类型信息和动态属性系统提供了信号和槽机制。元对象系统基于三个方面:

  1. QObject类为可以利用元对象系统的对象提供了一个基类。

  2. 类声明的私有部分中的Q_OBJECT宏用于启用元对象功能,如动态属性、信号和插槽。

  3. 元对象编译器(moc)为每个QObject子类提供实现元对象特性所需的代码。

我们可以理解为,元对象系统就是Qt的一个“C#化”的尝试,即将原来在C++中不可见的一切

moc工具读取一个C++源文件。如果它找到一个或多个包含Q_OBJECT宏的类声明,它将生成另一个C++源文件,该文件包含每个类的元对象代码。这个生成的源文件要么被#包含到类的源文件中,要么更常见的是,被编译并链接到类的实现中。

除了提供用于对象之间通信的信号和槽机制(引入该系统的主要原因)之外,元对象代码还提供以下附加功能:

  • QObject::metaObject()返回类的关联元对象。

  • QMetaObject::className()在运行时以字符串形式返回类名,而不需要通过C++编译器支持本机运行时类型信息(RTTI)。

  • 函数返回对象是否是继承QObject继承树中指定类的类的实例。

  • QObject::tr()转换字符串以进行国际化。

  • QObject::setProperty()和QOobject::property()按名称动态设置和获取属性。

  • QMetaObject::newInstance()构造类的一个新实例。

还可以使用qobject_cast()对qobject类执行动态强制转换。qobject_cast()函数的行为类似于标准C++dynamic_cast(),其优点是不需要RTTI支持,并且可以跨动态库边界工作。它尝试将其参数强制转换为尖括号中指定的指针类型,如果对象的类型正确(在运行时确定),则返回非零指针,如果对象类型不兼容,则返回nullptr。

虽然可以在没有Q_OBJECT宏和元对象代码的情况下使用QObject作为基类,但如果不使用Q_OBJECT宏,则信号和插槽以及此处描述的其他功能都不可用。

从元对象系统的角度来看,一个没有元代码的QObject子类等价于它最接近的有元对象代码的祖先。

这意味着,例如,QMetaObject::className()不会返回类的实际名称,而是返回该祖先的类名。
因此,我们强烈建议QObject的所有子类使用Q_OBJECT宏,无论它们是否实际使用信号、槽和属性。

2.信号与槽

在Qt中的信号与槽可以说是Qt的头牌系统,也是Qt这套东西能够如此流行的重要原因,也是整个Qt框架最重要的基石。

当然了,其实自己实现一套Qt的Signal - Slot的系统其实并不复杂,而且肯定很多人已经能开发一套类似的东西了。比如我简单打个样:

class Caller {
public:
	using CallMethod = void(*)(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra);
	using SendCMD = void(*)(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra);

	void RegisterCallMethod(CallMethod callback) {
		callbacks_.append(callback);
	}
	void RegisterSendCMD(SendCMD callback) {
		sendcmds_.append(callback);
	}

	void Signal_CallMethod(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra) {
		for (CallMethod callback : callbacks_) {
			if (callback) {
				callback(sModule, sDescribe, sVariable, extra);
			}
		}
	}

	void Signal_SendCMD(const QString& sModule, const QString& sDescribe, const QString& sVariable, const QVariant& extra) {
		for (SendCMD callback : callbacks_) {
			if (callback) {
				callback(sModule, sDescribe, sVariable, extra);
			}
		}
	}

private:
	QList<CallMethod> callbacks_;
	QList<SendCMD> sendcmds_;
};

但是Qt的signal - slot 强大的地方就在于它的封装性和灵活性,各种注销注册操作相对自己写回调函数还是简单很多很多的。你想啊,原先需要这么多代码的地方,现在只需要一个宏,或者一句话,难易程度几乎无法比较。

由于Qt独特的signal索引机制,导致其网络相关的库效率可能是C++回调函数的百分之一,这是非常夸张的性能损失,但是这在某些性能不关键的场景仍然是可以接受的。

Signals & Slots

Signals 和Slots用于对象之间的通信。Signals 和Slots机制是Qt的一个核心功能,可能也是与其他框架提供的功能最不同的部分。Qt的元对象系统使Signals 和Slots成为可能。

其他工具包使用回调来实现这种通信。回调是指向函数的指针,因此,如果您希望处理函数通知您某个事件,您可以将指向另一个函数的指针(回调)传递给处理函数。然后,处理函数在适当的时候调用回调。虽然使用这种方法的成功框架确实存在,但回调可能是不直观的,并且在确保回调参数的类型正确性方面可能会遇到问题。

在Qt中,我们有一种替代回调技术的方法:我们使用Signals 和Slots。当特定事件发生时,会发出一个信号。Qt的小部件有许多预定义的Signals ,但我们总是可以对小部件进行子类化,以向它们添加我们自己的Signals。Slots是响应特定信号而调用的函数。Qt的小部件有许多预定义的Slots,但通常的做法是对小部件进行子类化,并添加自己的Slots,以便处理您感兴趣的Signals。

Signal和Slot机制是类型安全的:Signal的签名必须与接收Slot的签名匹配。(事实上,Slot的签名可能比它接收到的Signal更短,因为它可以忽略额外的参数。)

由于签名是兼容的,编译器可以在使用基于函数指针的语法时帮助我们检测类型不匹配。基于字符串的SIGNAL和SLOT语法将在运行时检测类型不匹配。

Signal和Slot是松散耦合的:发出Signal的类既不知道也不关心哪个Slot接收Signal。Qt的Signal和Slot机制确保,如果您将Signal连接到Slot,Slot将在正确的时间使用Signal的参数进行调用。Signal和Slot可以采用任何类型的任意数量的参数。

它们是完全类型安全的。 所有继承自QObject或其子类之一(例如,QWidget)的类都可以包含Signal和Slot。当对象以其他对象可能感兴趣的方式改变其状态时,它们会发出Signal。

这就是对象所做的所有通信。它不知道或不关心是否有任何东西正在接收它发出的Signal。这是真正的信息封装,并确保对象可以用作软件组件。

Slot可以用于接收Signal,但它们也是正常的成员功能。就像一个对象不知道是否有任何东西接收到它的Signal一样,一个Slot也不知道它是否有任何Signal连接到它。这确保了可以用Qt创建真正独立的组件。

您可以将任意数量的Signal连接到单个Slot,也可以将Signal连接到任意数量的Slot。甚至可以将一个Signal直接连接到另一个Signal。(无论何时发出第一个Signal,都会立即发出第二个Signal。)

Signal和Slot共同构成了一个强大的组件编程机制。 Signal 当对象的内部状态以某种可能对对象的客户端或所有者感兴趣的方式发生变化时,对象会发出Signal。Signal是公共访问函数,可以从任何地方发出,但我们建议只从定义Signal及其子类的类发出Signal。

当一个Signal发出时,连接到它的Slot通常会立即执行,就像正常的函数调用一样。当这种情况发生时,Signal和Slot机制完全独立于任何GUI事件循环。一旦所有Slot都返回,就会执行emit语句后面的代码。使用排队连接时,情况略有不同;

在这种情况下,emit关键字后面的代码将立即继续,稍后将执行Slot。 如果多个Slot连接到一个Signal,则当Signal发出时,这些Slot将按照连接的顺序依次执行。 Signal由moc自动生成,不得在.cpp文件中实现。它们永远不能有返回类型(即使用void)。

关于arguments的注意事项:我们的经验表明,如果Signal和Slot不使用特殊类型,它们将更易于重用。如果QScrollBar::valueChanged()使用一种特殊类型,如假设的QScrollBar::Range,则它只能连接到专门为QScrollBar设计的Slot。

将不同的输入小部件连接在一起是不可能的。 Slot 当连接到Slot的Signal发出时,就会调用该Slot。Slot是正常的C++函数,可以正常调用;它们唯一的特点是Signal可以连接到它们。 由于Slot是正常的成员函数,因此当直接调用时,它们遵循正常的C++规则。

但是,作为Slot,它们可以由任何组件通过SignalSlot连接调用,而不管其访问级别如何。这意味着,从任意类的实例发出的Signal可以导致在不相关类的实例中调用专用Slot。 您还可以将Slot定义为虚拟Slot,我们发现这在实践中非常有用。

与回调相比,Signal和Slot的速度稍慢,因为它们提供了更大的灵活性,尽管实际应用程序的差异并不显著。通常,发射连接到某些Slot的Signal比直接调用接收器(使用非虚拟函数调用)慢大约十倍。这是定位连接对象、安全地迭代所有连接(即检查后续接收器在发射过程中是否未被破坏)以及以通用方式整理任何参数所需的开销。虽然十个非虚拟函数调用听起来可能很多,但它的开销比任何新操作或删除操作都要小得多。

一旦执行了一个字符串、向量或列表操作,而该操作在后台需要新建或删除,则Signal和Slot开销只占整个函数调用成本的一小部分。无论何时进行系统调用都是如此

3.属性系统

Qt提供了一个复杂的属性系统,类似于一些编译器供应商提供的属性系统。然而,作为一个独立于编译器和平台的库,Qt不依赖于__property或[property]等非标准编译器功能。Qt解决方案可与Qt支持的每个平台上的任何标准C++编译器配合使用。它基于元对象系统,该系统还通过信号和插槽提供对象间通信。

他其实更像是C#中的一个get set方法,相当于是将这个属性注册到元对象系统中去,并且给每个对象提供了一个get set方法(当然了,get set方法也只是你定义的,这又不是真的c#)

具体的属性系统这里我不做过多介绍,详情可以参考Qt Document

The Property System

其中有非常详尽的解释。

三、关于Q_ENUMS

Q_ENUM这个宏经过了几次修改,早期貌似可以随意注册Q_ENUMS,但是在后续貌似只剩下了两种枚举类型的注册方法:

一个是在类内声明枚举类型,然后在类内声明这个Q_ENUM,当然了,用这个宏去注册枚举类型的前提是使用了Q_OBJECT宏

现在假设我们想在元对象系统中使用这个枚举类,也就是我想通过它的int值获得其映射的key(字符串形式),比如如下这个枚举类型

在这里插入图片描述

test_enum::Test_Enum_1 tester = test_enum::Test_Enum_1::none;

我现在可能是传递Json字符串,或者是别的什么,反正我就是要获得none这个关键字,那我该怎么做?

这个时候你有两个做法,但是实际上都是将其注册到元对象

1.将其注册到Q_NAMESPACE下

启用一个单独的namespace,通过Q_NAMESPACE宏的形式将这个命名空间注册到Qt的元对象系统内,举个例子:

namespace test_enum {
	Q_NAMESPACE	//Q_NAMESPACE宏将整个命名空间注册进元对象列表中去
		enum class Test_Enum_1 {
		none,
		open,
		close,
		stop
	};
	Q_ENUM_NS(Test_Enum_1) //Q_ENUM_NS宏将我们需要的枚举类型对象注册进
}

2.类内注册

除此之外,还有另一种方法,那就是将枚举类型写入到用Q_OBJECT, Q_GADGET or Q_GADGET_EXPORT这三个宏之一标记的类内

需要注意的一点:Q_GADGET是Q_OBJECT宏的轻量化版本,用Q_GADGET意味着这个类不一定需要继承QObject类了

适用于不继承QObject但仍希望使用QMetaObject提供的一些反射功能的类。就像Q_OBJECT宏一样,它必须出现在类定义的私有部分中。

Q_GADGET可以有Q_ENUM、Q_PROPERTY和Q_INVOKABLE,但不能有信号或插槽。
Q_GADGET使类成员staticMetaObject可用。staticMetaObject的类型为QMetaObject,并提供对用Q_ENUM声明的枚举的访问。

如以下代码:

class TSG_Device : public TSG_Caller {
	/// <summary>
/// 设备状态
/// </summary>

public:
	enum class DeviceState
	{
		DS_None,
		DS_Unknown,
		DS_Disconnected,
		DS_Connected,
		DS_Working,
		DS_Pause,
		DS_Stop
	}; Q_ENUM(DeviceState)

		enum class DeviceOpen {
		DO_Open,
		DO_Close
	}; Q_ENUM(DeviceOpen)
}

这样一个内嵌的枚举类,也可以用QMetaEnum做到之前我们想要做的事

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/958950.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

让文字会说话,启英泰伦离线语音合成(TTS)技术全面升级!

• A01&#xff0c;请用餐 • 请001号到03号窗口办理业务 • 本次列车即将到达火车南站&#xff0c;请提前准备下车 语音合成&#xff08;TTS&#xff09;技术作为人工智能领域的一项重要技术&#xff0c;已经深入大众生活&#xff0c;无孔不入。通过将文字转化为生动自然的…

伦敦银杠杆怎么用才对?

在了解伦敦银这个投资方式后&#xff0c;投资者应该明白它是怎样的高杠杆的品种&#xff0c;能够为自己带来多高的回报&#xff0c;然而杠杆的作用是双向的&#xff0c;如果使用不当&#xff0c;对投资者的损害同样也会很大。那么伦敦银的杠杆&#xff0c;到底怎么使用才对呢&a…

如何优化 SketchUp 性能并避免卡顿、崩溃?

SketchUp 是建筑师、工程师、设计师和艺术家使用的流行 3D 建模软件。然而&#xff0c;随着模型变得更加复杂&#xff0c;用户可能会遇到延迟甚至崩溃的情况。在本文档中&#xff0c;我们将提供经过验证的技术来帮助您优化 SketchUp 性能并避免这些令人烦恼的问题。 1.使用高效…

Django(7)-项目实战-发布会签到管理系统

本文使用django实现一个简单的发布会签到管理系统 登录功能 模板页面 sign/templates/index.html <!DOCTYPE html> <html> <head><title>Login Page</title> </head> <body><h1>发布会管理</h1><form action=&qu…

js+canvas实现绘制正方形并插入文字,实现文字居中显示。

目录 一、实现效果二、实现思路三、代码实现四、代码解析五、问题六、改进后的代码1.效果图2.思路3.代码实现 一、实现效果 二、实现思路 1.先根据传入的文本内容&#xff0c;计算出文本的宽度。 2.文本宽度左右间距&#xff0c;得到正方形的边长、画布宽度。 3.在&#xff08…

Python数据分析与展示-Numpy

numpy入门 ndarray对象的属性 import numpy as npdef sun():a np.array([1,2,3,4])b np.array([5,6,7,8])c a**2 b**3return cprint(sun())a np.array([[1, 2, 3, 4],[4, 5, 6, 7]])print(a.ndim) print(a.shape) print(a.size) print(a.dtype) print(a.itemsize) ndarra…

vue3-styled-components inject导致死循环

npm: vue3-styled-components - npm 低版本浏览器报错(chrome 49) Uncaught RangeError: Maximum call stack size exceeded [Vue warn]: Avoid app logic that relies on enumerating keys on a component instance, The keys will be empty in production mode to avoid p…

Vue安装过程的困惑解答——nodejs和vue关系、webpack、vue-cli、vue的项目结构

文章目录 1、为什么在使用vue前要下载nodejs&#xff1f;2、为什么安装nodejs后就能使用NPM包管理工具&#xff1f;3、为什么是V8引擎并且使用C实现&#xff1f;4、为什么会安装淘宝镜像&#xff1f;5、什么是webpack模板&#xff1f;6、什么是脚手架 vue-cli&#xff1f;6.1 安…

【C++】C++11新特性 lambda表达式

C11新特性 lambda表达式1、引入2、lambda表达式语法3、 捕获列表说明4、 lambda表达式的原理5、 lambda对象的大小 lambda表达式 1、引入 在C98中&#xff0c;如果想要对一个数据集合中的元素进行排序&#xff0c;可以使用std::sort方法&#xff0c;如果待排序元素为自定义类…

前端:html实现页面切换、顶部标签栏(可删、可切换,点击左侧超链接出现标签栏)

一、在一个页面&#xff08;不跨页面&#xff09; 效果&#xff1a; 代码 <!DOCTYPE html> <html><head><style>/* 设置标签页外层容器样式 */.tab-container {width: 100%;background-color: #f1f1f1;overflow: hidden;}/* 设置标签页选项卡的样式 …

Synchronized 锁升级的原理

问题解析 Synchronized 在 jdk1.6 版本之前&#xff0c;是通过重量级锁的方式来实现线程之间锁的竞争。 之所以称它为重量级锁&#xff0c;是因为它的底层依赖操作系统的 Mutex Lock 来实现互斥功能。 如下图所示&#xff0c; Mutex 是系统方法&#xff0c;由于权限隔…

【数据分析】使用Python对数据的操作转换

使用Python对数据的操作转换 目录 1、列表加值转字典2、字典键新增值数据3、转换新的字典格式4、两组数据比较筛选5、将两段独立代码合并1、列表加值转字典 在Python中&#xff0c;将列表的值转换为字典的键可以使用以下代码&#xff1a; #!/usr/bin/env python # -*- cod…

详细教程-Chatbot具体需要如何搭建

最近建设企业专属的帮助中心或者博客是很流行的趋势&#xff0c;尤其是在AI功能的介入下&#xff0c;可以帮到企业和用户进行更自助的操作。但是很多企业会遇到以下问题&#xff1a;我们已经有了一个原有的知识库了&#xff0c;再建设一个新的需要把数据迁移。这个操作很麻烦和…

科技资讯|微软获得AI双肩包专利,Find My防丢背包大火

根据美国商标和专利局&#xff08;USPTO&#xff09;近日公示的清单&#xff0c;微软于今年 5 月提交了一项智能双肩包专利&#xff0c;其亮点在于整合了 AI 技术&#xff0c;可以识别佩戴者周围环境、自动响应用户聊天请求、访问基于云端的信息、以及和其它设备交互。 在此附…

单片机电子元器件-按键

电子元器件 按键上有 四个引脚 1 2 、 3 4 按下之后 导通 1 3 、 2 4 初始导通 通常按键开关为机械弹性开关&#xff0c;开关在闭合不会马上稳定的接通&#xff0c;会有一连串的抖动 抖动时间的长短有机械特性来决定的&#xff0c;一般为5ms 到10 ms 。 消抖的分类 硬件消…

LSM树详解

LSM树(Log-Structured-Merge-Tree)的名字往往会给初识者一个错误的印象&#xff0c;事实上&#xff0c;LSM树并不像B树、红黑树一样是一颗严格的树状数据结构&#xff0c;它其实是一种存储结构&#xff0c;目前HBase,LevelDB,RocksDB这些NoSQL存储都是采用的LSM树。 LSM树的核…

开源PHP 代挂机源码,可对接QQ、网易云、哔哩哔哩、QQ空间、等级加速等等

本程序运行环境PHP5.6 95dg/config.php修改系统数据库 进入数据库绑定 你搭建的域名即可 部署完成 进入数据库 找到data 输入绑定授权域名即可进行授权打开此网站 网站是无对接接口 需要您自行找对接接口即可 本源码有点乱 有实力的铁铁 可以修改一下哦&#xff01;

linux kvm网桥br简单理解和持久化配置

linux网桥简单理解和持久化配置 文章目录 前言一、Linux 网桥是什么&#xff1f;二、网桥主要作用三、网桥配置命令及安装(CentOS系统) 1 网桥配置命令2.持久化网桥配置 前言 linux bridge是网络虚拟化中非常重要的一种设备&#xff0c;今天就来学习下linux bridge的相关知…

360牛盾点选

网址&#xff1a;https://info.so.com/cache_remove.html 360旗下的产品&#xff0c;协议并不难。 感兴趣的话大家可以去看看&#xff0c;一个AES&#xff0c;坐标需要缩放处理。 鱼导就是牛&#xff0c;还没失败过。 完事儿了哦&#xff0c;大表哥们。以上需要算法&#xff0…

Qt5界面Qt Designer上添加资源图片后,ModuleNotFoundError: No module named ‘rcc_rc‘ 的终极解决方案

在网上找了很久都没弄明白&#xff0c;最后还是自己思考解决了。 起因&#xff1a; 用 Qt Designer 添加资源文件作为背景图&#xff0c;编译 \resource\static\qrc> pyuic5 -o .\xx.py .\xx.ui发现在 xx.py 文件末尾中多了一个语句&#xff1a; import rcc_rc然后运行就…