Qt编写自定义控件属性设计器

之前作.NET开发中,.NET直接就集成了属性设计器,VS不愧是宇宙第一IDE,你可以想到的都给你封装好了,用起来不要太爽!由于项目须要自从全面转Qt开发已经6年有余,在工业控制领域,有一些应用场景须要自定义绘制一些控件知足特定的需求,好比仪器仪表、组态等,并且须要直接用户经过属性设计的形式生成导出控件及界面数据,下次导入使用,要想从内置控件或者自定义控件拿到对应的属性方法等,首先联想到的就是反射,Qt反射对应的类叫QMetaObject,着实强大,其实整个Qt开发框架也是超级强大的,本人自从转为Qt开发为主后,就深深的爱上了她,在其余跨平台的GUI开发框架平台面前,都会被Qt秒成渣,Qt的跨平台性是毋庸置疑的,几十兆的内存存储空间便可运行,尤为是嵌入式linux这种资源至关紧张的状况下,Qt的性能发挥到极致。node

接下来咱们就一步步利用QMetaObject类和QtPropertyBrower(第三方开源属性设计器)来实现本身的控件属性设计器,其中包含了所见即所得的控件属性控制,以及xml数据的导入导出。linux

第一步:获取控件的属性名称集合。android

全部继承自QObject类的类,都有元对象,均可以经过这个QObject类的元对象metaObject()获取属性+事件+方法等。canvas

代码以下:安全

QPushButton *btn = new QPushButton;
const QMetaObject *metaobject = btn->metaObject();
int count = metaobject->propertyCount();
for (int i = 0; i < count; ++i) {
    QMetaProperty metaproperty = metaobject->property(i);
    const char *name = metaproperty.name();
    QVariant value = btn->property(name);
    qDebug() << name << value;
}

打印输出以下:多线程

objectName QVariant(QString, "")
modal QVariant(bool, false)
windowModality QVariant(int, 0)
enabled QVariant(bool, true)
geometry QVariant(QRect, QRect(0,0 640x480))
frameGeometry QVariant(QRect, QRect(0,0 639x479))
normalGeometry QVariant(QRect, QRect(0,0 0x0))
省略后面不少…

能够看到打印了不少父类的属性,这些基本上咱们不须要的,那怎么办呢,放心,Qt确定帮咱们考虑好了,该propertyOffset上场了。metaObject->propertyOffset()表示出了父类外,本身类自己属性的偏移位置即索引开始的位置,这下就好办了。app

代码改成:框架

QPushButton *btn = new QPushButton;
const QMetaObject *metaobject = btn->metaObject();
int count = metaobject->propertyCount();
int index = metaobject->propertyOffset();
for (int i = index; i < count; ++i) {
    QMetaProperty metaproperty = metaobject->property(i);
    const char *name = metaproperty.name();
    QVariant value = btn->property(name);
    qDebug() << name << value;
}

就是将i的起始位置改成偏移位置便可。dom

打印输出以下:函数

autoDefault QVariant(bool, false)
default QVariant(bool, false)
flat QVariant(bool, false)

这个过滤很是有用,由于真实用到的大部分应用场景都是控件类自己的属性,而不是父类的。

第二步:将控件类绑定到属性设计器。

拿到了控件的属性是第一步,接下来就是须要拿到属性所关联的方法等,这里省略,由于QtPropertyBrower这个屌爆了的第三方开源的属性设计器,所有给咱们写好了,能够查看Qt帮助文档或者QMetaObject的头文件看到,QMetaObject提供了哪些接口去获取或使用这些元信息。好比classInfo获取类的信息、enumerator获取枚举值信息、method获取方法,property获取属性、superClass获取父类的名称等。

QtPropertyBrower中提供了ObjectController类,该类继承自QWidget,这样的话咱们在界面上拖一个QWidget控件,鼠标右键提高为ObjectController便可。

这个轮子造的不要太好,咱们只须要一行代码就可让全部属性自动罗列到属性设计器中,代码是ui->objectController->setObject(btn);

看下效果如图:

到这里是否是很兴奋呢,任意控件均可以这样来展现本身的属性。在右侧动态更改属性会当即应用生效。

第三步:获取自定义控件的插件的全部控件。

接下来这一步才是最关键的一步,以上举例是Qt自带控件的,若是是自定义控件插件好比就一个DLL文件呢,怎么办?放心,办法确定是有的。

