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打頭,你會發現別有一番天地,其餘人極可能作過!

相關文章
相關標籤/搜索