High Demand for Software Development and Quality Assurance Skills Persists Amid Tech Industry Challenges
Qt Academy Roadmap: Vote on courses and suggest new ones
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
Qt Safe Renderer 2.1.0 Beta 2 Provides a Demo for NXP i.MX 8QuadMax!
Qt Safe Renderer 2.1.0 Beta 2 installation has been updated with the system image, tool chain, and system root for NXP i.MX 8QuadMax. You can easily install them to your host, flash your target device, and launch the Qt Safe Renderer demo on the device. For example, you can test how Qt Safe Renderer continues rendering safety-critical elements on the screen even if there's a failure on Main UI.

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:
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
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:
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:
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:
# ...
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:
# ...
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:
# ...
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:
# ...
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
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:
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:
# ...
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:
# ...
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:
# ...
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:
# ...
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.
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.
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.
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.
# ...
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.
#...
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.
.
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.
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.
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.
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.
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.
The final result, showing our unified title bar and window design.
The complete code is shown below:
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.

Qt Creator 12: C++ Code Model Update
In our previous instance of this column, we introduced our then-brand new clangd support, so let's start with what has happened in that area since then.

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.
Qt Creator 12 released
We are happy to announce the release of Qt Creator 12!

Qt Insight Basics Part 4: Getting Started 3 - Funnel Analysis and Crash Analysis
In our previous post, we discussed how to use Qt Insight to check user flow and bounce rates and compare data. In this article, we will continue with an introduction to Funnels.

QML Extension for Visual Studio: Qt Quick Syntax Highlighting and Autocompletion
Microsoft Visual Studio is a powerful IDE with a robust ecosystem for efficient software creation. It is widely used and beloved by software developers, especially for native development on Windows.
With the right extensions and tools, you can set up and use Visual Studio to develop your Qt and QML projects. Read this guide to learn how:

