Qt for MCUs 2.5.2 LTS Released

Qt for MCUs 2.5.2 LTS (Long-Term Support) has been released and is available for download. As a patch release, Qt for MCUs 2.5.2 LTS provides bug fixes and other improvements, and maintains source compatibility with Qt for MCUs 2.5.x. It does not add any new functionality.

Qt Academy Roadmap: Vote on courses and suggest new ones

by Emilia Valkonen-Damjanovic (Qt Blog)

Qt Academy, our free eLearning platform for learning Qt, is constantly evolving and growing. In this process, we hope to involve the community and people interested in learning Qt. Therefore, we have published the Qt Academy Roadmap, which showcases all the courses and learning paths in progress as well as the courses suggested.  

MCUs are now part of Qt Educational Licenses

by Emilia Valkonen-Damjanovic (Qt Blog)

We are happy to announce that Qt Educational Licenses support MCUs starting November 2023. This means that all the MCU tools are available for educational use with the Qt Edu for Developers License.  

How to Create a Custom Title Bar for a PyQt Window — Customize Your Python App's Title Bars

PyQt provides plenty of tools for creating unique and visually appealing graphical user interfaces (GUIs). One aspect of your applications that you may not have considered customizing is the title bar. The title bar is the topmost part of the window, where your users find the app's name, window controls & other elements.

This part of the window is usually drawn by the operating system or desktop environment and it's default look & feel may not gel well with the rest of your application. However, you may want to customize it to add additional functionality. For example, in web browsers the document tabs are now typically collapsed into the title bar to maximize available space for viewing pages.

In this tutorial, you will learn how to create custom title bars in PyQt. By the end of this tutorial, you will have the necessary knowledge to enhance your PyQt applications with personalized and (hopefully!) stylish title bars.

Creating Frameless Windows in PyQt

The first step to providing a PyQt application with a custom title bar is to remove the default title bar and window decoration provided by the operating system. If we don't take this step, we'll end up with multiple title bars at the top of our windows.

In PyQt, we can create a frameless window using the setWindowFlags() method available on all QWidget subclasses, including QMainWindow. We call this method, passing in the FramelessWindowHint flag, which lives in the Qt namespace under the WindowType enumeration.

Here's the code of a minimal PyQt app whose main window is frameless:

python
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QApplication, QMainWindow

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Custom Title Bar")
        self.resize(400, 200)
        self.setWindowFlags(Qt.WindowType.FramelessWindowHint)

if __name__ == "__main__":
    app = QApplication([])
    window = MainWindow()
    window.show()
    app.exec()

After importing the required classes, we create a window by subclassing QMainWindow. In the class initializer method, we set the window's title and resize the window using the resize() method. Then we use the setWindowFlags() to make the window frameless. The rest is the usual boilerplate code for creating PyQt applications.

If you run this app from your command line, you'll get the following window on your screen:

A frameless window in PyQt A frameless window in PyQt

As you can see, the app's main window doesn't have a title bar or any other decoration. It's only a gray rectangle on your screen.

Because the window has no buttons, you need to press Alt-F4 on Windows and Linux or Cmd+Q on macOS to close the app.

This isn't very helpful, of course, but we'll be adding back in our custom title bar shortly.

Setting Up the Main Window

Before creating our custom title bar, we'll finish the initialization of our app's main window, import some additional classes and create the window's central widget and layouts.

Here's the code update:

python
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QVBoxLayout,
    QWidget,
)

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Custom Title Bar")
        self.resize(400, 200)
        self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
        central_widget = QWidget()
        self.title_bar = CustomTitleBar(self)

        work_space_layout = QVBoxLayout()
        work_space_layout.setContentsMargins(11, 11, 11, 11)
        work_space_layout.addWidget(QLabel("Hello, World!", self))

        centra_widget_layout = QVBoxLayout()
        centra_widget_layout.setContentsMargins(0, 0, 0, 0)
        centra_widget_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
        centra_widget_layout.addWidget(self.title_bar)
        centra_widget_layout.addLayout(work_space_layout)

        central_widget.setLayout(centra_widget_layout)
        self.setCentralWidget(central_widget)

# ...

First, we import the QLabel, QVBoxLayout, and QWidget classes. In our window's initializer, we create a central widget by instantiating QWidget(). Next, we create an instance attribute called title_bar by instantiating a class called CustomTitleBar. We still need to implement this class -- we'll do this in a moment.

The next step is to create a layout for our window's workspace. In this example, we're using a QVBoxLayout, but you can use the layout that better fits your needs. We also set some margins for the layout content and added a label containing the phrase "Hello, World!".

Next, we create a global layout for our central widget. Again, we use a QVBoxLayout. We set the layout's margins to 0 and aligned it on the top of our frameless window. In this layout, we need to add the title bar at the top and the workspace at the bottom. Finally, we set the central widget's layout and the app's central widget.

That's it! We have all the boilerplate code we need for our window to work correctly. Now we're ready to write our custom title bar.

Creating a Custom Title Bar for a PyQt Window

In this section, we will create a custom title bar for our main window. To do this, we will create a new class by inheriting from QWidget. First, go ahead and update your imports like in the code below:

python
from PyQt6.QtCore import QSize, Qt
from PyQt6.QtGui import QPalette
from PyQt6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QStyle,
    QToolButton,
    QVBoxLayout,
    QWidget,
)

# ...

Here, we've imported a few new classes. We will use these classes as building blocks for our title bar. Without further ado, let's get into the title bar code. We'll introduce the code in small consecutive chunks to facilitate the explanation. Here's the first piece:

python
# ...

class CustomTitleBar(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.setAutoFillBackground(True)
        self.setBackgroundRole(QPalette.ColorRole.Highlight)
        self.initial_pos = None
        title_bar_layout = QHBoxLayout(self)
        title_bar_layout.setContentsMargins(1, 1, 1, 1)
        title_bar_layout.setSpacing(2)

In this code snippet, we create a new class by inheriting from QWidget. This way, our title bar will have all the standard features and functionalities of any PyQt widgets. In the class initializer, we set autoFillBackground to true because we want to give a custom color to the bar. The next line of code sets the title bar's background color to QPalette.ColorRole.Highlight, which is a blueish color.

The next line of code creates and initializes an instance attribute called initial_pos. We'll use this attribute later on when we deal with moving the window around our screen.

The final three lines of code allow us to create a layout for our title bar. Because the title bar should be horizontally oriented, we use a QHBoxLayout class to structure it.

The piece of code below deals with our window's title:

python
# ...

class CustomTitleBar(QWidget):
    def __init__(self, parent):
        # ...
        self.title = QLabel(f"{self.__class__.__name__}", self)
        self.title.setStyleSheet(
            """font-weight: bold;
               border: 2px solid black;
               border-radius: 12px;
               margin: 2px;
            """
        )
        self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
        if title := parent.windowTitle():
            self.title.setText(title)
        title_bar_layout.addWidget(self.title)

The first line of new code creates a title attribute. It's a QLable object that will hold the window's title. Because we want to build a cool title bar, we'd like to add some custom styling to the title. To do this, we use the setStyleSheet() method with a string representing a CSS style sheet as an argument. The style sheet tweaks the font, borders, and margins of our title label.

Next, we center the title using the setAlignment() method with the Qt.AlignmentFlag.AlignCenter flag as an argument.

In the conditional statement, we check whether our window has a title. If that's the case, we set the text of our title label to the current window's title. Finally, we added the title label to the title bar layout.

The next step in our journey to build a custom title bar is to provide standard window controls. In other words, we need to add the minimize, maximize, close, and normal buttons. These buttons will allow our users to interact with our window. To create the buttons, we'll use the QToolButton class.

Here's the required code:

python
# ...

class CustomTitleBar(QWidget):
    def __init__(self, parent):
        # ...
        # Min button
        self.min_button = QToolButton(self)
        min_icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarMinButton
        )
        self.min_button.setIcon(min_icon)
        self.min_button.clicked.connect(self.window().showMinimized)

        # Max button
        self.max_button = QToolButton(self)
        max_icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarMaxButton
        )
        self.max_button.setIcon(max_icon)
        self.max_button.clicked.connect(self.window().showMaximized)

        # Close button
        self.close_button = QToolButton(self)
        close_icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarCloseButton
        )
        self.close_button.setIcon(close_icon)
        self.close_button.clicked.connect(self.window().close)

        # Normal button
        self.normal_button = QToolButton(self)
        normal_icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarNormalButton
        )
        self.normal_button.setIcon(normal_icon)
        self.normal_button.clicked.connect(self.window().showNormal)
        self.normal_button.setVisible(False)

In this code snippet, we define all the required buttons by instantiating the QToolButton class. The minimize, maximize, and close buttons follow the same pattern. We create the button, define an icon for the buttons at hand, and set the icon using the setIcon() method.

Note that we use the standard icons that PyQt provides. For example, the minimize button uses the SP_TitleBarMinButton icon. Similarly, the maximize and close buttons use the SP_TitleBarMaxButton and SP_TitleBarCloseButton icons. We find all these icons in the QStyle.StandardPixmap namespace.

Finally, we connect the button's clicked() signal with the appropriate slot. For the minimize buttons, the proper slot is .showMinimized(). For the maximize and close buttons, the right slots are .showMaximized() and close(), respectively. All these slots are part of the main window's class.

The normal button at the end of the above code uses the SP_TitleBarNormalButton icon and showNormal() slot. This button has an extra setting. We've set its visibility to False, meaning that the button will be hidden by default. It'll only appear when we maximize the window to allow us to return to the normal state.

Now that we've created and tweaked the buttons, we must add them to our title bar. To do this, we can use the following for loop:

python
# ...

class CustomTitleBar(QWidget):
    def __init__(self, parent):
        # ...
        buttons = [
            self.min_button,
            self.normal_button,
            self.max_button,
            self.close_button,
        ]
        for button in buttons:
            button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
            button.setFixedSize(QSize(28, 28))
            button.setStyleSheet(
                """QToolButton { border: 2px solid white;
                                 border-radius: 12px;
                                }
                """
            )
            title_bar_layout.addWidget(button)

