14. 自定义控件和库

在开发过程中,往往会遇到各种各样的需求,尽管Qt有非常多的原生控件,在面对层出不穷的需求时难免捉襟见肘, 这时我们可以从QWidget或者某个界面组件类继承自定义界面组件,来完成我们的界面设计。

自定义界面组件可以直接继承QWidget或者其他组件实现, 如果是在Qt Designer中,一种方法是通过提升法,例如创建摄像头取景框界面时,我们可以先放置一个普通窗口,然后右击 提升为... , 选择QCameraViewfinder创建,该方法也适用于自定义的组件。 另外一种方法是直接为Qt Designer设计自定义Widget界面,将其安装到Qt Designer组件面板里,然后直接使用即可。

本章将结合配套例程,介绍下自定义控件、自定义界面组件、自定义共享库等。

14.1. 自定义控件

本章的配套例程中,使用了一个自定义控件,该自定义控件来自我们 野火app Demo 中, 具体实现一个包含显示标题和返回键的组件,效果如下:

widget005

这个控件QtWidgetTitleBar声明在QtUi的qtwidgetbase.h文件中,具体是在qtwidgetbase.c中实现,最终是继承于QWidget,具体实现如下:

lubancat_qt_tutorial_code/Custom/QtUi/src/qtwidgetbase.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#ifdef QtUi
class QTUISHARED_EXPORT QtWidgetTitleBar : public QtWidgetBase {
#else
class QtWidgetTitleBar : public QtWidgetBase {
#endif
    Q_OBJECT
public:
    explicit QtWidgetTitleBar(QWidget *parent = 0);
    QtWidgetTitleBar(const QString &title, QWidget *parent  = 0);

    ~QtWidgetTitleBar();

    void SetBackground(const QColor &color);
    void SetBackground(const QPixmap &pixmap);

    QString title() const;
    void SetTitle(const QString &title);
    void SetTitle(const QString &title, const QColor &textClr, const int &fontSize = 18);

    void SetScalSize(int w, int h);
    void SetBtnHomePixmap(const QPixmap &normal, const QPixmap &pressed);
    void SetBtnVisible(bool bOk, int index = 0);

    void SetToolButtons(QMap<int, QtPixmapButton*> btns);
signals:

protected slots:
    virtual void SltBtnClicked(int index);

private:
    QPixmap     m_pixmapBackground;
    QColor      m_colorBackground;
    QColor      m_colorText;

    QString     m_strTitle;
    int         m_nFontSize;

