wangwenx190 / framelesshelper

Project moved to: https://github.com/stdware/qwindowkit Cross-platform window customization framework for Qt Widgets and Qt Quick. Supports Windows, Linux and macOS.
MIT License
846 stars 203 forks source link

关于 Win11 Snap Layout 按钮事件处理的提议 #257

Closed SineStriker closed 1 year ago

SineStriker commented 1 year ago

感觉一定要设一个 setHovered、setPressed、clicked 信号有以下缺点:

  1. 需要继承 QAbstractButton,麻烦
  2. 与 Qt 事件处理机制割裂,因此 style 和 paint 会有问题

我现在是这么做的。

// SystemButton is overrided from QPushButton or QToolButton
void SystemButton::setHovered(bool value) {
    auto pos = QCursor::pos();
    if (value) {
        if (underMouse()) {
            QHoverEvent e(QEvent::HoverMove, mapFromGlobal(pos), mapFromGlobal(pos),
                          QApplication::keyboardModifiers());
            QApplication::sendEvent(this, &e);
        } else {
            QEnterEvent e(mapFromGlobal(pos), mapFrom(window(), pos), pos);
            QHoverEvent e1(QEvent::HoverEnter, mapFromGlobal(pos), mapFromGlobal(pos),
                           QApplication::keyboardModifiers());
            QApplication::sendEvent(this, &e);
            QApplication::sendEvent(this, &e1);
        }
    } else {
        if (underMouse()) {
            QEvent e(QEvent::Leave);
            QHoverEvent e1(QEvent::HoverLeave, mapFromGlobal(pos), mapFromGlobal(pos),
                           QApplication::keyboardModifiers());
            QApplication::sendEvent(this, &e);
            QApplication::sendEvent(this, &e1);
        }
    }
}

void SystemButton::setPressed(bool value) {
    if (isDown() == value) {
        return;
    }

    QMouseEvent e(value ? QEvent::MouseButtonPress : QEvent::MouseButtonRelease,
                  mapFromGlobal(QCursor::pos()), Qt::LeftButton, Qt::NoButton,
                  QApplication::keyboardModifiers());
    QApplication::sendEvent(this, &e);
}

setHovered 和 setPressed 的处理可以参考这样的做法,clicked 可以直接不要(因为 QAbstractButton 在 mouseReleaseEvent 中会发出这个信号)。

其他可以参考的点:

  1. qapplication.cpp 中 QApplicationPrivate::notify_helper() 里,当收到 QEvent::Enter 时会设置目标的 Qt::WA_UnderMouse 属性,以 qwindowsvistastyle.cpp 中的 QWindowsVistaStyle::drawComplexControl 为例,它在重绘按钮时会参考这个属性,QStyleSheetStyle 里也是同理。所以针对 setHovered 我们可以自己发送 QEnterEvent 和 QEvent(QEvent::Leave);
  2. 在 Qt 处理鼠标点击事件的时候,会隐式 grab 一个 QWidget,这个实例的指针 qapplication.cpp 中的 extern Q_WIDGETS_EXPORT QWidget *qt_button_down;,是一个导出的符号,因此我们是可以使用的。它在 qwidgetwindow.cpp 与 qwidget.cpp 中都被引用了,其中 QWidgetWindow::handleMouseEvent 会对这个值进行更新,因此为了与Qt框架更好地 integrate,建议是在 setPressed 的处理中将鼠标点击和释放事件发送给 QWindow,并监视这个指针。(我上面为了方便就直接把事件发送给 widget 自己了,但是在 framelesshelper 框架中是没法拿到 QAbstractButton::isDown() 的,因为 widget 不一定是个按钮,所以推荐使用 qt_button_down)
SineStriker commented 1 year ago

具体例子在 https://github.com/SineStriker/qtmediate/blob/main/src/plugins/windowhandles/nativewindow/Widgets/SystemButton.cpp

wangwenx190 commented 1 year ago

我确实一直想改进这个地方,非常感谢你的帮助!

wangwenx190 commented 1 year ago

我已经参考你的代码修改了FramelessHelper的做法,现在确实不需要手动实现那几个接口了,都是用Qt自己的事件驱动。你有时间的话能拿最新代码测试一下吗?

SineStriker commented 1 year ago

我去试试

SineStriker commented 1 year ago

