目录
一、问题背景
在进行ETC测试工具开发过程中使用了无边框的界面,在重写鼠标相关事件进行拉伸移动等操作时发现部分边界区域会丢失鼠标事件,导致界面操作失败。
二、无边框窗口实现代码
Qt默认的窗口大部分时候无法满足项目自身的需求,且修改起来较为麻烦;为了更好的实现软件与用户的交互往往需要自定义窗口;而自定义的窗口本身没有边框,因此无法实现窗口的移动和拉伸等操作。以下是自定义无边框通过继承实现鼠标事件,从而实现窗口的移动与大小拉伸缩放代码。
2.1 头文件
class EtcTestTool : public QMainWindow
{
Q_OBJECT
public:
EtcTestTool(QWidget *parent = Q_NULLPTR);
private:
void InitialUi();
QString getVersion()
//...
public slots:
void on_btnMin_clicked();
void on_btnMax_clicked();
void on_btnClose_clicked();
void slUserManual();
void slAbout();
protected:
void mousePressEvent(QMouseEvent*event)override;
void mouseReleaseEvent(QMouseEvent*event)override;
void mouseMoveEvent(QMouseEvent*event)override;
private:
// 鼠标的 活动范围的枚举
enum MousePosition
{ /* 这里我们将一个窗口划分为9个区域,分别为
左上角(1, 1)、中上(2,1)、右上角(3, 1)
左中 (1, 2)、 中间(2, 2)、右中 (3, 2)
左下角(1, 3)、中下(2,3)、 右下角(3, 3)
10*x+y区分各个区域
*/
LeftTop = 11,
Top = 21,
RightTop = 31,
Left = 12,
Mid = 22,
Right = 32,
LeftBottom = 13,
Bottom = 23,
RightBottom = 33
};
//根据鼠标的设置鼠标样式,用于拉伸
void SetMouseCursor(int x, int y);
//判断鼠标的区域,用于拉伸
int GetMouseRegion(int x, int y);
private:
QPoint m_windowsLastPs;
QPoint m_mouseLastPs;
int m_mouse_press_region = MousePosition::Mid;
QPoint m_dragStartPos;
bool m_bPressing = false;
private:
Ui::EtcTestToolClass ui;
};
2.2 实现文件
EtcTestTool::EtcTestTool(QWidget *parent)
: QMainWindow(parent)
{
setWindowFlags(Qt::FramelessWindowHint);
ui.setupUi(this);
InitialUi();
//...
}
void EtcTestTool::InitialUi()
{
//mainwindow的鼠标跟踪事件被子控件遮挡拦截
this->setMouseTracking(true);
ui.centralWidget->setMouseTracking(true);
ui.frame->setMouseTracking(true);
ui.frmTitle->setMouseTracking(true);
ui.groupBox->setMouseTracking(true);
//...
//自定义最大化最小化 关闭 隐藏 提示等toolButton
ui.btnMin->setStyleSheet("QToolButton {border-image:url(:/images/icon/main_min.png);}");
//btnMax 根据属性Max = true与否设置border-image 完成最大化和最小化图标切换
string btnMaxStyleStr = R"(QToolButton#btnMax[Max=true]
{
border-image:url(:/images/icon/main_restore.png);
}
QToolButton#btnMax[Max=false]
{
border-image:url(:/images/icon/main_max.png);
})";
ui.btnMax->setStyleSheet(btnMaxStyleStr.c_str());
ui.btnClose->setStyleSheet("QToolButton{border-image:url(:/images/icon/main_close.png);}");
string btnInfoMenuStyle = R"(QToolButton#btnInfo::menu-indicator
{image:none;}
QToolButton{border-image:url(:/images/icon/about_white.svg);})";
ui.btnInfo->setStyleSheet(btnInfoMenuStyle.c_str());
auto menuInfo = new QMenu(ui.btnInfo);
menuInfo->addAction(QIcon(":/images/icon/userManual.svg"), tr("用户手册"), this, &PmsUpDater::slUserManual);
menuInfo->addAction(QIcon(":/images/icon/about_grey.svg"), tr("关于版本"), this, &PmsUpDater::slAbout);
ui.btnInfo->setPopupMode(QToolButton::InstantPopup);
ui.btnInfo->setMenu(menuInfo);
}
void EtcTestTool::on_btnMin_clicked()
{
showMinimized();
}
void EtcTestTool::on_btnMax_clicked()
{
if (isMaximized())
{
showNormal();
ui.btnMax->setProperty("Max", false);
}
else
{
showMaximized();
ui.btnMax->setProperty("Max", true);
}
ui.btnMax->style()->polish(ui.btnMax);
}
void EtcTestTool::on_btnClose_clicked()
{
close();
}
void EtcTestTool::mousePressEvent(QMouseEvent*event)
{
if (event->button() == Qt::LeftButton)
{
// 如果是鼠标左键
// 获取当前窗口位置,以窗口左上角为标定
m_windowsLastPs = pos();
// 获取鼠标在屏幕的位置 就是全局的坐标 以屏幕左上角为坐标系
m_mouseLastPs = event->globalPos();
m_bPressing = true;
m_mouse_press_region = GetMouseRegion(event->pos().x(), event->pos().y());
}
QWidget::mousePressEvent(event);
}
void EtcTestTool::mouseReleaseEvent(QMouseEvent*event)
{
if (event->button() == Qt::LeftButton)
{
m_bPressing = false;
}
setCursor(QCursor{});
QWidget::mousePressEvent(event);
}
void EtcTestTool::mouseMoveEvent(QMouseEvent*event)
{
// 设置鼠标的形状
SetMouseCursor(event->pos().x(), event->pos().y());
// 计算的鼠标移动偏移量, 就是鼠标全局坐标 - 减去点击时鼠标坐标
QPoint point_offset = event->globalPos() - m_mouseLastPs;
if ((event->buttons() == Qt::LeftButton) && m_bPressing)
{
if (m_mouse_press_region == Mid)
{
// 如果鼠标是在窗口的中间位置,就是移动窗口
move(m_windowsLastPs + point_offset);
}
else {
// 其他部分 是拉伸窗口
// 获取客户区
QRect rect = geometry();
switch (m_mouse_press_region)
{
// 左上角
case LeftTop:
rect.setTopLeft(rect.topLeft() + point_offset);
break;
case Top:
rect.setTop(rect.top() + point_offset.y());
break;
case RightTop:
rect.setTopRight(rect.topRight() + point_offset);
break;
case Right:
rect.setRight(rect.right() + point_offset.x());
break;
case RightBottom:
rect.setBottomRight(rect.bottomRight() + point_offset);
break;
case Bottom:
rect.setBottom(rect.bottom() + point_offset.y());
break;
case LeftBottom:
rect.setBottomLeft(rect.bottomLeft() + point_offset);
break;
case Left:
rect.setLeft(rect.left() + point_offset.x());
break;
default:
break;
}
setGeometry(rect);
m_mouseLastPs = event->globalPos();
}
}
QWidget::mousePressEvent(event);
}
void EtcTestTool::SetMouseCursor(int x, int y)
{
// 鼠标形状对象
Qt::CursorShape cursor{};
int region = GetMouseRegion(x, y);
switch (region)
{
case LeftTop:
case RightBottom:
cursor = Qt::SizeFDiagCursor; break;
case RightTop:
case LeftBottom:
cursor = Qt::SizeBDiagCursor; break;
case Left:
case Right:
cursor = Qt::SizeHorCursor; break;
case Top:
case Bottom:
cursor = Qt::SizeVerCursor; break;
caseMid:
cursor = Qt::ArrowCursor; break;
default:
break;
}
setCursor(cursor);
}
int EtcTestTool::GetMouseRegion(int x, int y)
{
int region_x = 0, region_y = 0;
// 鼠标的X坐标小于 边界 说明他在最上层区域 第一区域
if (x < kMouseBorderSize)
{
region_x = 1;
}
else if (x > (this->width() - kMouseBorderSize)) {
region_x = 3;
}
else {
region_x = 2;
}
if (y < kMouseBorderSize)
{
region_y = 1;
}
else if (y > (this->height() - kMouseBorderSize)) {
region_y = 3;
}
else {
region_y = 2;
}
return region_x * 10 + region_y;
}
//自定义放大缩小等
void EtcTestTool::on_btnMin_clicked()
{
showMinimized();
}
void EtcTestTool::on_btnMax_clicked()
{
if (isMaximized())
{
showNormal();
ui.btnMax->setProperty("Max", false);
}
else
{
showMaximized();
ui.btnMax->setProperty("Max", true);
}
ui.btnMax->style()->polish(ui.btnMax);
}
void EtcTestTool::on_btnClose_clicked()
{
close();
}
void EtcTestTool::slUserManual()
{
QString currentPath = QCoreApplication::applicationDirPath();
QString filePath = QCoreApplication::applicationDirPath() + QDir::separator() + QString("EtcTestTool使用指南.pdf");
if (!QFile::exists(filePath))
{
QMessageBox::warning(this, TR("用户手册"), QObject::tr("用户手册丢失!"));
return;
}
QString path = QString("file:///%1").arg(filePath);
if (!QDesktopServices::openUrl(QUrl(path, QUrl::TolerantMode)))
{
cLogger("EtcTestTool")->info(TR("用户手册打开失败,路径:%1").arg(path).toStdString());
}
}
void EtcTestTool::slAbout()
{
QString buildInfoStr = getVersion();
QMessageBox::information(this, TR("EtcTestTool版本信息"), buildInfoStr, QMessageBox::NoButton);
}
QString EtcTestTool::getVersion()
{
static QString s_strVersion;
static std::once_flag onceOnly;
std::call_once(onceOnly, [&]() {
char s_month[5];
int month, day, year;
struct tm t = { 0 };
static const char month_names[] = "JanFebMarAprMayJunJulAugSepOctNovDec";
sscanf(__DATE__, "%s %d %d", s_month, &day, &year);
month = (strstr(month_names, s_month) - month_names) / 3;
t.tm_mon = month;
t.tm_mday = day;
t.tm_year = year - 1900;
t.tm_isdst = -1;
int time = mktime(&t);
QDateTime compileDate = QDateTime::fromTime_t(time);
QString strFormat("EtcTestTool build%1_%2");
s_strVersion = strFormat.arg(compileDate.toString("yyyyMMdd")).arg(PMS_SVN_VERSION);
});
return s_strVersion;
}
三、鼠标移动事件无法触发
3.1 现象和原因
Qt默认鼠标跟踪事件是关闭的,只有按下鼠标左键移动时才会触发;所以需要setMouseTracking(true)开启鼠标追踪事件。但是在以上代码中开启鼠标追踪事件后仍然无法在不按左键情况下触发鼠标移动事件,后发现软件复写的QMainWindow的mouseMoveEvent函数,但是QMainWindow界面被centralWidget和布局的各种widget遮挡导致鼠标移动事件无法触发。
3.2 解决方案
将QMainWindow的控件centralWidget等同样开启鼠标跟踪事件setMouseTracking(true),则子控件的鼠标移动事件会通过事件循环传递到父类的QMainWindow中,最终触发QMainWindow的mouseMoveEvent函数。
void EtcTestTool::InitialUi()
{
//mainwindow的鼠标跟踪事件被子控件遮挡拦截,
this->setMouseTracking(true);
ui.centralWidget->setMouseTracking(true);
ui.frame->setMouseTracking(true);
ui.frmTitle->setMouseTracking(true);
ui.groupBox->setMouseTracking(true);
//...
}
四、总结
在Qt的事件循环系统中,对于组件的事件若组件没有默认的事件处理函数则会将该事件继续向组件的父对象进行传递,但是在以上自定义的无边框窗口中窗口的上面有其他的组件覆盖,这些组件屏蔽掉了鼠标事件,因此必须开启这些组件的鼠标跟踪事件,按照事件循环系统的事件流转顺序,最终这些鼠标事件必会传递到最底层的自定义窗口上,进而出发自定义的鼠标事件处理函数。总之,清醒的认识Qt的对象树和事件循环的流程对于该问题的分析和解决必不可少。