这次准备从头到尾记录一次建立的过程,算是对前面的总结。
一、向导建立项目
使用向导,在新建项目列表中,选择 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