该插件类QPluginLoader上场了。经过QPluginLoader载入后的实例,经过QDesignerCustomWidgetCollectionInterface类获取插件容器,而后逐个遍历容器找出单个插件,包括得到类名+图标。

 代码以下:

void frmMain::openPlugin(const QString &fileName)
{
    qDeleteAll(listWidgets);
    listWidgets.clear();
    listNames.clear();
    ui->listWidget->clear();
    //加载自定义控件插件集合信息,包括得到类名+图标
    QPluginLoader loader(fileName);
    if (loader.load()) {
        QObject *plugin = loader.instance();
        //获取插件容器,而后逐个遍历容器找出单个插件
        QDesignerCustomWidgetCollectionInterface *interfaces = qobject_cast<QDesignerCustomWidgetCollectionInterface *>(plugin);
        if (interfaces)  {
            listWidgets = interfaces->customWidgets();
            int count = listWidgets.count();
            for (int i = 0; i < count; i++) {
                QIcon icon = listWidgets.at(i)->icon();
                QString className = listWidgets.at(i)->name();
                QListWidgetItem *item = new QListWidgetItem(ui->listWidget);
                item->setText(className);
                item->setIcon(icon);
                listNames << className;
            }
        }
        //获取全部插件的类名
        const QObjectList objList = plugin->children();
        foreach (QObject *obj, objList) {
            QString className = obj->metaObject()->className();
            //qDebug() << className;
        }
    }
}

效果图以下:

第四步:实例化new出控件并放到窗体。

拿到了全部的控件,前面还有个对应控件的小图标,是否是又有点小激动呢,接下来就是怎么双击或者拖动该控件到界面上立马实例化一个控件出来。上一步咱们将全部控件放到了一个链表变量listWidgets中,该变量在头文件中定义以下:

QList<QDesignerCustomWidgetInterface *> listWidgets

这里写了个函数,传入列表中控件的索引,即该类的索引位置,和控件默认要放置的坐标,便可在主界面生成该控件。

代码以下:

void frmMain::newWidget(int row, const QPoint &point)
{
    //列表按照一样的索引生成的,因此这里直接对该行的索引就行
    QWidget *widget = listWidgets.at(row)->createWidget(ui->centralwidget);
    widget->move(point);
    widget->resize(widget->sizeHint());
    //实例化选中窗体跟随控件一块儿
    newSelect(widget);
    //当即执行获取焦点以及设置属性
    widgetPressed(widget);
}

第五步:动态绑定控件到设计器。

这一步就比较轻松了,上面提到过,直接获取当前界面上选中的是哪一个控件,遍历能够获得,而后设置object到属性设计器控件便可。

代码以下:

void frmMain::clearFocus()
{
    //将原有焦点窗体所有设置成无焦点
    foreach (SelectWidget *widget, selectWidgets) {
        widget->setDrawPoint(false);
    }
} 

void frmMain::widgetPressed(QWidget *widget)
{
    //清空全部控件的焦点
    clearFocus();
    //设置当前按下的控件有焦点
    foreach (SelectWidget *w, selectWidgets) {
        if (w->getWidget() == widget) {
            w->setDrawPoint(true);
            break;
        }
    }
    //设置自动加载该控件的全部属性
    ui->objectController->setObject(widget);
}

第六步:导入导出控件属性到xml文件。

这一步比较难,本人也是花了好几个小时才搞定,先后折腾了好屡次,由于遇到好几个棘手的问题,好比有些自定义控件中其实里边封装了Qt自带的控件例如QPushButton等,若是遍历控件设计窗体的全部控件,也会把该控件也遍历进去,因此要作过滤处理。

导入xml数据自动生成控件代码以下:

