一本正经建立 Qt 自定义设计师部件 Custom Designer Widget

目录 Content
[hide]

这次准备从头到尾记录一次建立的过程,算是对前面的总结。

一、向导建立项目

使用向导,在新建项目列表中,选择 Other Project ,再选择 Qt Custom Designer Widget ,点击按钮 Choose… , 工程名为 myLED。再继续,继续,Widget Classes 名称命名为 MyLED,对应的 Plugin class name 自动命名为:MyLEDPlugin,再继续, Plugin name 为 myledplugin。(此步可以看到 Collection Class 几项是灰色的,是因为只有一个widget 的缘故。如果创建2个以上的 widget,此处就会要求填写 集合类 的名称,本文不表,下篇文章再论 )

结构如下:

第一层包含 myledplugin.h 和 myledplugin.cpp ,第二层包括 myled.h 和 myled.cpp。这样好处是层次分明,控件的具体实现都在 myled.h和myled.cpp里。而 myledplugin.h 和 myledplugin.cpp 里则是  QDesignerCustomWidgetInterface 的实现。

二、默认代码

代码如下:

1. myLED.pro

CONFIG      += plugin debug_and_release
TARGET      = $$qtLibraryTarget(myledplugin)
TEMPLATE    = lib

HEADERS     = myledplugin.h
SOURCES     = myledplugin.cpp
RESOURCES   = icons.qrc
LIBS        += -L. 

greaterThan(QT_MAJOR_VERSION, 4) {
    QT += designer
} else {
    CONFIG += designer
}

target.path = $$[QT_INSTALL_PLUGINS]/designer
INSTALLS    += target

include(myled.pri)

其中 TARGET = $$qtLibraryTarget(myledplugin) 则指明编译后的插件dll名字为 myledplugind.dll 或 myledplugin.dll。

target.path = $$[QT_INSTALL_PLUGINS]/designer 指的是 Designer 对应编译版本的目录,例如:c:\Qt\Qt5.12.4\5.12.4\msvc2017_64\plugins\designer\ 。此处重点关注一下 INSTALL ,可参见前文《Qt Creator 中 INSTALLS 用法》。在 build 时可以自动安装到设置的目录当中。

include 这种写法,便于分类。如果是集合插件,则会有多个 include 关联。

2. myledplugin.h

#ifndef MYLEDPLUGIN_H
#define MYLEDPLUGIN_H

#include <QDesignerCustomWidgetInterface>

class MyLEDPlugin : public QObject, public QDesignerCustomWidgetInterface
{
    Q_OBJECT
    Q_INTERFACES(QDesignerCustomWidgetInterface)
#if QT_VERSION >= 0x050000
    Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QDesignerCustomWidgetInterface")
#endif // QT_VERSION >= 0x050000

public:
    MyLEDPlugin(QObject *parent = 0);

    bool isContainer() const;
    bool isInitialized() const;
    QIcon icon() const;
    QString domXml() const;
    QString group() const;
    QString includeFile() const;
    QString name() const;
    QString toolTip() const;
    QString whatsThis() const;
    QWidget *createWidget(QWidget *parent);
    void initialize(QDesignerFormEditorInterface *core);

private:
    bool m_initialized;
};

myledplugin 是对 widget 在 Designer 中接口的描述。此处用于定义widget的接口,如容器,图标,分组,头文件等。

要注意有几个宏:

Q_OBJECT 宏自不必说,Qt 的类少不了。

Q_INTERFACES() 宏用于告诉Qt 本类要实现的接口。例如 Q_INTERFACES(QDesignerCustomWidgetInterface)  要实现的接口是 QDesignerCustomWidgetInterface

Q_PLUGIN_METADATA() 宏,用于声明元数据。例如 Q_PLUGIN_METADATA(IID “org.qt-project.Qt.QDesignerCustomWidgetInterface”)#endif // QT_VERSION >= 0x050000

头文件要使用<QtUiPlugin/QDesignerCustomWidgetInterface> 不然会提示 Header <QtDesigner/QDesignerCustomWidgetInterface> is deprecated. Please include <QtUiPlugin/QDesignerCustomWidgetInterface> instead

3. myledplugin.cpp

#include "myled.h"
#include "myledplugin.h"

#include <QtPlugin>

MyLEDPlugin::MyLEDPlugin(QObject *parent)
    : QObject(parent)
{
    m_initialized = false;
}

void MyLEDPlugin::initialize(QDesignerFormEditorInterface * /* core */)
{
    if (m_initialized)
        return;

    // Add extension registrations, etc. here

    m_initialized = true;
}

bool MyLEDPlugin::isInitialized() const
{
    return m_initialized;
}

QWidget *MyLEDPlugin::createWidget(QWidget *parent)
{
    return new MyLED(parent);
}

