白话编程# Qt C++ Widget界面编程(给非计算机专业的小伙伴看的教程)
(PS:文末有整个过程视频链接)
我们开始具体的界面设计。
1 新建一个项目:

具体的过程请参考
2 规划布局
开始编辑界面之前,首先要对目标界面进行一下分析,规划相应的布局。
比如我们想看法一个这样的界面

我们该怎么去组织呢?
我们规划这样一个界面的时候,首先是自顶而下的,先总体再局部。
也就是我们不能直接去确定按钮在哪里,而是先分区,然后每个分区再分区!
比如,整体上分左右两部分,整个整体布局(Layout)用什么呢?对了 QHBoxLayout(水平布局) 。

我们进一步考虑分区的布局,比如左侧,又可以分为上下两个区域,上面是总体操作区,下面是不同功能区。用什么呢?对了 QVBoxLayout(垂直布局) 。

3 添加代码
这样依次分析完成之后,就需要开始添加具体的代码了。
往哪里写呢?
对了,在在界面类的头文件AFStart.h中。
最终的头文件代码如下:
pragma once
#include <QtWidgets/QMainWindow>
#include "ui_AFStart.h"
#include <QHBoxLayout>
#include <QTableWidget>
#include <QPushButton>
#include <QLabel>
#include "SysData.h"
#include "UiTabMotion.h"
class AFStart : public QMainWindow
Q_OBJECT
public:
AFStart(QWidget *parent = Q_NULLPTR);
private:
Ui::AFStartClass ui;
//====================================
//以下添加界面元素
// 布局
QHBoxLayout* m_wholeBoxLayout; // 整体布局
//=====左侧布局====
QVBoxLayout* m_leftBoxLayout; // 左侧布局
QVBoxLayout* m_leftUpBoxLayout; // 左侧上部布局,手工操作区域。
QTableWidget* m_statusTable; // 状态信息表
QPushButton* m_sysStopBtn; // 急停按钮,需双击
QPushButton* m_sysResetBtn; // 系统回复按钮,需双击
QPushButton* m_sysErasureBtn; // 报警消音按钮,需双击
QPushButton* m_sysCntBtn; // 系统重连按钮,需双击
QPushButton* m_refleshUIBtn; // 刷新界面按钮
QPushButton* m_selectAutoModeBtn; //自动模式按钮
QPushButton* m_selectManualModeBtn; //手动模式按钮
QHBoxLayout* m_leftDownBoxLayout; // 左侧下部布局,主要是各操作tab页面
QTabWidget* m_moduleCtrlTab; // Tab UI 集合,包括以下Tab
QWidget* m_TabMotionCtr; // 运动控制tab,各运动轴控制
UiTabMotion* m_TabMotion; // 运动控制tab 控制
QWidget* m_TabMesureCtr; // 测试控制tab,
QWidget* m_TabSetCtr; // 设置tab,用户、向导等
//=====右侧布局====
QHBoxLayout* m_rightBoxLayout; // 右侧布局
QTableWidget* m_ncTable; // NC列表
QVBoxLayout* m_rightRightBoxLayout; // 右侧布局,NC指令区分左右
QPushButton* m_BtnNCLoad; // 打开NC程序
QPushButton* m_BtnNCEdit; // 编辑NC程序
QPushButton* m_BtnNCSave; // 保存NC程序
QPushButton* m_BtnNCAutoRun; // 连续运行
QPushButton* m_BtnNCStepRun; // 单步运行
QPushButton* m_BtnNCStopRun; // 停止运行
QPushButton* m_BtnNCJumpLine; // 跳过当前行
QPushButton* m_BtnNCPrePage; // 上一页
QPushButton* m_BtnNCNextPage; // 下一页
// =======状态栏
QLabel* m_StatusBarUserLabel;
QLabel* m_StatusBarAuthorLabel;
// 初始化功能
int initUiWindow(); // 初始化界面函数,构造函数调用生成界面
void InitQSS();
Robot m_Robot;
// 信号和槽slot
//响应槽函数
void OnClickedSelectAutoManuModeBtn();
void refreshUI(); // 刷新界面
void connectSignalSlot(); // 把所有信号和槽函数关联
QTimer* m_refreshUiTimer;
void OnTimerToRefresh();
};
注意上述代码是一个逐步添加的过程,如果你在按照步骤操作,建议参照发布的视频,一部分一部分的添加,便于搞清楚每一个控件的含义。
★注意:至此,我们只是声明了这个界面类,但是这里只是声明了这个类里面有这些成员变量,实际没有任何显示,因为,系统并不知道要显示为什么样子。
显示设计的控件(在实现文件AFStart.CPP中操作)
初始化显示在哪里写呢?
当一个程序启动时,他的主界面是应该一开始就出来的。就好像我们房间,住进去的一开始就应该有所有装修。我们在里面的所有活动(吃喝拉撒)都是在初始状态之下的进一步操作。
那么这个初始活动应该怎么实现呢?
我们在用数据类型声明变量时,往往推荐采用初始化操作,这样可以避免变量初值不确定进而衍生出来一些意外的问题。那么对于用类声明对象呢?怎么定义一些初始化操作呢(包括公共和私有成员变量)?答案就是构造函数。
我们回想一下,这个应该在构造函数中实现。
实际上我们构造函数要实现的东西很多,不仅仅是界面的初始化,还有信号传递机制的初始化等等。
从模块化的原则来看,我们一般倾向于把一件事情放到一个专门的函数中实现,然后构造函数来调用。
一般可以用这样一个函数
int outMsg = initUiWindow();
#pragma execution_character_set("utf-8") // 解决中文问题
#include "AFStart.h"
#include <QHeaderView>
#include <QFile>
#include <QTimer>
AFStart::AFStart(QWidget *parent)
: QMainWindow(parent)
ui.setupUi(this);
InitQSS();
int outMsg = initUiWindow();
refreshUI();
// 刷新界面显示功能
// 初始化m_refreshTimer成员变量
m_refreshUiTimer = new QTimer(this);
// 每隔 300ms 会emit 一次timeout()的信号
m_refreshUiTimer->start(300);
connectSignalSlot();
void AFStart::InitQSS()
const QString filePath = "../White_Theme.qss";
QFile file(filePath);
if (file.open(QFile::ReadOnly))
QString qss = QLatin1String(file.readAll());
setStyleSheet(qss);
QString PaletteColor = qss.mid(20, 7);
setPalette(QPalette(QColor(PaletteColor)));
file.close();
int AFStart::initUiWindow()
// ====1.设置主程序名称======
QString uiTitle = "天线寻向系统";
setWindowTitle(uiTitle); // 设置主程序名称
setWindowIcon(QIcon("../NUAA2.ico")); //注意 ..是解决方案所在目录
//====2.UI界面布局===========
// ======2.1左半部分布局=====
// ========2.1.1 左上布局====
m_leftUpBoxLayout = new QVBoxLayout(); //定义左上布局
// =================
// 状态信息表区status table
m_statusTable = new QTableWidget(2, 8); //状态table为两行8列(不算表头)
m_statusTable->setFixedHeight(120); //状态table高度
m_statusTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
m_statusTable->setEditTriggers(QAbstractItemView::NoEditTriggers); //设置为不能编辑
QStringList headerTexts;
headerTexts << "X (mm)" << "Y (mm)" << "Z (mm)" << QString("I") << QString("J") << QString("K") << "状态" << "连接";
m_statusTable->setHorizontalHeaderLabels(headerTexts); //设置状态table表头为上面字符串
QStringList lineTexts;
lineTexts << QString("TCP(设备系)") << QString("TCP(产品系)");
m_statusTable->setVerticalHeaderLabels(lineTexts); //设置行名称
// ======================
// 手工操作设置区
m_sysStopBtn = new QPushButton(QString("急停"));
m_sysStopBtn->setIcon(QIcon("../Source/icon/stop1.jpg"));
m_sysStopBtn->setObjectName(QString("bigRedButton"));
m_sysStopBtn->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
m_sysResetBtn = new QPushButton(QString("复位"));
m_sysResetBtn->setIcon(QIcon("../reset.png"));
//m_sysResetBtn->->setObjectName(QString("bigYellowButton"));
m_sysResetBtn->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
m_sysErasureBtn = new QPushButton(QString("消音"));
m_sysErasureBtn->setIcon(QIcon("../mute.jpg"));
m_sysErasureBtn->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
m_sysCntBtn = new QPushButton(QString("重连"));
m_sysCntBtn->setIcon(QIcon("../cnt.jpg"));
m_sysCntBtn->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
m_refleshUIBtn = new QPushButton(QString("刷新"));
m_refleshUIBtn->setIcon(QIcon("../reflesh.png"));
m_refleshUIBtn->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
m_selectAutoModeBtn = new QPushButton(QString("自动"));
m_selectAutoModeBtn->setIcon(QIcon("../anto.png"));
m_selectAutoModeBtn->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
m_selectAutoModeBtn->setCheckable(true);
m_selectManualModeBtn = new QPushButton(QString("手动"));
m_selectManualModeBtn->setIcon(QIcon("../manual.png"));
m_selectManualModeBtn->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
m_selectManualModeBtn->setCheckable(true);
QGridLayout* pManOptLeftLayout = new QGridLayout(); // 定义网格布局,用来安排系统按钮
pManOptLeftLayout->setSpacing(3);
pManOptLeftLayout->addWidget(m_sysResetBtn, 1, 1);
pManOptLeftLayout->addWidget(m_sysErasureBtn, 2, 1);
pManOptLeftLayout->addWidget(m_sysCntBtn, 1, 2);
pManOptLeftLayout->addWidget(m_refleshUIBtn, 2, 2);
pManOptLeftLayout->addWidget(m_selectAutoModeBtn, 1, 3);
pManOptLeftLayout->addWidget(m_selectManualModeBtn, 2, 3);
QHBoxLayout* pManOptLayout = new QHBoxLayout(); // 注意,操作区是分两部分的,急停是一个(更大),其它是一个区域
pManOptLayout->addLayout(pManOptLeftLayout); // 将其它按钮布局放进操作区布局
pManOptLayout->addWidget(m_sysStopBtn); // 将急停按钮放到操作区布局
pManOptLayout->setStretch(0, 3); // 其它按钮宽度为急停的3倍(因为更多按钮)
pManOptLayout->setStretch(1, 1);
m_leftUpBoxLayout->addWidget(m_statusTable); // 把状态信息表区填加到左上布局中
m_leftUpBoxLayout->addLayout(pManOptLayout); // 把操作按钮区填加到左上布局中
// ==== 左上布局结束
// ========2.1.2 左下布局====
m_leftDownBoxLayout = new QHBoxLayout(); //定义左下布局
m_leftDownBoxLayout->setSpacing(10);
m_moduleCtrlTab = new QTabWidget(); // 左下布局是tab区
m_TabMotionCtr = new QWidget(); // 运动控制tab,各轴运动控制
m_TabMotion = new UiTabMotion(m_Robot,m_TabMotionCtr); // 运动控制tab 控制
m_moduleCtrlTab->addTab(m_TabMotionCtr, QIcon("../motion.png"), QString("运动"));
m_TabMesureCtr = new QWidget(); // 测试tab
m_moduleCtrlTab->addTab(m_TabMesureCtr, QIcon("../laser.jpg"), QString("测试"));
m_TabSetCtr = new QWidget(); // 设置tab,
m_moduleCtrlTab->addTab(m_TabSetCtr, QIcon("../systemSetting.png"), QString("设置"));
m_leftDownBoxLayout->addWidget(m_moduleCtrlTab); //把tab 增加到左下布局中
// ==== 左下布局结束
m_leftBoxLayout = new QVBoxLayout(); //定义左侧布局, 左侧布局是上下垂直分布的,所以用QVBoxLayout
m_leftBoxLayout->setSpacing(10);
m_leftBoxLayout->addLayout(m_leftUpBoxLayout); //将左上布局添加到左侧布局
m_leftBoxLayout->addLayout(m_leftDownBoxLayout); //将左下布局添加到左侧布局
// 设置两个布局t控件按 比例扩大或缩小时
m_leftBoxLayout->setStretch(0, 3);
m_leftBoxLayout->setStretch(1, 7);
//// === 左侧布局结束
// ======2.2右半部分布局=====
m_rightBoxLayout = new QHBoxLayout(); //定义右侧布局
m_ncTable = new QTableWidget(0, 2);
m_ncTable->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
QStringList headerTextsNC;
headerTextsNC << QString("行") << QString("NC 指令");
m_ncTable->setHorizontalHeaderLabels(headerTextsNC);
m_ncTable->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft);
m_rightRightBoxLayout = new QVBoxLayout();
m_BtnNCLoad = new QPushButton(QString("打开程序")); // 打开NC程序
m_BtnNCLoad->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
m_BtnNCEdit = new QPushButton(QString("编辑程序")); // 编辑NC程序
m_BtnNCEdit->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
m_BtnNCSave = new QPushButton(QString("保存程序")); // 保存NC程序
m_BtnNCSave->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
m_BtnNCAutoRun = new QPushButton(QString("连续运行")); // 连续运行
m_BtnNCAutoRun->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
m_BtnNCStepRun = new QPushButton(QString("单步运行")); // 单步运行
m_BtnNCStepRun->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
m_BtnNCStopRun = new QPushButton(QString("停止运行")); // 停止运行
m_BtnNCStopRun->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
m_BtnNCJumpLine = new QPushButton(QString("跳过此行")); // 跳过当前行
m_BtnNCJumpLine->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
m_BtnNCPrePage = new QPushButton(QString("上一页")); // 上一页
m_BtnNCPrePage->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
m_BtnNCNextPage = new QPushButton(QString("下一页")); // 下一页
m_BtnNCNextPage->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
m_rightRightBoxLayout->addWidget(m_BtnNCLoad);
m_rightRightBoxLayout->addWidget(m_BtnNCEdit);
m_rightRightBoxLayout->addWidget(m_BtnNCSave);
m_rightRightBoxLayout->addWidget(m_BtnNCAutoRun);
m_rightRightBoxLayout->addWidget(m_BtnNCStepRun);
m_rightRightBoxLayout->addWidget(m_BtnNCStopRun);
m_rightRightBoxLayout->addWidget(m_BtnNCJumpLine);
m_rightRightBoxLayout->addWidget(m_BtnNCPrePage);
m_rightRightBoxLayout->addWidget(m_BtnNCNextPage);
m_BtnNCLoad->setEnabled(true);
m_BtnNCSave->setEnabled(false);
m_BtnNCAutoRun->setEnabled(false);
m_BtnNCStepRun->setEnabled(false);
m_BtnNCStopRun->setEnabled(false);
m_BtnNCJumpLine->setEnabled(false);
m_rightBoxLayout->addWidget(m_ncTable, 2);
m_rightBoxLayout->addLayout(m_rightRightBoxLayout, 1);
//// === 右侧布局结束
//// === 整体布局=====
m_wholeBoxLayout = new QHBoxLayout(); //定义整体布局
m_wholeBoxLayout->addLayout(m_leftBoxLayout); //将左侧布局添加到整体布局
m_wholeBoxLayout->addSpacerItem(new QSpacerItem(30, 10));
m_wholeBoxLayout->addLayout(m_rightBoxLayout); // 将右侧布局添加到整体布局
m_wholeBoxLayout->setStretch(0, 6);
m_wholeBoxLayout->setStretch(2, 4);
ui.centralWidget->setLayout(m_wholeBoxLayout);
// =====
m_StatusBarUserLabel = new QLabel(QString("用户: "));
m_StatusBarAuthorLabel = new QLabel(QString("南京航空航天大学NUAA"));
ui.statusBar->addPermanentWidget(m_StatusBarUserLabel);
ui.statusBar->addPermanentWidget(m_StatusBarAuthorLabel);
return 0;
void AFStart::OnClickedSelectAutoManuModeBtn()
// 一些具体的操作
// 当有NC程序正在运行,不能随便切断,所以直接返回(可以弹出提示框,这里省略)
if (m_Robot.m_SysStatus.isNCRunning)
return;
// 手动、自动模式切换
if (m_Robot.m_SysStatus.mAutoOrManuMode == OperateModeFlag_Auto)
m_Robot.m_SysStatus.mAutoOrManuMode = OperateModeFlag_Manual;
else if (m_Robot.m_SysStatus.mAutoOrManuMode == OperateModeFlag_Manual)
m_Robot.m_SysStatus.mAutoOrManuMode = OperateModeFlag_Auto;
refreshUI();
return;
void AFStart::refreshUI()
// 功能按钮区域的刷新
m_selectAutoModeBtn->setChecked(m_Robot.m_SysStatus.mAutoOrManuMode == OperateModeFlag_Auto); // 自动模式为OperateModeFlag_Auto时打开
m_selectManualModeBtn->setChecked(m_Robot.m_SysStatus.mAutoOrManuMode == OperateModeFlag_Manual); // 手动模式为OperateModeFlag_Manual时打开
m_TabMotion->refreshUI();
return;
void AFStart::connectSignalSlot()
connect(m_selectAutoModeBtn, &QPushButton::clicked, this, &AFStart::OnClickedSelectAutoManuModeBtn);
connect(m_selectManualModeBtn, &QPushButton::clicked, this, &AFStart::OnClickedSelectAutoManuModeBtn);
connect(m_refreshUiTimer, &QTimer::timeout, this, &AFStart::OnTimerToRefresh); // 刷新界面
return;
void AFStart::OnTimerToRefresh()
refreshUI();
return;
可能一开始看到这么多代码会糊里糊涂,但是如果你是一部分一部分的逐步添加,就会明白这些代码的含义。
4 界面控件是如何执行相应的操作函数的?——信号和槽机制
我们的用户界面初始状态,当构造函数执行之后就确定了。但是之后如何知道我们点击哪个控件要执行什么操作呢?这个机制就是Qt著名的信号和槽机制了。
理解这个,我们先看看一些著名的场景:
三国演义之类的故事,总是频繁出现“埋伏五百刀斧手于帐后, 以摔杯为号 ……”。