Qt Insight Basics Part 3: Getting Started 2 - Analyzing User Flows
In the previous article, we introduced the registration process for the free trial of Qt Insight and the information that can be accessed through the Dashboard. In this article, we will continue from the previous one and delve into User Flows.

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:
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
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
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:
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:
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:
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:
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:
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
andsize
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:
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:
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:
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:
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:
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:
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
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.
RiveQtQuickPlugin now with Text Support
The RiveQtQuickPlugin has now integrated the latest rivecpp version. We've implemented rendering support for rive text elements. We ensured seamless text rendering compatibility across both software and hardware-backed renderers. Explore our latest blog post for a demonstration video and to learn about more rendering enhancements.
Continue reading RiveQtQuickPlugin now with Text Support at basysKom.
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.
Revolutionising industrial automation: Unleashing the potential of Qt-based HMI development
The post Revolutionising industrial automation: Unleashing the potential of Qt-based HMI development appeared first on Spyrosoft.
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.
Qt for VS Code, the TL;DR version
Our colleague Alessandro Ambrosano created a series of blogs (parts 1, 2, and 3) that explain how to get Visual Studio Code configured for Qt development. In that series, Alessandro covers all the details you need to get your VS Code environment configured exactly the way you want it. But there’s a lot there to read.
What if you’re short on time… or attention span? Then you want this blog. We’ve created the fastest way to get going for Qt developers who want to use the world’s best C++ UX framework with the powerful VS Code IDE. By putting most of the necessary ingredients into CMake, it really trims down what’s necessary to get things working. We’ve tested this basic setup on Windows, Mac, and Linux computers. (And if you need to tweak anything further, just go back and read Alessandro’s blogs for more info.)
Although there are a few steps here, there really isn’t much to do. We’ve just broken it down into all the individual bits with screen shots so you can follow along. We’ve also provided two ways to do this – one starting from a script, and one starting directly in the VS Code IDE. We’re going to explain the IDE-only version here, but the code and instructions in our Git repository supports both methods, so check out https://github.com/KDABLabs/blog-vscode-template.git if you prefer launching VS Code from a script.
If you are really in a hurry, check out https://github.com/KDABLabs/blog-vscode-template.git and read the readme for the platform you are working on. Just copy the files in your project and you are good to go/
Let’s go!
Prerequisites
We’re going to assume that you’ve already got VS Code and Qt If not, go take care of that now and come back when you’re done.
If you’re using Windows, you’ll need to add an environment variable that points to Qt and to make sure that Qt is in your path. You can do this in a VS Code launch script as we explain in README-Windows.md in our repo. Depending on what version of Qt you’re using, the command-line version of what you need will be something like this:
set QTDIR=C:\Qt\6.5.1\msvc2019_64\ set PATH=%PATH%;%QTDIR%\bin
If you prefer, you can also set these variables on a system-wide basis. This might be easier if you intend on launching VS Code from the GUI instead of the command-line. In this case, click “Start”, type “environment properties”, and when the System Properties window comes up, click Environment Variables. Then:
- Create a new variable called QTDIR set to C:\Qt\6.5.1\msvc2019_64\
- Add C:\Qt\6.5.1\msvc2019_64\bin to the end of the existing PATH variable.
Steps
- Create a test directory. For the sake of this example, we’re using a Mac and a test directory called ~/Development/VSCode. Replace this with whatever makes sense for your environment, and cd into this directory.
-
- To check out our example project, type
git clone https://github.com/KDAB/blog-vscode-template.git
-
- In a command shell, change into the blog-vscode-template directory and start VS Code:
cd blog-vscode-template code .
-
You should see VS Code pop up. If you get a dialog that says “
Do you trust the authors of the files in this folder?
-
”, click “Yes, I trust the Authors”
Troubleshooting tip: if VS Code doesn’t run and you get an error about “code: command not found” you probably don’t have VS Code in your PATH. You can easily fix this by running VS Code from the app launcher. Then, hit either Ctrl+Shift+P (for Windows/Linux) or Cmd+Shift+P (for Mac), type “shell command” in the search box, and then pick “Install ‘code’ command in PATH”.) Then start step 4 over at the beginning.
- If you are using MacOS or Linux, examine the CMakeUserPresets.json file and make sure the CMAKE_PREFIX_PATH variable has the correct path to the correct Qt platform. Because Qt can be installed in different locations, this might be different for different developers so this file wouldn’t normally be checked into Git. (We’ve done it here for the purposes of this blog.) To ensure that it will preserve any tweaks you make for your specific machine and that it won’t be deleted from your setup, add CMakeUserPresets.json to your .gitignore. (You’ll want to .gitignore this file no matter what anyway)
-
- Wait for a moment, and VS Code will find that we’ve configured some recommended extensions for this workspace; it will prompt you to install them. Click “Install”, and VS Code will then start installing extensions for C/C++, CMake Tools, CMake, and clangd. Wait for those to complete installing before going on to the next step.
- Once the extensions have completed installing, you’ll be asked “Do you want to configure project VSCode?” Answer “Yes”.
-
- Now you need to select a configure preset. This is necessary to get CMake to create the compile-commands.json Without that file, the code completion, code hints, and content assist features in the C++ extension don’t work properly.
On the bottom status bar if you see “No Configure Preset Selected”, then click that text and then pick the debug variant appropriate for your platform, per the following table. You can change the existing configuration if one is set too.
Launch Method | Debug Builds | Release Builds | Profiler Builds | |
Windows | IDE | debug-msvc | release-msvc | profile-msvc |
Command-line | debug | release | profile | |
Linux | IDE | debug-6.5.1-linux | release-6.5.1-linux | profile-6.5.1-linux |
Command-line | debug | release | profile | |
Mac | IDE | debug-6.5.1-macos | release-6.5.1-macos | profile-6.5.1-macos |
Command-line | debug | release | profile |
A couple notes on our naming structure:
-
- the command-line version expect the environment variables (compiler, Qt) to be set properly before launching VS Code
- we don’t hardcode the Qt version in the Microsoft presets like the other platforms do because it’s already set via an environment variable
After a few moments, you’ll see a compile-commands.json file get populated in the workspace directory. This step also builds the binary file.
That’s it – we’re done! If you click on the Run icon, you should see the app screen with our new Qt app.
The C++ autocomplete and link following features should be working as well since we’re using the clangd extension. Using the more common Microsoft C++ extension is an option as well. There are pros and cons to each – something we will cover in detail in our next blog.
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 Qt for VS Code, the TL;DR version appeared first on KDAB.
KDAB at Meeting C++ in Berlin
KDAB is proud to be a Silver Sponsor at this year’s Meeting C++, a highly recommended 3-day hybrid event for the European C++ community, offering 44 Talks in 4 tracks, November 12th – 14th.
While this is also an in-person event, there is a substantial concurrent online program, so that high-class international speakers can easily contribute, and C++ enthusiasts unable to get to Berlin can still attend parts of it in real-time. Online talks are pre-recorded so that speakers can interact with attendees during the event.
Check out the Schedule and find out more about the innovative way the organiser, Jens Weller, has set this event up for maximum benefit of the C++ community.
Talk highlights
On Day 3 in Track A-hybrid, KDAB’s Ivan Čukić will give the closing Keynote at 14:00. Ivan is a core KDE contributor and has been coding in C++ since 1998. Ivan also teaches modern C++ and functional programming at the University of Belgrade and wrote a very well-received book on the subject. His talks are always highly informative and entertaining.
On Day 2 at 17:00, also in Track A, KDAB’s Shivam Kunwar will be giving a talk on Optimizing Multithreaded Performance: Unveiling False Sharing and Harnessing Hardware Destructive Interference. Shivam has a deep-rooted interest and knowledge base in open source technologies, software performance, compilers, RISC-V, and modern C++. He also has a fascination for astronomy and particle physics.
Other highlights include Keynotes from:
- Kevlin Henney, a renowned trainer and speaker on technical topics
- KDE’s Lydia Pintscher, who brings a wealth of experience from her roles at KDE.
All in all, a prestigious and worthy event we hope you’ll attend also.
Sign up if you can.
The post KDAB at Meeting C++ in Berlin appeared first on KDAB.
How to Conduct Unit Tests Using BLoC
The use of unit tests for code built with a reactive programming structure can greatly reduce the margin of error.
Release 3.11.0: Speech-to-Text, PDF Preview Images, Network Service Discovery and Felgo Live Server Search
The Felgo 3.11.0 update brings many new features like Speech-to-Text capabilities, an image provider to preview PDFs, network service discovery with ZeroConf and the ability to search the Felgo Live Server log output. It also includes many improvements and fixes added during the development of Felgo 4.

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!
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 todecltype(context) captured_context{context};
, i.e.const std::shared_ptr<Data> captured_context{context};
[context=context]
is equivalent toauto 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 .
Squish for Qt – tips & tricks: Working with objects without unique properties
The post Squish for Qt – tips & tricks: Working with objects without unique properties appeared first on Spyrosoft.
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:

