【问题标题】:How can I intercept and cancel the minimizing of a Window?如何拦截和取消窗口的最小化?
【发布时间】:2018-07-25 01:07:10
【问题描述】:

我的项目中有一个Window 子类,在运行时该实例被创建并完全显示在 QML 端。我知道我可以通过在flags: 中不包含WindowMinimizeButtonHint 来防止窗口最小化,但我实际上需要存在并启用最小化按钮,但能够拦截最小化按钮单击,取消实际最小化,然后做其他事情(仅供参考,我的客户需要这种非标准的窗口行为,而不是我)。

到目前为止,我唯一能做到的就是处理onWindowStateChanged: 事件,检查windowState === Qt.WindowStateMinimized 并从计时器调用show()(直接在事件处理程序中调用它什么都不做)。这会导致窗口向下移动到系统托盘,然后突然恢复正常。

有没有办法做到这一点,比如可以取消的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 - 工作解决方案:

我无法找到任何方法来正确拦截最小化事件并取消它,所以我的解决方法是拦截窗口上的点击,确定点击是否在最小化按钮(或关闭或缩放按钮) 并取消事件(如果发生了点击,并向我的 qml 窗口发送通知)。我还处理了双击标题栏以缩放窗口,并使用 Command-M 键最小化窗口的情况。

第一步是实现QAbstractNativeEventFilter。在您的标题中:

#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());

【问题讨论】:

  • 这是一个非常奇怪的行为。您需要做什么而不是 OS 最小化?你不能把这个功能放在你应用程序内的控件上吗? (因为窗口管理是操作系统工作)。到目前为止,我知道的唯一方法是实现您自己的标题栏并将其隐藏在操作系统中,然后您可以进行任何您喜欢的行为(但它会破坏样式)。
  • @ymoreau 我的客户希望第二个窗口基本上最小化到应用程序的主窗口而不是标题栏中。我们实际上已经实现了一个自定义标题栏,它可以做我们想要的,但是对于 OSX(仅)他们想要一个原生标题栏。我完全同意这是一种奇怪的行为,但这不是我的决定。
  • 更正:而不是进入*系统托盘
  • 您的需求听起来很像一个停靠小部件,而不是第二个操作系统窗口,但我在 QDockWidget 中没有看到最小化功能,也没有 QML 项目可以做到这一点。跨度>
  • @ymoreau 我将用 QDockWidget 做一个快速实验,看看它是否能满足我的需要。对于我从现在起八小时后的最后期限而言,这可能是一项过于激烈的手术。

标签: qt qml qwindow


【解决方案1】:

一般来说,你应该使用事件系统而不是信号/槽来拦截事件和变化。

最简单的方法是对您使用的对象进行子类化并重新实现适当的事件处理程序,或者使用事件过滤器。

由于您使用的是 QML,子类化可能会很困难,因为您无法访问所有 Qt 内部类。

这是使用事件过滤时代码的样子。

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;
}

但是,您很快就会遇到与使用信号/槽机制时相同的问题:Qt 仅在窗口已经最小化时才通知您。这意味着此时恢复窗口将产生隐藏/显示效果。

所以你需要更深入,你需要一个原生事件过滤器。

以下代码适用于 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() 来检查本机事件所针对的窗口。

由于我不是 macOS 开发人员,我不知道您可以使用 NSEvent 做什么。 此外,NSWindowDelegate 课程似乎对您有用:https://developer.apple.com/documentation/appkit/nswindowdelegate 如果您可以从QWindow::winId() 中检索到NSWindow,您应该可以使用它。

【讨论】:

  • 所以这看起来很有希望,但我不知道如何使您的代码适应 OSX。我将在哪里/如何寻找如何做到这一点?
  • 我发现了这个:doc.qt.io/qt-5/qabstractnativeeventfilter.html,它大部分都在工作——例如,我可以用它来取消任何按键事件(但不能取消鼠标点击等)。我的问题是这段代码将事件转换为NSEvent,并且似乎没有任何情况下可以拦截和取消“最小化”事件的 NSEvent。似乎确实有一个系统事件,所以我要尝试一下。
  • @MusiGenesis 看来您已经找到了使用 Qt 文档的方法。我在回答中添加了一些细节,特别是关于NSWindowDelegate。我对 macOS 上的开发知之甚少,无法为您提供更多帮助。
  • 你可能是唯一能体会到这一点的人:在完成所有这些工作后,我的客户刚刚决定他们根本不想要这种非标准行为。
  • 嘿,既然你是 Windows 人,我想知道你是否可以看看这个问题:stackoverflow.com/questions/51757438/…
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多