当子控件获得焦点时,如何更改父控件的背景?

11

如果QFrame的一个子部件获得焦点,我想要突出显示它(这样用户就知道光标所在的位置;-)

可以使用以下方法:

ui->frame->setFocusPolicy(Qt::StrongFocus);
ui->frame->setStyleSheet("QFrame:focus {background-color: #FFFFCC;}");

当我点击QFrame时,它会变得突出显示,但是一旦选择了其中一个子部件,它就失去了焦点。

可能的解决方法:

  • 我可以connect() QApplication::focusChanged(old,now),并检查每个新对象是否是我的QFrame的子对象,但这会变得混乱。

  • 我还可以对每个子部件进行子类化,并重新实现focusInEvent()/focusOutEvent(),然后对其做出反应,但对于很多不同的部件,这也是很多工作。

有更优雅的解决方案吗?

3个回答

8

你可以扩展QFrame来监听其子窗口小部件的焦点变化。或者你也可以在子窗口小部件上安装事件过滤器,以捕获QFocusEvent

这里是一个示例:

MyFrame.h

#ifndef MYFRAME_H
#define MYFRAME_H

#include <QFrame>

class MyFrame : public QFrame
{
    Q_OBJECT

public:

    explicit MyFrame(QWidget* parent = 0, Qt::WindowFlags f = 0);

    void hookChildrenWidgetsFocus();

protected:

    bool eventFilter(QObject *object, QEvent *event);

private:

    QString m_originalStyleSheet;
};

#endif // MYFRAME_H

MyFrame.cpp

#include <QEvent>
#include "MyFrame.h"

MyFrame::MyFrame(QWidget *parent, Qt::WindowFlags f)
    : QFrame(parent, f)
{
    m_originalStyleSheet = styleSheet();
}

void MyFrame::hookChildrenWidgetsFocus()
{
    foreach (QObject *child, children()) {
        if (child->isWidgetType()) {
            child->installEventFilter(this);
        }
    }
}

bool MyFrame::eventFilter(QObject *object, QEvent *event)
{
    if (event->type() == QEvent::FocusIn) {
        setStyleSheet("background-color: #FFFFCC;");
    } else if (event->type() == QEvent::FocusOut) {
        setStyleSheet(m_originalStyleSheet);
    }

    return QObject::eventFilter(object, event);
}

MainWindow.cpp

#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QLineEdit>
#include "MyFrame.h"
#include "mainwindow.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent)
{
    setWindowTitle(tr("Test"));

    MyFrame *frame1 = new MyFrame(this);
    frame1->setLayout(new QVBoxLayout());
    frame1->layout()->addWidget(new QLineEdit());
    frame1->layout()->addWidget(new QLineEdit());
    frame1->layout()->addWidget(new QLineEdit());
    frame1->hookChildrenWidgetsFocus();

    MyFrame *frame2 = new MyFrame(this);
    frame2->setLayout(new QVBoxLayout());
    frame2->layout()->addWidget(new QLineEdit());
    frame2->layout()->addWidget(new QLineEdit());
    frame2->layout()->addWidget(new QLineEdit());
    frame2->hookChildrenWidgetsFocus();

    QHBoxLayout *centralLayout = new QHBoxLayout();
    centralLayout->addWidget(frame1);
    centralLayout->addWidget(frame2);

    QWidget *centralWidget = new QWidget();
    centralWidget->setLayout(centralLayout);

    setCentralWidget(centralWidget);
}

阿奇,谢谢你的回复。你能给我一个指针,告诉我如何扩展QFrame来实现这个目的吗? - Elwood
太好了。我特别喜欢hookChildrenWidgetsFocus()的想法。(但是我有点内疚,因为我从弗雷德那里拿走了他的认可,他之前提出了类似的方法。非常感谢你们两个!) - Elwood
这不应该是被接受的答案,恐怕是这样的。它适用于这个简单的情况,但远非是一个好的解决方案。它非常脆弱且容易出错。1)如果布局在运行时添加或删除了一些子小部件,会怎么样。2)如果对象想要在其他对象上安装事件过滤器及其焦点事件,而不仅仅是子对象,则可能会执行其他操作。 - HiFile.app - best file manager
@V.K. 我认为你试图将特定的实现问题引入其中。所提出的解决方案并不是通用的。作为开发人员,您知道您特定设计的限制(例如,您是否期望其他事件过滤器或使用动态布局),因此您应相应地调整此片段以适应自己的设计。无论如何,这些都没有在原始问题中作为前提条件提到。 - Archie
@Archie 我认为我们应该追求最简单和最普遍的解决方案。提出的解决方案既不简单也不普遍。所提出的设计非常脆弱,当你最不希望它发生时,它会给你带来麻烦。作为开发人员,你可能在那一刻知道限制,但是后来你会忘记它们。而你的团队成员根本不知道这些限制。他们期望好的解决方案,适用于广泛的条件(即不仅仅适用于真空中完美球形物体)。 :) 请参见下面我的答案。我认为这是简单而通用的。 - HiFile.app - best file manager
@V.K. 很高兴你在追求最佳解决方案。这正是我们在 SO 上所需要的。尽管你晚了 6 年,但你的提议看起来很不错。 - Archie