QString MyLEDPlugin::name() const
{
    return QLatin1String("MyLED");
}

QString MyLEDPlugin::group() const
{
    return QLatin1String("");
}

QIcon MyLEDPlugin::icon() const
{
    return QIcon();
}

QString MyLEDPlugin::toolTip() const
{
    return QLatin1String("");
}

QString MyLEDPlugin::whatsThis() const
{
    return QLatin1String("");
}

bool MyLEDPlugin::isContainer() const
{
    return false;
}

QString MyLEDPlugin::domXml() const
{
    return QLatin1String("<widget class=\"MyLED\" name=\"myLED\">\n</widget>\n");
}

QString MyLEDPlugin::includeFile() const
{
    return QLatin1String("myled.h");
}
#if QT_VERSION < 0x050000
Q_EXPORT_PLUGIN2(myledplugin, MyLEDPlugin)
#endif // QT_VERSION < 0x050000

创建widget 则 returun new xxx(parent) 等。

下面则是对 widget 本身的实现描述。

4. myled.pri

HEADERS += myled.h
SOURCES += myled.cpp

5. myled.h

#ifndef MYLED_H
#define MYLED_H

#include <QWidget>

class MyLED : public QWidget
{
    Q_OBJECT

public:
    MyLED(QWidget *parent = 0);
};

#endif // MYLED_H

在有的教程里提到要在类前添加 QDESIGNER_WIDGET_EXPORT  宏,但是 Qt5 向导生成的代码并没有这个宏。如果使用,加上头文件

#include <QtUiPlugin/QDesignerExportWidget>

教程里说  QDESIGNER_WIDGET_EXPORT 插入操作系统相关的代码,以确保被正确导出到共享库 Shared library 或者 DLL 文件。(也就是说编译的的时候会报链接错误,应该是找不到 lib 中相应的函数)

帮助文件里说在某些平台,编译器从插件中删除Desinger创建新部件所需的符号,从而导致其无法使用。使用 QDESIGNER_WIDGET_EXPORT 宏可确保符号保留在那些平台上,并且在其他平台上没有副作用。这个宏是 Qt4.1 引进的。

6. myled.cpp

#include "myled.h"

MyLED::MyLED(QWidget *parent) :
    QWidget(parent)
{
}

三、编译和部署到 Designer

以上默认代码不做任何更改,编译和安装(路径为 $$[QT_INSTALL_PLUGINS]/designer,  指的是 Designer 对应编译版本的目录,例如:c:\Qt\Qt5.12.4\5.12.4\msvc2017_64\plugins\designer\ ),打开 Desinger ,就会发现在 widget box 里出现了一个分组 “自定义窗口部件”,里面有一个名称为 MyLED,图标为默认Qt的控件。

此时这个控件没有什么功能,可以拖到窗体,但是什么都没有,也没有属性。这是因为默认 myled.h 和 myled.cpp 是空的。

四、完善代码

针对具体实现,对已有代码进行修改。

1. 更改 widget box 里的名称

修改 myledplugins.cpp 可设置分组名称,否则默认为 “Custom Widgets”

QString MyLEDPlugin::group() const
{
    return QLatin1String("LT WIdgets");
}

2. 更改 widget box 里显示图标

添加资源,修改 icon.qrc,修改 myledplugins.cpp

QIcon MyLEDPlugin::icon() const
{
    return QIcon(":/myled");
}

3. 实现 widget

默认 myled.h 和 myled.cpp 都是空的,需要完善代码。

考虑到 LED 属性,要有下面属性,并能够读取和设置。

  • 直径 diameter
  • 颜色 color
  • 亮灭状态  state
  • 闪烁 flashing
  • 闪烁频率 flashRate

声明一些私有变量用以存储这些属性值

private:
    double diameter_;
    QColor color_;
    bool state_;
    bool flashing_;
    int flashRate_;

再分别声明如下方法进行读写操作:

public:
    explicit MyLED(QWidget *parent = nullptr);
    
    double diameter() const;
    void setDiameter(double diameter);
    
    QColor color() const;
    void setColor(const QColor& color);
    
    bool state() const;
    
    bool isFlashing() const;
    
    int flashRate() const;
    
public slots:
    void setState(bool state);
    void toggleState();
    
    void setFlashing(bool flashing);
    void setFlashRate(int rate);
    void startFlashing();
    void stopFlashing();

注意到 setState()  等方法被声明成槽,用以被其他 widget 的信号来控制。

在构造函数中初始化这些属性值

MyLED::MyLED(QWidget *parent) :
    QWidget(parent),
    diameter_(5),
    color_(QColor("green")),
    state_(true),
    flashing_(false),
    flashRate_(500)
{
}

并实现相应的读写方法