    QtPixmapButton  *m_btnHome;

protected:
    void paintEvent(QPaintEvent *);
};

这个控件主要是实现一个标题栏,提供了设置标题,设备标题栏背景,大小等属性接口。 当我们调用这些方法对标题栏的属性进行设置时,控件会根据给定的参数重新进行绘图,绘图仍然是通过QWidget的绘图事件 paintEvent() 完成。

要讲清楚这个控件,就得先了解下QtWidgetTitleBar的父类QtWidgetBase, QtWidgetBase的继承于QWidget,定义了一个QMap数据集,专门用来存储QtPixmapButton。

在QtWidgetBase中,重写了鼠标事件*mouseReleaseEvent*,用来响应鼠标按下事件和抬起事件,当窗体产生鼠标抬起事件的时候,就会一次遍历QMap,如果发现有QtPixmapButton被按下, 就发送signalBtnClicked信号,这个信号还传递了QtPixmapButton的编号。

lubancat_qt_tutorial_code/Custom/QtUi/src/qtwidgetbase.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void QtWidgetBase::mouseReleaseEvent(QMouseEvent *e)
{
    foreach (QtPixmapButton *btn, m_btns) {
        if (btn->isPressed()) {
            btn->setPressed(false);
            this->update();
            emit signalBtnClicked(btn->id());
            break;
        }
    }

    QWidget::mouseReleaseEvent(e);
}

QtWidgetTitleBar继承自QtWidgetBase,QtWidgetTitleBar同样是靠信号和槽来监控QtPixmapButton的状态。

在初始化QtPixmapButton的时候,使用connect将signalBtnClicked信号和SltBtnClicked槽函数绑定起来, 当QtPixmapButton被按下时,就会发送signalBtnClicked信号,进而调用SltBtnClicked槽函数。

lubancat_qt_tutorial_code/QtUi/src/qtwidgetbase.cpp
1
2
3
4
5
m_btnHome = new QtPixmapButton(0, QRect(746, 0, 54, 54),
            QPixmap(":/images/music/menu_icon.png"),
            QPixmap(":/images/music/menu_icon_pressed.png"));
m_btns.insert(m_btnHome->id(), m_btnHome);
connect(this, SIGNAL(signalBtnClicked(int)), this, SLOT(SltBtnClicked(int)));

信号和槽函数之间会传递一个int类型的参数,这个参数就是按钮的ID,在SltBtnClicked槽函数中,我们通过判断ID就可以知道是那个按钮被按下, 如果按钮的ID为0时(即这个按钮是返回按钮),就发送signalBackHome信号。

lubancat_qt_tutorial_code/QtUi/src/qtwidgetbase.cpp
1
2
3
4
void QtWidgetTitleBar::SltBtnClicked(int index)
{
    if (0 == index) emit signalBackHome();
}

我们再回到Widget工程中,我们来程序窗口是如何切换的。

在MainWindow中,我们定义了一个LauncherWidget,用来显示桌面窗口。

LauncherWidget中有一个信号 currentItemClicked(int),这个信号和顶层窗口中的槽函数SltCurrentAppChanged进行了绑定。

connect(m_launcherWidget, SIGNAL(currentItemClicked(int)), this, SLOT(SltCurrentAppChanged(int)));

当桌面窗口的小图标被点击后,就会有SltCurrentAppChanged槽函数响应,传递的参数用于确定哪一个小图标被点击了。

该自定义控件,没有加到Qt Designer中使用,而是最后QtUi被编译成库,在我们配套例程中,通过链接库来调用这个自定义控件。

14.2. 自定义对话框

模仿酷狗音乐播放器做的一个简单的登录窗口Ui,效果如下:

kugo001

这个自定义对话框相对比较简单简单,自定义控件,其本质就是重新实现UI。

lubancat_qt_tutorial_code/Custom/mywidget/logon.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#ifndef LOGIN_H
#define LOGIN_H

#define IDtype 0;
#define Phonetype 1;

#include <QDialog>

class QLineEdit;
class QLabel;
class QCheckBox;
class QToolButton;
class QStackedWidget;
class QComboBox;

class Logon : public QDialog
{
    Q_OBJECT

public:
    explicit Logon(QWidget *parent = nullptr);
    ~Logon();

    QLabel      *signtypeLab;
    QToolButton *signiconTbtn;
    QPushButton *signidBtn;
    QPushButton *signphoneBtn;
    QPushButton *getMessageBtn;
    QLineEdit   *userName;
    QLineEdit   *passWord;
    QCheckBox   *saveBox;
    QCheckBox   *autoBox;
    QLabel      *forgetpswdLab;

    QStackedWidget *widgetMain;
    QStackedWidget *widgetType;

    int         loginMode;
    int         loginType;

