目录
一、效果演示
二、业务实现
1、业务需求
实现一个界面,可以接收数据并自动将数据添加到下拉式窗口中,此窗口可以显示并更新数据,当数据不再发送则判断发送端已离线;发送端具备优先级,一级排列在布局最前端,二级排在之后,三级排在末尾;此外,窗口还具有自我删除、更改优先级的功能。
代码主要分为四类:MyMainWidget、TestWidget、GridWidget、ExWidget
①MyMainWidget主窗口类,包含了左侧的发送数据窗口和右侧接收数据并管理布局的窗口
#include "MyMainWidget.h"
#include "ui_MyMainWidget.h"
MyMainWidget::MyMainWidget(QWidget *parent) :
QWidget(parent),
ui(new Ui::MyMainWidget)
{
ui->setupUi(this);
//设置左右窗口尺寸
ui->testWidget->setFixedWidth(400);
ui->gridWidget->setMinimumWidth(ui->gridWidget->width()/5+12);
//设置窗口图标&标题
this->setWindowIcon(QIcon(":/image/DeviceLinker.png"));
this->setWindowTitle("DeviceLinker");
//创建托盘
m_Tray = new QSystemTrayIcon();
trayMenu = new QMenu();
trayShow = new QAction(tr("&显示主窗口"));
trayExit = new QAction(tr("&退出"));
trayMenu->addAction(trayShow);
trayMenu->addAction(trayExit);
m_Tray->setContextMenu(trayMenu);
m_Tray->setIcon(QIcon(":/image/DeviceLinker.png"));
m_Tray->show();
//模拟接收数据信号槽
connect(ui->testWidget,&TestWidget::sendData,ui->gridWidget,&GridWidget::_recvData);
//托盘功能信号槽
connect(m_Tray, &QSystemTrayIcon::activated,
this, &MyMainWidget::_Tray_Clicked);
connect(trayShow, &QAction::triggered, this, &MyMainWidget::_TrayMenu_showWindow_slot);
connect(trayExit, &QAction::triggered, this, &MyMainWidget::_TrayMenu_Exit_slot);
}
MyMainWidget::~MyMainWidget()
{
delete ui;
this->close();
}
int MyMainWidget::_Tray_Clicked(QSystemTrayIcon::ActivationReason reason)
{
//双击托盘显示主窗口
if(reason == QSystemTrayIcon::DoubleClick)
{
this->showNormal();
}
return 0;
}
void MyMainWidget::_TrayMenu_showWindow_slot()
{
this->showNormal();
}
void MyMainWidget::_TrayMenu_Exit_slot()
{
this->close();
QApplication::exit();
}
②TestWidget发送数据窗口类,用于模拟数据连接
#ifndef TESTWIDGET_H
#define TESTWIDGET_H
#include <QWidget>
#include <QTimer>
#include "ExWidget.h"
namespace Ui {
class TestWidget;
}
class TestWidget : public QWidget
{
Q_OBJECT
public:
explicit TestWidget(QWidget *parent = 0);
~TestWidget();
signals:
void sendData(WidgetData);
protected slots:
void _pBtnSend_Clicked();
void _timeout();
private:
Ui::TestWidget *ui;
WidgetData m_Data;//数据包
QTimer *m_Timer;//定时发送
};
#endif // TESTWIDGET_H
③GridWidget布局管理类,用于管理下拉式窗口的布局
#ifndef GRIDWIDGET_H
#define GRIDWIDGET_H
#include <QWidget>
#include <QMouseEvent>
#include <QContextMenuEvent>
#include <QAction>
#include <QMenu>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QGridLayout>
#include <QList>
#include <QScrollArea>
#include <QScrollBar>
#include <QTimer>
#include <stdint.h>
#include <string>
#include "ExWidget.h"
#include "TestWidget.h"
namespace Ui {
class GridWidget;
}
class GridWidget : public QWidget
{
Q_OBJECT
public:
explicit GridWidget(QWidget *parent = 0);
~GridWidget();
public slots:
//设置优先级
void _changeFlag();
//定时接收
void _recvData(WidgetData);
protected:
//初始化
void init_MainWidget();
//收到数据创建窗口
void createExWidget();
//窗口尺寸改变事件
void resizeEvent(QResizeEvent *) override;
//连接信号槽
void signal_slots();
//右键菜单
void contextMenuEvent(QContextMenuEvent *event) override;
//布局管理
void layoutManager(ExWidget *, int);
//更新布局
void updateLayout();
//根据展开位置将所在行全部展开
void expandRow(int);
//根据展开位置将所在行全部折叠
void foldRow(int);
//释放
void releaseMainWidget();
protected slots:
//扩展窗口
void _addExWidget();
//展开窗口
void _expandWidget();
//折叠窗口
void _foldWidget();
//判断窗口是否扩展 更新尺寸
void _expandSize();
private:
Ui::GridWidget *ui;
QWidget *mainWidget;//滚动区域的主窗口
QScrollArea *m_scrollArea;//滚动区域
QMenu *myContextMenu;//右键菜单
QMenu *addWidget;//右键添加设备菜单
QAction *Xbox;//设备Xbox
QAction *PS;//设备PS
QAction *Switch;//设备Switch
QAction *expandAll;//全部展开
QAction *foldAll;//全部折叠
QGridLayout *gridLay;
QVBoxLayout *vBox;
QList<ExWidget *> exWidgets;//窗口添加顺序列表
QList<ExWidget *> oldWidgets;//当前布局顺序列表
//窗口尺寸(用于初始化时根据主窗口的尺寸参数来规范小窗口的尺寸)
int m_width;
int m_width_Max;
int m_width_Min;
int m_height_Min;
int m_height_Max;
QSize m_widgetSize;
WidgetData m_widgetData;//小窗口数据包
QString widgetName;//小窗口设备名称(适用于发送窗口中自定义的设备)
ExWidget::WidgetName exWidgetName;//小窗口设备名称(适用于右键中的默认设备)
};
#endif // GRIDWIDGET_H
④ExWidget下拉式可扩展窗口类,可以显示接收到的数据,具有折叠&扩展两种状态
#ifndef EXWIDGET_H
#define EXWIDGET_H
#include <QWidget>
#include <QEvent>
#include <QMouseEvent>
#include <QContextMenuEvent>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QLabel>
#include <QMenu>
#include <QAction>
#include <QTimer>
namespace Ui {
class ExWidget;
}
#pragma once
//数据包
struct WidgetData{
QString Device = "PC"; //设备名称
QString Ip = "0.0.0.0"; //发送端地址
QString User = "UserName"; //用户名
unsigned int currentTime = 0; //发送时间
int Flag = 0; //优先级
};
class ExWidget : public QWidget
{
Q_OBJECT
public:
//设备名称(默认设备)
enum WidgetName{Xbox,PS,Switch};
//设备优先级
enum WidgetType{first, second, third};
//返回设备名称
WidgetName widgetName() const {return myName;}
//返回设备优先级
WidgetType widgetType() const {return myType;}
//获取设备名称(自定义设备)
QString deviceName() const {return myDevice;}
//构造函数:参数为设备名称与父窗口尺寸
explicit ExWidget(WidgetName widgetName, QSize size, QWidget *parent = nullptr);
//构造函数:参数为自定义数据包与父窗口尺寸
ExWidget(WidgetData widgetData, QSize size, QWidget *parent = nullptr);
~ExWidget();
bool eventFilter(QObject *watched, QEvent *event) override;
bool expand, expandManual, foldManual;
//更新数据
void updateData(WidgetData);
//扩展&折叠
void expandWidget();
signals:
void delThisWidget();
void changeThisFlag();
void isExpand();
public slots:
//删除功能
void _deleteWidget();
//设置优先级
void _changeFlag();
protected slots:
//检测在线状态
void _timeout_Detect();
protected:
//初始化
void init_ExWidget();
//窗口尺寸改变事件
void resizeEvent(QResizeEvent *) override;
//连接信号槽
void signal_slots();
//右键菜单
void contextMenuEvent(QContextMenuEvent *event) override;
//创建标题窗口
void createTitleWidget();
//创建扩展窗口
void createExpandWidget();
private:
Ui::ExWidget *ui;
WidgetType myType;
WidgetName myName;
WidgetData myData;
QString myDevice;
QTimer *myTimer;
//右键功能
QMenu *m_Menu;
QMenu *m_ChangeFlag;
QAction *m_Delete;
QAction *m_toFirst;
QAction *m_toSecond;
QAction *m_toThird;
QFrame *frame;
//标题窗口布局
QLabel *lb_Arrow;
QLabel *lb_Title;
QLabel *lb_Light;
QHBoxLayout *hBox;
QVBoxLayout *vBox;
//窗口尺寸
float widgetWidth;
float widgetHeight;
float parentWidth;
float parentHeight;
//扩展窗口布局
QWidget *m_widget;
QWidget *m_widget2;
QVBoxLayout *vBox_ex;
QVBoxLayout *vBox_ex2;
QHBoxLayout *hBox_ex;
QLabel *Ex_Icon;
QLabel *Ex_IP;
QLabel *Ex_IP_Data;
QLabel *Ex_User;
QLabel *Ex_User_Data;
QLabel *Ex_LoginTime;
QLabel *Ex_LoginTime_Data;
QLabel *Ex_OnLineState;
QLabel *Ex_OnLineState_Data;
};
#endif // EXWIDGET_H
2、 界面布局
主界面整体分为发送窗口与接收窗口,左边发送窗口固定宽度,设置最小高度;右边接收窗口设置最小宽度(即一个小窗口的宽度)。添加的小窗口采用网格布局管理,整体与接收窗口左上对齐,并且布局可以随窗口变化而调整。
核心代码——布局管理:
/*************************************************************
*函数名称:layoutManager
*函数功能:将参数中的窗口重新布局(添加、删除、更换顺序)
*函数参数:new_widget需要布局的自定义窗口,mode决定如何布局
*************************************************************/
void GridWidget::layoutManager(ExWidget *new_widget, int mode)
{
//检查新加窗口优先级
ExWidget::WidgetType new_type = new_widget->widgetType();
//添加、删除、更改顺序
if(mode == 1)
{
//添加
oldWidgets.clear();
int temp=0;
for(int i=0;i<gridLay->count();i++)
{
//遍历当前布局 依次放入旧窗口列表
oldWidgets << qobject_cast<ExWidget *>(gridLay->itemAt(i)->widget());
//统计优先级最高的窗口数量
if((int)oldWidgets.value(i)->widgetType() == 0)
{temp++;}
}
//根据优先级将新窗口插入列表
switch (new_type) {
case ExWidget::first:
oldWidgets.insert(0,new_widget);
break;
case ExWidget::second:
oldWidgets.insert(temp,new_widget);
break;
case ExWidget::third:
oldWidgets.append(new_widget);
break;
default:
break;
}
updateLayout();
}
else if(mode == 0)
{
//删除
gridLay->removeWidget(new_widget);
oldWidgets.clear();
for(int i=0;i<gridLay->count();i++)
{
//遍历当前布局 依次放入旧窗口列表
oldWidgets << qobject_cast<ExWidget *>(gridLay->itemAt(i)->widget());
}
//全部删除时 窗口列表为零会导致计算异常 所以先更新布局再将窗口移出列表
updateLayout();
exWidgets.removeOne(new_widget);
}
else if(mode == 2)
{
//更换顺序
oldWidgets.removeOne(new_widget);
int temp=0;
for(int i=0;i<oldWidgets.size();i++)
{
//统计优先级最高的窗口数量
if((int)oldWidgets.value(i)->widgetType() == 0)
{temp++;}
}
//根据优先级将新窗口插入列表
switch (new_type) {
case ExWidget::first:
oldWidgets.insert(0,new_widget);
break;
case ExWidget::second:
oldWidgets.insert(temp,new_widget);
break;
case ExWidget::third:
oldWidgets.append(new_widget);
break;
default:
break;
}
updateLayout();
}
}
核心代码——更新布局:
/*************************************************************
*函数名称:updateLayout
*函数功能:根据当前窗口尺寸对布局进行重新调整
* (即计算网格布局的行数与列数并把所有小窗口重新排列)
*函数参数:无
*************************************************************/
void GridWidget::updateLayout()
{
//根据当前窗体尺寸 重新将列表窗口依次放入布局
m_width = this->width();
int colum_num = m_width/exWidgets.last()->width();//最大列数
int count_num = oldWidgets.size();//窗口总数
int temp_num = count_num % colum_num;//最后一行是否完整
int row_num = 0;//总行数
if(temp_num == 0)//最后一行窗口个数正好为列数 没有剩余
{
row_num = count_num/colum_num;
for(int x=0;x<row_num;x++)
{
for(int y=0;y<colum_num;y++)
{
gridLay->addWidget(oldWidgets.value((x*colum_num)+y),x,y);
}
}
}
else//最后一行有剩余
{
row_num = (count_num/colum_num)+1;
for(int x=0;x<row_num-1;x++)
{
for(int y=0;y<colum_num;y++)
{
gridLay->addWidget(oldWidgets.value((x*colum_num)+y),x,y);
}
}
for(int y=0;y<temp_num;y++)
{
gridLay->addWidget(oldWidgets.value(((row_num-1)*colum_num)+y),row_num-1,y);
}
}
//每项设置顶部对齐 否则默认居中对齐
for(int i=0;i<gridLay->count();i++)
{
gridLay->itemAt(i)->setAlignment(Qt::AlignTop);
}
gridLay->update();
}
3、 创建下拉式窗口
下拉式窗口(后文用小窗口代替)用于折叠和展开两种状态,在主窗口布局中添加小窗口时默认处于折叠状态。每个小窗口实际又包含上下垂直布局的两个窗口,只不过折叠状态下将垂直布局下方的窗口移除并隐藏了。
折叠状态
展开状态
点击箭头可切换两种状态,是通过事件过滤器来实现的,将箭头图标Label安装过滤器,识别到鼠标点击时就会触发扩展&折叠函数。
在这个项目初期,小窗口是单独进行展开或折叠的,但后续为了美观,改成了:点击某一个小窗口,把它所在行的所有小窗口都进行展开或折叠。所以,我又添加了foldManual手动折叠、expandManual手动展开两个标志位,以此来判断小窗口是否是主动展开&折叠的。
手动的判断标准是:必须鼠标点击箭头!如果判断是手动的,那么需要向主窗口发送一个信号isExpand(),让主窗口记录该窗口的位置,以此来决定每个小窗口的大小。
bool ExWidget::eventFilter(QObject *watched, QEvent *event)
{
//鼠标点击箭头 显示扩展窗口
if(event->type() == QEvent::MouseButtonPress && watched == lb_Arrow)
{
if(expand)
{
foldManual = true;//手动折叠
expandManual = false;
}
else
{
expandManual = true;//手动展开
foldManual = false;
}
expandWidget();
return true;
}
else
return QObject::eventFilter(watched,event);
}
void ExWidget::expandWidget()
{
if(expand)
{
expand = false;
lb_Arrow->setStyleSheet("border-image: url(:/image/arrow.PNG);");
vBox->removeWidget(m_widget2);
vBox->update();
if(foldManual)
{
emit isExpand();
}
return;
}
expand = true;
lb_Arrow->setStyleSheet("border-image: url(:/image/arrow_down.PNG);");
vBox->addWidget(m_widget2);
vBox->update();
if(expandManual)
{
emit isExpand();
}
}
4、检测在线状态
当发送端发送数据时,小窗口中的数据也会更新,小窗口会定时检测当前时间与数据的更新时间之间的差值(为方便计算,将时间转换成了UTC时间),如果发送端不再发送数据超过10s,那么就会判断该设备离线。
myTimer = new QTimer(this);
myTimer->start(2000);
connect(myTimer,&QTimer::timeout,this,&ExWidget::_timeout_Detect);
void ExWidget::_timeout_Detect()
{
unsigned int time = QDateTime::currentDateTime().toTime_t();
int diff = time - myData.currentTime;
WidgetData offData;
if(abs(diff) > 10)
{
Ex_OnLineState_Data->setText("离线");
lb_Light->setStyleSheet("border-image: url(:/image/offLine.png);");
Ex_IP_Data->setText(offData.Ip);
Ex_User_Data->setText(offData.User);
Ex_LoginTime_Data->setText(QString::number(offData.currentTime));
}
}
三、资源代码
百度网盘:https://pan.baidu.com/s/1DA_e8uolTxQGDRHeTa7y5w?pwd=demo
四、小结
该项目的核心难点在于:如何跟随主窗口尺寸的变化对布局中的窗口进行相应调整。总体来说难度不大,可以去掉多余功能当个轮子用。