This loop iterates over our four buttons in a predefined order. The first thing to do inside the loop is to define the focus policy of each button. We don't want these buttons to steal focus from buttons in the window's working space , therefore we set their focus policy to NoFocus.

Next, we set a fixed size of 28 by 28 pixels for the three buttons using the setFixedSize() method with a QSize object as an argument.

Our main goal in this section is to create a custom title bar. A handy way to customize the look and feel of PyQt widgets is to use CSS style sheets. In the above piece of code, we use the setStyleSheet() method to apply a custom CSS style sheet to our four buttons. The sheet defines a white and round border for each button.

The final line in the above code calls the addWidget() method to add each custom button to our title bar's layout. That's it! We're now ready to give our title bar a try. Go ahead and run the application from your command line. You'll see a window like the following:

A PyQt window with a custom title bar A PyQt window with a custom title bar

This is pretty simple styling, but you get the idea. You can tweak the title bar further, depending on your needs. For example, you can change the colors and borders, customize the title's font, add other widgets to the bar, and more.

We'll apply some nicer styles later, once we have the functionality in place! Keep reading.

Even though the title bar looks different, it has limited functionality. For example, if you click the maximize button, then the window will change to its maximized state. However, the normal button won't show up to allow you to return the window to its previous state.

In addition to this, if you try to move the window around your screen, you'll quickly notice a problem: it's impossible to move the window!

In the following sections, we'll write the necessary code to fix these issues and make our custom title bar fully functional. To kick things off, let's start by fixing the state issues.

Updating the Window's State

To fix the issue related to the window's state, we'll write two new methods. We need to override one method and write another. In the MainWindow class, we'll override the changeEvent() method. The changeEvent() method is called directly by Qt whenever the window state changes: for example if the window is maximized or hidden. By overriding this event we can add our own custom behavior.

Here's the code that overrides the changeEvent() method:

python
from PyQt6.QtCore import QSize, Qt, QEvent
# ...

class MainWindow(QMainWindow):
    # ...

    def changeEvent(self, event):
        if event.type() == QEvent.Type.WindowStateChange:
            self.title_bar.window_state_changed(self.windowState())
        super().changeEvent(event)
        event.accept()

This method is fairly straightforward. We check the event type to see if it is a WindowStateChange. If that's the case, we call the window_state_changed() method of our custom title bar, passing the current window's state as an argument. In the final two lines, we call the parent class's changeEvent() method and accept the event to signal that we've correctly processed it.

Here's the implementation of our window_state_changed() method:

python
# ...

class CustomTitleBar(QWidget):
    # ...

    def window_state_changed(self, state):
        if state == Qt.WindowState.WindowMaximized:
            self.normal_button.setVisible(True)
            self.max_button.setVisible(False)
        else:
            self.normal_button.setVisible(False)
            self.max_button.setVisible(True)

This method takes a window's state as an argument. Depending on the value of the state parameter we will optionally show and hide the maximize & restore buttons.

First, if the window is currently maximized we will show the normal button and hide the maximize button. Alternatively, if the window is currently not maximized we will hide the normal button and show the maximize button.

The effect of this, together with the order we added the buttons above, is that when you maximize the window the maximize button will appear to be replaced with the normal button. When you restore the window to it's normal size, the normal button will be replaced with the maximize button.

Go ahead and run the app again. Click the maximize button. You'll note that when the window gets maximized, the middle button changes its icon. Now you have access to the normal button. If you click it, then the window will recover its previous state.

Handling Window's Moves

Now it's time to write the code that enables us to move the window around the screen while holding your mouse's left-click button on the title bar. To fix this issue, we only need to add code to the CustomTitleBar class.

In particular, we need to override three mouse events:

  • mousePressEvent() will let us know when the user clicks on our custom title bar using the mouse's left-click button. This may indicate that the window movement should start.
  • mouseMoveEvent() will let us process the window movements.
  • mouseReleaseEvent() will let us know when the user has released the mouse's left-click button so that we can stop moving the window.

Here's the code that overrides the mousePressEvent() method:

python
# ...

class CustomTitleBar(QWidget):
    # ...

    def mousePressEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            self.initial_pos = event.position().toPoint()
        super().mousePressEvent(event)
        event.accept()

In this method, we first check if the user clicks on the title bar using the mouse's left-click button. If that's the case, then we update our initial_pos attribute to the clicked point. Remember that we defined initial_pos and initialized it to None back in the __init__() method of CustomTitleBar.

Next, we need to override the mousePressEvent() method. Here's the required code:

python
# ...

class CustomTitleBar(QWidget):
    # ...

    def mouseMoveEvent(self, event):
        if self.initial_pos is not None:
            delta = event.position().toPoint() - self.initial_pos
            self.window().move(
                self.window().x() + delta.x(),
                self.window().y() + delta.y(),
            )
        super().mouseMoveEvent(event)
        event.accept()

This if statement in mouseMoveEvent() checks if the initial_pos attribute is not None. If this condition is true, then the if code block executes because we have a valid initial position.

The first line in the if code block calculates the difference, ordelta, between the current and initial mouse positions. To get the current position, we call the position() method on the event object and convert that position into a QPoint object using the toPoint() method.

The following four lines update the position of our application's main window by adding the delta values to the current window position. The move() method does the hard work of moving the window.

In summary, this code updates the window position based on the movement of our mouse. It tracks the initial position of the mouse, calculates the difference between the initial position and the current position, and applies that difference to the window's position.

Finally, we can complete the mouseReleaseEvent() method:

python
# ...

class CustomTitleBar(QWidget):
    # ...

    def mouseReleaseEvent(self, event):
        self.initial_pos = None
        super().mouseReleaseEvent(event)
        event.accept()

This method's implementation is pretty straightforward. Its purpose is to reset the initial position by setting it back to None when the mouse is released, indicating that the drag is complete.

That's it! Go ahead and run your app again. Click on your custom title bar and move the window around while holding the mouse's left-click button. Can you move the window? Great! Your custom title bar is now fully functional.

The completed code for the custom title bar is shown below.

python
from PyQt6.QtCore import QSize, Qt, QEvent
from PyQt6.QtGui import QPalette
from PyQt6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QStyle,
    QToolButton,
    QVBoxLayout,
    QWidget,
)


class CustomTitleBar(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.setAutoFillBackground(True)
        self.setBackgroundRole(QPalette.ColorRole.Highlight)
        self.initial_pos = None
        title_bar_layout = QHBoxLayout(self)
        title_bar_layout.setContentsMargins(1, 1, 1, 1)
        title_bar_layout.setSpacing(2)

        self.title = QLabel(f"{self.__class__.__name__}", self)
        self.title.setStyleSheet(
            """font-weight: bold;
               border: 2px solid black;
               border-radius: 12px;
               margin: 2px;
            """
        )
        self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
        if title := parent.windowTitle():
            self.title.setText(title)
        title_bar_layout.addWidget(self.title)
        # Min button
        self.min_button = QToolButton(self)
        min_icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarMinButton
        )
        self.min_button.setIcon(min_icon)
        self.min_button.clicked.connect(self.window().showMinimized)

        # Max button
        self.max_button = QToolButton(self)
        max_icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarMaxButton
        )
        self.max_button.setIcon(max_icon)
        self.max_button.clicked.connect(self.window().showMaximized)

        # Close button
        self.close_button = QToolButton(self)
        close_icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarCloseButton
        )
        self.close_button.setIcon(close_icon)
        self.close_button.clicked.connect(self.window().close)

        # Normal button
        self.normal_button = QToolButton(self)
        normal_icon = self.style().standardIcon(
            QStyle.StandardPixmap.SP_TitleBarNormalButton
        )
        self.normal_button.setIcon(normal_icon)
        self.normal_button.clicked.connect(self.window().showNormal)
        self.normal_button.setVisible(False)
        # Add buttons
        buttons = [
            self.min_button,
            self.normal_button,
            self.max_button,
            self.close_button,
        ]
        for button in buttons:
            button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
            button.setFixedSize(QSize(28, 28))
            button.setStyleSheet(
                """QToolButton { border: 2px solid white;
                                 border-radius: 12px;
                                }
                """
            )
            title_bar_layout.addWidget(button)

    def window_state_changed(self, state):
        if state == Qt.WindowState.WindowMaximized:
            self.normal_button.setVisible(True)
            self.max_button.setVisible(False)
        else:
            self.normal_button.setVisible(False)
            self.max_button.setVisible(True)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Custom Title Bar")
        self.resize(400, 200)
        self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
        central_widget = QWidget()
        self.title_bar = CustomTitleBar(self)

        work_space_layout = QVBoxLayout()
        work_space_layout.setContentsMargins(11, 11, 11, 11)
        work_space_layout.addWidget(QLabel("Hello, World!", self))

        centra_widget_layout = QVBoxLayout()
        centra_widget_layout.setContentsMargins(0, 0, 0, 0)
        centra_widget_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
        centra_widget_layout.addWidget(self.title_bar)
        centra_widget_layout.addLayout(work_space_layout)

        central_widget.setLayout(centra_widget_layout)
        self.setCentralWidget(central_widget)

    def changeEvent(self, event):
        if event.type() == QEvent.Type.WindowStateChange:
            self.title_bar.window_state_changed(self.windowState())
        super().changeEvent(event)
        event.accept()

    def window_state_changed(self, state):
        self.normal_button.setVisible(state == Qt.WindowState.WindowMaximized)
        self.max_button.setVisible(state != Qt.WindowState.WindowMaximized)

    def mousePressEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            self.initial_pos = event.position().toPoint()
        super().mousePressEvent(event)
        event.accept()

    def mouseMoveEvent(self, event):
        if self.initial_pos is not None:
            delta = event.position().toPoint() - self.initial_pos
            self.window().move(
                self.window().x() + delta.x(),
                self.window().y() + delta.y(),
            )
        super().mouseMoveEvent(event)
        event.accept()

    def mouseReleaseEvent(self, event):
        self.initial_pos = None
        super().mouseReleaseEvent(event)
        event.accept()

if __name__ == "__main__":
    app = QApplication([])
    window = MainWindow()
    window.show()
    app.exec()