Creating Reusable Libraries with Cmake
In part 3 of our CMake series we show you how to create a library that you can use for your own applications
QML vs Qt Widgets – detailed comparison
In the expansive world of the Qt framework, developers often find themselves at a crossroads: Should they delve into the […]
Qt OPC UA – Data Type Code Generation
The type system of OPC UA permits the creation of complex and nested data types. With the merge of the generic struct decoding and encoding feature, the Qt OPC UA module has greatly improved the comfort of handling such types. But for large projects with lots of custom data types, its QVariant based interface might still feel a bit too complicated.
What if we could have C++ data classes for custom types that feel and handle like QOpcUaQualifiedName or QOpcUaEuInformation in Qt OPC UA?
We’ve had the same thought and decided to implement a code generator for Qt OPC UA that takes OPC UA .bsd files as input and generates C++ data classes and an encoder/decoder class based on QOpcUaBinaryDataEncoding.
Continue reading Qt OPC UA – Data Type Code Generation at basysKom.
See you in Berlin in November 2023?
In just a couple of months, there’s going to be not one,
not two,
not three,
but four fantastic developer events in Berlin! We are not going to miss any of them, and so shouldn’t you.
November 12-14: Meeting C++
If you are a C++ developer, do not miss the 2023 edition of Meeting C++. This year the conference will be again in a hybrid format, partially online and partially live in Berlin. The list of talks is, as usual, of the utmost quality.
The closing keynote will be held by Dr. Ivan Čukić, Senior Software Engineer here at KDAB.
You can find more information, including how to register, here.
November 27: KDAB Training Day
KDAB organizes a Training Day in Berlin! You can choose from five different 1-day courses, featuring content which is part of our regular 3 to 4-days courses:
- What’s new in C++23 with Giuseppe D’Angelo
- QML Application Architecture with André Somers
- Profiling on Linux with Milian Wolff
- Porting to Qt 6 with Nicolas Fella
- A taste of Rust (with a drop of Qt) with Florian Gilcher
You can get your tickets from here!
November 28-29: Qt World Summit 2023
The Qt World Summit is back in person! Once again, it’s going to be hosted at the fantastic Berlin Congress Center in Alexanderplatz. This year’s program features several keynotes and over 25 talks in 4 tracks. KDAB will also be present:
- Till Adam, Chief Commercial Officer of the KDAB Group, will talk about The Future of Interventional Cardiac Imaging;
- Nicolas Arnaud-Cormos, senior software engineer and trainer at KDAB, will talk about script automation in Qt applications.
Tickets are available here.
November 30-December 1: Qt Contributors’ Summit 2023
Last but not the least: join us into shaping the future of Qt!
Did you know that Qt is a free software project, with an open governance model? At the Qt Contributors’ Summit the developers of Qt will meet for two days and will discuss Qt’s roadmap: what features are missing? What is being worked upon? In which direction should Qt evolve?
You can see the sessions that have been scheduled so far on Qt Project’s wiki. Anyone who’s interested contributing something to Qt (not just development! Community work, documentation, etc.) is very welcome. You can find all the relevant practical information here.
See you in Berlin!
The post See you in Berlin in November 2023? appeared first on KDAB.