11

I would like to highlight a QFrame, if one of it's child widgets has focus (so the users know where to look for the cursor ;-)

using something along

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

highlights the QFrame when I click on it, but it loses its focus once one of its child widgets is selected.

Possible approaches:

  • I could connect() QApplication::focusChanged(old,now) and check each new object if it is a child of my QFrame, but this gets messy.

  • I could also subclass each child widget and reimplement focusInEvent()/focusOutEvent() and react on that, but with a lot of different widgets, this is also a lot of work.

Is there a more elegant solution?

Fred
  • 4,894
  • 1
  • 31
  • 48
Elwood
  • 268
  • 4
  • 9

3 Answers3

8

Well, you can extend QFrame to make it listen on focus change of its children widgets. Or you can also install an event filter on children widgets to catch QFocusEvent.

Here is an example:

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);
}
Archie
  • 2,644
  • 21
  • 21
  • Archie, thanks for you reply. Can you give me a pointer on how to extend QFrame for this purpose? – Elwood Dec 28 '12 at 18:32
  • Perfect. I particularly liked the idea of `hookChildrenWidgetsFocus()`. (But I have a little bit of a bad conscience because I took away the Acceptance from Fred, who came up with a similar approach earlier. Thank you two very much!) – Elwood Dec 28 '12 at 20:35
  • This should not be the accepted answer, I am afraid. It works for this simple case but is far from being a good solution. It is very fragile and easy to get wrong. 1) What if the layout gets dynamic with some child widgets being added or removed on the run. 2) What if the object wants to install event filter on other objects and their focus events, other than the children, possible doing something else. – HiFile.app - best file manager Apr 24 '18 at 12:21
  • @V.K. I think you are trying to bring specific implementation issues into this. The proposed solution is not meant to be universal. As a developer you know limitations of your particular design (e.g. whether you expect other event filters or use of dynamic layouts), so you should adapt this snippet accordingly to your own design. Anyways, none of these are mentioned as preconditions in the original question. – Archie Apr 24 '18 at 12:34
  • @Archie I think one should aim for the simplest and the most universal solution. The proposed solutions are not simple nor universal. The proposed design is extremely fragile and it will bite you later when you expect it least. As a developer you may know the limitations at the very moment but you forget about them later. And your team members do not know about them at all. They expect good solutions which work in broad range of conditions (i.e. not just for perfectly spherical objects in vacuum). :) See my answer below. IMHO this is simple and universal. – HiFile.app - best file manager Apr 24 '18 at 12:46
  • @V.K. I am glad you're aiming for the best solution. That exactly what we need on SO. Despite that you are 6 years late on this, your proposal looks good. – Archie Apr 24 '18 at 12:59
6

I believe the both answers you were given are wrong. They work for simple cases but are extremely fragile and clumsy. I believe that the best solution is what you actually suggested in your question. I would go for connecting to QApplication::focusChanged(from, to). You simply connect your main frame object to this signal and in the slot you check if the to object (the one which received focus) is a child of your frame object.

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

The advantage is obvious. This code is short and clean. You connect only one signal-slot, you do not need to catch and handle events. It responds well to any layout changes done after the object is created. And if you want to optimize away unnecessary redrawing, you should cache the information whether any child is focused and change the stylesheet only and only if this cached value gets changed. Then the solution would be prefect.

  • See https://forum.qt.io/topic/93641/ for how to fix potential issues with parenting of modal windows. Essentially, you must pass instance of top level window (usually `QMainWindow`) as a parent to, say, `QDialog`. Otherwise *standard* buttons in `QDialog` will be stuck, etc. This is true for at least for Qt 5.11. – Pugsley Aug 18 '18 at 16:58
3

First, create a simple subclass of QFrame which reimplements the eventFilter(QObject*, QEvent*) virtual function:

class MyFrame : public QFrame {
    Q_OBJECT

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

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

Use MyFrame instead of QFrame to contain your widgets. Then, somewhere in your code where you create the widgets contained in MyFrame, install an event filter on those widgets:

    // ...
    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);
    //...

At that point, MyFrame::eventFilter() will be called before any event is delivered to the widget, letting you act on it before the widget is aware of it. Within MyFrame::eventFilter(), return true if you want to filter the event out (i.e. you don't want the widget to process the event), or return false otherwise.

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
  • 4,894
  • 1
  • 31
  • 48
  • That's what I was looking for, thanks a lot, Fred! Is there a reason for `if (watched == m_button)` in `eventFilter()`? It works fine without and I can add as many widgets without caring... – Elwood Dec 28 '12 at 20:12
  • In your particular case, you don't need it, but `MyFrame` might want to watch several widgets for several different events, so a simple check on `watched` lets you know which widget received an event (e.g. you want to use a different background color depending on which widget has focus) – Fred Dec 28 '12 at 20:25
  • Argh, why can't I accept two answers? You and Archie were working at the same time to help me, both of you came up with a similar solution. I'm sorry that I finally accepted Archie's answer as his solution is a tiny bit more 'complete'. But your explanations were better. I hope you can forgive me... ;) – Elwood Dec 28 '12 at 20:31