Making it a little more beautiful

So far we've covered the technical aspects of styling our window with a custom title bar, and added the code to make it function as expected. But it doesn't look great. In this section we'll take our existing code & tweak the styling and buttons to produce something that's a little more professional looking.

One common reason for wanting to apply custom title bars to a window is to integrate the title bar with the rest of the application. This technique is called a unified title bar and can be seen in some popular applications such as web browsers, or Spotify.

Unified title bar in Spotify

In this section we'll look at how we can reproduce the same effect in PyQt using a combination of stylesheets & icons. Below is a screenshot of the final result which we'll be building.

Style custom title bar in PyQt6

As you can see the window & the toolbar blend nicely together and the window has rounded corners. There are a few different ways to do this, but we'll cover a simple approach using Qt stylesheets to apply styling over the entire window.

In order to customize the shape of the window, we need to first tell the OS to stop drawing the default window outline and background for us. We do that by setting a window attribute on the window. This is similar to the flags we already discussed, in that it turns on & off different window manager behaviors.

python
# ...
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Custom Title Bar")
        self.resize(400, 200)
        self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
        # ...

We've added a call to self.setAttribute which sets the attribute Qt.WidgetAttribute.WA_TranslucentBackground on the window. If you run the code now you will see the window has become transparent, with only the widget text & toolbar visible.

Next we'll tell Qt to draw a new custom background for us. If you've worked with QSS before, the most obvious way to apply curved edges to the window using QSS stylesheets would be to set border-radius: styles on the main window directly, e.g.

python
#...
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        # ...
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
        self.setStyleSheet("background-color: gray; border-radius: 10px;")
#...

However, if you try this you'll notice that it doesn't work. If you enable a translucent background, the background of the window is not drawn (including your styles). If you don't set translucent background, the window is filled to the edges with a solid color ignoring the border radius.

Stylesheets can't alter window shape.

The good news is that, with a bit of lateral thinking, there is a simple solution. We already know that we can construct interfaces by nesting widgets in layouts. Since we can't style the border-radius of a window, but we can style any other widget, the solution is to simply add a container widget into our window & apply the curved-edge and background styles to that.

On our MainWindow object we already have a central widget which contains our layout, so we can apply the styles there.

python
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        # ...
        central_widget = QWidget()
        # This container holds the window contents, so we can style it.
        central_widget.setObjectName("Container")
        central_widget.setStyleSheet("""#Container {
            background: qlineargradient(x1:0 y1:0, x2:1 y2:1, stop:0 #051c2a stop:1 #44315f);
            border-radius: 5px;
        }""")
        self.title_bar = CustomTitleBar(self)
        # ...

We've taken the existing central_widget object and assigned an object name to it. This is a ID which we can use to refer to the widget from QSS, to apply our styles specifically to that widget.

If you're familiar with CSS you might expect that IDs like #Container must be unique. However, they are not: you can give multiple widgets the same object name if you like. So you can re-use this technique and QSS on multiple windows in your application without problems.

With this style applied on our window, we have a nice gradient background with curved corners.

Unfortunately, the title bar we created is drawn filled, and so the background and curved corners of our window are over-written. To make things look coherent we need to make our title bar also transparent by removing the background color & auto-fill behavior we set earlier.

We don't need to set any flags or attributes this widget because it is not a window. A QWidget object is transparent by default.

We can also make some tweaks to the style of the title label, such as adjusting the font size and making the title capitalized using text-transform: uppercase -- feel free to customize this yourself.