void frmMain::openFile(const QString &fileName)
{
    //打开文件
    QFile file(fileName);
    if (!file.open(QFile::ReadOnly | QFile::Text)) {
        return;
    }

    //将文件填充到dom容器
    QDomDocument doc;
    if (!doc.setContent(&file)) {
        file.close();
        return;
    }
    file.close();
    //先清空原有控件
    QList<QWidget *> widgets = ui->centralwidget->findChildren<QWidget *>();
    qDeleteAll(widgets);
    widgets.clear();
    //先判断根元素是否正确
    QDomElement docElem = doc.documentElement();
    if (docElem.tagName() == "canvas") {
        QDomNode node = docElem.firstChild();
        QDomElement element = node.toElement();
        while(!node.isNull()) {
            QString name = element.tagName();
            //存储坐标+宽高
            int x, y, width, height;
            //存储其余自定义控件属性
            QList<QPair<QString, QVariant> > propertys;
            //节点名称不为空才继续
            if (!name.isEmpty()) {
                //遍历节点的属性名称和属性值
                QDomNamedNodeMap attrs = element.attributes();
                for (int i = 0; i < attrs.count(); i++) {
                    QDomNode n = attrs.item(i);
                    QString nodeName = n.nodeName();
                    QString nodeValue = n.nodeValue();
                    //qDebug() << nodeName << nodeValue;
                    //优先取出坐标+宽高属性,这几个属性不能经过setProperty实现
                    if (nodeName == "x") {
                        x = nodeValue.toInt();
                    } else if (nodeName == "y") {
                        y = nodeValue.toInt();
                    } else if (nodeName == "width") {
                        width = nodeValue.toInt();
                    } else if (nodeName == "height") {
                        height = nodeValue.toInt();
                    } else {
                        propertys.append(qMakePair(nodeName, QVariant(nodeValue)));
                    }
                }
            }
            //qDebug() << name << x << y << width << height;
            //根据不一样的控件类型实例化控件
            int count = listWidgets.count();
            for (int i = 0; i < count; i++) {
                QString className = listWidgets.at(i)->name();
                if (name == className) {
                    QWidget *widget = listWidgets.at(i)->createWidget(ui->centralwidget);

                    //逐个设置自定义控件的属性
                    int count = propertys.count();
                    for (int i = 0; i < count; i++) {
                        QPair<QString, QVariant> property = propertys.at(i);
                        widget->setProperty(property.first.toLatin1().constData(), property.second);
                    }
                    //设置坐标+宽高
                    widget->setGeometry(x, y, width, height);
                    //实例化选中窗体跟随控件一块儿
                    newSelect(widget);
                    break;
                }
            }
            //移动到下一个节点
            node = node.nextSibling();
            element = node.toElement();
        }
    }
}

导出全部控件到xml文件代码以下:

void frmMain::saveFile(const QString &fileName)
{
    QFile file(fileName);
    if (!file.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) {
        return;
    }
    //以流的形式输出文件
    QTextStream stream(&file);
    //构建xml数据
    QStringList list;
    //添加固定头部数据
    list << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
    list << QString("<canvas width=\"%1\" height=\"%2\">")
         .arg(ui->centralwidget->width()).arg(ui->centralwidget->height());
    //从容器中找到全部控件,根据控件的类名保存该类的全部属性
    QList<QWidget *> widgets = ui->centralwidget->findChildren<QWidget *>();
    foreach (QWidget *w, widgets) {
        const QMetaObject *metaObject = w->metaObject();
        QString className = metaObject->className();
        QStringList values;
        //若是当前控件的父类不是主窗体则无需导出,有些控件有子控件无需导出
        if (w->parent() != ui->centralwidget || className == "SelectWidget") {
            continue;
        }
        //metaObject->propertyOffset()表示当前控件的属性开始索引,0开始的是父类的属性
        int index = metaObject->propertyOffset();
        for (int i = index; i < metaObject->propertyCount(); i++) {
            QMetaProperty p = metaObject->property(i);
            QString nodeName = p.name();
            QVariant nodeValue = p.read(w);
            //枚举值要特殊处理,须要以字符串形式写入,否则存储到配置文件数据为int
            if (p.isEnumType()) {
                QMetaEnum enumValue = p.enumerator();
                nodeValue = enumValue.valueToKey(nodeValue.toInt());
            }
            QString temp = nodeValue.toString().toLocal8Bit().constData();
            values << QString("%1=\"%2\"").arg(nodeName).arg(temp);
            //qDebug() << nodeName << nodeValue;
        }
        //逐个添加界面上的控件的属性
        QString str = QString("\t<%1 x=\"%2\" y=\"%3\" width=\"%4\" height=\"%5\" %6/>")
                      .arg(className).arg(w->x()).arg(w->y()).arg(w->width()).arg(w->height()).arg(values.join(" "));
        list << str;
    }
    //添加固定尾部数据
    list << "</canvas>";
    //写入文件
    QString data = list.join("\n");
    stream << data;
    file.close();}

  xml数据格式效果图:

