Spencer
Spencer

Reputation: 2110

Qt: change QFrame border color on focus in

I have a bunch of widgets in a layout, and the layout is the child of a QFrame. This allows me to create a border around this layout. Now when any of the children receive focus, I would like to change the border color of the QFrame to indicate to the user that is where the focus currently is. How best to do this without subclassing the focuInEvent/focusOutEvent of every child with callbacks to the stylesheet of their parent widget (the QFrame)? When testing to focusInEvent of the QFrame I can never get it to trigger. Is there some sort of child focus event or something?

Upvotes: 0

Views: 2783

Answers (2)

mike rodent
mike rodent

Reputation: 15642

The only other answer (by Spencer) is using a sledgehammer to crack a nut. Unleash the power of CSS. Here's an extract from some code where I get the focused Qwidget to have a khaki background and the selected item (if applicable, e.g. in QTreeView) to have a dark kharki background. NB check out about 100 very useful colour names to use in a PyQt5 CSS context.

NB in what follows self is the QMainWindow object.

    for widget in self.findChildren(QtWidgets.QWidget):
        # exclude certain types from getting fancy CSS
        if isinstance(widget, QtWidgets.QMenuBar) or isinstance(widget, QtWidgets.QScrollBar):
            continue
        widget.setStyleSheet("""
        QWidget {background: azure;} # this is actually an off-white: see list above

        QWidget::focus {background: khaki;} # background turns khaki only on focus! 
        # ... so obviously you can add some change to the border here too if you want

        QWidget::item::focus {background: darkkhaki;}
        """)      

    # NB this next stuff is not relevant to the "how to get a nice focus colouring" question,  
    # but just to illustrate some of the power and flexibility of CSS

    self.ui.menubar.setStyleSheet('QWidget {border-bottom: 1px solid black;}')
    # bolding and colour for an isolated element: note that you don't 
    # have to stipulate "QLabel {...}" unless it makes sense.
    self.get_details_panel().ui.breadcrumbs_label.setStyleSheet('font-weight: bold; color: slategrey') 
    
    self.get_details_panel().setFrameStyle(QtWidgets.QFrame.Box)
    # with this we identify the specific object to stop the style propagating to descendant objects
    self.get_details_panel().setStyleSheet('QFrame#"details panel"{border-top: 1px solid black; }')

... NB in the last case the object ("details panel", a QFrame subclass) has been given an object name, which you can then use in CSS (i.e. in CSS terminology, its "id"):

self.setObjectName('details panel')

Upvotes: 0

Spencer
Spencer

Reputation: 2110

I think I came up with a pretty good solution for this after trying a few things out and learning a ton more about eventFilter's. Basically I found that you need to install an event filter in the parent and catch all focus events of the children. It's easier to show an example, this is a bit more complicated then it perhaps needs to be but it illustrates some important points:

import os
import sys
from PyQt4 import QtGui, QtCore


class BasePanel(QtGui.QWidget):
    """This is more or less abstract, subclass it for 'panels' in the main UI"""
    def __init__(self, parent=None):
        super(BasePanel, self).__init__(parent)
        self.frame_layout = QtGui.QVBoxLayout()
        self.frame_layout.setContentsMargins(0, 0, 0, 0)
        self.setLayout(self.frame_layout)

        self.frame = QtGui.QFrame()
        self.frame.setObjectName("base_frame")
        self.frame.setFrameStyle(QtGui.QFrame.Box | QtGui.QFrame.Plain)
        self.frame.setLineWidth(1)
        self.frame_layout.addWidget(self.frame)

        self.base_layout = QtGui.QVBoxLayout()
        self.frame.setLayout(self.base_layout)

        self.focus_in_color = "rgb(50, 255, 150)"
        self.focus_out_color = "rgb(100, 100, 100)"
        self.frame.setStyleSheet("#base_frame {border: 1px solid %s}" % self.focus_out_color)
        self.installEventFilter(self) # this will catch focus events
        self.install_filters()

    def eventFilter(self, object, event):
        if event.type() == QtCore.QEvent.FocusIn:
            self.frame.setStyleSheet("#base_frame {border: 1px solid %s}" % self.focus_in_color)
        elif event.type() == QtCore.QEvent.FocusOut:
            self.frame.setStyleSheet("#base_frame {border: 1px solid %s}" % self.focus_out_color)
        return False # passes this event to the child, i.e. does not block it from the child widgets

    def install_filters(self):
        # this will install the focus in/out event filter in all children of the panel
        for widget in self.findChildren(QtGui.QWidget):
            widget.installEventFilter(self)


class LeftPanel(BasePanel):
    def __init__(self, parent=None):
        super(LeftPanel, self).__init__(parent)

        title = QtGui.QLabel("Left Panel")
        title.setAlignment(QtCore.Qt.AlignCenter)
        self.base_layout.addWidget(title)

        edit = QtGui.QLineEdit()
        self.base_layout.addWidget(edit)



class RightPanel(BasePanel):
    def __init__(self, parent=None):
        super(RightPanel, self).__init__(parent)

        title = QtGui.QLabel("Right Panel")
        title.setAlignment(QtCore.Qt.AlignCenter)
        self.base_layout.addWidget(title)

        edit = QtGui.QLineEdit()
        self.base_layout.addWidget(edit)

class MainApp(QtGui.QMainWindow):
    def __init__(self):
        super(MainApp, self).__init__()

        main_layout = QtGui.QHBoxLayout()
        central_widget = QtGui.QWidget()
        central_widget.setLayout(main_layout)
        self.setCentralWidget(central_widget)

        left_panel = LeftPanel()
        main_layout.addWidget(left_panel)

        right_panel = RightPanel()
        main_layout.addWidget(right_panel)


if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    ex = MainApp()
    ex.show()

    sys.exit(app.exec_())

Upvotes: 1

Related Questions