罗贯中《三国演义》第十七回:“吾与杨将军反戈击之。但看 火起为号 ,温侯以兵也。(我与杨奉将军率部调过枪头攻打袁术,只要看到军中起了大火,吕布就骑起兵来攻,夹击袁术。)
这里就是一种很明确的信号机制,操作发起人(用户)通过摔杯或点火等某个动作(点击按钮等)给出一个信号,然后执行的人发现这个信号立即执行某一固定的操作(槽函数)。
信号和槽机制从应用者的角度并不复杂,但程序某个操作可以引发一个信号,然后定义的槽函数执行这个信号。这里面其实涉及几部分:信号的定义;槽函数的定义;信号和槽函数的绑定。
对于已有的标准控件来说,信号和槽机制特别简单。因为信号都已经被定义好了,直接用就行了。
比如,我们希望定义一个按钮,被单击之后的操作。以自动模式按钮为例:

我们首先需要定义一个响应这个操作的槽函数。
在头文件中增加定义:
//响应槽函数
void OnClickedSelectAutoManuModeBtn();
在实现文件中增加函数的实现(注意命名还是要体现功能特征):
void AFstart::OnClickedSelectAutoManuModeBtn()
// 一些具体的操作
return;
}
然后在构造函数中调用关联函数
connect(m_selectAutoModeBtn, &QPushButton::clicked, this, &AFstart::OnClickedSelectAutoManuModeBtn);
connect(m_selectManualModeBtn, &QPushButton::clicked, this, &AFstart::OnClickedSelectAutoManuModeBtn);
然后就可以使用了。
你看,定义实际的响应槽函数,然后用这一句话把信号和槽函数就绑定到一起去了。
你看这个connect函数就是关联信号和槽函数的,很好理解:
- 第一个参数就是发信号的对象。这里是m_selectAutoModeBtn(选择自动模式)按钮;
- 第二个参数就是发的信号。QPushButton::clicked就是button按钮被单击了。标准的控件往往预定义了很多信号,比如还有双击。
- 第三个参数就是接受信号并响应的对象。This就是自身对象指针的意思,注意我们这个窗口类定义相应的时候,并不知道实例化对象的名字,所以用了this指针。
- 第四个参数就是响应信号的对象的实际执行操作的函数(槽函数)。
★注意:需要特别提醒的,信号和槽函数机制是Qt的最基本的功能,非常常用和好用,但是前提是, 用到这个机制的类必须继承于QObject 。当然由于QMainWindow都派生于QObject,所以Qt自带的类的派生类不需要写这个基类,但是如果要自行定义一个类,那么这个基类就不可缺少了。否则无法使用这一机制。
5 增加定时器来刷新数据(最简单的多线程例子)
截止到前面为之,我们都是从界面上操作的,然而很多时候,我们需要从其他地方获取数据,然后做一些操作,界面可能也要随着变化。
如何实现这个操作呢?我们需要一个定时器,然后每过一段时间,自动刷新以下数据和界面。
这个非常简单,其实就是一个时间循环而已。
但是问题来了,如果我们UI一直在这个循环里,那么就没法响应我们其它操作了。
怎么办呢?就要依靠多线程。
换句话说,开一个专门的线程,每隔一段时间去刷新一下数据和界面。这样由于操作只占用非常小的时间,所以不影响我们人工操作其它按钮。
幸运的是,Qt对于定时器,有着非常简便的多线程操作方式。
首先在头文件的界面类添加一个定时器成员变量和对应的响应槽函数:
QTimer* m_refreshUiTimer;
// 响应槽函数
void OnTimerToRefresh();
然后在CPP文件,给出一个槽函数实现:
// 刷新系统界面的槽函数
void AFstart::OnTimerToRefresh()
refreshData();
refreshUI();
return;
}
然后用connect把信号和槽连起来
connect(m_refreshUiTimer, &QTimer::timeout, this, &AFstart::OnTimerToRefresh); // 刷新界面
最后在构造函数中启动线程:
AFstart::AFstart(QWidget *parent)
: QMainWindow(parent)
ui.setupUi(this);
// 以下是添加的代码
InitQSS();
int outMsg = initUiWindow();
refreshUI();
// 刷新界面显示功能
// 初始化m_refreshTimer成员变量