基于Qt的网格布局实现下拉式窗口

目录

一、效果演示

二、业务实现

1、业务需求

2、 界面布局

3、 创建下拉式窗口

 4、检测在线状态

三、资源代码

四、小结


一、效果演示

二、业务实现

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

四、小结

        该项目的核心难点在于:如何跟随主窗口尺寸的变化对布局中的窗口进行相应调整。总体来说难度不大,可以去掉多余功能当个轮子用。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Qt是一个功能强大的GUI开发框架,它提供了多种布局管理器来帮助我们创建可扩展和可定制的用户界面。 Qt网格布局(QGridLayout)是一种方便且灵活的布局管理器,它将控件放置在一个网格中。网格布局允许在一个窗口部件中以行和列的形排列控件,使得控件之间的位置可以更容易地调整。 要在Qt网格布局中移动位置,可以使用以下步骤: 1. 创建一个新的Qt网格布局对象,可以使用QGridLayout类来创建。例如: QGridLayout *gridLayout = new QGridLayout; 2. 创建需要放置在网格布局中的控件对象。 3. 使用addWidget()函数将控件添加到网格布局中。该函数接受四个参数:要添加的控件、控件要放置的行索引、控件要放置的列索引、控件要占据的行数和列数。例如: gridLayout->addWidget(widget, row, column, rowSpan, columnSpan); 4. 可以使用setSpacing()函数来设置控件之间的间距。 5. 若要移动位置,可以使用addWidget()函数动态改变控件的行和列索引。例如: gridLayout->addWidget(widget, newRow, newColumn, rowSpan, columnSpan); 6. 最后,将网格布局设置给需要应用布局的窗口部件对象,可以使用setLayout()函数。例如: QWidget *widget = new QWidget; widget->setLayout(gridLayout); 通过这些步骤,我们可以实现网格布局中移动控件的位置。需要注意的是,网格布局是自适应的,控件的大小会根据窗口部件的大小和其他控件的大小进行调整,可以通过调整行列索引和跨度来自定义控件在布局中的位置和大小。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值