我是5.15.2,QHoverEvent不支持5个参数的构造函数,另外QEnterEvent是在Qt5里就引入了。已经提交了一个PR,但是还有一个问题,就是鼠标按下按钮然后在外面放开的话,虽然点击事件没有触发,但是hover状态并不会解除。

SineStriker commented 1 year ago

输出了一下每次emulateQtMouseEvent的参数,觉得现在这样是有问题的,因为每次Hover/Press/Release会发送好几个Normal,没看过细节不知道这是为什么。

wangwenx190 commented 1 year ago

但是还有一个问题,就是鼠标按下按钮然后在外面放开的话,虽然点击事件没有触发,但是hover状态并不会解除。

这个我也发现了,正想办法解决。不过暂时不知道为什么会这样

wangwenx190 commented 1 year ago

每次Hover/Press/Release会发送好几个Normal

Normal不会发给正在被hover或者press的按钮的,应该没什么问题。你看到的应该是我发给其他按钮的normal

SineStriker commented 1 year ago

每次Hover/Press/Release会发送好几个Normal

Normal不会发给正在被hover或者press的按钮的,应该没什么问题。你看到的应该是我发给其他按钮的normal

你说得对,我发现了,那我试试看能不能解决这个问题。

SineStriker commented 1 year ago

我明白了,是因为鼠标在外部松开的时候不会发送 Release 事件,而 press 状态的解除必须发送 Release 事件。在 QAbstractButton::mouseReleaseEvent 的处理中,如果鼠标不在按钮内那么是不会触发 clicked 的。另外,MouseMove事件不应该在 Hover 状态下一直发送,应当判断是否设置了mouseTracking(由于 utils.cpp 没有链接 QtWidgets 只能读取 QProperty),如果没有设,那么只能在 Pressed 的时候才发送 MouseMove。

正常的控件是这样的: 鼠标移入,变成 hover;鼠标按下,变成 pressed;按下后鼠标移到外部,变成 hover;不松开鼠标再移回,变成 pressed。在鼠标放开之前,是不会变回 normal 的。但要完成这个功能,估计得在框架内标记控件的状态了。

wangwenx190 commented 1 year ago

感谢你的深入研究!不过Qt内部应该有记录控件状态的系统吧,我们能否通过Qt的私有接口对接进去?如果我们自己实现一套,感觉可能会与Qt自身失去同步。

wangwenx190 commented 1 year ago

我还有一点疑惑,就是我们模拟的鼠标事件究竟应该发给谁,是控件本身,还是其顶层窗口?我试验了几次,好像发给控件和窗口都有不同的效果。

wangwenx190 commented 1 year ago

鼠标在外部松开的时候不会发送 Release 事件,而 press 状态的解除必须发送 Release 事件。在 QAbstractButton::mouseReleaseEvent 的处理中,如果鼠标不在按钮内那么是不会触发 clicked 的

明白了,我想办法额外发送一次release事件就可以了

MouseMove事件不应该在 Hover 状态下一直发送,应当判断是否设置了mouseTracking(由于 utils.cpp 没有链接 QtWidgets 只能读取 QProperty),如果没有设,那么只能在 Pressed 的时候才发送 MouseMove。

明白了,非常感谢。我去修改。

SineStriker commented 1 year ago

我还有一点疑惑,就是我们模拟的鼠标事件究竟应该发给谁,是控件本身,还是其顶层窗口?我试验了几次,好像发给控件和窗口都有不同的效果。

鼠标事件的话,MousePress和MouseRelease建议是都发送给QWindow,因为我上面提过有个隐式 grab 的指针 qt_button_down,这个东西就是 Qt 自己记录状态的数据结构之一,在控件外部放开鼠标等价于一个不明目标的 MouseRelease 产生出来,QApplication就会把它分发给 qt_button_down,这个指针我们也可以用(但是需要链接 QtWidgets,你看怎么方便怎么处理,但是对 QtQuick 可能会有点影响)。其他鼠标事件直接发送到 widget 就行了,但是我没用过QtQuick...所以这里面就不了解了。

wangwenx190 commented 1 year ago

我又尝试了很多做法,有以下发现:

(1) 发送release事件必定会触发click信号,不管鼠标在哪里 (2) 对于QWidget,如果把hover事件发给QWindow,则无法触发QWidget自己的事件,只有把事件发给具体的QWidget才行,而QtQuick则正好相反。