8. 自定义控件¶
在开发过程中,往往会遇到各种各样的需求,尽管Qt有非常多的原生控件,在面对层出不穷的需求时难免捉襟见肘,
不过Qt为我们提供很多的机制,像是事件机制、2D绘图、信号和槽, 这些机制为我们实现特定需求的控件提供了非常大的帮助,利用好Qt的这些特性,会极大的提升我们的开发效率。
当然这一章的要是就是要提前掌握好这些基础。
8.1. 自定义控件¶
在我们野火app中,所有的控件都是自定义控件,也全部开源了。 我们就以一个简单的标题栏控件,来看看如何写一个自定标控件。
这个控件在QtUi中qtwidgetbase.h中实现,最后QtUi被编译成库。在我们的示例中,通过链接库来调用这个控件。
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 *);
};
|
只看头文件的代码我们大概能知道这个类给我们留了那些接口,能实现那些功能,当然我们也可以去查看cpp文件。
这个控件主要是一个标题栏,我们可以设置标题,设备标题栏背景,大小等等属性。 当我们调用这些方法对标题栏的属性进行设置时,控件会根据给定的参数重新进行绘图,绘图仍然是通过 paintEvent 完成。
标题栏中嵌套了另外一个自定义控件 m_btnHome ,类型为QtPixmapButton。
要讲清楚这个控件,就得从QtWidgetTitleBar的父类QtWidgetBase开始讲:
QtWidgetBase中有一个QMap数据集,专门用来存储QtPixmapButton。
QWidget有一个重要的虚函数 mouseReleaseEvent,用来响应鼠标按下事件和抬起事件, QtWidgetBase继承自QWidget,自然能接收这个事件,这个函数被重写如下。
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);
}
|
当窗体产生鼠标抬起事件的时候,就会一次遍历QMap,如果发现有QtPixmapButton被按下, 就发送signalBtnClicked信号,这个信号还传递了QtPixmapButton的编号。
QtWidgetTitleBar继承自QtWidgetBase,QtWidgetTitleBar同样是靠信号和槽来监控QtPixmapButton的状态。
在初始化QtPixmapButton的时候,使用connect将signalBtnClicked信号和SltBtnClicked槽函数绑定起来, 当QtPixmapButton被按下时,就会发送signalBtnClicked信号,进而调用SltBtnClicked槽函数。
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信号。
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槽函数响应,传递的参数用于确定哪一个小图标被点击了。
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 | void MainWindow::SltCurrentAppChanged(int index)
{
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);
}
}
|
在这个槽函数会执行下面的任务,首先定义了一个窗口MyWidget,根据继承的关系MyWidget中就包含signalBackHome信号。 我们先将MyWidget的signalBackHome信号和MainWindow中的SltBackHome槽函数绑定。
connect(m_widgetWorkSpace, SIGNAL(signalBackHome()), this, SLOT(SltBackHome()));
并将MyWidget显示在最前面,覆盖掉原来的桌面窗口。这时就看到了我们的自定义窗口。
当我们点击MyWidget的标题栏的返回按钮时,经过信号和槽的重重响应, 最终MainWindow将我们又将桌面窗口显示到最前面,这样就完成了窗口的切换。
8.2. 自定义登录框¶
模仿酷狗音乐播放器做的一个简单的登录窗口Ui。
如果说上一个控件理解起来有点复杂,那么这个自定义对话框就很简单。 自定义控件,其本质就是重新实现UI。
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来对每个控件的样式进行设置。
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()来响应操作。
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;
}
...
}
|
8.3. 自定义窗口¶
前面提到的MyWidget就是一个自定义的窗口,示例程序的桌面图标窗口也是一个自定义窗口。
示例程序主界面运行效果和demo相差不大,但点击图标只会创建一个没有其他功能的窗口。
下面我们对其源码进行简单分析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// 初始化皮肤文件
Skin::InitSkin();
MainWindow w;
w.setWindowTitle(QStringLiteral("野火 @ Linux Qt Demo"));
#ifdef __arm__
w.showFullScreen();
#else
w.resize(800, 480);
w.show();
#endif
return a.exec();
}
|
首先在main函数中,初始化Skin,以便调用共享库libSkin中的资源。 随后定义一个窗口MainWindow,作为顶层窗口显示。定义MainWindow时,执行第一步就是执行该类的构造函数。
1 2 3 | MainWindow::MainWindow(QWidget *parent) : QWidget(parent)
{
}
|
在这个构造函数中,MainWindow首先会继承QWidget所有Public属性和方法。
这时运行窗口就是如下效果,整个程序只有一个顶层窗口,窗口内无任何内容。
在继承的基础上,我们又对MainWindow添加了新的属性和方法,例如我们在类中添加了成员函数 InitWidget(),该函数实现如下。
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);
}
|
随后我们在构造函数调用 InitWidget() 对窗口进行初始化和布局。
首先我们添加了一个垂直布局,随后我们又添加了一个自定义窗口 LauncherWidget, 这个类在QtUi中实现,用来显示启动小部件,也就是我们所预览到的类似于桌面的窗口。 我们通过调用LauncherWidget的SetWallpaper方法来设置窗口背景。
然后关联m_launcherWidget的currentItemClicked(int)信号,这个信号会发送窗口中被点击的小部件的编号。
随后我们调用LauncherWidget的SetItems来设置桌面图标,文字。
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)槽函数中, 在这个槽函数中,我们通过判断编号以实现具体的操作。
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。
MyWidget是个比较简单的自定义窗口,这个窗口是继承自QtAnimationWidget,在QtUi中实现,层层追溯最终也是继承QWidget。 所以MyWidget也具备QWidget的属性,除此还具备QtAnimationWidget等后来添加的属性。
MyWidget头文件,源文件如下:
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
|
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)进行重写,当每次产生绘图事件的时候,函数都会整个窗口区域绘制成一个黑色的矩形。
8.4. 例程说明¶
野火提供的Qt Demo已经开源,仓库地址在:
文档所涉及的示例代码也提供下载,仓库地址在:
本章例程在 embed_qt_develop_tutorial_code/Custom
例程展示了大多数Qt基础控件,并提供了一些简单的示例应用。
8.4.1. 编程思路¶
实现并引用自定义标题栏QtWidgetTitleBar
实现并引用自定义登录框Logon
创建主程序,使用自定义窗口LauncherWidget来填充界面
连接信号和槽来实现窗口界面的切换
8.4.2. 代码讲解¶
本章实际上讲解了野火demo类似的桌面程序是如何实现的。
这里就大致讲讲如何把我们的代码加到桌面上。比如点击某个图标弹出我们的登录对话框。
首先我们需要在桌面窗口上添加一个图标。
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);
}
|
使用 m_launchItems 向桌面插入一个Icon, 然后将当前显示的页面设置为4页。
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;
}
...
}
|
当我们点击桌面的Icon时候,或调用槽函数SltCurrentAppChanged(),函数中实现自己的代码。 例程中就会调用我们模仿的酷狗登录框。
8.4.3. 编译构建¶
Ubuntu 选择 Desktop Qt 5.11.3 GCC 64bit 套件,编译运行
LubanCat 选择 ebf_lubancat,只编译
提示
当两种构建套件进行切换时,请重新构建项目或清除之前项目。针对我们的工程还需要手动重新构建QtUI和Skin。
8.4.4. 运行结果¶
8.4.4.2. LubanCat实验¶
通过SCP或者NFS将编译好的程序拷贝到LubanCat上
NFS环境搭建 参考这里
SCP命令如下:
# scp传输文件
# scp 文件名 服务器上的某个用户@服务器ip:/文件保存路径
scp filename server_user_name@192.168.0.205:server_file_path
# 从服务器拉取文件
# scp 服务器上的某个用户@服务器ip:/服务器文件存放路径 拉取文件保存路径
scp server_user_name@192.168.0.229:server_file_path local_path
编译好的程序在 embed_qt_develop_tutorial_code/app_bin 目录中
scp root@192.168.0.174:/home/embed_qt_develop_tutorial_code/app_bin/Custom /usr/local/qt-app/
在LubanCat运行程序,使用run_myapp.sh配置好环境,并执行 Custom 。
sudo /usr/local/qt-app/run_myapp.sh /usr/local/qt-app/Custom