python
class CustomTitleBar(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        # self.setAutoFillBackground(True) # <-- remove
        # self.setBackgroundRole(QPalette.ColorRole.Highlight) # <-- remove
        self.initial_pos = None
        title_bar_layout = QHBoxLayout(self)
        title_bar_layout.setContentsMargins(1, 1, 1, 1)
        title_bar_layout.setSpacing(2)
        self.title = QLabel(f"{self.__class__.__name__}", self)
        self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.title.setStyleSheet("""
        QLabel { text-transform: uppercase; font-size: 10pt; margin-left: 48px; }
        """)

QSS is very similar to CSS, especially for text styling.

The margin-left: 48px is to compensate for the 3 * 16px window icons on the right hand side so the text align centrally.

The icons are currently using built-in Qt icons which are a little bit plain & ugly. Next let's update the icons, using custom SVG icons of simple colored circles, for the minimize, maximize, close & restore buttons.

python
from PyQt6.QtGui import QIcon
# ...
class CustomTitleBar(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        # ...
        # Min button
        self.min_button = QToolButton(self)
        min_icon = QIcon()
        min_icon.addFile('min.svg')
        self.min_button.setIcon(min_icon)
        self.min_button.clicked.connect(self.window().showMinimized)

        # Max button
        self.max_button = QToolButton(self)
        max_icon = QIcon()
        max_icon.addFile('max.svg')
        self.max_button.setIcon(max_icon)
        self.max_button.clicked.connect(self.window().showMaximized)

        # Close button
        self.close_button = QToolButton(self)
        close_icon = QIcon()
        close_icon.addFile('close.svg') # Close has only a single state.
        self.close_button.setIcon(close_icon)
        self.close_button.clicked.connect(self.window().close)

        # Normal button
        self.normal_button = QToolButton(self)
        normal_icon = QIcon()
        normal_icon.addFile('normal.svg')
        self.normal_button.setIcon(normal_icon)
        self.normal_button.clicked.connect(self.window().showNormal)
        self.normal_button.setVisible(False)
        # ...

This code follows the same basic structure as before, but instead of using the built-in icons here we're loading our icons from SVG images. These images are very simple, consisting of a single circle in green, red or yellow for the different states mimicking macOS.

The normal.svg file for returning a maximized window to normal size shows a semi-transparent green circle for simplicity's sake, but you can include iconography and hover behaviors on the buttons if you prefer.

You can download these icons & all source code for this tutorial here: https://downloads.pythonguis.com/custom-title-bar-pyqt6.zip

The final step is to iterate through the created buttons, adding them to title bar layout. This is slightly tweaked from before to remove the border styling replacing it with simple padding & setting the icon sizes to 16px. Because we are using SVG files the icons will automatically scale to the available space.

python
class CustomTitleBar(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        # ...
        for button in buttons:
            button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
            button.setFixedSize(QSize(16, 16))
            button.setStyleSheet(
                """QToolButton {
                    border: none;
                    padding: 2px;
                }
                """
            )
            title_bar_layout.addWidget(button)

And that's it! With these changes, you can now run your application and you'll see a nice sleek modern-looking UI with unified title bar and custom controls.

Style custom titlebar in PyQt6 The final result, showing our unified title bar and window design.

The complete code is shown below:

python
from PyQt6.QtCore import QEvent, QSize, Qt
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import (
    QApplication,
    QHBoxLayout,
    QLabel,
    QMainWindow,
    QStyle,
    QToolButton,
    QVBoxLayout,
    QWidget,
)


class CustomTitleBar(QWidget):
    def __init__(self, parent):
        super().__init__(parent)
        self.initial_pos = None
        title_bar_layout = QHBoxLayout(self)
        title_bar_layout.setContentsMargins(1, 1, 1, 1)
        title_bar_layout.setSpacing(2)
        self.title = QLabel(f"{self.__class__.__name__}", self)
        self.title.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.title.setStyleSheet(
            """
        QLabel { text-transform: uppercase; font-size: 10pt; margin-left: 48px; }
        """
        )

        if title := parent.windowTitle():
            self.title.setText(title)
        title_bar_layout.addWidget(self.title)
        # Min button
        self.min_button = QToolButton(self)
        min_icon = QIcon()
        min_icon.addFile("min.svg")
        self.min_button.setIcon(min_icon)
        self.min_button.clicked.connect(self.window().showMinimized)

        # Max button
        self.max_button = QToolButton(self)
        max_icon = QIcon()
        max_icon.addFile("max.svg")
        self.max_button.setIcon(max_icon)
        self.max_button.clicked.connect(self.window().showMaximized)

        # Close button
        self.close_button = QToolButton(self)
        close_icon = QIcon()
        close_icon.addFile("close.svg")  # Close has only a single state.
        self.close_button.setIcon(close_icon)
        self.close_button.clicked.connect(self.window().close)

        # Normal button
        self.normal_button = QToolButton(self)
        normal_icon = QIcon()
        normal_icon.addFile("normal.svg")
        self.normal_button.setIcon(normal_icon)
        self.normal_button.clicked.connect(self.window().showNormal)
        self.normal_button.setVisible(False)
        # Add buttons
        buttons = [
            self.min_button,
            self.normal_button,
            self.max_button,
            self.close_button,
        ]
        for button in buttons:
            button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
            button.setFixedSize(QSize(16, 16))
            button.setStyleSheet(
                """QToolButton {
                    border: none;
                    padding: 2px;
                }
                """
            )
            title_bar_layout.addWidget(button)

    def window_state_changed(self, state):
        if state == Qt.WindowState.WindowMaximized:
            self.normal_button.setVisible(True)
            self.max_button.setVisible(False)
        else:
            self.normal_button.setVisible(False)
            self.max_button.setVisible(True)


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Custom Title Bar")
        self.resize(400, 200)
        self.setWindowFlags(Qt.WindowType.FramelessWindowHint)
        self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
        central_widget = QWidget()
        # This container holds the window contents, so we can style it.
        central_widget.setObjectName("Container")
        central_widget.setStyleSheet(
            """#Container {
            background: qlineargradient(x1:0 y1:0, x2:1 y2:1, stop:0 #051c2a stop:1 #44315f);
            border-radius: 5px;
        }"""
        )
        self.title_bar = CustomTitleBar(self)

        work_space_layout = QVBoxLayout()
        work_space_layout.setContentsMargins(11, 11, 11, 11)
        work_space_layout.addWidget(QLabel("Hello, World!", self))

        centra_widget_layout = QVBoxLayout()
        centra_widget_layout.setContentsMargins(0, 0, 0, 0)
        centra_widget_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
        centra_widget_layout.addWidget(self.title_bar)
        centra_widget_layout.addLayout(work_space_layout)

        central_widget.setLayout(centra_widget_layout)
        self.setCentralWidget(central_widget)

    def changeEvent(self, event):
        if event.type() == QEvent.Type.WindowStateChange:
            self.title_bar.window_state_changed(self.windowState())
        super().changeEvent(event)
        event.accept()

    def window_state_changed(self, state):
        self.normal_button.setVisible(state == Qt.WindowState.WindowMaximized)
        self.max_button.setVisible(state != Qt.WindowState.WindowMaximized)

    def mousePressEvent(self, event):
        if event.button() == Qt.MouseButton.LeftButton:
            self.initial_pos = event.position().toPoint()
        super().mousePressEvent(event)
        event.accept()

    def mouseMoveEvent(self, event):
        if self.initial_pos is not None:
            delta = event.position().toPoint() - self.initial_pos
            self.window().move(
                self.window().x() + delta.x(),
                self.window().y() + delta.y(),
            )
        super().mouseMoveEvent(event)
        event.accept()

    def mouseReleaseEvent(self, event):
        self.initial_pos = None
        super().mouseReleaseEvent(event)
        event.accept()


if __name__ == "__main__":
    app = QApplication([])
    window = MainWindow()
    window.show()
    app.exec()


Conclusion

In this tutorial, we have learned the fundamentals of creating custom title bars in PyQt. To do this, we have combined PyQt's widgets, layouts, and styling capabilities to create a visually appealing title bar for a PyQt app.

With this skill under your belt, you're now ready to create title bars that align perfectly with your application's unique style and branding. This will allow you to break away from the standard window decoration provided by your operating system and add a personal touch to your user interface.

Now let your imagination run and transform your PyQt application's UX.

Qt 6.6.1 Released

We have released Qt 6.6.1 today. As a patch release, Qt 6.6.1 does not introduce any new features but contains more than 400 bug fixes, security updates, and other improvements to the top of the Qt 6.6.0 release. See more information about the most important changes and bug fixes from Qt 6.6.1 release note.

CXX-Qt 0.6 Release

We just released CXX-Qt version 0.6!

CXX-Qt is a set of Rust crates for creating bidirectional Rust ⇄ C++ bindings with Qt. It can be used to integrate Rust into C++ applications using CMake or build Rust applications with Cargo. CXX-Qt provides tools for implementing QObject subclasses in Rust that can be used from C++, QML, and JavaScript.

For 0.6, we’re excited to announce that we’re on the road to stabilization! 🥳 We’ve done a (hopefully final) iteration of our API and are now happier than ever to release it.

The new API is now truly in the spirit of CXX. All implementation has moved out of the #[cxx_qt::bridge] and is now simply outside the bridge, like it is for CXX as well. To get an overview of the new API, check out our the planned 1.0 API, which is available starting with this release.

Check out the new release through the usual channels:

Some of the more extensive developer-facing changes are listed below.

New CXX-style API

extern “C++Qt”

Like extern "C++" in CXX, the extern "C++Qt" block now only uses a declaration-like syntax to expose functionality to C++/Qt. This is an extension to CXX’s extern "C++" blocks, which allow defining methods as #[qsignal]. These signals can then be emitted from Rust, as well as connected to.

extern “RustQt”

Like extern "Rust" in CXX, the extern "RustQt" block now only uses a declaration-like syntax to expose functionality to C++/Qt. Most existing features have moved to an attribute-syntax, which should look very familiar to existing Qt developers.

The new API includes:

  • #[qobject] is now on a type alias to distinguish between QObject and Rust struct clearer
  • #[qproperty(...)] is now just an attribute on the #[qobject]
  • #[inherit],#[qsignal] & #[qinvokable] have now moved to attributes of external function declarations. They can also now be freely mixed
  • #[cxx_override], #[cxx_final], #[cxx_virtual] are now independent attributes rather than imbedded in #[qinvokable]
Support for Qt enums as shared types

Like shared types in CXX, enums for Qt can be declared using a shared enum in the bridge. These can be defined using the #[qenum] attribute and qnamespace!(...) macro.

Implementation is outside the bridge

Like CXX the implementations on types are now defined outside of the bridge rather than inside. This allows you to choose the prefix for QObjects, as the hard-coded qobject:: prefix is now simply the name of the #[cxx_qt::bridge] module.

Another great new feature in this release: Connecting to Signals via Rust is now possible! Simply use the on_[SIGNAL-NAME]/connect_[SIGNAL-NAME] methods generated for each signal. Closures are supported when connecting to signals as well.

QML modules can now be built

The build system has been improved so that QML modules can now be output by CXX-Qt. This allows for using the declarative syntax of #[qml_element] and #[qml_singleton] on a #[qobject], whereas before Rust QObject types needed to be registered in C++ with the QML engine.

use cxx_qt_build::{CxxQtBuilder, QmlModule};
fn main() {
    CxxQtBuilder::new()
        .qml_module(QmlModule {
            uri: "com.kdab.cxx_qt.demo",
            rust_files: &["src/cxx_qt_bridge.rs"],
            qml_files: &["qml/main.qml"],
            ..Default::default()
        })
        .build();
}

This should also allow for tooling to inspect QML modules that were generated from Rust in the future.

New traits for shaping code generation and interacting with features

Most features of the CXX-Qt code generation are now expressed through Rust traits and can be enabled/disabled or otherwise configured using a syntax similar to CXX shim trait impls.

New features

  • Custom constructors through cxx_qt::Constructor – this drops the requirement for Default on the Rust struct.
  • Easy default-constructor implementation through cxx_qt::Initialize.
  • cxx_qt::CxxQtType trait for reflection and less “magic” methods.

Existing features that can now be enabled/disabled:

  • Threading through the cxx_qt::Threading trait.
  • Locking through the cxx_qt::Locking trait.

Thanks to all of our contributors that helped us with this release

  • Be Wilson
  • Laurent Montel
  • Olivier Le Doeuff
  • Jimmy van Hest
  • Michael (aka @mmMike)
  • Cyril Jacquet
  • Magnus Groß
  • jimmyvanhest
About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

The post CXX-Qt 0.6 Release appeared first on KDAB.

Hello, RHI – How to get started with Qt RHI

Hello, RHI – How to get started with Qt RHI

For some time now, Qt has been internally utilizing RHI (Rendering Hardware Interface), a new cross-platform technology for graphic rendering. Since Qt 6.6, this API has been semi-public, meaning that the API is mature for practical use but may still be subject to potential changes between major Qt versions.

In this blog post, we demonstrate how to to get started with RHI.

Continue reading Hello, RHI – How to get started with Qt RHI at basysKom.

How to Restore the Window's Geometry in a PyQt App — Make Your Windows Remember Their Last Geometry

In GUI applications the window's position & size are known as the window geometry. Saving and restoring the geometry of a window between executions is a useful feature in many applications. With persistent geometry users can arrange applications on their desktop for an optimal workflow and have the applications return to those positions every time they are launched.

In this tutorial, we will explore how to save and restore the geometry and state of a PyQt window using the QSettings class. With this functionality, you will be able to give your applications a usability boost.

To follow along with this tutorial, you should have prior knowledge of creating GUI apps with Python and PyQt. Additionally, having a basic understanding of using the QSettings class to manage an application's settings will be beneficial.

Understanding a Window's Geometry

PyQt defines the geometry of a window using a few properties. These properties represent a window's position on the screen and size. Here's a summary of PyQt's geometry-related properties:

Property Description Access Method
x Holds the x coordinate of a widget relative to its parent. If the widget is a window, x includes any window frame and is relative to the desktop. This property defaults to 0. x()
y Holds the y coordinate of a widget relative to its parent. If the widget is a window, y includes any window frame and is relative to the desktop. This property defaults to 0. y()
pos Holds the position of the widget within its parent widget. If the widget is a window, the position is relative to the desktop and includes any frame. pos()
geometry Holds the widget's geometry relative to its parent and excludes the window frame. geometry()
width Holds the width of the widget, excluding any window frame. width()
height Holds the height of the widget, excluding any window frame. height()
size Holds the size of the widget, excluding any window frame. size()

In PyQt, the QWidget class provides the access methods in the table above. Note that when your widget is a window or form, the first three methods operate on the window and its frame, while the last four methods operate on the client area, which is the window's workspace without the external frame.

Additionally, the x and y coordinates are relative to the screen of your computer. The origin of coordinates is the upper left corner of the screen, at which point both x and y are 0.

Let's create a small demo app to inspect all these properties in real time. To do this, go ahead and fire up your code editor or IDE and create a new Python file called geometry_properties.py. Then add the following code to the file and save it in your favorite working directory:

python
from PyQt6.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

class Window(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Window's Geometry")
        self.resize(400, 200)
        self.central_widget = QWidget()
        self.global_layout = QVBoxLayout()
        self.geometry_properties = [
            "x",
            "y",
            "pos",
            "width",
            "height",
            "size",
            "geometry",
        ]
        for prop in self.geometry_properties:
            self.__dict__[f"{prop}_label"] = QLabel(f"{prop}:")
            self.global_layout.addWidget(self.__dict__[f"{prop}_label"])
        button = QPushButton("Update Geometry Properties")
        button.clicked.connect(self.update_labels)
        self.global_layout.addWidget(button)
        self.central_widget.setLayout(self.global_layout)
        self.setCentralWidget(self.central_widget)

    def update_labels(self):
        for prop in self.geometry_properties:
            self.__dict__[f"{prop}_label"].setText(
                f"{prop}: {getattr(self, prop)()}"
            )

if __name__ == "__main__":
    app = QApplication([])
    window = Window()
    window.show()
    app.exec()

Wow! There's a lot of code in this file. First, we import the required classes from PyQt6.QtWidgets. Then, we create our app's main window by inheriting from QMainWindow.

In the initializer method, we set the window's title and size using setWindowTitle() and resize(), respectively. Next, we define a central widget and a layout for our main window.

We also define a list of properties. We'll use that list to add some QLabel objects. Each label will show a geometry property and its current values. The Update Geometry Properties button allows us to update the value of the window's geometry properties.

Finally, we define the update_labels() method to update the values of all the geometry properties using their corresponding access methods. That's it! Go ahead and run the app. You'll get the following window on your screen:

A Window Showing Labels for Every Geometry Property A Window Showing Labels for Every Geometry Property

Looking good! Now go ahead and click the Update Geometry Properties button. You'll see how all the properties get updated. Your app's window will look something like this:

A Window Showing the Current Value of Every Geometry Property A Window Showing the Current Value of Every Geometry Property

As you can see, x and y are numeric values, while pos is a QPoint object with x and y as its coordinates. These properties define the position of this window on your computer screen.

The width and height properties are also numeric values, while the size property is a QSize object defined after the current width and height.

Finally, the geometry property is a QRect object. In this case, the rectangle comprises x, y, width, and height.

Great! With this first approach to how PyQt defines a window's geometry, we're ready to continue digging into this tutorial's main topic: restoring the geometry of a window in PyQt.

Keeping an App's Geometry Settings: The QSetting Class

Users of GUI apps will generally expect the apps to remember their settings across sessions. This information is often referred to as settings or preferences. In PyQt applications, you'll manage settings and preferences using the QSettings class. This class allows you to have persistent platform-independent settings in your GUI app.

A commonly expected feature is that the app remembers the geometry of its windows, particularly the main window.

In this section, you'll learn how to save and restore the window's geometry in a PyQt application. Let's start by creating a skeleton PyQt application to kick things off. Go ahead and create a new Python file called geometry.py. Once you have the file opened in your favorite code editor or IDE, then add the following code:

python
from PyQt6.QtWidgets import QApplication, QMainWindow

class Window(QMainWindow):
    def __init__(self):
        super(Window, self).__init__()
        self.setWindowTitle("Window's Geometry")
        self.move(50, 50)
        self.resize(400, 200)

if __name__ == "__main__":
    app = QApplication([])
    window = Window()
    window.show()
    app.exec()

This code creates a minimal PyQt app with an empty main window. The window will appear at 50 pixels from the upper left corner of your computer screen and have a size of 400 by 200 pixels.

We'll use the above code as a starting point to make the app remember and restore the main window's geometry across sessions.

First, we need to have a QSettings instance in our app. Therefore, you have to import QSettings from PyQt6.QtCore and instantiate it as in the code below:

python
from PyQt6.QtCore import QSettings
from PyQt6.QtWidgets import QApplication, QMainWindow

class Window(QMainWindow):
    def __init__(self):
        super(Window, self).__init__()
        self.setWindowTitle("Window's Geometry")
        self.move(50, 50)
        self.resize(400, 200)
        self.settings = QSettings("PyhonGUIs", "GeometryApp")

When instantiating QSettings, we must provide the name of our company or organization and the name of our application. We use "PyhonGUIs" as the organization and "GeometryApp" as the application name.

Now that we have a QSettings instance, we should implement two methods. The first method should allow you to save the app's settings and preferences. The second method should help you read and load the settings. In this tutorial, we'll call these methods write_settings() and read_settings(), respectively:

python
class Window(QMainWindow):
    # ...

    def write_settings(self):
        # Write settings here...

    def read_settings(self):
        # Read settings here...

Note that our methods don't do anything yet. You'll write them in a moment. For now, they're just placeholders.

The write_settings() method must be called when the user closes or terminates the application. This way, you guarantee that all the modified settings get saved for the next session. So, the appropriate place to call write_settings() is from the main window's close event handler.

Let's override the closeEvent() method as in the code below:

python
class Window(QMainWindow):
    # ...

    def closeEvent(self, event):
        self.write_settings()
        super().closeEvent(event)
        event.accept()

In this code, we override the closeEvent() handler method. The first line calls write_settings() to ensure that we save the current state of our app's settings. Then, we call the closeEvent() of our superclass QMainWindow to ensure the app's window closes correctly. Finally, we accept the current event to signal that it's been processed.

Now, where should we call read_settings() from? In this example, the best place for calling the read_settings() method is .__init__(). Go ahead and add the following line of code to the end of your __init__() method:

python
class Window(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Window's Geometry")
        self.move(50, 50)
        self.resize(400, 200)
        self.settings = QSettings("PythonGUIs", "GeometryApp")
        self.read_settings()

By calling the read_settings() method from __init__(), we ensure that our app will read and load its settings every time the main window gets created and initialized.

Great! We're on the way to getting our application to remember and restore its window's geometry. First, you need to know that you have at least two ways to restore the geometry of a window in PyQt:

  • Using the pos and size properties
  • Using the geometry property

In both cases, you need to save the current value of the selected property and load the saved value when the application starts. To kick things off, let's start with the first approach.

Restoring the Window's Geometry With pos and size

In this section, we'll first write the required code to save the current value of pos and size by taking advantage of our QSettings object. The code snippet below shows the changes that you need to make on your write_settings() method to get this done:

python
class Window(QMainWindow):
    # ...

    def write_settings(self):
        self.settings.setValue("pos", self.pos())
        self.settings.setValue("size", self.size())

This code is straightforward. We call the setValue() method on our setting object to set the "pos" and "size" configuration parameters. Note that we get the current value of each property using the corresponding access method.

With the write_settings() method updated, we're now ready to read and load the geometry properties from our app's settings. Go ahead and update the read_settings() method as in the code below:

python
class Window(QMainWindow):
    # ...

    def read_settings(self):
        self.move(self.settings.value("pos", defaultValue=QPoint(50, 50)))
        self.resize(self.settings.value("size", defaultValue=QSize(400, 200)))

The first line inside read_settings() retrieves the value of the "pos" setting parameter. If there's no saved value for this parameter, then we use QPoint(50, 50) as the default value. Next, the move() method moves the app's window to the resulting position on your screen.

The second line in read_settings() does something similar to the first one. It retrieves the current value of the "size" parameter and resizes the window accordingly.

Great! It's time for a test! Go ahead and run your application. Then, move the app's window to another position on your screen and resize the window as desired. Finally, close the app's window to terminate the current session. When you run the app again, the window will appear in the same position. It will also have the same size.

If you have any issues completing and running the example app, then you can grab the entire code below:

python
from PyQt6.QtCore import QPoint, QSettings, QSize
from PyQt6.QtWidgets import QApplication, QMainWindow

class Window(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Window's Geometry")
        self.move(50, 50)
        self.resize(400, 200)
        self.settings = QSettings("PyhonGUIs", "GeometryApp")
        self.read_settings()

    def write_settings(self):
        self.settings.setValue("pos", self.pos())
        self.settings.setValue("size", self.size())

    def read_settings(self):
        self.move(self.settings.value("pos", defaultValue=QPoint(50, 50)))
        self.resize(self.settings.value("size", defaultValue=QSize(400, 200)))

    def closeEvent(self, event):
        self.write_settings()
        super().closeEvent(event)
        event.accept()

if __name__ == "__main__":
    app = QApplication([])
    window = Window()
    window.show()
    app.exec()

Now you know how to restore the geometry of a window in a PyQt app using the pos and size properties. It's time to change gears and learn how to do this using the geometry property.

Restoring the Window's Geometry With geometry

We can also restore the geometry of a PyQt window using its geometry property and the restoreGeometry() method. To do that, we first need to save the current geometry using our QSettings object.

Go ahead and create a new Python file in your working directory. Once you have the file in place, add the following code to it:

python
from PyQt6.QtCore import QByteArray, QSettings
from PyQt6.QtWidgets import QApplication, QMainWindow

class Window(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Window's Geometry")
        self.move(50, 50)
        self.resize(400, 200)
        self.settings = QSettings("PythonGUIs", "GeometryApp")
        self.read_settings()

    def write_settings(self):
        self.settings.setValue("geometry", self.saveGeometry())

    def read_settings(self):
        self.restoreGeometry(self.settings.value("geometry", QByteArray()))

    def closeEvent(self, event):
        self.write_settings()
        super().closeEvent(event)
        event.accept()

if __name__ == "__main__":
    app = QApplication([])
    window = Window()
    window.show()
    app.exec()

There are only two changes in this code compared to the code from the previous section. We've modified the implementation of the write_settings() and read_settings() methods.

In write_settings(), we use the setValue() to save the current geometry of our app's window. The saveGeometry() allows us to access and save the current window's geometry. In read_settings(), we call the value() method to retrieve the saved geometry value. Then, we use restoreGeometry() to restore the geometry of our window.

Again, you can run the application consecutive times and change the position and size of its main window to ensure your code works correctly.

Restoring the Window's Geometry and State

If your app's window has toolbars and dock widgets, then you want to restore their state on the parent window. To do that, you can use the restoreState() method. To illustrate this, let's reuse the code from the previous section.

Update the content of write_settings() and read_settings() as follows:

python
class Window(QMainWindow):
    # ...

    def write_settings(self):
        self.settings.setValue("geometry", self.saveGeometry())
        self.settings.setValue("windowState", self.saveState())

    def read_settings(self):
        self.restoreGeometry(self.settings.value("geometry", QByteArray()))
        self.restoreState(self.settings.value("windowState", QByteArray()))

In write_settings(), we add a new setting value called "windowState". To keep this setting, we use the saveState() method, which saves the current state of this window's toolbars and dock widgets. Meanwhile, in read_settings(), we restore the window's state by calling the value() method, as usual, to get the state value back from our QSettings object. Finally, we use restoreState() to restore the state of toolbars and dock widgets.

Now, to make sure that this new code works as expected, let's add a sample toolbar and a dock window to our app's main window. Go ahead and add the following methods right after the __init__() method:

python
from PyQt6.QtCore import QByteArray, QSettings, Qt
from PyQt6.QtWidgets import QApplication, QDockWidget, QMainWindow

class Window(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Window's State")
        self.resize(400, 200)
        self.settings = QSettings("PythonGUIs", "GeometryApp")
        self.create_toolbar()
        self.create_dock()
        self.read_settings()

    def create_toolbar(self):
        toolbar = self.addToolBar("Toolbar")
        toolbar.addAction("One")
        toolbar.addAction("Two")
        toolbar.addAction("Three")

    def create_dock(self):
        dock = QDockWidget("Dock", self)
        dock.setAllowedAreas(
            Qt.DockWidgetArea.LeftDockWidgetArea
            | Qt.DockWidgetArea.RightDockWidgetArea
        )
        self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, dock)

    # ...

In this new update, we first import the Qt namespace from PyQt6.QtCore and QDockWidget from PyQt6.QtWidgets. Then we call the two new methods from __init__() to create the toolbar and dock widget at initialization time.

In the create_toolbar() method, we create a sample toolbar with three sample buttons. This toolbar will show at the top of our app's window by default.

Next, we create a dock widget in create_dock(). This widget will occupy the rest of our window's working area.

That's it! You're now ready to give your app a try. You'll see a window like the following:

A Window Showing a Sample Toolbar and a Dock Widget A Window Showing a Sample Toolbar and a Dock Widget

Play with the toolbar and the dock widget. Move them around. Then close the app's window and run the app again. Your toolbar and dock widget will show in the last position you left them.

Conclusion

Through this tutorial, you have learned how to restore the geometry and state of a window in PyQt applications using the QSettings class. By utilizing the pos, size, geometry, and state properties, you can give your users the convenience of persistent position and size on your app's windows.

With this knowledge, you can enhance the usability of your PyQt applications, making your app more intuitive and user-friendly.

Supercharging VS Code with C++ Extensions

In a previous blog we demonstrated the most straightforward method to optimize Visual Studio Code for a Qt / C++ environment: simply let the tools do all the work! The example GitHub project we discussed automatically installs both the Microsoft C/C++ and clangd extensions into VS Code. You might wonder why you need both C++ extensions. The short answer is that having both will maximize your productivity in every situation… but read on for more detail.

The power of extensions

The thing that makes VS Code an amazing productivity tool is that you can add all kinds of extensions to it – tools to help you write, document, and debug code. While the C/C++ extensions from Microsoft and clangd do many of the same things, they each have individual strong suits.

A rather simple block diagram of our dual C++ extensions looks something like this.

VS Code with both the C/C++ and clangd extensions installed.

Extensions that use the language server protocol (LSP) can add feature support tailored to the programming language in use. LSP allows a “mere” editor application like VS Code to provide features like auto completion, identifier location, grammar-sensitive tips, parameter name hints, and refactoring operations, and both the Microsoft C/C++ extension and clangd use LSP.

However, you can also see that the Microsoft C/C++ extension uses a debug adaptor to connect to different compiler toolchains. This provides debugging functionality for all the debuggers that the C/C++ extension supports. That means that if you want to debug C++ code from within VS Code, you need the Microsoft C/C++ extension installed (or an equivalent C++ extension that has a debug adapter – something that clangd notably does not have).

Why not just Microsoft?

But if the Microsoft C/C++ extension provides both language support and debugging support, why do you need clangd at all?

There are a few small differences between the two in the refactoring or code actions that they offer, but the biggest difference is in speed.

For finding identifiers and taking you to their definition or their references, clangd is significantly faster than Microsoft. What is almost instantaneous with clangd takes a second or two with the Microsoft C/C++ extension, requiring a progress bar. Most developers spend more time reading code than writing it, and I almost always prefer enabling clangd because of this. Especially for developers who are frequently navigating big source trees, those little delays can be a minor annoyance that adds up.

Thankfully, you can have both extensions enabled, but disable the Microsoft C/C++ Intellisense features. That lets you use the faster clangd source navigation and still use the Microsoft extension when it comes to debugging your code.

Configuring clangd

When installing the clangd extension, it will automatically disable the Microsoft C/C++ Intellisense features for you. If it’s not the case, you can change this setting:

"C_Cpp.intelliSenseEngine": "disabled",

You do need a couple settings to make clangd work properly. These are automatically configured in our VS Code C++/Qt template but to do it by hand you’ll need the following:

    • Insert the line set(CMAKE_EXPORT_COMPILE_COMMANDS ON) near the top of your CMakeLists.txt file or set the CMAKE_EXPORT_COMPILE_COMMANDS environment variable to 1 in the environment where you will run CMake.
    • Add this setting
"cmake.copyCompileCommands": "${workspaceFolder}/compile_commands.json",

 

to the .vscode/settings.json file in your project directory.

These two commands ensure cmake will capture the compiler commands that it needs to build the project during the configure stage. The file containing those commands is then copied to the root of the workspace where the clangd extension expects them. Without these two pieces, clangd won’t work properly.

You don’t need to do a full build to create and install your compile_commands.json file, you can just select the appropriate cmake preset on the bottom status bar. Alternatively, you can type Ctrl+Shift+P (Command+Shift+P on a Mac) to get the command palette and type “CMake: Configure”.

Using clangd configuration files

Everything said up to now is more than enough for most cases, but there are still some open issues we may need to take care of:

  • Using clangd works out of the box when using a compilation database using the MSVC compiler, because clangd is able to pick correctly the right driver for clang from the compilation database. However, warning/error flags for MSVC are not always correctly mapped to clang ones. How can we get a more accurate code model in that case?
  • Only one instance of clangd is running for each vscode window, so by default we can use only one compile_commands.json file. What happens if we are working in a multi-folder workspace, where each folder is an independently buildable application or library?

We can get to a solution for both cases by using clang configuration files.

Clangd provides a configuration mechanism starting from version 11, which allows among other things loading multiple compile_commands.json files and customizing compilation flags for the code model. A complete documentation for this mechanism can be found here

To set up a clangd configuration file, you just have to create a .clangd file in your project folder or one of its ancestors (e.g. if you’re working on two sibling folders, you should have the .clangd file on their parent folder).

Then, to have VSCode’s clangd extension to pick up you configuration file, you should add --enable-config among your clangd settings, like this:

"clangd.arguments": [
    ... // 
    "--enable-config"
]

Customize compilation flags through clangd configuration file

To customize compilation flags we need to use a CompileFlags section in our configuration files. The section supports the following options:

  • Add: to append compilation flags to the compile commands
  • Remove: to remove compilation flags
  • CompilationDatabase: to specify a custom folder where to fetch the compilation database (compile_commands.json)
  • Compiler: allows to specify the compiler executable who needs to run (clang, clang++, clang-cl, …).

Here’s an example to add custom flags:

CompileFlags:
    Add: [-Wall, -Wextras, -Wno-c++98-compat, -Wno-pre-c++17-compat]

And if you want to add specific flags for a given file or a given folder you can wrap the CompileFlags section into an If section:

If:
    PathMatch: .*/sourcesubdir/.*\.cpp
CompilerFlags:
    Add: [...]

Working with a multi-folder workspace with clangd

Using the conditional If section you can get one step ahead and configure clangd to correctly process all our source files in a multi-folder workspace.

First, you just set up the cmake configuration to generate a compiler database using the guide shown in the previous sections.

Then you set up the clangd configuration file to correctly map different compilation database with the correct subfolder in you workspace, as shown in the following example:

If:
    PathMatch: Project1/.*
CompileFlags:
    CompilationDatabase: Project1 // the parent folder for your compile_commands.json
    // You can optionally customize specific compiler flags for Project1 here
---

If:
    PathMatch: Project2/.*
CompileFlags:
    CompilationDatabase: Project2
    // You can optionally customize specific compiler flags for Project2 here

Having trouble?

If your clangd extension doesn’t seem to be working properly, here’s a few troubleshooting tips:

  • Confirm that compile_commands.json is in the root of the workspace directory. If it’s not, either use our GitHub example and instructions per this blog, or double-check the configuration settings in the above section.
  • Make sure clangd indexing has been finished. For small projects, this happens nearly instantly. But for big projects, you might see a progress indicator in the bottom status bar that says, “indexing X/Y”. Wait until that goes away and try again.
  • Restart clangd. This isn’t necessary too often, but if nothing else works, it’s worth a try: type Ctrl+Shift+P (or Command+Shift+P) and type “Restart Language Server”.

That’s it!

VS Code is an amazing tool and we’re always learning new ways to optimize our development environment. If you’ve got any related tips to share, leave a comment!

About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

The post Supercharging VS Code with C++ Extensions appeared first on KDAB.

QtGamepad ported to Qt 6

For a project of mine I need gamepad support. In the past, I’ve happily used QtGamepad, but that has not been ported to Qt 6. It’s not dead, but Andy (QtGamepad’s maintainer) wants to do some re-architecting for a Qt 6 release.

I need QtGamepad now, however, so I’ve ported it myself. It’s not a whole lot of code and Qt’s pro2cmake.py made it a breeze. I’ve renamed the whole thing to QtGamepadLegacy and pushed it to GitHub. So whenever the official QtGamepad is released there should be no naming conflicts. I’ve tested with Qt 6.6.0 and the evdev plugin.

I don’t plan on adding any new features to the port. I’ll try to keep it compatible with upcoming Qt releases, though.

Embed Rive in your QtQuick applications

Embed Rive in your QtQuick applications

Learn how to use Rive within Qt and Qt Quick.

Rive is a tool (and file format) that enables you to create interactive vector animations. With the RiveQtQuickPlugin, you can effortlessly load and display Rive animations within your QtQuick projects.

In this article, we will demonstrate how to embed Rive files, use different rendering backends, load artboards and trigger animations.

Continue reading Embed Rive in your QtQuick applications at basysKom.

PSA: QPointer has a terrible name

Today’s blog post is about a small utility class in Qt with a… questionable name: QPointer. If you’re new to Qt, maybe don’t check out QPointer’s documentation just yet, and try to guess what the class does based on its name alone.

I’ve seen countless users being very confused by it. Some end up using it extensively, thinking that it’s the Qt way to store pointers, or a better kind of pointer, or maybe yet another Not Invented Here Qt class solving a non-problem, or something along those lines.

So what is QPointer?

QPointer is a weak pointer for QObjects. What’s a weak pointer, you ask? A weak pointer is a smart pointer that does not participate in the ownership of a given object. Destroying the weak pointer has no effect whatsoever on the pointed-to object. Instead, a weak pointer acts as an observer, and is able to tell us whether the object has been already destroyed (by its owner(s)) or not.

QWidget *w = new QWidget(parent); // create a new object; `parent` owns it

QPointer<QWidget> ptr = obj;      // create a weak pointer, watching over *w
Q_ASSERT(ptr);                    // ptr is valid, *w still exist 

destroyWidget();                  // destroy *w somehow
Q_ASSERT(!ptr);                   // ptr is automatically reset,
                                  //   to signal that *w is gone

In the snippet above it doesn’t matter how exactly we destroy our object (an explicit delete, or through the parent object, etc.).

The interesting part is the last line: ptr knows that the object is destroyed, and automatically resets itself to a null pointer. Compare this with a “raw” C++ pointer, which does not get automatically reset, and would dangle instead.

Therefore, if we compose this a little bit further:

class MyClass {
  QPointer<SomeObject> m_resource;  // pointer to some resource we don't own

  void doSomething() {
    if (m_resource)        // check that the resource is still alive;
      m_resource->use();   // if so, use it
  }
};

This is pretty interesting, because our code now is much safer (and simpler), compared to using raw pointers, or connecting to the QObject::destroyed() signal or some other mechanism.

Here’s a very fancy scenario:

class MyObject : public QObject {
  Q_OBJECT

signals:
  void aSignal(int value);

public:
  void doSomething() {
    int value = calculateValue();

    QPointer<MyObject> guard(this);
    emit aSignal(value);
    if (!guard)
       return;

    // do some other things
  }
};

Here we’re guarding against the fact then, when we emit a signal, slots connected to the signal may end up destroying the sender object. This usually doesn’t have the form of delete sender, but it’s more indirect. For instance, the sender may be a button, connected to a slot that re-arranges the UI of your application, and while doing so, it may end up destroying the button itself.

While deleting a sender object is supported by the signal/slot mechanism, it’s important to realize that once all the slots connected to a signal have been called, the execution will continue in the code that emitted the signal. In there, the sender object (i.e. *this) is now destroyed. In the above snippet the guard lets the execution stop right after we return from the signal emission, in case we detect that *this has indeed been destroyed.

This pattern is used all over the place in widgets code (a place where the chances of deleting a sender are pretty high).

Please note: I’m not saying that you should protect all your signal emissions like the above. Deleting a sender object directly (via operator delete) is, generally speaking, a poor idea; one should use deleteLater() instead. Ideally here I’d like some debug mechanism from Qt telling us that we are indeed deleting an object in the middle of a signal emission, but I fear that the signal-to-noise ratio is going to be pretty close to 0.

QPointer is a weak pointer, std::weak_ptr is a weak pointer, are they different?

The principle behind QPointer and std::weak_ptr (or Qt’s reimplementation, QWeakPointer) is pretty much the same: both are smart pointers classes pointing to object that are not owned through that smart pointer. There’s however some important differences.

1. QPointer only works with QObject subclasses; std::weak_ptr works with any type.

This isn’t an “arbitrary” limitation put in place by QPointer; it has strictly to do with how these weak pointer classes detect that the resource they’re observing has been destroyed.

std::weak_ptr achieves this because it observes the control block that another smart pointer class set up for it: std::shared_ptr. That’s right, std::weak_ptr and std::shared_ptr go hand-in-hand; you must manage a given resource with std::shared_ptr in order to use std::weak_ptr with it. This is because std::weak_ptr observes the strong counter in the control block in order to know if the resource is still alive. This is a (very simplified!) example of how std::shared_ptr and std::weak_ptr work together:

On the other hand, a QObject observed by QPointer doesn’t have to be managed in any specific way. It can be owned using a raw pointer, or by an owning smart pointer class, or through QObject’s parent/child mechanism, or it can be a data member of another class… It doesn’t matter: QPointer simply doesn’t impose a particular owning strategy.

So how does QPointer know when the object it points to has been destroyed? QPointer needs some help from that object. Specifically, that object is going to set up a control block so that QPointer can then use it. This is what happens inside QObject guts (see the sharedRefcount member of QObjectPrivate and its usages).

And this is why QPointer requires the pointed-to type to be a QObject (subclass): it needs some special logic that only QObject implements.

By the way, if you’re curious: the mechanism used behind the scenes by QObject and QPointer is actually exactly the very same one that in Qt is used by QSharedPointer and QWeakPointer: a control block with two counters, the strong counter and a weak counter. The strong counter signals whether the QObject has already been destroyed (it’s not really used as a “counter” in this scenario; it’s actually set to -1, and reset to 0 when the object is destroyed); the weak counter counts how many QPointer objects are tracking a given QObject. Here’s how it looks like:

Again, this is a simplification. I’m deliberately omitting some details such as the fact that the control block also stores the deleter, a pointer back to the object, and especially whether the weak counter only tracks weak pointers or the sum of strong pointers and weak pointers.

From this point of view, QPointer acts as an intrusive pointer, expecting the pointed-to object to obey a certain protocol. This is vaguely similar to the (in)famous std::enable_shared_from_this class template, that allows one to create shared pointers (and weak pointers, as of C++17) out of an arbitrary object, because the object itself keeps a pointer to the control block used to manage it. (It does so by inheriting from std::enable_shared_from_this). However, std::enable_shared_from_this on an object which is not already managed by a shared pointer will not magically start the refcounting process, while instead QPointer does so.

And while at it, here’s another little nugget: the fact that this specific implementation was used by QPointer is also why QWeakPointer supported direct construction from QObjects back in Qt 5, even if they were not managed by a QSharedPointer. This has made a lot of people very angry and been widely regarded as a bad move.

2. QPointer doesn’t use a lock()ing protocol

If you have a weak pointer, how do you use it? The whole purpose of such a smart pointer class is to let you test if the pointed-to object is still alive, and only if it is, use it.

Therefore, one can try a strategy like this one:

WeakPointer<Foo> weakPtr; // declared somewhere

// Here's how one would use it:
if (weakPtr)               // is the object still alive?
  weakPtr->doSomething();  // use it!

This specific snippet looks OK, but actually it doesn’t even compile with std::weak_ptr. The reason is that code written like this could exhibit a TOCTOU bug: even if the check passes, by the time we try to use the object, the object could’ve been destroyed.

How in the world could the object disappear between the if and the usage? But because of multiple threads, of course! std::shared_ptr and std::weak_ptr’s reference counting mechanism is thread safe. (The reference counting is; the objects themselves are merely re-entrant.) So while you execute the above snippet in thread 1, thread 2 could be destroying the last shared pointer owning the object, therefore destroying the object. If thread 2 happens to do this right between the check of the weak pointer by thread 1 and its subsequent access, your program is in trouble.

This is why, when dealing with std::weak_ptr, we ask “is the object still alive?”; but we word the question slightly differently. We ask “can I become a co-owner of the object, if it’s still alive?”

Becoming a co-owner means getting a std::share_ptr pointing to the object; therefore we are going to call a function that will return one. We somehow ask to “upgrade” our status: from mere observer (std::weak_ptr) to co-owner (std::shared_ptr). The returned std::shared_ptr object will be loaded in case the object is still alive, or empty in case the object has already been destroyed. Now, for a number of reasons, this “upgrade request” has a very weird name: it’s called std::weak_ptr::lock() (docs). In my humble opinion, this just gets C++ newcomers very confused, as they immediately think about mutex locking or some other multithreading primitives that have no role in what’s happening here.

Now, being a co-owner is a pretty good thing for us, because it means that we can safely check if the object is still alive (by checking the returned shared pointer). If the object is alive, we can safely access it: no other threads can now lose the last owning pointer to our object and destroy it under our nose. There’s always at least one owner: yourself, through the std::shared_ptr you’ve just obtained. Hence, the code above has to spelled like this when using std::weak_ptr:

std::weak_ptr<Foo> weakPtr; // declared somewhere

// Test and use:
std::shared_ptr<Foo> sharedPtr = weakPtr.lock();
if (sharedPtr)               // sharedPtr, not weakPtr!
  sharedPtr->doSomething();  // object is alive, use it

What about QPointer? Well, QPointer lets you get away with it:

QPointer<MyQObject> ptr;

if (ptr)               // is the object still alive?
  ptr->doSomething();  // use it!

Doesn’t this code suffer from the problem that weak_ptr works so hard to prevent? Well yes, it does. However, in practice it does not constitute a problem, because we are never supposed to touch QObjects from arbitrary threads — and this includes deleting them.

If we stick to a single-thread scenario, the code above works just fine. This is why I keep repeating that, although QObject is technically speaking reentrant, it’s better to treat it as thread-unsafe: only use a QObject from one thread (the thread the object lives into, or has affinity with).

Here’s the full details in case you want to know more:

Conclusions

That’s it for today’s blog. I hope I’ve explained what QPointer does, and when to use it.

There’s only one thing missing, which is the title of the blog post: why is QPointer a pretty bad name?

In my opinion, it should have been called for what it is: a weak pointer for QObject subclasses. So, how about QObjectWeakPointer? It’s not that Qt strives for very short names anyhow (hello, QXmlStreamNotationDeclaration); giving it a more descriptive name would remove confusion and actually ease discoverability.

Thank you for reading, and I’ll see you next time.

About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

The post PSA: QPointer has a terrible name appeared first on KDAB.

Heaptrack v1.5.0 released

I’m happy to announce the immediate availability of Heaptrack v1.5.0. Heaptrack is a heap memory profiler targeting mainly Linux, as well as FreeBSD. To learn more, please visit the project website.

Version 1.5.0 incorporates about 70 changes since the v1.4.0 release from June last year.

The highlights include:

– Elfutils is now used for symbolizing backtraces, which makes the code much more future proof: We can now handle DWARF5, split debug info, compressed debug info and gain transparent support for debuginfod as well as some performance benefits too

– Various QOL improvements to the charts, including the ability to export the charts

– Various bug fixes and better platform support, resulting in overall higher stability and accuracy

– The code can now be compiled with Qt6 too

You can download the sources and an AppImage for heaptrack v1.5.0 from the official KDE mirrors: https://download.kde.org/stable/heaptrack/1.5.0/ Packages in modern Linux distributions will be updated over time too, so stay tuned.

Many thanks to all the contributors that made this release possible! We can all celebrate together, as at this year’s KDE Akademy conference, heaptrack won the Best Application award!

Best Application Akademy Award 2023

Cheers to that and happy profiling!

The post Heaptrack v1.5.0 released appeared first on KDAB.

Pitfalls of lambda capture initialization

Recently, I’ve stumbled across some behavior of C++ lambda captures that has, initially, made absolutely no sense to me. Apparently, I wasn’t alone with this, because it has resulted in a memory leak in QtFuture::whenAll() and QtFuture::whenAny() (now fixed; more on that further down).

I find the corner cases of C++ quite interesting, so I wanted to share this. Luckily, we can discuss this without getting knee-deep into the internals of QtFuture. So, without further ado:

Time for an example

Consider this (godbolt):

#include <iostream>
#include <functional>
#include <memory>
#include <cassert>
#include <vector>

struct Job
{
    template<class T>
    Job(T &&func) : func(std::forward<T>(func)) {}

    void run() { func(); hasRun = true; }

    std::function<void()> func;
    bool hasRun = false;
};

std::vector<Job> jobs;

template<class T>
void enqueueJob(T &&func)
{
    jobs.emplace_back([func=std::forward<T>(func)]() mutable {
        std::cout << "Starting job..." << std::endl;
        // Move func to ensure that it is destroyed after running
        auto fn = std::move(func);
        fn();
        std::cout << "Job finished." << std::endl;
    });
}

int main()
{
    struct Data {};
    std::weak_ptr<Data> observer;
    {
        auto context = std::make_shared<Data>();
        observer = context;
        enqueueJob([context] {
            std::cout << "Running..." << std::endl;
        });
    }
    for (auto &job : jobs) {
        job.run();
    }
    assert((observer.use_count() == 0) 
                && "There's still shared data left!");
}

Output:

Starting job...
Running...
Job finished.

The code is fairly straight forward. There’s a list of jobs to which we can be append with enqueueJob(). enqueueJob() wraps the passed callable with some debug output and ensures that it is destroyed after calling it. The Job objects themselves are kept around a little longer; we can imagine doing something with them, even though the jobs have already been run.
In main(), we enqueue a job that captures some shared state Data, run all jobs, and finally assert that the shared Data has been destroyed. So far, so good.

Now you might have some issues with the code. Apart from the structure, which, arguably, is a little forced, you might think “context is never modified, so it should be const!”. And you’re right, that would be better. So let’s change it (godbolt):

--- old
+++ new
@@ -34,7 +34,7 @@
     struct Data {};
     std::weak_ptr<Data> observer;
     {
-        auto context = std::make_shared<Data>();
+        const auto context = std::make_shared<Data>();
         observer = context;
         enqueueJob([context] {
             std::cout << "Running..." << std::endl;

Looks like a trivial change, right? But when we run it, the assertion fails now!

int main(): Assertion `(observer.use_count() == 0) && "There's still shared data left!"' failed.

How can this be? We’ve just declared a variable const that isn’t even used once! This does not seem to make any sense.
But it gets better: we can fix this by adding what looks like a no-op (godbolt):

--- old
+++ new
@@ -34,9 +34,9 @@
     struct Data {};
     std::weak_ptr<Data> observer;
     {
-        auto context = std::make_shared<Data>();
+        const auto context = std::make_shared<Data>();
         observer = context;
-        enqueueJob([context] {
+        enqueueJob([context=context] {
             std::cout << "Running..." << std::endl;
         });
     }

Wait, what? We just have to tell the compiler that we really want to capture context by the name context – and then it will correctly destroy the shared data? Would this be an application for the really keyword? Whatever it is, it works; you can check it on godbolt yourself.

When I first stumbled across this behavior, I just couldn’t wrap my head around it. I was about to think “compiler bug”, as unlikely as that may be. But GCC and Clang both behave like this, so it’s pretty much guaranteed not to be a compiler bug.

So, after combing through the interwebs, I’ve found this StackOverflow answer that gives the right hint: [context] is not the same as [context=context]! The latter drops cv qualifiers while the former does not! Quoting cppreference.com:

Those data members that correspond to captures without initializers are direct-initialized when the lambda-expression is evaluated. Those that correspond to captures with initializers are initialized as the initializer requires (could be copy- or direct-initialization). If an array is captured, array elements are direct-initialized in increasing index order. The order in which the data members are initialized is the order in which they are declared (which is unspecified).

https://en.cppreference.com/w/cpp/language/lambda

So [context] will direct-initialize the corresponding data member, whereas [context=context] (in this case) does copy-initialization! In terms of code this means:

  • [context] is equivalent to decltype(context) captured_context{context};, i.e. const std::shared_ptr<Data> captured_context{context};
  • [context=context] is equivalent to auto capture_context = context;, i.e. std::shared_ptr<Data> captured_context = context;

Good, so writing [context=context] actually drops the const qualifier on the captured variable! Thus, for the lambda, it is equivalent to not having written it in the first place and using direct-initialization.

But why does this even matter? Why do we leak references to the shared_ptr<Data> if the captured variable is const? We only ever std::move() or std::forward() the lambda, right up to the place where we invoke it. After that, it goes out of scope, and all captures should be destroyed as well. Right?

Nearly. Let’s think about the compiler generates for us when we write a lambda. For the direct-initialization capture (i.e. [context]() {}), the compiler roughly generates something like this:

struct lambda
{
    const std::shared_ptr<Data> context;
    // ...
};

This is what we want to to std::move() around. But it contains a const data member, and that cannot be moved from (it’s const after all)! So even with std::move(), there’s still a part of the lambda that lingers, keeping a reference to context. In the example above, the lingering part is in func, the capture of the wrapper lambda created in enqueueJob(). We move from func to ensure that all captures are destroyed when the it goes out of scope. But for the const std::shared_ptr<Data> context, which is hidden inside func, this does not work. It keeps holding the reference. The wrapper lambda itself would have to be destroyed for the reference count to drop to zero.
However, we keep the already-finished jobs around, so this never happens. The assertion fails.

How does this matter for Qt?

QtFuture::whenAll() and whenAny() create a shared_ptr to a Context struct and capture that in two lambdas used as continuations on a QFuture. Upon completion, the Context stores a reference to the QFuture. Similar to what we have seen above, continuations attached to QFuture are also wrapped by another lambda before being stored. When invoked, the “inner” lambda is supposed to be destroyed, while the outer (wrapper) one is kept alive.

In contrast to our example, the QFuture situation had created an actual memory leak, though (QTBUG-116731): The “inner” continuation references the Context, which references the QFuture, which again references the continuation lambda, referencing the Context. The “inner” continuation could not be std::move()d and destroyed after invocation, because the std::shared_ptr data member was const. This had created a reference cycle, leaking memory. I’ve also cooked this more complex case down to a small example (godbolt).

The patch for all of this is very small. As in the example, it simply consists of making the capture [context=context]. It’s included in the upcoming Qt 6.6.0.

Bottom line

I seriously didn’t expect there to be these differences in initialization of by-value lambda captures. Why doesn’t [context] alone also do direct- or copy-initialization, i.e. be exactly the same as [context=context]? That would be the sane thing to do, I think. I guess there is some reasoning for this; but I couldn’t find it (yet). It probably also doesn’t make a difference in the vast majority of cases.

In any case, I liked hunting this one down and getting to know another one of those dark corners of the C++ spec. So it’s not all bad 😉.

GammaRay 3.0.0 is released

KDAB has released GammaRay 3.0.0.

GammaRay is a software introspection tool for Qt applications developed by KDAB for internal use, and now shared with the developer community on GitHub.

This release is a special one, as we have now added Qt 6 support.

An overview of changes since the last release

Things we have added or improved

  • Support for Qt6
  • An option to allow capturing HTTP response body in Network operation.
  • You can now ‘favorite’ Objects via context menu. A favorite object appears in a separate item view above the main itemview
  • Support for modifying QMargins in properties
  • You can now zoom with a mouse wheel in Signals view
  • When you filter for an object, the tree will now expand automatically and select the object
  • When an object is re-parented, it no longer crashes
  • You get much improved performance
    • in signals view
    • when a target application triggers massive object destructions or constructions

Things we took away

  • The KDAB commercial license.
  • The 3D Widget Inspector View.
  • The experimental VTK-based Object Visualizer.

Download GammaRay and see the full change log at https://github.com/KDAB/GammaRay/releases/tag/v3.0.0.

Find out more about KDAB’s Qt tools.

Videos
About KDAB

If you like this article and want to read similar material, consider subscribing via our RSS feed.

Subscribe to KDAB TV for similar informative short video content.

KDAB provides market leading software consulting and development services and training in Qt, C++ and 3D/OpenGL. Contact us.

The post GammaRay 3.0.0 is released appeared first on KDAB.

Unlock the Power of Qt with Felgo's Qt Explained Series on YouTube

With powerful SDK components and unique tools tailored to the daily needs of developers, Felgo’s mission is to enable developers to work efficiently and create better Qt applications. As a Qt Technology and Service Partner, Felgo supports developers and businesses in bringing ideas to life and reaching their goals.  

To make Qt development more accessible and strengthen the community, we are now thrilled to announce a brand-new video series: Qt Explained. It is designed to empower developers of all levels with the magic of Qt:

KDAB at CppCon October 1-6th

CppCon is the annual, week-long face-to-face gathering for the entire C++ community – the biggest C++ event in the world. This year, CppCon takes place in Aurora, Colorado, near the Denver airport, and including multiple diverse tracks, the conference will appeal to anyone from C++ novices to experts.

At CppCon, you can expect lots of interesting talks and panels, presentations by the C++ community, lightning talks, and evening events.

KDAB’s Nate Rogers and Matt Aber will be there to welcome you all at the KDAB table on October 2nd-3rd. After that, feel free to grab them whenever you see them strolling around the facility. Maybe you want an introduction to what it’s like to work at KDAB, or perhaps what we do in and for the C++ community?

We hope to see you there!

KDAB at CppCon

 

The post KDAB at CppCon October 1-6th appeared first on KDAB.