如何拦截和取消 Window 的最小化?
How can I intercept and cancel the minimizing of a Window?
我的项目中有一个 Window
子类,在运行时创建实例并完全在 QML 端显示。我知道我可以通过不在 flags:
中包含 WindowMinimizeButtonHint
来防止 window 被最小化,但我实际上需要存在并启用最小化按钮但能够拦截最小化按钮点击,取消实际的最小化,然后做其他事情(仅供参考,我的客户需要这种 non-standard windowing 行为,而不是我)。
到目前为止,我唯一能做到的就是处理 onWindowStateChanged:
事件,检查是否 windowState === Qt.WindowStateMinimized
并从计时器调用 show()
(在内部调用它事件处理程序直接什么都不做)。这导致 window 向下移动到系统托盘,然后突然恢复正常。
有没有办法做到这一点,比如可以取消的 OnMinimized
事件?
编辑:根据 Benjamin T 的回答,我至少部分解决了 OSX:
#import <AppKit/AppKit.h>
bool NativeFilter::nativeEventFilter(const QByteArray &eventType,
void *message, long *result)
{
if (eventType == "mac_generic_NSEvent") {
NSEvent *event = static_cast<NSEvent *>(message);
if ([event type] == NSKeyDown) {
return true;
}
}
return false;
}
在这个例子中,我能够拦截并取消所有 NSKeyDown 事件(同时让其他事件如鼠标点击等仍然有效)。剩下的问题是 我仍然不知道要拦截最小化事件 - NSEvent.h 似乎没有任何内容可以解决这个问题。也许我需要转换为不同类型的事件?
编辑 2 - 工作解决方案:
我找不到任何方法来正确拦截最小化事件并取消它,所以我的解决方法是拦截对 window 的点击,确定点击是否在最小化按钮上方(或关闭或缩放按钮)并取消事件(如果发生)(并向我的 qml window 发送点击发生的通知)。我还处理了 double-clicking 标题栏缩放 window 并使用 Command-M 键最小化 window.
的情况
第一步是实施 QAbstractNativeEventFilter
。在你的 header:
#include <QAbstractNativeEventFilter>
class NativeFilter : public QAbstractNativeEventFilter {
public:
bool nativeEventFilter(const QByteArray &eventType, void *message,
long *result);
};
实施:
#import <AppKit/AppKit.h>
#import <AppKit/NSWindow.h>
#import <AppKit/NSButton.h>
bool NativeFilter::nativeEventFilter(const QByteArray &eventType, void
*message, long *result)
{
if (eventType == "mac_generic_NSEvent") {
NSEvent *event = static_cast<NSEvent *>(message);
NSWindow *win = [event window];
// TODO: determine whether or not this is a window whose
// events you want to intercept. I did this by checking
// [win title] but you may want to find and use the
// window's id instead.
// Detect a double-click on the titlebar. If the zoom button
// is enabled, send the full-screen message to the window
if ([event type] == NSLeftMouseUp) {
if ([event clickCount] > 1) {
NSPoint pt = [event locationInWindow];
CGRect rect = [win frame];
// event coordinates have y going in the opposite direction from frame coordinates, very annoying
CGFloat yInverted = rect.size.height - pt.y;
if (yInverted <= 20) {
// TODO: need the proper metrics for the height of the title bar
NSButton *btn = [win standardWindowButton:NSWindowZoomButton];
if (btn.enabled) {
// notify qml of zoom button click
}
return true;
}
}
}
if ([event type] == NSKeyDown) {
// detect command-M (for minimize app)
if ([event modifierFlags] & NSCommandKeyMask) {
// M key
if ([event keyCode] == 46) {
// notify qml of miniaturize button click
return true;
}
}
// TODO: we may be requested to handle keyboard actions for close and zoom buttons. e.g. ctrl-cmd-F is zoom, I think,
// and Command-H is hide.
}
if ([event type] == NSLeftMouseDown) {
NSPoint pt = [event locationInWindow];
CGRect rect = [win frame];
// event coordinates have y going in the opposite direction from frame coordinates, very annoying
CGFloat yInverted = rect.size.height - pt.y;
NSButton *btn = [win standardWindowButton:NSWindowMiniaturizeButton];
CGRect rectButton = [btn frame];
if ((yInverted >= rectButton.origin.y) && (yInverted <= (rectButton.origin.y + rectButton.size.height))) {
if ((pt.x >= rectButton.origin.x) && (pt.x <= (rectButton.origin.x + rectButton.size.width))) {
// notify .qml of miniaturize button click
return true;
}
}
btn = [win standardWindowButton:NSWindowZoomButton];
rectButton = [btn frame];
if (btn.enabled) {
if ((yInverted >= rectButton.origin.y) && (yInverted <= (rectButton.origin.y + rectButton.size.height))) {
if ((pt.x >= rectButton.origin.x) && (pt.x <= (rectButton.origin.x + rectButton.size.width))) {
// notify qml of zoom button click
return true;
}
}
}
btn = [win standardWindowButton:NSWindowCloseButton];
rectButton = [btn frame];
if ((yInverted >= rectButton.origin.y) && (yInverted <= (rectButton.origin.y + rectButton.size.height))) {
if ((pt.x >= rectButton.origin.x) && (pt.x <= (rectButton.origin.x + rectButton.size.width))) {
// notify qml of close button click
return true;
}
}
}
return false;
}
return false;
}
然后在 main.cpp:
Application app(argc, argv);
app.installNativeEventFilter(new NativeFilter());
一般来说,你应该使用事件系统而不是signal/slots来拦截事件和变化。
最简单的方法是子class您使用的对象并重新实现适当的事件处理程序,或者使用事件过滤器。
由于您使用的是 QML,因此子classing 可能会很困难,因为您无权访问所有 Qt 内部 classes。
下面是使用事件过滤时的代码。
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
auto root = engine.rootObjects().first();
root->installEventFilter(new EventFilter());
return app.exec();
}
class EventFilter : public QObject
{
Q_OBJECT
public:
explicit EventFilter(QObject *parent = nullptr);
bool eventFilter(QObject *watched, QEvent *event) override;
};
bool EventFilter::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() == QEvent::WindowStateChange) {
auto e = static_cast<QWindowStateChangeEvent *>(event);
auto window = static_cast<QWindow *>(watched);
if (window->windowStates().testFlag(Qt::WindowMinimized)
&& ! e->oldState().testFlag(Qt::WindowMinimized))
{
// Restore old state
window->setWindowStates(e->oldState());
return true;
}
}
// Do not filter event
return false;
}
但是,您很快就会 运行 遇到与使用 signal/slot 机制时相同的问题:Qt 仅在 window 已经最小化时通知您。这意味着此时恢复 window 将产生 hide/show 效果。
所以你需要更深入,你需要一个本机事件过滤器。
以下代码适用于 Windows,您应该针对 macOS 进行调整:
class NativeFilter : public QAbstractNativeEventFilter {
public:
bool nativeEventFilter(const QByteArray &eventType, void *message, long *result);
};
bool NativeFilter::nativeEventFilter(const QByteArray &eventType, void *message, long *result)
{
/* On Windows we interceot the click in the title bar. */
/* If we wait for the minimize event, it is already too late. */
#ifdef Q_OS_WIN
auto msg = static_cast<MSG *>(message);
// Filter out the event when the minimize button is pressed.
if (msg->message == WM_NCLBUTTONDOWN && msg->wParam == HTREDUCE)
return true;
#endif
/* Example macOS code from Qt doc, adapt to your need */
#ifdef Q_OS_MACOS
if (eventType == "mac_generic_NSEvent") {
NSEvent *event = static_cast<NSEvent *>(message);
if ([event type] == NSKeyDown) {
// Handle key event
qDebug() << QString::fromNSString([event characters]);
}
}
#endif
return false;
}
在你的 main() 中:
QGuiApplication app(argc, argv);
app.installNativeEventFilter(new NativeFilter());
有关详细信息,您可以阅读有关 QAbstractNativeEventFilter
的 Qt 文档。
您可能需要使用 QWindow::winId()
检查 window 本机事件的目标。
由于我不是 macOS 开发人员,所以我不知道您可以用 NSEvent
做什么。
此外,NSWindowDelegate
class 似乎对您有用:https://developer.apple.com/documentation/appkit/nswindowdelegate
如果您可以从 QWindow::winId()
中检索到 NSWindow
,您应该可以使用它。
我的项目中有一个 Window
子类,在运行时创建实例并完全在 QML 端显示。我知道我可以通过不在 flags:
中包含 WindowMinimizeButtonHint
来防止 window 被最小化,但我实际上需要存在并启用最小化按钮但能够拦截最小化按钮点击,取消实际的最小化,然后做其他事情(仅供参考,我的客户需要这种 non-standard windowing 行为,而不是我)。
到目前为止,我唯一能做到的就是处理 onWindowStateChanged:
事件,检查是否 windowState === Qt.WindowStateMinimized
并从计时器调用 show()
(在内部调用它事件处理程序直接什么都不做)。这导致 window 向下移动到系统托盘,然后突然恢复正常。
有没有办法做到这一点,比如可以取消的 OnMinimized
事件?
编辑:根据 Benjamin T 的回答,我至少部分解决了 OSX:
#import <AppKit/AppKit.h>
bool NativeFilter::nativeEventFilter(const QByteArray &eventType,
void *message, long *result)
{
if (eventType == "mac_generic_NSEvent") {
NSEvent *event = static_cast<NSEvent *>(message);
if ([event type] == NSKeyDown) {
return true;
}
}
return false;
}
在这个例子中,我能够拦截并取消所有 NSKeyDown 事件(同时让其他事件如鼠标点击等仍然有效)。剩下的问题是 我仍然不知道要拦截最小化事件 - NSEvent.h 似乎没有任何内容可以解决这个问题。也许我需要转换为不同类型的事件?
编辑 2 - 工作解决方案:
我找不到任何方法来正确拦截最小化事件并取消它,所以我的解决方法是拦截对 window 的点击,确定点击是否在最小化按钮上方(或关闭或缩放按钮)并取消事件(如果发生)(并向我的 qml window 发送点击发生的通知)。我还处理了 double-clicking 标题栏缩放 window 并使用 Command-M 键最小化 window.
的情况第一步是实施 QAbstractNativeEventFilter
。在你的 header:
#include <QAbstractNativeEventFilter>
class NativeFilter : public QAbstractNativeEventFilter {
public:
bool nativeEventFilter(const QByteArray &eventType, void *message,
long *result);
};
实施:
#import <AppKit/AppKit.h>
#import <AppKit/NSWindow.h>
#import <AppKit/NSButton.h>
bool NativeFilter::nativeEventFilter(const QByteArray &eventType, void
*message, long *result)
{
if (eventType == "mac_generic_NSEvent") {
NSEvent *event = static_cast<NSEvent *>(message);
NSWindow *win = [event window];
// TODO: determine whether or not this is a window whose
// events you want to intercept. I did this by checking
// [win title] but you may want to find and use the
// window's id instead.
// Detect a double-click on the titlebar. If the zoom button
// is enabled, send the full-screen message to the window
if ([event type] == NSLeftMouseUp) {
if ([event clickCount] > 1) {
NSPoint pt = [event locationInWindow];
CGRect rect = [win frame];
// event coordinates have y going in the opposite direction from frame coordinates, very annoying
CGFloat yInverted = rect.size.height - pt.y;
if (yInverted <= 20) {
// TODO: need the proper metrics for the height of the title bar
NSButton *btn = [win standardWindowButton:NSWindowZoomButton];
if (btn.enabled) {
// notify qml of zoom button click
}
return true;
}
}
}
if ([event type] == NSKeyDown) {
// detect command-M (for minimize app)
if ([event modifierFlags] & NSCommandKeyMask) {
// M key
if ([event keyCode] == 46) {
// notify qml of miniaturize button click
return true;
}
}
// TODO: we may be requested to handle keyboard actions for close and zoom buttons. e.g. ctrl-cmd-F is zoom, I think,
// and Command-H is hide.
}
if ([event type] == NSLeftMouseDown) {
NSPoint pt = [event locationInWindow];
CGRect rect = [win frame];
// event coordinates have y going in the opposite direction from frame coordinates, very annoying
CGFloat yInverted = rect.size.height - pt.y;
NSButton *btn = [win standardWindowButton:NSWindowMiniaturizeButton];
CGRect rectButton = [btn frame];
if ((yInverted >= rectButton.origin.y) && (yInverted <= (rectButton.origin.y + rectButton.size.height))) {
if ((pt.x >= rectButton.origin.x) && (pt.x <= (rectButton.origin.x + rectButton.size.width))) {
// notify .qml of miniaturize button click
return true;
}
}
btn = [win standardWindowButton:NSWindowZoomButton];
rectButton = [btn frame];
if (btn.enabled) {
if ((yInverted >= rectButton.origin.y) && (yInverted <= (rectButton.origin.y + rectButton.size.height))) {
if ((pt.x >= rectButton.origin.x) && (pt.x <= (rectButton.origin.x + rectButton.size.width))) {
// notify qml of zoom button click
return true;
}
}
}
btn = [win standardWindowButton:NSWindowCloseButton];
rectButton = [btn frame];
if ((yInverted >= rectButton.origin.y) && (yInverted <= (rectButton.origin.y + rectButton.size.height))) {
if ((pt.x >= rectButton.origin.x) && (pt.x <= (rectButton.origin.x + rectButton.size.width))) {
// notify qml of close button click
return true;
}
}
}
return false;
}
return false;
}
然后在 main.cpp:
Application app(argc, argv);
app.installNativeEventFilter(new NativeFilter());
一般来说,你应该使用事件系统而不是signal/slots来拦截事件和变化。
最简单的方法是子class您使用的对象并重新实现适当的事件处理程序,或者使用事件过滤器。
由于您使用的是 QML,因此子classing 可能会很困难,因为您无权访问所有 Qt 内部 classes。
下面是使用事件过滤时的代码。
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
auto root = engine.rootObjects().first();
root->installEventFilter(new EventFilter());
return app.exec();
}
class EventFilter : public QObject
{
Q_OBJECT
public:
explicit EventFilter(QObject *parent = nullptr);
bool eventFilter(QObject *watched, QEvent *event) override;
};
bool EventFilter::eventFilter(QObject *watched, QEvent *event)
{
if (event->type() == QEvent::WindowStateChange) {
auto e = static_cast<QWindowStateChangeEvent *>(event);
auto window = static_cast<QWindow *>(watched);
if (window->windowStates().testFlag(Qt::WindowMinimized)
&& ! e->oldState().testFlag(Qt::WindowMinimized))
{
// Restore old state
window->setWindowStates(e->oldState());
return true;
}
}
// Do not filter event
return false;
}
但是,您很快就会 运行 遇到与使用 signal/slot 机制时相同的问题:Qt 仅在 window 已经最小化时通知您。这意味着此时恢复 window 将产生 hide/show 效果。
所以你需要更深入,你需要一个本机事件过滤器。
以下代码适用于 Windows,您应该针对 macOS 进行调整:
class NativeFilter : public QAbstractNativeEventFilter {
public:
bool nativeEventFilter(const QByteArray &eventType, void *message, long *result);
};
bool NativeFilter::nativeEventFilter(const QByteArray &eventType, void *message, long *result)
{
/* On Windows we interceot the click in the title bar. */
/* If we wait for the minimize event, it is already too late. */
#ifdef Q_OS_WIN
auto msg = static_cast<MSG *>(message);
// Filter out the event when the minimize button is pressed.
if (msg->message == WM_NCLBUTTONDOWN && msg->wParam == HTREDUCE)
return true;
#endif
/* Example macOS code from Qt doc, adapt to your need */
#ifdef Q_OS_MACOS
if (eventType == "mac_generic_NSEvent") {
NSEvent *event = static_cast<NSEvent *>(message);
if ([event type] == NSKeyDown) {
// Handle key event
qDebug() << QString::fromNSString([event characters]);
}
}
#endif
return false;
}
在你的 main() 中:
QGuiApplication app(argc, argv);
app.installNativeEventFilter(new NativeFilter());
有关详细信息,您可以阅读有关 QAbstractNativeEventFilter
的 Qt 文档。
您可能需要使用 QWindow::winId()
检查 window 本机事件的目标。
由于我不是 macOS 开发人员,所以我不知道您可以用 NSEvent
做什么。
此外,NSWindowDelegate
class 似乎对您有用:https://developer.apple.com/documentation/appkit/nswindowdelegate
如果您可以从 QWindow::winId()
中检索到 NSWindow
,您应该可以使用它。