文章目录
上一篇QMainWindow | 对话框
3、布局
所谓 GUI 界面,归根结底,就是一堆组件的叠加。我们创建一个窗口,把按钮放上面,把图标放上面,这样就成了一个界面。在放置时,组件的位置尤其重要。我们必须要指定组件放在哪里,以便窗口能够按照我们需要的方式进行渲染。这就涉及到组件布局定位的机制。
Qt 提供了两种组件布局定位机制:静态布局和动态布局。
- 静态布局就是一种最原始的定位方法:给出这个组件的坐标和长宽值。
这样,Qt 就知道该把组件放在哪里以及如何设置组件的大小。但是这样做带来的一个问题是,如果用户改变了窗口大小,比如点击最大化按钮或者使用鼠标拖动窗口边缘,采用静态布局的组件是不会有任何响应的。这也很自然,因为你并没有告诉 Qt,在窗口变化时,组件是否要更新自己以及如何更新。或者,还有更简单的方法:禁止用户改变窗口大小。但这总不是长远之计。
- 动态布局:你只要把组件放入某一种布局(layout),布局由专门的布局管理器进行管理。当需要调整大小或者位置的时候,Qt 使用对应的布局管理器进行调整。
动态布局解决了使用静态布局的缺陷。
Qt 提供的动态布局中以下三种是我们最常用的:
QHBoxLayout:按照水平方向从左到右布局;
QVBoxLayout:按照竖直方向从上到下布局;
QGridLayout:在一个网格中进行布局,类似于 HTML 的 table;
3.1 系统提供的布局控件
这4个为系统给我们提供的布局的控件,但是使用起来不是非常的灵活,第一个是垂直布局,第二个水平布局,第三个栅格布局,第四个表单布局。
3.2 利用widget做布局
第二种布局方式是利用控件里的widget来做布局,在Containers中
在widget中的控件可以进行水平、垂直、栅格布局等操作,比较灵活。
再布局的同时我们需要灵活运用弹簧的特性让我们的布局更加的美观,必须达到可动态布局效果才能添加弹簧成功
最后一步就是,用户名和密码框没有对齐,我们需要全部打破布局,把用户名和密码抽出来排整理好
重新放到QWidget中栅格对齐,然后整体来一次垂直对齐,最后把弹簧加回去;
我们可以更改它们的策略,设置它们的占比为固定,也可以设置它们里距离窗口的间隙,处理图片用
把windowTitle改为登录窗口,下面是一个登陆窗口,它们之间各占二分之一,利用widget可以搭建出如下登陆界面:
我们还可以设置窗口大小,不能最大化,把最大值和最小值改成一样
在输入框我们不显示密码那就选择密码框QLineEdit找到echoMode属性改为passwd默认是第一个正常,第二个不能输入,第四个是输入中显示密码,不在输入框就显示*
3.3 利用表单做布局
我们选择一个Qt模板为Qt设计师界面类
然后选择Widget
非常方便
两类:静态、动态
静态就是位置和大小不会跟着外部窗口变化而变化
动态就是位置和大小会跟着外部窗口变化而变化
常用动态布局
水平、垂直、栅格、表单布局
推荐使用widget的自带的布局功能
使用弹簧来调整布局的位置,居中
栅格布局可以将空间分为几行几列的表格,方便对齐
大小策略:默认情况下动态布局,子窗口的大小会跟着父窗口的大小变化而变化,调整水平或者垂直策略,变成固定
调整子窗口和父窗口之间的间隙,设置父窗口的margin ,调整子窗口之间的间隙就调整spacing
调整窗口的固定大小,就是将窗口的最大值和最小值都设为同一个值
4、按钮组
Tool Button
显示图片资源,我们选中这个控件找到icon选择我们创建的资源文件,
调整图片大小
更改文字
调整图片文字方向
预览效果
RadioButton
单选框:
单选会有互斥域的问题,如果想将某些单选按钮隔离开,就用容器将他们隔离,一般用Group Box分别框起来
我们可建立信号和槽,点击打印信息,可选择按键右键转到槽
- check box
多选按钮 ,有三态 tristate ,每次点击按钮的时候stateChange信号里边传进来
- Dialog Button Box
多个可选按键,相当于多个push Button组合在一起
5、项目构建组
- List widget
有序列表(ui界面操作)
代码
ui->setupUi(this);
QListWidgetItem *item = new QListWidgetItem("床前明月光");
QListWidgetItem *item = new QListWidgetItem("疑是地上霜");
item->setTextAlignment(Qt::AlignHCenter);
ui->listWidget->addItem(item);
使用方式 addItem 或者 addItems
QStringList -> std::liststd::string
//使用左移操作符添加成员
QStringList list;
list<<“床前明月光”<<“疑似地上霜”<<“举头望明月”<<“低头思故乡”;
- treeWidget
树形项目
//使用treeWidget
//1 设置标题
ui->treeWidget->setHeaderLabels(QStringList()<<"英雄"<<"简介");
//2 添加根节点
QTreeWidgetItem *liliangItem = new QTreeWidgetItem(QStringList()<<"力量");
QTreeWidgetItem *minjieItem = new QTreeWidgetItem(QStringList()<<"敏捷");
QTreeWidgetItem *zhiliItem = new QTreeWidgetItem(QStringList()<<"智力");
ui->treeWidget->addTopLevelItem(liliangItem);
ui->treeWidget->addTopLevelItem(minjieItem);
ui->treeWidget->addTopLevelItem(zhiliItem);
//3 添加相应的子节点
QStringList heroL1,heroL2,heroM1,heroM2,heroZ1,heroZ2;
heroL1 << "刚背猪" << "前排坦克,能在吸收伤害的同时造成可观的范围输出";
heroL2 << "船长" << "前排坦克,能肉能输出能控场的全能英雄";
heroM1 << "月骑" << "中排物理输出,可以使用分裂利刃攻击多个目标";
heroM2 << "小鱼人" << "前排战士,擅长偷取敌人的属性来增强自身战力";
heroZ1 << "死灵法师" << "前排法师坦克,魔法抗性较高,拥有治疗技能";
heroZ2 << "巫医" << "后排辅助法师,可以使用奇特的巫术诅咒敌人与治疗队友";
liliangItem->addChild(new QTreeWidgetItem(heroL1));
liliangItem->addChild(new QTreeWidgetItem(heroL2));
minjieItem->addChild(new QTreeWidgetItem(heroM1));
minjieItem->addChild(new QTreeWidgetItem(heroM2));
zhiliItem->addChild(new QTreeWidgetItem(heroZ1));
zhiliItem->addChild(new QTreeWidgetItem(heroZ2));
使用方式
1 设置标题,会根据setHeaderLabels 里边的成员数自己生成有多少列
2 添加根节点 treeWidget->addTopLevelItem
3 根节点下边添加子节点 item->addChild
- tableWidgeet
表格项目
1 设置行数、列数 setRowCount setColumnCount
2 设置水平的标题 setHorizontalLabels
3 设置表格某行某列的数据 setItem(row,col,item)
//1 设置行数、列数
ui->tableWidget->setRowCount(5);
ui->tableWidget->setColumnCount(3);
//2 设置标题
ui->tableWidget->setHorizontalHeaderLabels(QStringList()<<"英雄"<<"性别"<<"年龄");
//3 添加数据
QStringList heroNames = QStringList()<<"亚瑟"<<"妲己"<<"安其拉"<<"赵云"<<"孙悟空";
QStringList heroGenders = QStringList()<<"男"<<"女"<<"女"<<"男"<<"雄性";
for(int row =0 ;row <5 ; ++row)
{
ui->tableWidget->setItem(row,0,new QTableWidgetItem(heroNames[row]));
ui->tableWidget->setItem(row,1,new QTableWidgetItem(heroGenders[row]));
ui->tableWidget->setItem(row,2,new QTableWidgetItem(QString::number(row + 18)));
}
6、容器
效果:
tacked Widget 页面切换需要我们自己去实现,一般使用按钮点击的时候切换
setCurrentIndex 方式切换到第几页,序号从0开始
7、常用控件
Qt为我们应用程序界面开发提供的一系列的控件,下面我们介绍两种最常用一些控件,所有控件的使用方法我们都可以通过帮助文档获取。
7.1 QLabel控件使用
QLabel是我们最常用的控件之一,其功能很强大,我们可以用来显示文本,图片和动画等。
7.1.1 显示文字 (普通文本、html)
通过QLabel类的setText函数设置显示的内容:
void setText(const QString &)
- 可以显示普通文本字符串
QLable *label = new QLable;
label->setText(“Hello, World!”);
- 可以显示HTML格式的字符串
比如显示一个链接:
QLabel * label = new QLabel(this);
label ->setText("Hello, World");
label ->setText("<h1><a href=\"https://www.baidu.com\">百度一下</a></h1>");
label ->setOpenExternalLinks(true);
其中setOpenExternalLinks()函数是用来设置用户点击链接之后是否自动打开链接,如果参数指定为true则会自动打开。
7.1.2 显示图片
可以使用QLabel的成员函数setPixmap设置图片
void setPixmap(const QPixmap &)
首先定义QPixmap对象
QPixmap pixmap;
然后加载图片
pixmap.load(“:/Image/boat.jpg”);
最后将图片设置到QLabel中
QLabel *label = new QLabel;
label.setPixmap(pixmap);
7.1.3 显示动画
可以使用QLabel 的成员函数setMovie加载动画,可以播放gif格式的文件
void setMovie(QMovie * movie)
首先定义QMovied对象,并初始化:
QMovie *movie = new QMovie(“:/Mario.gif”);
播放加载的动画:
movie->start();
将动画设置到QLabel中:
QLabel *label = new QLabel;
label->setMovie(movie);
7.2 QLineEdit
Qt提供的单行文本编辑框。
7.2.1 设置/获取内容
获取编辑框内容使用text(),函数声明如下:
QString text() const
设置编辑框内容
void setText(const QString &)
7.2.2 设置显示模式
使用QLineEdit类的setEchoMode () 函数设置文本的显示模式,函数声明:
void setEchoMode(EchoMode mode)
EchoMode是一个枚举类型,一共定义了四种显示模式:
QLineEdit::Normal 模式显示方式,按照输入的内容显示。
QLineEdit::NoEcho 不显示任何内容,此模式下无法看到用户的输入。
QLineEdit::Password 密码模式,输入的字符会根据平台转换为特殊字符。
QLineEdit::PasswordEchoOnEdit 编辑时显示字符否则显示字符作为密码。
另外,我们再使用QLineEdit显示文本的时候,希望在左侧留出一段空白的区域,那么,就可以使用QLineEdit给我们提供的setTextMargins函数:
void setTextMargins(int left, int top, int right, int bottom)
用此函数可以指定显示的文本与输入框上下左右边界的间隔的像素数。
7.3 自定义控件
在搭建Qt窗口界面的时候,在一个项目中很多窗口,或者是窗口中的某个模块会被经常性的重复使用。一般遇到这种情况我们都会将这个窗口或者模块拿出来做成一个独立的窗口类,以备以后重复使用。
在使用Qt的ui文件搭建界面的时候,工具栏栏中只为我们提供了标准的窗口控件,如果我们想使用自定义控件怎么办?
例如:我们从QWidget派生出一个类SmallWidget,实现了一个自定窗口,
// smallwidget.h
class SmallWidget : public QWidget
{
Q_OBJECT
public:
explicit SmallWidget(QWidget *parent = 0);
signals:
public slots:
private:
QSpinBox* spin;
QSlider* slider;
};
// smallwidget.cpp
SmallWidget::**SmallWidget**(QWidget *parent) : QWidget(parent)
{
spin = new QSpinBox(this);
slider = new QSlider(Qt::Horizontal, this);
// 创建布局对象
QHBoxLayout* layout = new QHBoxLayout;
// 将控件添加到布局中
layout->addWidget(spin);
layout->addWidget(slider);
// 将布局设置到窗口中
setLayout(layout);
// 添加消息响应
connect(spin,
static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged),
slider, &QSlider::setValue);
connect(slider, &QSlider::valueChanged,
spin, &QSpinBox::setValue);
}
那么这个SmallWidget可以作为独立的窗口显示,也可以作为一个控件来使用:
打开Qt的.ui文件,因为SmallWidget是派生自Qwidget类,所以需要在ui文件中先放入一个QWidget控件, 然后再上边鼠标右键
弹出提升窗口部件对话框
添加要提升的类的名字,然后选择 添加
添加之后,类名会显示到上边的列表框中,然后单击提升按钮,完成操作.
我们可以看到, 这个窗口对应的类从原来的QWidget变成了SmallWidget
再次运行程序,这个widget_3中就能显示出我们自定义的窗口了.
8、消息事件机制
8.1 事件
事件(event)是由系统或者 Qt 应用程序本身在不同的时刻发出的。当用户按下鼠标、敲下键盘,或者是窗口需要重新绘制的时候,都会发出一个相应的事件。一些事件在对用户操作做出响应时发出,如键盘事件等;另一些事件则是由系统自动发出,如计时器事件。
8.2 事件处理函数
在所有组件的父类QWidget中,定义了很多事件处理的函数,如
- keyPressEvent():键盘按键按下事件
- keyReleaseEvent():键盘按键松开事件
- mouseDoubleClickEvent():鼠标双击事件
- mouseMoveEvent():鼠标移动事件
- mousePressEvent():鼠标按键按下事件
- mouseReleaseEvent() :鼠标按键松开事件
这些函数都是 protected virtual
的,也就是说,我们可以在子类中重新实现这些函数。下面来看一个例子:
我们先设置ui文件,给一个label控件,并设置带边框属性
然后再新建个c++文件Mylabel重写label,再回去把ui文件的label对象类提升为Mylabel
按键处理函数代码:
protected:
//重写鼠标按键处理函数
void mousePressEvent(QMouseEvent *ev);
void Mylabel::mousePressEvent(QMouseEvent *ev)
{
//输出鼠标事件一些信息
//获取坐标
int x = ev->x();
int y = ev->y();
//Qstring("%d,%d",x,y);
QString str = QString("[%1,%2]").arg(x).arg(y) ;
this->setText(str);
}
效果已经出来了,样式需要完善,label是可以使用html语言的,比如居中(center)、标题(h1)、链接等;
我们在来添加获取鼠标按键
//获取鼠标按键
Qt::MouseButton btn = ev->button();
QString strButton = "";
if(btn == Qt::LeftButton)
{
strButton = "LeftButton" ;
}
if(btn == Qt::RightButton)
{
strButton = "RightButton" ;
}
if(btn == Qt::MidButton)
{
strButton = "MidButton" ;
}
QString str = QString("<h1><center>[%1,%2,%3]</center></h1>").arg(x).arg(y).arg(strButton) ;
EventLabel继承了QLabel,覆盖了mousePressEvent()、mouseMoveEvent()和MouseReleaseEvent()三个函数。我们并没有添加什么功能,只是在鼠标按下(press)、鼠标移动(move)和鼠标释放(release)的时候,把当前鼠标的坐标值显示在这个Label上面。由于QLabel是支持 HTML 代码的,因此我们直接使用了 HTML 代码来格式化文字。
QString的arg()函数可以自动替换掉QString中出现的占位符。其占位符以 % 开始,后面是占位符的位置,例如 %1,%2 这种。
QString(“[%1, %2]”).arg(x).arg(y);
语句将会使用x替换 %1,y替换 %2,因此,生成的QString为[x, y]。
鼠标移动函数代码:
//重写鼠标移动的处理函数
void mouseMoveEvent(QMouseEvent *ev) ;
我们把上面的鼠标按键定义代码Ctrl+v下来,运行之后,发现有问题,首先我们按动鼠标移动,没有显示是左键还是右键,这是因为,函数是根据位操作进行识别的,我们需要使用位判断,这里就不能是==,还有长按移动鼠标是一个连续的动作,不再是单击按键,这里也是需要使用另一个按键Buttons
有个s,也是按位或
Qt::MouseButtons btns = ev->buttons();
QString strButton = "";
if(btns & Qt::LeftButton)
{
strButton = "LeftButton" ;
}
从mouseButton->改为mouseButtons,button->改为buttons,==->改为&
为了可以同时按多个按键,可以使用+=
if(btns & Qt::LeftButton)
{
strButton += "LeftButton" ;
}
并且为了分清楚是按键还是移动,添加打印信息,如果是按键打印press和如果是移动鼠标就打印move
为什么要点击鼠标之后才能在mouseMoveEvent()函数中显示鼠标坐标值?
这是因为QWidget中有一个mouseTracking属性,该属性用于设置是否追踪鼠标。只有鼠标被追踪时,mouseMoveEvent()才会发出。如果mouseTracking是 false(默认即是),组件在至少一次鼠标点击之后,才能够被追踪,也就是能够发出mouseMoveEvent()事件。如果mouseTracking为 true,则mouseMoveEvent()直接可以被发出。
this->setMouseTracking(true);
8.3 事件分发函数event
事件对象创建完毕后,Qt 将这个事件对象传递给QObject的event()函数。event()函数并不直接处理事件,而是将这些事件对象按照它们不同的类型,分发给不同的事件处理器(event handler)。
bool event(QEvent *e);
如上所述,event()函数主要用于事件的分发。所以,如果你希望在事件分发之前做一些操作,就可以重写这个event()函数了。
事件先会到达窗口的event函数 , event函数 返回值:true表示该事件得到处理,如果是false,没被处理,事件会继续传递到父窗口
if(e->type()==QEvent::MouseMove)
{
this->mouseMoveEvent(static_cast<QMouseEvent *>(e));//强转
return true;
}
其他没有被处理的事件,就使用父类的eventreturn QLabel::event(e);
bool Mylabel::event(QEvent *e)
{
if(e->type()==QEvent::MouseMove)
{
//注释了下面一行,将不再触发鼠标移动事件
this->mouseMoveEvent(static_cast<QMouseEvent*>(e) );
return true;
}
return QLabel::event(e);
}
Mylabel是一个普通的QWidget子类。我们重写了它的event()函数,这个函数有一个QEvent对象作为参数,也就是需要转发的事件对象。函数返回值是 bool 类型。
如果传入的事件已被识别并且处理,则需要返回 true,否则返回 false。如果返回值是 true,那么 Qt会认为这个事件已经处理完毕,不会再将这个事件发送给其它对象,而是会继续处理事件队列中的下一事件。
在event()函数中,调用事件对象的accept()和ignore()函数是没有作用的,不会影响到事件的传播。
8.4 事件过滤器
有时候,对象需要查看、甚至要拦截发送到另外对象的事件。例如,对话框可能想要拦截按键事件,不让别的组件接收到;或者要修改回车键的默认处理。
通过前面的章节,我们已经知道,Qt 创建了QEvent事件对象之后,会调用QObject的event()函数处理事件的分发。显然,我们可以在event()函数中实现拦截的操作。由于event()函数是 protected 的,因此,需要继承已有类。如果组件很多,就需要重写很多个event()函数。这当然相当麻烦,更不用说重写event()函数还得小心一堆问题。好在 Qt 提供了另外一种机制来达到这一目的:事件过滤器。
QObject有一个eventFilter()函数,用于建立事件过滤器。函数原型如下:
virtual bool QObject::eventFilter ( QObject * watched, QEvent * event );
这个函数正如其名字显示的那样,是一个“事件过滤器”。所谓事件过滤器,可以理解成一种过滤代码。事件过滤器会检查接收到的事件。如果这个事件是我们感兴趣的类型,就进行我们自己的处理;如果不是,就继续转发。这个函数返回一个 bool 类型,如果你想将参数 event 过滤出来,比如,不想让它继续转发,就返回 true,否则返回 false。事件过滤器的调用时间是目标对象(也就是参数里面的watched对象)接收到事件对象之前。也就是说,如果你在事件过滤器中停止了某个事件,那么,watched对象以及以后所有的事件过滤器根本不会知道这么一个事件。
事件过滤器的使用
1 窗口调用installEventFilter来安装一个事件过滤器
2 参数是一个事件过滤器对象QObject,该对象的类要重写eventFilter的函数
事件过滤的时候,事件会先到达事件过滤器的eventFilter函数
对象可以使用自己作为自己的过滤器this->installEventFilter(this)
拦截鼠标移动事件:
bool MyLabel::eventFilter(QObject *watched, QEvent *event)
{
if(event->type()==QEvent::MouseMove)
{
//返回true 表示拦截该事件
return true;
}
return false;
}
8.5 定时器事件
闹钟就是定时器,闹钟响了就是定时器事件:timerEvent
protected:
void timerEvent(QTimerEvent *event);
//创建定时器
startTimer(1000);//1秒响
void Widget::timerEvent(QTimerEvent *event)
{
static int num = 1;
qDebug()<<num++;
}
每过一秒都会响一次,为了实现下面的,我们再编写代码
打开ui设计:添加两个按键,和一个LCD Number,把对象名改为start和stop
然后我们点击按键转到槽,在start中我们创建一个定时器startTimer(1000)
,在stop中我们需要杀死这个killTimer(int *id*)
id,我们需要在头文件定义一个变量,接受定时器的idint mTimerId;
void Widget::on_pushButton_start_clicked()
{
this->mTimerId= startTimer(1000);
}
void Widget::on_pushButton_stop_clicked()
{
//停止计时器
killTimer(mTimerId);
}
我们需要把计时的数字显示到lcd中,需要调用lcdNumber: this->ui->lcdNumber->display(num++)
我们已经实现了一个计时器,再实现一个只需把上面的ui设计Ctrl+V一下;为了区分不重复问题后面都标识个2
修改完槽和信后之后,还有就是lcd显示,需要区分定时器的id,找到这个id并显示在lcd中。
当你以为大公告成的时候,运行一看,会出现一点小问题,就是第二次运行start的时候,第一个定时器会出问题,这就是初始化的问题了;所以我们需要初始化和清零
//widget.h
private:
int mTimerId;
int mTimerId2;
private slots:
void on_pushButton_start_clicked();
void on_pushButton_stop_clicked();
void on_pushButton_start_2_clicked();
void on_pushButton_stop_2_clicked();
//widget.cpp
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget),
mTimerId(0),
mTimerId2(0)
{
ui->setupUi(this);
}
void Widget::timerEvent(QTimerEvent *event)
{
if(event->timerId()==this->mTimerId)
{
static int num = 1;
this->ui->lcdNumber->display(num++);
}
if(event->timerId()==this->mTimerId2)
{
static int num2= 1;
this->ui->lcdNumber_2->display(num2++);
}
}
Widget::~Widget()
{
delete ui;
}
void Widget::on_pushButton_start_clicked()
{
this->mTimerId= startTimer(1000);
}
void Widget::on_pushButton_stop_clicked()
{
//停止计时器
killTimer(mTimerId);
this->mTimerId=0;
}
void Widget::on_pushButton_start_2_clicked()
{
this->mTimerId2= startTimer(100);
}
void Widget::on_pushButton_stop_2_clicked()
{
killTimer(mTimerId2);
this->mTimerId2=0;
}
另一种定时器
使用QTimer 的方式创建定时器
通过关注信号 timeout 来接受定时器到时间的信号
通过start 来启动定时器,参数是信号触发间隔的毫秒
通过stop来停止定时器