    bool        windowsDrag;
    QPoint      mouseStartPoint;
    QPoint      windowTopLeftPoint;

protected:
    //拖拽窗口
    void mousePressEvent(QMouseEvent *event);
    void mouseMoveEvent(QMouseEvent *event);
    void mouseReleaseEvent(QMouseEvent *event);

private slots:
    void setloginMode();
    void setidType();
    void setphoneType();
    void trysignin();
    void exit();

private:

};

#endif // LOGIN_H

在这个自定义对话框中,我们定义了很多的Qt原生控件,在控件初始化的时候,使用setGeometry()来对这些控件的大小,显示位置进行设置, 然后通过QSS来对每个控件的样式进行设置。

lubancat_qt_tutorial_code/custom/logon.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Logon::Logon(QWidget *parent) :
    QDialog(parent)
{
    this->setWindowFlags(Qt::FramelessWindowHint);//设置窗口不显示最大化最小化关闭控件
    //this->setWindowTitle(tr("请登录"));
    this->setFixedSize(405, 435);//设置窗口大小 设置后窗口大小不可通过鼠标拖动改变大小
    this->setStyleSheet("background-color:white;border: 1px solid #C6C6C6");//设置窗口样式 背景为白色 边框1xp 颜色C6C6C6
    ...
    widgetMain->addWidget(widgetTrad);
    widgetMain->addWidget(widgetScan);
    widgetMain->setGeometry(60,80,285, 320);
    widgetMain->show();
}

当我们点击控件的时候,最终就调用accept()或reject()来响应操作。

lubancat_qt_tutorial_code/custom/mainwindows.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void MainWindow::SltCurrentAppChanged(int index)
{
    if(index==25)
    {
        Logon *logon= new Logon(this);
        logon->exec();

        delete logon;
        return;
    }

    ...
}

14.3. 自定义窗口

配套例程中的MyWidget就是一个自定义的窗口(主要由一个自定义的控件QtWidgetTitleBar和窗口组成),显示效果如下:

widget001

MyWidget是个比较简单的自定义窗口,这个窗口是继承自QtAnimationWidget(也是一个自定义窗口),在QtUi中实现,层层追溯最终也是继承QWidget。 所以MyWidget也具备QWidget的属性,除此还具备QtAnimationWidget等后来添加的属性。

MyWidget头文件和源文件(mywidget.cpp/h)如下:

lubancat_qt_tutorial_code/Custom/mywidget/mywidget.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifndef MYWIDGET_H
#define MYWIDGET_H

#include "qtwidgetbase.h"

class MyWidget : public QtAnimationWidget
{
public:
    explicit MyWidget(QWidget *parent = 0);
    ~MyWidget();
private:
    void InitWidget();
protected:
    void paintEvent(QPaintEvent *);
    void showEvent(QShowEvent *e);
    void hideEvent(QHideEvent *e);
    void resizeEvent(QResizeEvent *e);

private:
    QtWidgetTitleBar    *m_widgetTitle;   //一个自定义的标题栏
};

#endif // MYWIDGET_H
lubancat_qt_tutorial_code/custom/mywidget/mywidget.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
...
void MyWidget::InitWidget()
{
    m_widgetTitle= new QtWidgetTitleBar(this);
    m_widgetTitle->SetScalSize(Skin::m_nScreenWidth, 80);
    m_widgetTitle->SetBackground(Qt::transparent);
    m_widgetTitle->SetBtnHomePixmap(QPixmap(":/images/backlight/menu_icon.png"), QPixmap(":/images/backlight/menu_icon_pressed.png"));
    m_widgetTitle->setFont(QFont(Skin::m_strAppFontBold));
    m_widgetTitle->SetTitle(tr("自定义窗口"), "#ffffff", 32);
    connect(m_widgetTitle, SIGNAL(signalBackHome()), this, SIGNAL(signalBackHome()));

    QVBoxLayout *m_verLayoutAll = new QVBoxLayout(this);
    m_verLayoutAll->setContentsMargins(0, 0, 0, 0);
    m_verLayoutAll->setSpacing(0);
    m_verLayoutAll->addWidget(m_widgetTitle, 1);
    m_verLayoutAll->addStretch(5);
}

void MyWidget::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);
    painter.scale(m_scaleX, m_scaleY);
    painter.fillRect(QRect(0, 0, m_nBaseWidth, m_nBaseHeight), QColor("#000000"));
}

void MyWidget::showEvent(QShowEvent *e)
{
    QWidget::showEvent(e);
}

void MyWidget::hideEvent(QHideEvent *e)
{
    QWidget::hideEvent(e);
}

void MyWidget::resizeEvent(QResizeEvent *e)
{
    SetScaleValue();
    QWidget::resizeEvent(e);
}

在MyWidget中,我们定义了一个 QtWidgetTitleBar,这是一个自定义控件, 在QtUi中实现,通过其提供的方法我们可以对其属性进行设置,并最终添加到布局中。

除此我们对widget的四个虚函数进行了重写:

  • void paintEvent(QPaintEvent *); 绘图事件

  • void showEvent(QShowEvent *e); 显示事件

  • void hideEvent(QHideEvent *e); 隐藏事件

  • void resizeEvent(QResizeEvent *e); 尺寸改变事件

Qt程序运行过程中,一直处于一个事件循环的状态。Qt的主事件循环 QCoreApplication::exec() 从事件队列中获取本机窗口系统事件, 将其转换为QEvents,然后将转换后的事件发送给QObject。QObject通过调用其 QObject::event() 函数来接收事件。 QWidget是QObject的子类,故QWidget能接收到一系列事件。我们在MyWidget中重新实现这些函数,自定义处理这些事件。

例如我们对(paintEvent)进行重写,当每次产生绘图事件的时候,函数都会整个窗口区域绘制成一个黑色的矩形。