double MyLED::diameter() const
{
    return diameter_;
}

void MyLED::setDiameter(double diameter)
{
    diameter_ = diameter;

}

QColor MyLED::color() const
{
    return color_;
}

void MyLED::setColor(const QColor &color)
{
    color_ = color;
}

bool MyLED::state() const
{
    return state_;
}

bool MyLED::isFlashing() const
{
    return flashing_;
}

int MyLED::flashRate() const
{
    return flashRate_;

}

void MyLED::setState(bool state)
{
    state_ = state;
}

void MyLED::toggleState()
{
    state_ = !state_;
}

void MyLED::setFlashing(bool flashing)
{
    flashing_ = flashing;
}

void MyLED::setFlashRate(int rate)
{
    flashRate_ = rate;
}

void MyLED::startFlashing()
{
    setFlashing(true);
}

void MyLED::stopFlashing()
{
    setFlashing(false);
}

此时完成编译和安装,在Designer 中,属性编辑器是看不到这些属性的,还需要用宏 Q_PROPERTY 将其声明出来。

  Q_PROPERTY(type name
             (READ getFunction [WRITE setFunction] |
              MEMBER memberName [(READ getFunction | WRITE setFunction)])
             [RESET resetFunction]
             [NOTIFY notifySignal]
             [REVISION int]
             [DESIGNABLE bool]
             [SCRIPTABLE bool]
             [STORED bool]
             [USER bool]
             [CONSTANT]
             [FINAL])

#include <QtUiPlugin/QDesignerExportWidget>

class QDESIGNER_WIDGET_EXPORT MyLED : public QWidget
{
    Q_OBJECT
    Q_PROPERTY(double diameter READ diameter WRITE setDiameter)
    Q_PROPERTY(QColor color READ color WRITE setColor)
    Q_PROPERTY(bool state READ state WRITE setState)
    Q_PROPERTY(bool flashing READ isFlashing WRITE setFlashing)
    Q_PROPERTY(int flashRate READ flashRate WRITE setFlashRate)

如此一来,便可以在属性编辑器里设置上述属性了。至此,Desinger 的完整框架基本实现。但是关于 LED 本身物理特性前面只设置了属性值的读取和设置,下步则需要绘制这个LED的发光。

4. 绘制 LED

绘图感觉是最核心的部分

paintEvent(QPaintEvent* event) 是 QWidget 类中的虚函数,用于 ui 绘制,多数情况下会被其他函数调用,比如 update() 时。

运行时机:

  • repaint() 或 update() 被调用
  • 被隐藏的 widget 现在被重新显示
  • 其他一些原因

补充位置信息

    Qt::Alignment alignment() const;
    void setAlignment(Qt::Alignment alignment);

并初始化

    alignment_(Qt::AlignCenter)

并用 Q_PROPERTY 暴露给属性编辑器

    Q_PROPERTY(Qt::Alignment alignment READ alignment WRITE setAlignment)

需要转化像素物理长度,设置LED直径用的是毫米

void MyLED::setDiameter(double diameter)
{
    diameter_ = diameter;

    pixX_ = int(round(double(height())/heightMM()));
    pixY_ = int(round(double(width())/widthMM()));

    diamX_ = int(diameter_*pixX_);
    diamY_ = int(diameter_*pixY_);

    update();

}

然后绘制 LED ,画一个长轴和短轴相等的椭圆

void MyLED::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event);

    QPainter p(this);

    QRect geo = geometry();
    int width = geo.width();
    int height = geo.height();

    int x = 0 , y = 0;
    if ( alignment_ & Qt::AlignLeft )
        x = 0;
    else if ( alignment_ & Qt::AlignRight )
        x = width-diamX_;
    else if ( alignment_ & Qt::AlignHCenter )
        x = (width-diamX_)/2;
    else if ( alignment_ & Qt::AlignJustify )
        x = 0;

    if ( alignment_ & Qt::AlignTop )
        y = 0;
    else if ( alignment_ & Qt::AlignBottom )
        y = height-diamY_;
    else if ( alignment_ & Qt::AlignVCenter )
        y = (height-diamY_)/2;

    if (state_){
        p.setPen(color_);
        p.setBrush(color_);
    }else {
        p.setPen(QColor(125,125,125,255));
        p.setBrush(QColor(125,125,125,255));
    }

    p.drawEllipse(x, y, diamX_-1, diamY_-1);

}

这样一个 LED 就画出来了。不过此时还不具有闪烁功能,下步则需要加入此功能。使用定时器来完成这个功能。

    timer_ = new QTimer(this);
    connect(timer_,&QTimer::timeout,this,&MyLED::toggleState);

在 paintevent 里启动和停止

    if ( flashRate_ > 0 && flashing_ )
        timer_->start(flashRate_);
    else
        timer_->stop();