完整效果图:

最后分享一些本身整理好的Qt开发过程当中的小技巧,Qt武林秘籍。

1:当编译发现大量错误的时候,从第一个看起,一个一个的解决,不要急着去看下一个错误,每每后面的错误都是因为前面的错误引发的,第一个解决后极可能都解决了。

2:定时器是个好东西,学会好使用它,有时候用QTimer::singleShot能够解决意想不到的问题。

3:打开creator,在构建套件的环境中增长MAKEFLAGS=-j8,能够不用每次设置多线程编译。珍爱时间和生命。

4:若是你想顺利用QtCreator部署安卓程序,首先你要在AndroidStudio 里面配置成功,把坑所有趟平。

5:不少时候找到Qt对应封装的方法后,记得多看看该函数的重载,多个参数的,你会发现不同的世界,有时候会恍然大悟,原来Qt已经帮咱们封装好了。

6:能够在pro文件中写上标记版本号+ico图标

VERSION             = 2018.7.25
win32:RC_ICONS      = main0.ico 

7:管理员运行程序,限定在MSVC编译器。

QMAKE_LFLAGS += /MANIFESTUAC:\"level=\'requireAdministrator\' uiAccess=\'false\'\" #以管理员运行
QMAKE_LFLAGS += /SUBSYSTEM:WINDOWS,\"5.01\" #VS2013 在XP运行 

8:运行文件附带调试输出窗口,有时候程序双击了没有反应,这样能够很方便的知道哪里出了问题。

CONFIG += console pro 

9:绘制平铺背景QPainter::drawTiledPixmap

绘制圆角矩形QPainter::drawRoundedRect(),而不是QPainter::drawRoundRect(); 

10:移除旧的样式

style()->unpolish(ui->btn);

从新设置新的该控件的样式。

style()->polish(ui->btn); 

11:获取类的属性

const QMetaObject *metaobject = object->metaObject();
int count = metaobject->propertyCount();
for (int i = 0; i < count; ++i) {
    QMetaProperty metaproperty = metaobject->property(i);
    const char *name = metaproperty.name();
    QVariant value = object->property(name);
    qDebug() << name << value;
} 

12:Qt内置图标封装在QStyle中,总共七十多个,能够直接拿来用。

QStyle :: SP_TitleBarMenuButton 

13:根据操做系统位数判断加载

win32 {
    contains(DEFINES, WIN64) {
        DESTDIR = $${PWD}/../../bin64
    } else {
        DESTDIR = $${PWD}/../../bin32
    }
} 

14:Qt5加强了不少安全性验证,若是出现setGeometry: Unable to set geometry,请将该控件的可见移到加入布局以后。

15:能够将控件A添加到布局,而后控件B设置该布局,这种灵活性大大提升了控件的组合度,好比能够在文本框左侧右侧增长一个搜索按钮,按钮设置图标便可。

QPushButton *btn = new QPushButton;
btn->resize(30, ui->lineEdit->height());
QHBoxLayout *layout = new QHBoxLayout(ui->lineEdit);
layout->setMargin(0);
layout->addStretch();
layout->addWidget(btn);

16:对QLCDNumber控件设置样式,须要将QLCDNumber的segmentstyle设置为flat。

17:巧妙的使用findChildren能够查找该控件下的全部子控件。findChild为查找单个。

//查找指定类名objectName的控件
QList<QWidget *> widgets = parentWidget.findChildren<QWidget *>("widgetname");
//查找全部QPushButton
QList<QPushButton *> allPButtons = parentWidget.findChildren<QPushButton *>();
//查找一级子控件,否则会一直遍历全部子控件
QList<QPushButton *> childButtons = parentWidget.findChildren<QPushButton *>(QString(), Qt::FindDirectChildrenOnly);

18:巧妙的使用inherits判断是否属于某种类。

QTimer *timer = new QTimer;         // QTimer inherits QObject
timer->inherits("QTimer");          // returns true
timer->inherits("QObject");         // returns true
timer->inherits("QAbstractButton"); // returns false

19:使用弱属性机制,能够存储临时的值用于传递判断。 

20:若是遇到问题搜索Qt方面找不到答案,试着将关键字用JAVA C# android打头,你会发现别有一番天地,其余人极可能作过!

相关文章
相关标签/搜索