14.3.1. 调用自定义窗口

初始化一个QMainWindow工程, 设置窗口的名称和大小等。

lubancat_qt_tutorial_code/Custom/mywidget/main.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    MainWindow w;
    w.setWindowTitle(QStringLiteral("野火 @ Linux Qt Demo"));
    w.resize(800, 480);
    w.show();

    return a.exec();
}

定义一个窗口MainWindow,作为顶层窗口显示,定义MainWindow时,执行第一步就是执行该类的构造函数, 在这个构造函数中,MainWindow首先会继承QWidget所有Public属性和方法。 在继承的基础上,我们又对MainWindow添加了新的属性和方法,例如我们在类中添加了成员函数 InitWidget(),该函数实现如下:

lubancat_qt_tutorial_code/Custom/mywidget/main.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void MainWindow::InitWidget() {
    QVBoxLayout *verLayout = new QVBoxLayout(this);
    verLayout->setContentsMargins(0, 0, 0, 0);
    verLayout->setSpacing(0);

    m_launcherWidget = new LauncherWidget(this);
    m_launcherWidget->SetWallpaper(QPixmap(":/images/mainwindow/background.png"));
    connect(m_launcherWidget, SIGNAL(currentItemClicked(int)), this, SLOT(SltCurrentAppChanged(int)));
    verLayout->addWidget(m_launcherWidget, 1);
}

首先我们添加了一个垂直布局,随后我们又添加了一个自定义窗口 LauncherWidget, 这个类在QtUi中实现,用来显示启动小部件,也就是我们所预览到的类似于桌面的窗口。 我们通过调用LauncherWidget的SetWallpaper方法来设置窗口背景。

然后关联m_launcherWidget的currentItemClicked(int)信号,这个信号会发送窗口中被点击的小部件的编号。

widget003

随后我们调用LauncherWidget的SetItems来设置桌面图标,文字。

lubancat_qt_tutorial_code/Custom/mywidget/mainwindow.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void MainWindow::InitDesktop()
{
    // 第一页
    int nPage = 0;
    m_launchItems.insert(0, new LauncherItem(0, nPage, tr("文件管理"), QPixmap(":/images/mainwindow/ic_file.png")));
    ...
    // 第二页
    nPage++;
    m_launchItems.insert(12, new LauncherItem(12, nPage, tr("RGB彩灯"), QPixmap(":/images/mainwindow/ic_light.png")));
    ...
    // 第三页
    nPage++;
    m_launchItems.insert(24, new LauncherItem(24, nPage, tr("InfoNES模拟器"), QPixmap(":/images/mainwindow/ic_game.png")));

    m_launcherWidget->SetItems(m_launchItems);
}

其中m_launchItems为QMap类型的数据集,依次记录了每一个小部件的编号,图标,文字等。

当我们点击某个小部件时,小部件的编号会通过信号和槽函数传递到SltCurrentAppChanged(int)槽函数中, 在这个槽函数中,我们通过判断编号以实现具体的操作。

在这个槽函数会执行下面的任务,首先定义了一个窗口MyWidget,根据继承的关系MyWidget中就包含signalBackHome信号。 我们先将MyWidget的signalBackHome信号和MainWindow中的SltBackHome槽函数绑定。

并将MyWidget显示在最前面,覆盖掉原来的桌面窗口,这时就看到了我们的自定义窗口。

当我们点击MyWidget的标题栏的返回按钮时,经过信号和槽的重重响应, 最终MainWindow将我们又将桌面窗口显示到最前面,这样就完成了窗口的切换。