这样就基本实现了LED功能。

5. 优化 LED 绘制

用上述代码绘制的LED没有渐变(光影)效果,所以还需要加以美化,使用 QRadialGradient 来实现。(渐变知识需要补充

    QRadialGradient g(x+diamX_/2, y+diamY_/2, diamX_*0.4,
                      diamX_*0.4, diamY_*0.4);
    
    g.setColorAt(0, Qt::white);
    if ( state_ )
        g.setColorAt(1, color_);
    else
        g.setColorAt(1, QColor(125,125,125,255));
    QBrush brush(g);
    
    p.setPen(color_);
    p.setBrush(brush);
    p.setRenderHint(QPainter::Antialiasing, true);
    p.drawEllipse(x, y, diamX_-1, diamY_-1);

这样看起来效果就好多了。

6. 布局尺寸

但是这样还有一些问题,拖入 Widget 时,明显尺寸小于 LED默认尺寸,所以需要自适应LED的尺寸。用重写 sizeHint() 实现

QSize MyLED::sizeHint() const
{
    return QSize(diamX_+4,diamY_+4);
}

sizeHint()  是布局时建议大小,还有 minimumSizeHint()  是建议最小大小。

QSize MyLED::minimumSizeHint() const
{
    return QSize(diamX_+2,diamY_+2); 
}

此外,还考虑 heightForWidth()

int MyLED::heightForWidth(int width) const
{
    return width;
}

以上步骤可以达到 Designer 中实现布局和预览功能,但是不能在 Creator 中使用。

五、编译和部署到 Creator 中

Creator 则需要对应其编译版本,查看 Creator 关于可得到此信息,比如 MSVC 2017 32bit 。然后编译相应版本的 dll (release版本),放入Creator 的插件目录当中,例如:c:\Qt\Qt5.12.4\Tools\QtCreator\bin\plugins\designer\  这样,就可以在 Creator 的 Design 页面看到和 拖放 led 控件。

六、编译构建应用

但是,现在不行,编译会报错:error: dependent ‘myled.h’ does not exist. 原来还得将头文件放对位置。参见文章 《Qt5 自定义 Widget 控件 LED(插件)》。

考虑要点就是:要指定头文件位置,release 和 debug 版本的 lib 文件位置。 另,release 和 debug 版本的 dll 文件,可放入默认 bin 目录。并将相关路径信息写入 pri 文件。

以5.12.7 msvc2017_64 为例:

  • 将 release 和 debug 版本的 dll 文件,拷贝到 C:\Qt\Qt5.12.7\5.12.7\msvc2017_64\bin\ 文件夹下,以便 QtCraetor 环境中能找到 dll 文件。这个路径在QtCraetor 调试环境,以及 Qt 5.12.7 (MSVC 2017 64-bit) 命令行环境下,都可以被找到。可以考虑将此路径添加到系统环境变量 path 中。
  • 将 release 和 debug 版本的 lib 文件( 以及 pdb 文件),拷贝到 C:\Qt\Qt5.12.7\5.12.7\msvc2015_64\lib 路径下。这个路径以及文件名需要在 pri 文件中指定。
  • 将 widget 的头文件 .h 放入到合理命名的文件夹中,比如 LED,再将此文件拷贝到 c:\Qt\Qt5.12.2\5.12.2\msvc2015_64\include\ 路径下。这个路径需要在 pri 文件中指定。
  • 将头文件和lib信息写入到 pri 文件。使用时,在 pro 文件中 include 此 pri文件。

例如 LED.pri 文件如下:

win32{
    INCLUDEPATH += $$(QTDIR)/include/LED/
    LIBS += -L$$(QTDIR)/lib/

    CONFIG(debug,debug|release) {
        LIBS += -lmyledplugind
    } else {
        LIBS += -lmyledplugin
    }
}

使用时,在 Pro 中包含此文件

include("C:/Qt/LED.pri")

即可编译成功!

七、官方例子

自带的官方例子 Custom Widget Plugin Example 比较全面介绍了各个部分的功能,可详细阅读参考。

八、未解问题

默认pro配置,通过 nmake install 可将 dll 安装到 $$[QT_INSTALL_PLUGINS]/designer 目录下

target.path = $$[QT_INSTALL_PLUGINS]/designer
INSTALLS    += target

如何再将其他部分分别安装到相应的目录?

源码:myLED.zip

延伸阅读

  • 浅析 Qt 布局系统
    https://www.jianshu.com/p/16494e022470
  • QT中PRO文件写法的详细介绍
    https://www.cnblogs.com/rainbow70626/p/10017365.html
  • Qt编写自定义控件及插件的使用
    https://www.cnblogs.com/georgeOfChina/p/7773141.html

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.