6
我相信你所得到的两个答案都是错误的。它们适用于简单情况,但是非常脆弱而且笨拙。我认为最好的解决方案是您在问题中实际建议的方法。我会选择连接到 QApplication::focusChanged(from, to)。您只需将主框架对象连接到此信号,在插槽中检查 to 对象(即接收焦点的对象)是否是您框架对象的子级。
Frame::Frame(...)
{
// ...
  connect(qApp, &QApplication::focusChanged, this, &Frame::onFocusChanged);
// ...
}

// a private method of your Frame object
void Frame::onFocusChanged(QWidget *from, QWidget *to)
{
  auto w = to;
  while (w != nullptr && w != this)
    w = w->parentWidget();

  if (w == this) // a child (or self) is focused
    setStylesheet(highlightedStylesheet);
  else // something else is focused
    setStylesheet(normalStylesheet);
}

优势显而易见。这段代码简短而干净。您只需连接一个信号槽,无需捕获和处理事件。它对创建对象后进行的任何布局更改都有很好的响应。如果您想优化掉不必要的重绘,您应该缓存任何子项是否聚焦的信息,并仅在此缓存值发生变化时更改样式表。然后解决方案就会完美。

请参考 https://forum.qt.io/topic/93641/ 了解如何解决模态窗口父级问题。基本上,您必须将顶层窗口(通常为 QMainWindow)的实例作为父级传递给 QDialog 等控件。否则,在 QDialog 中,标准按钮将会卡住等问题。至少在 Qt 5.11 中是如此。 - Pugsley

3

首先,创建一个简单的QFrame子类,重新实现eventFilter(QObject*, QEvent*)虚函数:

class MyFrame : public QFrame {
    Q_OBJECT

public:
    MyFrame(QWidget *parent = 0, Qt::WindowFlags f = 0);
    ~MyFrame();

    virtual bool eventFilter(QObject *watched, QEvent *event);
};

使用 MyFrame 代替 QFrame 来包含您的小部件。然后,在您创建包含在 MyFrame 中的小部件的代码中的某个地方,为这些小部件安装一个事件过滤器:

    // ...
    m_myFrame = new MyFrame(parentWidget);
    QVBoxLayout *layout = new QVBoxLayout(myFrame);
    m_button = new QPushButton("Widget 1", myFrame);

    layout->addWidget(m_button);
    m_button->installEventFilter(myFrame);
    //...

在这种情况下,MyFrame::eventFilter()会在任何事件传递到小部件之前被调用,让您在小部件意识到事件之前对其进行操作。在MyFrame::eventFilter()内部,如果您想过滤掉事件(即不希望小部件处理事件),则返回true,否则返回false

bool MyFrame::eventFilter(QObject *watched, QEvent *event)
{
    if (watched == m_button) { // An event occured on m_button
        switch (event -> type()) {
            case QEvent::FocusIn:
                // Change the stylesheet of the frame
                break;
            case QEvent::FocusOut:
                // Change the stylesheet back
                break;
            default:
                break;
        }
    }

    return false; // We always want the event to propagate, so always return false
}

这正是我在寻找的,非常感谢你,Fred!在eventFilter()中使用if (watched == m_button)有什么原因吗?即使没有它也能正常工作,我可以添加任意数量的小部件而不必担心... - Elwood
在您的特定情况下,您不需要它,但是MyFrame可能希望监视多个小部件以获取多个不同的事件,因此对watched进行简单检查可以让您知道哪个小部件接收到了事件(例如,您想根据具有焦点的小部件使用不同的背景颜色)。 - Fred
哎呀,为什么不能接受两个答案呢?你和阿奇同时在帮助我解决问题,而且你们俩提供了相似的解决方案。很抱歉最后我选择了阿奇的答案,因为他的解决方案稍微更加“完整”一些。但是你的解释更好。希望你可以原谅我...;) - Elwood

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接