主界面布局
MainWindow是继承于QMainWindow的无边框主界面,主界面放了一个QWidget,左右布局,左边widgetLeft是快捷功能导航,右边widgetRight是主要功能区。右边widgetRight也是左右布局,左边widgetRightLeft是中间导航区,右边是点击中间导航后呈现的内容区widgetRightRight,mainwindow.ui如下图
widgetLeft左边快捷功能导航
UI布局说明
一级功能快捷导航为自定义的QWidget,整体垂直布局。
-
- 最上面是用户头像QLabel,用户更新了头像可以加载显示到QLabel中,可点击弹出个人信息的非模态对话框。
-
- 中间widgetNav为自定义的QWidget,主要承载大的功能的QStackWidget切换。
-
- H为与host通信状态QLabel,S为与服务器通信状态QLable。
-
- 最下面更多功能按钮btnMore,弹出菜单执行用户登录、注册、修改密码、等功能。
- 最下面更多功能按钮btnMore,弹出菜单执行用户登录、注册、修改密码、等功能。
widgetRight右边功能导航/title/内容展示区
右侧为QSplitter分割的左右布局,左边widgetRightLeft为中间功能导航区,middleStackedWidge承载左侧一级功能导航窗口。
widgetRightRight为上下布局,上面是自定义title,承载app最大化/最小化/关闭功能按钮;下面部分rightStacktWidget承载中间二级功能导航窗口。
关键功能代码
初始化左侧工具栏
//初始化左侧工具条的按钮
void MainWindow::initLeftToolBar()
{
//1.头像QLabel
ui->headerLabel->setFixedSize(40, 40);
ui->headerLabel->setAlignment(Qt::AlignCenter);
ui->headerLabel->setFrameStyle(QFrame::Panel | QFrame::Sunken);
ui->headerLabel->setStyleSheet("border:none; ");
ui->headerLabel->installEventFilter(this); //1.给label安装事件过滤器
if (userInfo_->headerImage.empty()) {
//设置默认头像
ui->headerLabel->setStyleSheet("QLabel{"
"image:url(:/res/image/header.png) 4 4 4 4 stretch stretch;border-radius:15px;"
"}");
}
//2. stackWidget功能导航区
initNav(); //左侧QToolbutton导航
initIcon(); //QToolbutton的字体图标设置
//3. 清空与host和server通信状态文字
ui->ipcStateLabel->setText("");
ui->serverStateLabel->setText("");
//4. 更多功能
initBtnMore(); //初始化左侧更多按钮和样式
}
这里代码的关键点在于给QLable安装事件监听,用户点击头像弹出个人详细信息对话框。
bool MainWindow::eventFilter(QObject* object, QEvent* event)
{
if (object == ui->headerLabel) {//2.监听头像label
if (event->type() == QEvent::MouseButtonPress && ((QMouseEvent*)event)->button() == Qt::LeftButton) {
//头像label点击, 弹出个人信息
on_headerLabel_clicked();
return true;
}
else {
return QWidget::eventFilter(object, event);
}
}
if (object = ui->widgetTitle) {
//双击标题栏最大化
if (event->type() == QEvent::MouseButtonDblClick) {
on_btnMenuMax_clicked();
}
}
return QWidget::eventFilter(object, event);
}
在事件监听处理函数中,通过QObject来失败事件源和鼠标左键单击事件做on_headerLabel_clicked单出对话框处理。
//头像按钮点击弹出个人信息
void MainWindow::on_headerLabel_clicked()
{
// 获取按钮的位置和大小
QRect buttonRect = ui->headerLabel->geometry();
// 计算对话框的位置
QPoint dialogPos = ui->headerLabel->mapToGlobal(buttonRect.bottomLeft());
dialogPos.setX(dialogPos.x() + ui->widgetLeft->rect().width());
dialogPos.setY(dialogPos.y());
//如果按钮没有创建则重新创建
if (!userInfoDialog_) {
LOG(LS_INFO) << "new UserInfoDialog";
userInfoDialog_.reset(new UserInfoDialog(dialogPos, userInfo_, loginAuthServerState_, this));
userInfoDialog_->setModal(false);
userInfoDialog_->setFixedSize(QSize(400, 350));
connect(userInfoDialog_.get(), &UserInfoDialog::hideDialog, this, &MainWindow::onHideUserInfoDialog); //隐藏对话框
connect(userInfoDialog_.get(), &UserInfoDialog::editUserInfo, this, &MainWindow::onEditUserInfo); //编辑个人信息
this->installEventFilter(userInfoDialog_.get());
}
if (!showUserInfoDialog) {
userInfoDialog_->setGeometry(QRect(dialogPos, QSize(400, 350))); // 宽度200,高度100
userInfoDialog_->show();
showUserInfoDialog = true;
}
else {
userInfoDialog_->hide();
showUserInfoDialog = false;
}
}
个人信息弹框的位置是处理的关键点有两点:1.每次点击需要获取头像QLabel的左下角位置,根据此信息重新设置个人信息对话框位置;2. 个人信息对话框为非模态对话框,点击时如果没有创建则新创建,如果当前是显示状态则隐藏,再次点击则显示。
一级功能导航stackWidgets
这里一次性初始化了四个功能QToolButton,保存到按钮列表QList<QAbstractButton*> leftToolbarBtns_,并给所有按钮绑定了事件响应函数buttonClicked(),点击按钮切换中间导航,关键代码如下:
void MainWindow::initNav()
{
//按钮文字集合
QStringList names, tips;
names << tr("IM") << tr("Friends") << tr("Meetings") << tr("Settings");
tips << tr("Chat messages") << tr("My friends") << tr("Join a meeting") << tr("Settings");
int topMargin = 3, otherMargin = 3;
ui->layoutNav->setContentsMargins(otherMargin, topMargin, otherMargin, otherMargin);
//自动生成按钮
for (int i = 0; i < names.count(); i++) {
QToolButton* btn = new QToolButton;
//设置按钮固定高度
btn->setFixedHeight(45);//调整到合适大小, 解决字体图标与文字的距离
//设置按钮的文字
//btn->setText(QString("%1").arg(names.at(i)));
//设置按钮提示文字
btn->setToolTip(QString("%1").arg(tips.at(i)));
//设置按钮可选中按下类似复选框的功能
btn->setCheckable(true);
//设置按钮图标在左侧文字在右侧
btn->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
//设置按钮拉伸策略为横向填充
btn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred);
//关联按钮单击事件
connect(btn, SIGNAL(clicked(bool)), this, SLOT(buttonClicked()));
//将按钮加入到布局
//ui->layoutNav->addWidget(btn);
ui->widgetNav->layout()->addWidget(btn);
//可以指定不同的图标
leftToolbarBtns_ << btn;
}
//设置flag的方向要与下面 icon的 position保持一致
ui->widgetNav->setProperty("flag", "left");
//底部加个弹簧
QSpacerItem* verticalSpacer = new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding);
ui->widgetNav->layout()->addItem(verticalSpacer);
//根据配置文件存储的子窗体的索引触发按钮单击进行切换
AppConfig::IndexDemo = AppConfig::IndexDemo >= leftToolbarBtns_.count() ? 0 : AppConfig::IndexDemo;
leftToolbarBtns_.at(AppConfig::IndexDemo)->click();
}
按钮的事件响应函数,leftToolbarBtns_.indexOf(btn)即是上面按钮初始化顺序的索引。
//选中按钮切换到对应的页面
void MainWindow::buttonClicked()
{
//切换到当前窗体
QAbstractButton* btn = (QAbstractButton*)sender();//触发的按钮,按钮的顺序与middleStackedWidget有一一对应
ui->middleStackedWidget->setCurrentIndex(leftToolbarBtns_.indexOf(btn));
//保存最后的窗体索引
AppConfig::LastFormMain = btn->objectName();
AppConfig::writeConfig();
//取消其他按钮选中
foreach(QAbstractButton * button, leftToolbarBtns_)
{
button->setChecked(button == btn);
}
}
中间功能导航stackWidgets的创建和切换
创建中间聊天导航和通信录导航widget,创建的顺序要与左侧widgetNav的功能顺序保持一致,通过头文件的枚举可以直观的看到,每次点击左侧快捷功能导航按钮切换,保存当前点击中间导航索引,关键代码如下:
//中间导航窗口widget索引
enum EMiddleNavWidget
{
MIDDLE_NAV_CHAT = 0, //0. 聊天
MIDDLE_NAV_CONTACT, //1. 通信录
MIDDLE_NAV_MEETING, //2. 会议
MIDDLE_NAV_BUTT //3. ....
};
//初始化中间导航
void MainWindow::initMiddleNav()
{
/*---------------- 关联切换widget --------------*/
//0. 好友聊天导航
chatNavWidget_.reset(new ChatNavWidget(serverApi_, userInfo_, friendsMap_, myGroupsMap_, joinedGroupsMap_));
ui->middleStackedWidget->addWidget(chatNavWidget_.get());
//1. 通信录导航 关联
contactNavWidget_ = new ContactNavWidget(userInfo_, friendsMap_, myGroupsMap_, joinedGroupsMap_);
ui->middleStackedWidget->addWidget(contactNavWidget_);
//2. 会议
//最后, 关联中间导航切换的时间响应
connect(ui->middleStackedWidget, &QStackedWidget::currentChanged, this, &MainWindow::middleNavWidgetChanged);
}
//中间导航窗口发生了切换, 恢复窗口上次的选中状态
void MainWindow::middleNavWidgetChanged(int index)
{
switch (index) {
case MIDDLE_NAV_CHAT:
if (chatNavWidget_)
chatNavWidget_->restoreLastCheck();
break;
case MIDDLE_NAV_CONTACT:
contactNavWidget_->restoreLastCheck();
break;
case MIDDLE_NAV_MEETING:
break;
default:
break;
}
lastMiddleNavIndex_ = index;
//LOG(LS_INFO) << "Main middle stactwidgets changed, index:" << index;
}
rightWidget右侧stackWidgets创建
右侧stackwidgets是根据中间导航点击切换显示对应的内容,如默认空窗口rightDefaultWidget,聊天主窗口chatInfoWidget,好友通知窗口friendInfoWidget,设备信息窗口deviceInfoWidget,好友信息窗口friendInfoWidget,群信息窗口groupInfoWidget等,关键代码如下:
//初始化右侧内容widget
void MainWindow::initRightWidget()
{
/*---------------- 右侧 widgets --------------*/
//0. 默认空窗口
rightDefaultWidget_.reset(new RightDefaultWidget());
ui->rightStackedWidget->addWidget(rightDefaultWidget_.get());
//1. 聊天
chatInfoWidget_.reset(new ChatInfoWidget(serverApi_, userInfo_, friendsMap_, myGroupsMap_, joinedGroupsMap_));
connect(chatInfoWidget_.get(), &ChatInfoWidget::sendMessageToIpc, this, &MainWindow::sendMessageToIpc); //发送Ipc消息
ui->rightStackedWidget->addWidget(chatInfoWidget_.get());
LOG(LS_INFO) << "ui->rightStackedWidget width: " << ui->rightStackedWidget->width() << ", rightRightWidget width: " << ui->widgetRightRight->width() << ", widget right width: " << ui->widgetRight->width();
//2. 朋友通知列表
friendNoticeList_ = new FriendNoticeList(ipcThread_);
connect(friendNoticeList_, &FriendNoticeList::sendMessageToIpc, ipcThread_.get(), &IpcThread::onSendMessage);
connect(friendNoticeList_, &FriendNoticeList::newFriendNotice, this, [&](uint32_t newNoticeCount) {
contactNavWidget_->setNewFriendNoticeCount(newNoticeCount);
});
ui->rightStackedWidget->addWidget(friendNoticeList_);
//3. 设备信息: ContactNavWidget中点击某个设备, 切换为展示某个设备的详细信息
deviceInfoWidget_ = new DeviceInfoWidget(ipcThread_);
ui->rightStackedWidget->addWidget(deviceInfoWidget_);
/*4. 好友信息展示 ----------------- 点击好友列表某个好友, 展示好友详细信息 -----------------*/
friendInfoWidget_ = new FriendInfoWidget(ipcThread_);
ui->rightStackedWidget->addWidget(friendInfoWidget_);
//5. 群信息展示 -----------------------------
groupInfoWidget_.reset(new GroupInfoWidget(userInfo_));
ui->rightStackedWidget->addWidget(groupInfoWidget_.get());
}
中间stackWidgets点击切换展示到右侧窗口在后续章节讲解。
总结
主界面功能比较多,一个章节很难讲清楚,本节主要概述了主窗口布局,左边一级功能导航工具条,中间导航stackWidgets创建与切换,右侧stackWidgets窗口的创建,未尽之处欢迎加入QQ群 738529266 交流。
鸣谢:感谢飞扬青云大侠开源了很多有用的功能,链接:https://gitee.com/feiyangqingyun/qtkaifajingyan