lubancat_qt_tutorial_code/Custom/mywidget/mainwindow.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void MainWindow::SltCurrentAppChanged(int index)
{
    if(index==25)
    {
        Logon *logon= new Logon(this);
        logon->exec();

        delete logon;
        return;
    }

    if (m_bStartApp) return;

    m_launcherWidget->setEnabled(false);
    m_bStartApp = true;

    if (NULL != m_widgetWorkSpace ) {
        if (m_nCurrentIndex != index) {
            disconnect(m_widgetWorkSpace, SIGNAL(signalBackHome()), this, SLOT(SltBackHome()));
            delete m_widgetWorkSpace;
            m_widgetWorkSpace = NULL;
        } else {
            m_widgetWorkSpace->setVisible(true);
            m_widgetWorkSpace->StartAnimation(QPoint(this->width(), this->height()), QPoint(0, 0), 300, true);
            return;
        }
    }

    m_widgetWorkSpace = new MyWidget(this);

    if (NULL != m_widgetWorkSpace) {
        m_widgetWorkSpace->resize(this->size());
        connect(m_widgetWorkSpace, SIGNAL(signalBackHome()), this, SLOT(SltBackHome()));
        connect(m_widgetWorkSpace, SIGNAL(signalAnimationFinished()), this, SLOT(SltAppStartOk()));

        m_nCurrentIndex = index;
        m_widgetWorkSpace->setVisible(true);
        m_widgetWorkSpace->StartAnimation(QPoint(this->width(), this->height()), QPoint(0, 0), 300, true);
    }
}

例程中,点击第25个图标的时候会弹出我们之前的模仿酷狗的自定义登录框,点击其他图标则会创建一个黑色的窗口MyWidget。

14.4. 例程说明

本章例程在 lubancat_qt_tutorial_code/Custom 目录下,其中有两个工程文件,以一个是自定义共享库(QTUI) ,一个是自定义窗口例程(mywidget),两个工程文件配合使用。

自定义窗口例程(mywidget)会调用自定义共享库。

14.4.1. 编程思路

  • 实现并引用自定义标题栏QtWidgetTitleBar

  • 实现并引用自定义登录框Logon

  • 创建主程序,使用自定义窗口LauncherWidget来填充界面

  • 连接信号和槽来实现窗口界面的切换

14.4.2. 代码讲解

本章实际上讲解了 野火app Demo 类似的自定义桌面程序是如何实现的。 这里就大致讲讲如何把我们的代码加到桌面上,比如点击某个图标弹出我们的登录对话框。

首先我们需要在桌面窗口上添加一个图标,使用 m_launchItems 向桌面插入一个Icon, 然后将当前显示的页面设置为4页:

lubancat_qt_tutorial_code/Custom/mywidget/mainwindow.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void MainWindow::InitDesktop()
{
    ...
    nPage++;
    // 第四页
    m_launchItems.insert(25, new LauncherItem(25, nPage, tr("登录对话框"), QPixmap(":/images/mainwindow/ic_webview.png")));

    m_launcherWidget->SetPageCount(nPage+1);
    m_launcherWidget->SetItems(m_launchItems);
}

当我们点击桌面的Icon时候,或调用槽函数SltCurrentAppChanged(),函数中实现自己的代码。 例程中就会调用我们自定义的酷狗登录框:

lubancat_qt_tutorial_code/Custom/mywidget/mainwindow.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void MainWindow::SltCurrentAppChanged(int index)
{
    if(index==25)
    {
        Logon *logon= new Logon(this);
        logon->exec();

        delete logon;
        return;
    }
    ...
}

14.4.3. 编译构建

本章的例程,需要先编译QTUI工程,生成共享库,然后编译Custom工程,运行自定义的窗口。 编译套件的选择:

  • Ubuntu 上测试选择 Desktop Qt 5.15.2 GCC 64bit 套件,编译运行;

  • LubanCat板卡上运行选择 LubanCat_RK,只编译生成可执行文件。

提示

当两种构建套件进行切换时,请重新构建项目或清除之前项目。

例如在使用Ubuntu 上测试,先编译QTUI生成库:

widget006

然后编译Custom工程,调用前面生成的库,生成可执行文件。

14.4.4. 运行结果

14.4.4.1. PC 虚拟机上实验

直接点击编译并运行Custom工程, 自定义的窗口显示如下:

widget001

自定义的登录框:

widget006

14.4.4.2. LubanCat上实验

使用相关命令如下将修改库和可执行文件拷贝到板卡上(下面是简单测试):

# 使用scp命令传输可执行文件到板卡上,IP地址根据实际板卡
scp Custom cat@192.168.103.132:~/qt/test/

# 使用rsync命令,同步库文件到板卡测试目录下
rsync -avz  libqtui cat@192.168.103.132:~/qt/

在板卡上执行 Custom可执行文件:

# 测试是在带桌面的系统,使用xcb
LD_LIBRARY_PATH=/opt/qt-everywhere-src-5.15.8/lib:/home/cat/qt/libqtui/lib ./Custom -platform xcb

点击最后的图标,会运行显示:

widget007