1. 程式人生 > >Qt編寫自定義控制元件屬性設計器

Qt編寫自定義控制元件屬性設計器

以前做.NET開發中,.NET直接就集成了屬性設計器,VS不愧是宇宙第一IDE,你能夠想到的都給你封裝好了,用起來不要太爽!因為專案需要自從全面轉Qt開發已經6年有餘,在工業控制領域,有一些應用場景需要自定義繪製一些控制元件滿足特定的需求,比如儀器儀表、組態等,而且需要直接使用者通過屬性設計的形式生成匯出控制元件及介面資料,下次匯入使用,要想從內建控制元件或者自定義控制元件拿到對應的屬性方法等,首先聯想到的就是反射,Qt反射對應的類叫QMetaObject,著實強大,其實整個Qt開發框架也是超級強大的,本人自從轉為Qt開發為主後,就深深的愛上了她,在其他跨平臺的GUI開發框架平臺面前,都會被Qt秒成渣,Qt的跨平臺性是毋庸置疑的,幾十兆的記憶體儲存空間即可執行,尤其是嵌入式linux這種資源相當緊張的情況下,Qt的效能發揮到極致。

接下來我們就一步步利用QMetaObject類和QtPropertyBrower(第三方開源屬性設計器)來實現自己的控制元件屬性設計器,其中包含了所見即所得的控制元件屬性控制,以及xml資料的匯入匯出。

第一步:獲取控制元件的屬性名稱集合。

所有繼承自QObject類的類,都有元物件,都可以通過這個QObject類的元物件metaObject()獲取屬性+事件+方法等。

程式碼如下:

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()表示出了父類外,自己類本身屬性的偏移位置即索引開始的位置,這下就好辦了。

程式碼改為:

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的起始位置改為偏移位置即可。

列印輸出如下:

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();

        }

    }

}
View Code

匯出所有控制元件到xml檔案程式碼如下:

  1 void frmMain::saveFile(const QString &fileName)
  2 
  3 {
  4 
  5     QFile file(fileName);
  6 
  7     if (!file.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) {
  8 
  9         return;
 10 
 11     }
 12 
 13     //以流的形式輸出檔案
 14 
 15     QTextStream stream(&file);
 16 
 17     //構建xml資料
 18 
 19     QStringList list;
 20 
 21     //新增固定頭部資料
 22 
 23     list << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
 24 
 25     list << QString("<canvas width=\"%1\" height=\"%2\">")
 26 
 27          .arg(ui->centralwidget->width()).arg(ui->centralwidget->height());
 28 
 29     //從容器中找到所有控制元件,根據控制元件的類名儲存該類的所有屬性
 30 
 31     QList<QWidget *> widgets = ui->centralwidget->findChildren<QWidget *>();
 32 
 33     foreach (QWidget *w, widgets) {
 34 
 35         const QMetaObject *metaObject = w->metaObject();
 36 
 37         QString className = metaObject->className();
 38 
 39         QStringList values;
 40 
 41         //如果當前控制元件的父類不是主窗體則無需匯出,有些控制元件有子控制元件無需匯出
 42 
 43         if (w->parent() != ui->centralwidget || className == "SelectWidget") {
 44 
 45             continue;
 46 
 47         }
 48 
 49         //metaObject->propertyOffset()表示當前控制元件的屬性開始索引,0開始的是父類的屬性
 50 
 51         int index = metaObject->propertyOffset();
 52 
 53         for (int i = index; i < metaObject->propertyCount(); i++) {
 54 
 55             QMetaProperty p = metaObject->property(i);
 56 
 57             QString nodeName = p.name();
 58 
 59             QVariant nodeValue = p.read(w);
 60 
 61             //列舉值要特殊處理,需要以字串形式寫入,不然儲存到配置檔案資料為int
 62 
 63             if (p.isEnumType()) {
 64 
 65                 QMetaEnum enumValue = p.enumerator();
 66 
 67                 nodeValue = enumValue.valueToKey(nodeValue.toInt());
 68 
 69             }
 70 
 71             QString temp = nodeValue.toString().toLocal8Bit().constData();
 72 
 73             values << QString("%1=\"%2\"").arg(nodeName).arg(temp);
 74 
 75             //qDebug() << nodeName << nodeValue;
 76 
 77         }
 78 
 79         //逐個新增介面上的控制元件的屬性
 80 
 81         QString str = QString("\t<%1 x=\"%2\" y=\"%3\" width=\"%4\" height=\"%5\" %6/>")
 82 
 83                       .arg(className).arg(w->x()).arg(w->y()).arg(w->width()).arg(w->height()).arg(values.join(" "));
 84 
 85         list << str;
 86 
 87     }
 88 
 89     //新增固定尾部資料
 90 
 91     list << "</canvas>";
 92 
 93     //寫入檔案
 94 
 95     QString data = list.join("\n");
 96 
 97     stream << data;
 98 
 99     file.close();
100 
101 }
View Code

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