Qt for Android Automotive 6.5.7 is released
The latest patch release for Android Automotive 6.5.7 is just released. This release is based on Qt LTS 6.5.7 with 280 bug fixes, security updates, and other improvements.
The latest patch release for Android Automotive 6.5.7 is just released. This release is based on Qt LTS 6.5.7 with 280 bug fixes, security updates, and other improvements.
As we continue to evolve and adapt the Qt Framework to the needs of our users and upcoming regulation changes, we are excited to announce some significant changes to our Long-Term Support (LTS) policy from Qt 6.8 onwards. The changes are designed to provide a more robust and predictable support strategy, ensuring your projects remain secure and stable over their entire lifecycle.
We are happy to announce the release of Qt Creator 15 Beta!
With the Qt 6.8 LTS release, Qt's framework documentation at https://doc.qt.io/qt-6/ is now machine-translated to five additional languages:
Chinese (Simplified), Japanese, Korean, German, and French.
We’re very happy to announce the latest release of Qt for Python 6.8. With every new release, we try to bring great things with Qt's new features and new trending ideas. For your convenience, you can check out what's new in Qt for Python 6.8 and what’s improved, along with the entire change log.
Scythe Studio is hosting a special meetup for everyone who loves Qt, and you’re invited to join us! Whether you’re […]
The latest patch release for Android Automotive 6.7.3 is just released. This release is based on Qt 6.7.3 with many bug fixes, security updates, and other improvements on the top of Qt 6.7 release.
With the release of Qt 6.8.0, it's a good time to go into more details on the new features brought to popups and menus in Qt Quick Controls.
For developers familiar with Qt Quick, it is important to understand that the current popup implementation involves creating a unique item that encompasses all of the popup's content. This item, known internally as QQuickPopupItem, is derived from the Page class and, when displayed, becomes a visual child of the Overlay. The Overlay itself is a child of the window with a high z-index value of 1000001, ensuring it appears above other elements.
Translating a Qt application, can be a daunting task. This is an overview from Qt 5 to Qt 6 and what new functionality Qt 6.7 brings.
Continue reading Translating Qt Applications at basysKom GmbH.
The Qt Graphics View Framework allows you to develop fast and efficient 2D vector graphic scenes. Scenes can contain millions of items, each with their own features and behaviors. By using the Graphics View via PySide6 you get access to this highly performant graphics layer in Python. Whether you're integrating vector graphics views into an existing PySide6 application, or simply want a powerful vector graphics interface for Python, Qt's Graphics View is what you're looking for.
Some common uses of the Graphics View include data visualization, mapping applications, 2D design tools, modern data dashboards and even 2D games.
In this tutorial we'll take our first steps looking at the Qt Graphics View framework, building a scene with some simple vector items. This will allow us to familiarize ourselves with the API and coordinate system, which we'll use later to build more complex examples.
The Graphics View framework consists of 3 main parts QGraphicsView
, QGraphicsScene
, and QGraphicsItem
, each with different responsibilities.
The framework can be interpreted using the Model-View paradigm, with the QGraphicsScene
as the Model and the QGraphicsView
as the View. Each scene can have multiple views. The QGraphicsItems within the scene can be considered as items within the model, holding the visual data that the scene combines to define the complete image.
QGraphicsScene
is the central component that glues everything together. It acts as a whiteboard on which all items are drawn (circles, rectangles, lines, pixmaps, etc). The QGraphicsView
has the responsibility of rendering a given scene -- or part of it, with some transformation (scaling, rotating, shearing) -- to display it to the user. The view is a standard Qt widget and can be placed inside any Qt layout.
QGraphicsScene
provides some important functionalities out of the box, so we can use them to develop advanced applications without struggling with low-level details. For example --
To define a QGraphicsScene
you define it's boundaries or sceneRect which defines the x & y origins and dimensions of the scene. If you don't provide a sceneRect it will default to the minimum bounding rectangle for all child items -- updating as items are added, moved or removed. This is flexible but less efficient.
Items in the scene are represented by QGraphicsItem
objects. These are the basic building block of any 2D scene, representing a shape, pixmap or SVG image to be displayed in the scene. Each item has a relative position inside the sceneRect
and can have different transformation effects (scale, translate, rotate, shear).
Finally, the QGraphicsView
is the renderer of the scene, taking the scene and displaying it -- either wholly or in part -- to the user. The view itself can have transformations (scale, translate, rotate and shear) applied to modify the display without affecting the underlying scene. By default the view will forward mouse and keyboard events to the scene allowing for user interaction. This can be disabled by calling view.setInteractive(False)
.
Let's start by creating a simple scene. The following code creates QGraphicsScene
, defining a 400 x 200 scene, and then displays it in a QGraphicsView
.
import sys
from PySide6.QtWidgets import QGraphicsScene, QGraphicsView, QApplication
app = QApplication(sys.argv)
# Defining a scene rect of 400x200, with it's origin at 0,0.
# If we don't set this on creation, we can set it later with .setSceneRect
scene = QGraphicsScene(0, 0, 400, 200)
view = QGraphicsView(scene)
view.show()
app.exec()
If you run this example you'll see an empty window.
The empty graphics scene, shown in a QGraphicsView window.
Not very exciting yet -- but this is our QGraphicsView
displaying our empty scene.
As mentioned earlier, QGraphicsView
is a widget. In Qt any widgets without a parent display as windows. This is why our QGraphicsView
appears as a window on the desktop.
Let's start adding some items to the scene. There are a number of built-in graphics items which you can customize and add to your scene. In the example below we use QGraphicsRectItem
which draws a rectangle. We create the item passing in it's dimensions, and then set it's position pen and brush before adding it to the scene.
import sys
from PySide6.QtWidgets import QGraphicsScene, QGraphicsView, QGraphicsRectItem, QApplication
from PySide6.QtGui import QBrush, QPen
from PySide6.QtCore import Qt
app = QApplication(sys.argv)
# Defining a scene rect of 400x200, with it's origin at 0,0.
# If we don't set this on creation, we can set it later with .setSceneRect
scene = QGraphicsScene(0, 0, 400, 200)
# Draw a rectangle item, setting the dimensions.
rect = QGraphicsRectItem(0, 0, 200, 50)
# Set the origin (position) of the rectangle in the scene.
rect.setPos(50, 20)
# Define the brush (fill).
brush = QBrush(Qt.red)
rect.setBrush(brush)
# Define the pen (line)
pen = QPen(Qt.cyan)
pen.setWidth(10)
rect.setPen(pen)
scene.addItem(rect)
view = QGraphicsView(scene)
view.show()
app.exec()
Running the above you'll see a single, rather ugly colored, rectangle in the scene.
A single rectangle in the scene
Adding more items is simply a case of creating the objects, customizing them and then adding them to the scene. In the example below we add an circle, using QGraphicsEllipseItem
-- a circle is just an ellipse with equal height and width.
import sys
from PySide6.QtWidgets import QGraphicsScene, QGraphicsView, QGraphicsRectItem, QGraphicsEllipseItem, QApplication
from PySide6.QtGui import QBrush, QPen
from PySide6.QtCore import Qt
app = QApplication(sys.argv)
# Defining a scene rect of 400x200, with it's origin at 0,0.
# If we don't set this on creation, we can set it later with .setSceneRect
scene = QGraphicsScene(0, 0, 400, 200)
# Draw a rectangle item, setting the dimensions.
rect = QGraphicsRectItem(0, 0, 200, 50)
# Set the origin (position) of the rectangle in the scene.
rect.setPos(50, 20)
# Define the brush (fill).
brush = QBrush(Qt.red)
rect.setBrush(brush)
# Define the pen (line)
pen = QPen(Qt.cyan)
pen.setWidth(10)
rect.setPen(pen)
ellipse = QGraphicsEllipseItem(0, 0, 100, 100)
ellipse.setPos(75, 30)
brush = QBrush(Qt.blue)
ellipse.setBrush(brush)
pen = QPen(Qt.green)
pen.setWidth(5)
ellipse.setPen(pen)
# Add the items to the scene. Items are stacked in the order they are added.
scene.addItem(ellipse)
scene.addItem(rect)
view = QGraphicsView(scene)
view.show()
app.exec()
The above code will give the following result.
A scene with two items
The order you add items affects the stacking order in the scene -- items added later will always appear on top of items added first. However, if you need more control you can set the stacking order using .setZValue
.
ellipse.setZValue(500)
rect.setZValue(200)
Now the circle (ellipse) appears above the rectangle.
Using Zvalue to order items in the scene
Try experimenting with setting the Z value of the two items -- you can set it before or after the items are in the scene, and can change it at any time.
Z in this context refers to the Z coordinate. The X & Y coordinates are the horizontal and vertical position in the scene respectively. The Z coordinate determines the relative position of items toward the front and back of the scene -- coming "out" of the screen towards the viewer.
There are also the convenience methods .stackBefore()
and .stackAfter()
which allow you to stack your QGraphicsItem
behind, or in front of another item in the scene.
ellipse.stackAfter(rect)
Our two QGraphicsItem
objects are currently fixed in position where we place them, but they don't have to be! As already mentioned Qt's Graphics View framework allows items to respond to user input, for example allowing them to be dragged and dropped around the scene at will. Simple functionality like is actually already built in, you just need to enable it on each QGraphicsItem
. To do that we need to set the flag QGraphicsItem.GraphicsItemFlags.ItemIsMoveable
on the item.
The full list of graphics item flags is available here.
import sys
from PySide6.QtWidgets import QGraphicsScene, QGraphicsView, QGraphicsItem, QGraphicsRectItem, QGraphicsEllipseItem, QApplication
from PySide6.QtGui import QBrush, QPen
from PySide6.QtCore import Qt
app = QApplication(sys.argv)
# Defining a scene rect of 400x200, with it's origin at 0,0.
# If we don't set this on creation, we can set it later with .setSceneRect
scene = QGraphicsScene(0, 0, 400, 200)
# Draw a rectangle item, setting the dimensions.
rect = QGraphicsRectItem(0, 0, 200, 50)
# Set the origin (position) of the rectangle in the scene.
rect.setPos(50, 20)
# Define the brush (fill).
brush = QBrush(Qt.red)
rect.setBrush(brush)
# Define the pen (line)
pen = QPen(Qt.cyan)
pen.setWidth(10)
rect.setPen(pen)
ellipse = QGraphicsEllipseItem(0, 0, 100, 100)
ellipse.setPos(75, 30)
brush = QBrush(Qt.blue)
ellipse.setBrush(brush)
pen = QPen(Qt.green)
pen.setWidth(5)
ellipse.setPen(pen)
# Add the items to the scene. Items are stacked in the order they are added.
scene.addItem(ellipse)
scene.addItem(rect)
ellipse.setFlag(QGraphicsItem.ItemIsMovable)
view = QGraphicsView(scene)
view.show()
app.exec()
In the above example we've set ItemIsMovable
on the ellipse only. You can drag the ellipse around the scene -- including behind the rectangle -- but the rectangle itself will remain locked in place. Experiment with adding more items and configuring the moveable status.
If you want an item to be selectable you can enable this by setting the ItemIsSelectable
flag, for example here using .setFlags()
to set multiple flags at the same time.
ellipse.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable)
If you click on the ellipse you'll now see it surrounded by a dashed line to indicate that it is selected. We'll look at how to use item selection in more detail in a later tutorial.
A selected item in the scene, highlighted with dashed lines
So far we've been creating items by creating the objects and then adding them to the scene. But you can also create an object in the scene directly by calling one of the helper methods on the scene itself, e.g. scene.addEllipse()
. This creates the object and returns it so you can modify it as before.
import sys
from PySide6.QtWidgets import QGraphicsScene, QGraphicsView, QGraphicsRectItem, QApplication
from PySide6.QtGui import QBrush, QPen
from PySide6.QtCore import Qt
app = QApplication(sys.argv)
scene = QGraphicsScene(0, 0, 400, 200)
rect = scene.addRect(0, 0, 200, 50)
rect.setPos(50, 20)
# Define the brush (fill).
brush = QBrush(Qt.red)
rect.setBrush(brush)
# Define the pen (line)
pen = QPen(Qt.cyan)
pen.setWidth(10)
rect.setPen(pen)
view = QGraphicsView(scene)
view.show()
app.exec()
Feel free to use whichever form you find most comfortable in your code.
You can only use this approach for the built-in QGraphicsItem
object types.
So far we've built a simple scene using the basic QGraphicsRectItem
and QGraphicsEllipseItem
shapes. Now let's use some other QGraphicsItem
objects to build a more complex scene, including lines, text and QPixmap
(images).
from PySide6.QtCore import QPointF, Qt
from PySide6.QtWidgets import QGraphicsRectItem, QGraphicsScene, QGraphicsView, QApplication
from PySide6.QtGui import QBrush, QPainter, QPen, QPixmap, QPolygonF
import sys
app = QApplication(sys.argv)
scene = QGraphicsScene(0, 0, 400, 200)
rectitem = QGraphicsRectItem(0, 0, 360, 20)
rectitem.setPos(20, 20)
rectitem.setBrush(QBrush(Qt.red))
rectitem.setPen(QPen(Qt.cyan))
scene.addItem(rectitem)
textitem = scene.addText("QGraphics is fun!")
textitem.setPos(100, 100)
scene.addPolygon(
QPolygonF(
[
QPointF(30, 60),
QPointF(270, 40),
QPointF(400, 200),
QPointF(20, 150),
]),
QPen(Qt.darkGreen),
)
pixmap = QPixmap("cat.jpg")
pixmapitem = scene.addPixmap(pixmap)
pixmapitem.setPos(250, 70)
view = QGraphicsView(scene)
view.setRenderHint(QPainter.Antialiasing)
view.show()
app.exec()
If you run the example above you'll see the following scene.
Scene with multiple items including a rectangle, polygon, text and a pixmap.
Let's step through the code looking at the interesting bits.
Polygons are defined using a series of QPointF
objects which give the coordinates relative to the items position. So, for example if you create a polygon object with a point at 30, 20 and then move this polygon object X & Y coordinates 50, 40 then the point will be displayed at 80, 60 in the scene.
Points inside an item are always relative to the item itself, and item coordinates are always relative to the scene -- or the item's parent, if it has one. We'll take a closer look at the Graphics View coordinate system in the next tutorial.
To add an image to the scene we can open it from a file using QPixmap()
. This creates a QPixmap
object, which can then in turn add to the scene using scene.addPixmap(pixmap)
. This returns a QGraphicsPixmapItem
which is the QGraphicsItem
type for the pixmap -- a wrapper than handles displaying the pixmap in the scene. You can use this object to perform any changes to item in the scene.
The multiple layers of objects can get confusing, so it's important to choose sensible variable names which make clear the distinction between, e.g. the pixmap itself and the pixmap item that contains it.
Finally, we set the flag RenderHint,Antialiasing
on the view to smooth the edges of diagonal lines. You almost always want to enable this on your views as otherwise any rotated objects will look very ugly indeed. Below is our scene without antialiasing enabled, you can see the jagged lines on the polygon.
Scene with antialiasing disabled.
Antialiasing has a (small) performance impact however, so if you are building scenes with millions of rotated items it may in some cases make sense to turn it off.
The QGraphicsView
is subclassed from QWidget
, meaning it can be placed in layouts just like any other widget. In the following example we add the view to a simple interface, with buttons which perform a basic effect on the view -- raising and lowering selected item's ZValue. This has the effect of allowing us to move items in front and behind other objects.
The full code is given below.
import sys
from PySide6.QtCore import Qt
from PySide6.QtGui import QBrush, QPainter, QPen
from PySide6.QtWidgets import (
QApplication,
QGraphicsEllipseItem,
QGraphicsItem,
QGraphicsRectItem,
QGraphicsScene,
QGraphicsView,
QHBoxLayout,
QPushButton,
QSlider,
QVBoxLayout,
QWidget,
)
class Window(QWidget):
def __init__(self):
super().__init__()
# Defining a scene rect of 400x200, with it's origin at 0,0.
# If we don't set this on creation, we can set it later with .setSceneRect
self.scene = QGraphicsScene(0, 0, 400, 200)
# Draw a rectangle item, setting the dimensions.
rect = QGraphicsRectItem(0, 0, 200, 50)
rect.setPos(50, 20)
brush = QBrush(Qt.red)
rect.setBrush(brush)
# Define the pen (line)
pen = QPen(Qt.cyan)
pen.setWidth(10)
rect.setPen(pen)
ellipse = QGraphicsEllipseItem(0, 0, 100, 100)
ellipse.setPos(75, 30)
brush = QBrush(Qt.blue)
ellipse.setBrush(brush)
pen = QPen(Qt.green)
pen.setWidth(5)
ellipse.setPen(pen)
# Add the items to the scene. Items are stacked in the order they are added.
self.scene.addItem(ellipse)
self.scene.addItem(rect)
# Set all items as moveable and selectable.
for item in self.scene.items():
item.setFlag(QGraphicsItem.ItemIsMovable)
item.setFlag(QGraphicsItem.ItemIsSelectable)
# Define our layout.
vbox = QVBoxLayout()
up = QPushButton("Up")
up.clicked.connect(self.up)
vbox.addWidget(up)
down = QPushButton("Down")
down.clicked.connect(self.down)
vbox.addWidget(down)
rotate = QSlider()
rotate.setRange(0, 360)
rotate.valueChanged.connect(self.rotate)
vbox.addWidget(rotate)
view = QGraphicsView(self.scene)
view.setRenderHint(QPainter.Antialiasing)
hbox = QHBoxLayout(self)
hbox.addLayout(vbox)
hbox.addWidget(view)
self.setLayout(hbox)
def up(self):
""" Iterate all selected items in the view, moving them forward. """
items = self.scene.selectedItems()
for item in items:
z = item.zValue()
item.setZValue(z + 1)
def down(self):
""" Iterate all selected items in the view, moving them backward. """
items = self.scene.selectedItems()
for item in items:
z = item.zValue()
item.setZValue(z - 1)
def rotate(self, value):
""" Rotate the object by the received number of degrees """
items = self.scene.selectedItems()
for item in items:
item.setRotation(value)
app = QApplication(sys.argv)
w = Window()
w.show()
app.exec()
If you run this, you will get a window like that shown below. By selecting an item in the graphics view and then clicking either the "Up" or "Down" button you can move items up and down within the scene -- behind and in front of one another. The items are all moveable, so you can drag them around too. Clicking on the slider will rotate the currently selected items by the set number of degrees.
A graphics scene with some custom controls
The raising and lowering is handled by our custom methods up
and down
, which work by iterating over the currently selected items in the scene -- retrieved using scene.selectedItems()
and then getting the items z value and increasing or decreasing it respectively.
def up(self):
""" Iterate all selected items in the view, moving them forward. """
items = self.scene.selectedItems()
for item in items:
z = item.zValue()
item.setZValue(z + 1)
While rotation is handled using the item.setRotation
method. This receives the current angle from the QSlider
and again, applies it to any currently selected items in the scene.
def rotate(self, value):
""" Rotate the object by the received number of degrees. """
items = self.scene.selectedItems()
for item in items:
item.setRotation(value)
Take a look at the QGraphicsItem documentation for some other properties you can control with widgets and try extending the interface to allow you to change them dynamically.
Hopefully this quick introduction to the Qt Graphics View framework has given you some ideas of what you can do with it. In the next tutorials we'll look at how events and user interaction can be handled on items and how to create custom & compound items for your own scenes.
We’re pleased to announce the release of KD Reports 2.3.0, the latest version of our reporting tool for Qt applications. This marks our first major update in two years, bringing several bug fixes and new features that further improve the experience of generating reports.
KD Reports is a versatile tool for generating reports directly from Qt applications. It supports creating printable and exportable reports using code or XML, featuring elements like text, tables, charts, headers, and footers. Whether for visualizing database content, generating invoices, or producing formatted printouts, KD Reports makes it easy to create structured reports within your Qt projects.
The new release includes essential bug fixes and feature enhancements that make KD Reports even more robust and user-friendly.
The 2.3.0 release addresses several important issues to improve stability and compatibility. One major fix resolves an infinite loop and other problems caused by changes in QTextFormat
behavior in Qt 6.7. Right-aligned tabs, which previously didn’t work when paragraph margins were set, have also been corrected. High-DPI rendering has been improved to eliminate blurriness in displays where the device pixel ratio (DPR) is not equal to 1. Furthermore, an issue with result codes being overwritten in the KDReportsPreviewDialog
has been fixed. Finally, table borders, which were lost after upgrading to Qt 6.8, now behave as expected, maintaining their cell borders throughout.
KD Reports 2.3.0 introduces several new features aimed at providing more customization and flexibility in report generation. For instance, the AutoTableElement
now supports customization of header styling via the new setHorizontalHeaderFormatFunction
and setVerticalHeaderFormatFunction
, which are demonstrated in the PriceList example. Additionally, individual table cell formatting has been enhanced with the setCellFormatFunction
, allowing for customization of borders and padding. Text alignment within table cells has also been improved with the new setVerticalAlignment
feature, making it easy to vertically center or top-align text when using different font sizes within the same row.
The AbstractTableElement
now allows setting column constraints while leaving some columns without constraints—just pass {}
for unconstrained columns. This feature is particularly useful when setting constraints for columns further to the right. Also, the TableElement
has gained rowCount()
and columnCount()
methods, which can be used in dynamic scenarios, such as applying alternate background colors to rows.
Lastly, you can now disable the progress dialog during printing or PDF export using setProgressDialogEnabled(false)
. This is useful for applications that generate multiple documents or handle progress tracking internally, offering more control over the user interface during these operations.
You can explore all the new features and improvements in KD Reports 2.3.0 on its GitHub page. Download the latest release and check out the detailed changes to see how they can enhance your reporting tasks. Feel free to share your feedback or report any issues you encounter along the way.
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 KD Reports 2.3.0 appeared first on KDAB.
In Part 1, we covered PCM audio and superimposing waveforms, and developed an algorithm to combine an arbitrary number of audio streams into one.
Now we need to use these ideas to finish a full implementation using Qt Multimedia.
So what do we need? Well, we want to use a single QAudioOutput
, which we pass an audio device and a supported audio format.
We can get those like this:
const QAudioDeviceInfo &device = QAudioDeviceInfo::defaultOutputDevice();
const QAudioFormat &format = device.preferredFormat();
Let’s construct our QAudioOutput
object using the device and format:
static QAudioOutput audioOutput(device, format);
Now, to use it to write data, we have to call start
on the audio output, passing in a QIODevice *
.
Normally we would use the QIODevice
subclass QBuffer
for a single audio buffer. But in this case, we want our own subclass of QIODevice
, so we can combine multiple buffers into one IO device.
We’ll call our subclass MixerStream
. This is where we will do our bufferCombine
, and keep our member list of streams mStreams
.
We will also need another stream type for mStreams
. For now let’s just call it DecoderStream
, forward declare it, and worry about its implementation later.
One thing that’s good to know at this point is DecoderStream
objects will get the data buffers we need by decoding audio data from a file. Because of this, we’ll need to keep our audio format from above to as a data member mFormat
. Then we can pass it to decoders when they need it.
MixerStream
Since we are subclassing QIODevice
, we need to provide reimplementations for these two protected virtual
functions:
virtual qint64 QIODevice::readData(char *data, qint64 maxSize);
virtual qint64 QIODevice::writeData(const char *data, qint64 maxSize);
We also want to provide a way to open new streams that we’ll add to mStreams
, given a filename. We’ll call this function openStream
. We can also allow looping a stream multiple times, so let’s add a parameter for that and give it a default value of 1
.
Additionally, we’ll need a user-defined destructor to delete any pointers in the list that might remain if the MixerStream
is abruptly destructed.
// mixerstream.h
#pragma once
#include <QAudioFormat>
#include <QAudioOutput>
#include <QIODevice>
class DecodeStream;
class MixerStream : public QIODevice
{
Q_OBJECT
public:
explicit MixerStream(const QAudioFormat &format);
~MixerStream();
void openStream(const QString &fileName, int loops = 1);
protected:
qint64 readData(char *data, qint64 maxSize) override;
qint64 writeData(const char *data, qint64 maxSize) override;
private:
QAudioFormat mFormat;
QList<DecodeStream *> mStreams;
};
Notice that combineSamples
isn’t in the header. It’s a pretty basic function that doesn’t require any members, so we can just implement it as a free function.
Let’s put it in a header mixer.h
and wrap it in a namespace
:
// mixer.h
#pragma once
#include <QtGlobal>
#include <limits>
namespace Mixer
{
inline qint16 combineSamples(qint32 samp1, qint32 samp2)
{
const auto sum = samp1 + samp2;
if (std::numeric_limits<qint16>::max() < sum)
return std::numeric_limits<qint16>::max();
if (std::numeric_limits<qint16>::min() > sum)
return std::numeric_limits<qint16>::min();
return sum;
}
} // namespace Mixer
There are some very basic things we can get out of the way quickly in the MixerStream
cpp file. Recall that we must implement these member functions:
explicit MixerStream(const QAudioFormat &format);
~MixerStream();
void openStream(const QString &fileName, int loops = 1);
qint64 readData(char *data, qint64 maxSize) override;
qint64 writeData(const char *data, qint64 maxSize) override;
The constructor is very simple:
MixerStream::MixerStream(const QAudioFormat &format)
: mFormat(format)
{
setOpenMode(QIODevice::ReadOnly);
}
Here we use setOpenMode
to automatically open our device in read-only mode, so we don’t have to call open()
directly from outside the class.
Also, since it’s going to be read-only, our reimplementation of QIODevice::writeData
will do nothing:
qint64 MixerStream::writeData([[maybe_unused]] const char *data,
[[maybe_unused]] qint64 maxSize)
{
Q_ASSERT_X(false, "writeData", "not implemented");
return 0;
}
The custom destructor we need is also quite simple:
MixerStream::~MixerStream()
{
while (!mStreams.empty())
delete mStreams.takeLast();
}
readData
will be almost exactly the same as the implementation we did earlier, but returning qint64
. The return value is meant to be the amount of data written, which in our case is just the maxSize
argument given to it, as we write fixed-size buffers.
Additionally, we should call qAsConst
(or std::as_const
) on mStreams
in the range-for to avoid detaching the Qt container. For more on qAsConst
and range-based for
loops, see Jesper Pederson’s blog post on the topic.
qint64 MixerStream::readData(char *data, qint64 maxSize)
{
memset(data, 0, maxSize);
constexpr qint16 bitDepth = sizeof(qint16);
const qint16 numSamples = maxSize / bitDepth;
for (auto *stream : qAsConst(mStreams))
{
auto *cursor = reinterpret_cast<qint16 *>(data);
qint16 sample;
for (int i = 0; i < numSamples; ++i, ++cursor)
if (stream->read(reinterpret_cast<char *>(&sample), bitDepth))
*cursor = Mixer::combineSamples(sample, *cursor);
}
return maxSize;
}
That only leaves us with openStream
. This one will require us to discuss DecodeStream
and its interface.
The function should construct a new DecodeStream
on the heap, which will need a file name and format. DecodeStream
, as implied by its name, needs to decode audio files to PCM data. We’ll use a QAudioDecoder
within DecodeStream
to accomplish this, and for that, we need to pass mFormat
to the constructor. We also need to pass loops
to the constructor, as each stream can have a different number of loops.
Now our constructor call will look like this:
DecodeStream(fileName, mFormat, loops);
We can then use operator<<
to add it to mStreams
.
Finally, we need to remove it from the list when it’s done. We’ll give it a Qt signal, finished
, and connect it to a lambda expression that will remove the stream from the list and delete the pointer.
Our completed openStream
function now looks like this:
void MixerStream::openStream(const QString &fileName, int loops)
{
auto *decodeStream = new DecodeStream(fileName, mFormat, loops);
mStreams << decodeStream;
connect(decodeStream, &DecodeStream::finished, this, [this, decodeStream]() {
mStreams.removeAll(decodeStream);
decodeStream->deleteLater();
});
}
Recall from earlier that we call read
on a stream, which takes a char *
to which the read data will be copied and a qint64
representing the size of the data.
This is a QIODevice
function, which will internally call readData
. Thus, DecoderStream
also needs to be a QIODevice
.
DecodeStream
In DecodeStream
, we need readData
to spit out PCM data, so we need to decode our audio file to get its contents in PCM format. In Qt Multimedia, we use a QAudioDecoder
for this. We pass it an audio format to decode to, and a source device, in this case a QFile
file handle for our audio file.
When a QAudioDecoder
‘s start
method is called, it will begin decoding the source file in a non-blocking manner, emitting a signal bufferReady
when a full buffer of decoded PCM data is available.
On that signal, we can call the decoder’s read
method, which gives us a QAudioBuffer
. To store in a data member in DecodeStream
, we use a QByteArray
, which we can interact with using QBuffers
to get a QIODevice
interface for reading and writing. This is the ideal way to work with buffers of bytes to read or write in Qt.
We’ll make two QBuffers
: one for writing data to the QByteArray
(we’ll call it mInputBuffer
), and one for reading from the QByteArray
(we’ll call it mOutputBuffer
). The reason for using two buffers rather than one read/write buffer is so the read and write positions can be independent. Otherwise, we will encounter more stuttering.
So when we get the bufferReady
signal, we’ll want to do something like this:
const QAudioBuffer buffer = mDecoder.read();
mInputBuf.write(buffer.data<char>(), buffer.byteCount());
We’ll also need to have some sort of state enum
. The reason for this is that when we are finished with the stream and emit finished()
, we remove and delete the stream from a connected lambda expression, but read
might still be called before that has completed. Thus, we want to only read from the buffer when the state is Playing
.
Let’s update mixer.h
to put the enum
in namespace Mixer
:
#pragma once
#include <QtGlobal>
#include <limits>
namespace Mixer
{
enum State
{
Playing,
Stopped
};
inline qint16 combineSamples(qint32 samp1, qint32 samp2)
{
const auto sum = samp1 + samp2;
if (std::numeric_limits<qint16>::max() < sum)
return std::numeric_limits<qint16>::max();
if (std::numeric_limits<qint16>::min() > sum)
return std::numeric_limits<qint16>::min();
return sum;
}
} // namespace Mixer
DecodeStream
Now that we understand all the data members we need to use, let’s see what our header for DecodeStream
looks like:
// decodestream.h
#pragma once
#include "mixer.h"
#include <QAudioDecoder>
#include <QBuffer>
#include <QFile>
class DecodeStream : public QIODevice
{
Q_OBJECT
public:
explicit DecodeStream(const QString &fileName, const QAudioFormat &format, int loops);
protected:
qint64 readData(char *data, qint64 maxSize) override;
qint64 writeData(const char *data, qint64 maxSize) override;
signals:
void finished();
private:
QFile mSourceFile;
QByteArray mData;
QBuffer mInputBuf;
QBuffer mOutputBuf;
QAudioDecoder mDecoder;
QAudioFormat mFormat;
Mixer::State mState;
int mLoopsLeft;
};
In the constructor, we’ll initialize our private
members, open the DecodeStream
in read-only (like we did earlier), make sure we open the QFile
and QBuffer
s successfully, and finally set up our QAudioDecoder
.
DecodeStream::DecodeStream(const QString &fileName, const QAudioFormat &format, int loops)
: mSourceFile(fileName)
, mInputBuf(&mData)
, mOutputBuf(&mData)
, mFormat(format)
, mState(Mixer::Playing)
, mLoopsLeft(loops)
{
setOpenMode(QIODevice::ReadOnly);
const bool valid = mSourceFile.open(QIODevice::ReadOnly) &&
mOutputBuf.open(QIODevice::ReadOnly) &&
mInputBuf.open(QIODevice::WriteOnly);
Q_ASSERT(valid);
mDecoder.setAudioFormat(mFormat);
mDecoder.setSourceDevice(&mSourceFile);
mDecoder.start();
connect(&mDecoder, &QAudioDecoder::bufferReady, this, [this]() {
const QAudioBuffer buffer = mDecoder.read();
mInputBuf.write(buffer.data<char>(), buffer.byteCount());
});
}
Once again, our QIODevice
subclass is read-only, so our writeData
method looks like this:
qint64 DecodeStream::writeData([[maybe_unused]] const char *data,
[[maybe_unused]] qint64 maxSize)
{
Q_ASSERT_X(false, "writeData", "not implemented");
return 0;
}
Which leaves us with the last part of the implementation, DecodeStream
‘s readData
function.
We zero out the char *
with memset
to avoid any noise if there are areas that are not overwritten. Then we simply read from the QByteArray
into the char *
if mState
is Mixer::Playing
.
We check to see if we finished reading the file with QBuffer::atEnd()
, and if we are, we decrement the loops remaining. If it’s zero now, that was the last (or only) loop, so we set mState
to stopped, and emit finished()
. Either way we seek
back to position 0. Now if there are loops left, it starts reading from the beginning again.
qint64 DecodeStream::readData(char *data, qint64 maxSize)
{
memset(data, 0, maxSize);
if (mState == Mixer::Playing)
{
mOutputBuf.read(data, maxSize);
if (mOutputBuf.size() && mOutputBuf.atEnd())
{
if (--mLoopsLeft == 0)
{
mState = Mixer::Stopped;
emit finished();
}
mOutputBuf.seek(0);
}
}
return maxSize;
}
Now that we’ve implemented DecodeStream
, we can actually use MixerStream
to play two audio files at the same time!
MixerStream
Here’s an example snippet that shows how MixerStream
can be used to route two simultaneous audio streams into one system mixer channel:
const auto &device = QAudioDeviceInfo::defaultOutputDevice();
const auto &format = device.preferredFormat();
auto mixerStream = std::make_unique<MixerStream>(format);
auto *audioOutput = new QAudioOutput(device, format);
audioOutput->setVolume(0.5);
audioOutput->start(mixerStream.get());
mixerStream->openStream(QStringLiteral("/path/to/some/sound.wav"));
mixerStream->openStream(QStringLiteral("/path/to/something/else.mp3"), 3);
The code in this series of posts is largely a reimplementation of Lova Widmark’s project QtMixer. Huge thanks to her for a great and lightweight implementation. Check the project out if you want to use something like this for a GPL-compliant project (and don’t mind that it uses qmake
).
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 Implementing an Audio Mixer, Part 2 appeared first on KDAB.
When using Qt Multimedia to play audio files, it’s common to use QMediaPlayer
, as it supports a larger variety of formats than QSound
and QSoundEffect
. Consider a Qt application with several audio sources; for example, different notification sounds that may play simultaneously. We want to avoid cutting notification sounds off when a new one is triggered, and we don’t want to construct a queue for notification sounds, as sounds will play at the incorrect time. We instead want these sounds to overlap and play simultaneously.
Ideally, an application with audio has one output stream to the system mixer. This way in the mixer control, different applications can be set to different volume levels. However, a QMediaPlayer
instance can only play one audio source at a time, so each notification would have to construct a new QMediaPlayer
. Each player in turn opens its own stream to the system.
The result is a huge number of streams to the system mixer being opened and closed all the time, as well as QMediaPlayer
s constantly being constructed and destructed.
To resolve this, the application needs a mixer of its own. It will open a single stream to the system and combine all the audio into the one stream.
Before we can implement this, we first need to understand how PCM audio works.
As defined by Wikipedia:
Pulse-code modulation (PCM) is a method used to digitally represent sampled analog signals. It is the standard form of digital audio in computers, compact discs, digital telephony and other digital audio applications. In a PCM stream, the amplitude of the analog signal is sampled at uniform intervals, and each sample is quantized to the nearest value within a range of digital steps.
Here you can see how points are sampled in uniform intervals and quantized to the closest number that can be represented.
Description from Wikipedia: Sampling and quantization of a signal (red) for 4-bit LPCM over a time domain at specific frequency.
Think of a PCM stream as a humongous array of bytes. More specifically, it’s an array of samples, which are either integer or float values and a certain number of bytes in size. The samples are these discrete amplitude values from a waveform, organized contiguously. Think of the each element as being a y-value of a point along the wave, with the index representing an offset from x=0 at a uniform time interval.
Here is a graph of discretely sampled points along a sinusoidal waveform similar to the one above:
Image Source: Wikimedia Commons
Description from Wikimedia Commons: Image of a discrete time sinusoid
Let’s say we have an audio waveform that is a simple sine wave, like the above examples. Each point taken at discrete intervals along the curve here is a sample, and together they approximate a continuous waveform. The distance between the samples along the x-axis is a time delta; this is the sample period. The sample rate is the inverse of this, the number of samples that are played in one second. The typical standard sample rate for audio on CDs is 44100 Hz – we can’t really hear that this data is discrete (plus, the resultant sound wave from air movement is in fact a continuous waveform).
We also have to consider the y-axis here, which represents the amplitude of the waveform at each sampled point. In the image above, amplitude A is normalized such that A\in[−1,1]. In digital audio, there are a few different ways to represent amplitude. We can’t represent all real numbers on a computer, so the representation of the range of values varies in precision.
For example, let’s say we have two different representations of the wave above: 8-bit signed integer and 16-bit signed integer. The normalized value 1 from the image above maps to (2^{8}\div{2})−1=127 with 8-bit representation and (2^{16}\div2)−1=32767 with 16-bit. Therefore, with 16-bit representation, we have 128 times as many possible values to represent the same range; it is more precise, but the required size to store each 16-bit sample is double that of 8-bit samples.
We call the chosen representation, and thus the size of each sample, the bitdepth. Some common bitdepths are 16-bit int, 24-bit int, and 32-bit float, but there are many others in existence.
Let’s consider a huge stream of 16-bit samples and a sample rate of 44100 Hz. We write samples to the audio device periodically with a fixed-size buffer; let’s say it is 4096 bytes. The device will play each sample in the buffer at the aforementioned rate. Since each sample is a contiguous 2-byte short
, we can fit 2048 samples into the buffer at once. We need to write 44100 samples in one second, so the whole buffer will be written around 21.5 times per second.
What if we have two different waveforms though, and what if one starts halfway through the other one? How do we mix them so that this buffer contains the data from both sources?
In the study of waves, you can superimpose two waves by adding them together. Let’s say we have two different discrete wave approximations, each represented by 20 signed 8-bit integer values. To superimpose them, for each index, add the values at that index. Some of these sums will exceed the limits of 8-bit representation, so we clamp them at the end to avoid signed integer overflow. This is known as hard clipping and is the phenomenon responsible for digital overdrive distortion.
x | Wave 1 (y_1) | Wave 2 (y_2) | Sum (y_1+y_2) | Clamped Sum |
---|---|---|---|---|
0 | +60 | −100 | −40 | −40 |
1 | −120 | +80 | −40 | −40 |
2 | +40 | +70 | +110 | +110 |
3 | −110 | −100 | −210 | −128 |
4 | +50 | −110 | −60 | −60 |
5 | −100 | +60 | −40 | −40 |
6 | +70 | +50 | +120 | +120 |
7 | −120 | −120 | −240 | −128 |
8 | +80 | −100 | −20 | −20 |
9 | −80 | +40 | −40 | −40 |
10 | +90 | +80 | +170 | +127 |
11 | −100 | −90 | −190 | −128 |
12 | +60 | −120 | −60 | −60 |
13 | −120 | +70 | −50 | −50 |
14 | +80 | −120 | −40 | −40 |
15 | −110 | +80 | −30 | −30 |
16 | +90 | −100 | −10 | −10 |
17 | −110 | +90 | −20 | −20 |
18 | +100 | −110 | −10 | −10 |
19 | −120 | −120 | −240 | −128 |
Now let’s implement this in C++. We’ll start small, and just combine two samples.
Note: we will use
qint
types here, butqint16
will be the same asint16_t
andshort
on most systems, and similarlyqint32
will correspond toint32_t
andint
.
qint16 combineSamples(qint32 samp1, qint32 samp2)
{
const auto sum = samp1 + samp2;
if (std::numeric_limits<qint16>::max() < sum)
return std::numeric_limits<qint16>::max();
if (std::numeric_limits<qint16>::min() > sum)
return std::numeric_limits<qint16>::min();
return sum;
}
This is quite a simple implementation. We use a function combineSamples
and pass in two 16-bit values, but they will be converted to 32-bit as arguments and summed. This sum is clamped to the limits of 16-bit integer representation using std::numeric_limits
in the <limits>
header of the standard library. We then return the sum, at which point it is re-converted to a 16-bit value.
Now consider an arbitrary number of audio streams n
. For each sample position, we must sum the samples of all n
streams.
Let’s assume we have some sort of audio stream type (we’ll implement it later), and a list called mStreams
containing pointers to instances of this stream type. We need to implement a function that loops through mStreams
and makes calls to our combineSamples
function, accumulating a sum into a new buffer.
Assume each stream in mStreams
has a member function read(char *, qint64)
. We can copy one sample into a char *
by passing it to read
, along with a qint64
representing the size of a sample (bitdepth). Remember that our bitdepth is 16-bit integer, so this size is just sizeof(qint16)
.
Using read
on all the streams in mStreams
and calling combineSamples
to accumulate a sum might look something like this:
qint16 accumulatedSum = 0;
for (auto *stream : mStreams)
{
// call stream->read(char *, qint64)
// to read a sample from the stream into streamSample
qint16 streamSample;
stream->read(reinterpret_cast<char *>(&streamSample), sizeof(qint16)));
// accumulate
accumulatedSum = combineSamples(sample, accumulatedSum);
}
The first pass will add samples from the first stream to zero, effectively copying it to accumulatedSum
. When we move to another stream, the samples from the second stream will be added to those copied values from the first stream. This continues, so the call to combineSamples
for a third stream would combine the third stream’s sample with the sum of the first two. We continue to add directly into the buffer until we have combined all the streams.
Now let’s use this concept to add all the samples for a buffer. We’ll make a function that takes a buffer char *data
and its size qint64 maxSize
. We’ll write our accumulated samples into this buffer, reading all samples from the streams and adding them using the method above.
The function signature looks like this:
void readData(char *data, qint64 maxSize);
Let’s achieve more efficiency by using a constexpr
variable for the bitdepth:
constexpr qint16 bitDepth = sizeof(qint16);
There’s no reason to call sizeof
multiple times, especially considering sizeof(qint16)
can be evaluated as a literal at compile-time.
With the size of each sample and the size of the buffer, we can get the total number of samples to write:
const qint16 numSamples = maxSize / bitDepth;
For each stream in mStreams
we need to read each sample up to numSamples
. As the sample index increments, a pointer to the buffer data needs to also be incremented, so we can write our results at the correct location in the buffer.
That looks like this:
void readData(char *data, qint64 maxSize)
{
// start with 0 in the buffer
memset(data, 0, maxSize);
constexpr qint16 bitDepth = sizeof(qint16);
const qint16 numSamples = maxSize / bitDepth;
for (auto *stream : mStreams)
{
// this pointer will be incremented across the buffer
auto *cursor = reinterpret_cast<qint16 *>(data);
qint16 sample;
for (int i = 0; i < numSamples; ++i, ++cursor)
if (stream->read(reinterpret_cast<char *>(&sample), bitDepth))
*cursor = combineSamples(sample, *cursor);
}
}
The idea here is that we can start playing new audio sources by adding new streams to mStreams
. If we add a second stream halfway through a first stream playing, the next buffer for the first stream will be combined with the first buffer of this new stream. When we’re done playing a stream, we just drop it from the list.
In Part 2, we’ll use Qt Multimedia to fully implement our mixer, connect to our audio device, and test it on some audio files.
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 Implementing an Audio Mixer, Part 1 appeared first on KDAB.
The Felgo 4.2.0 update adds many new features and improvements for Felgo QML Hot Reload. These include support for singletons, better handling of JavaScript and bindings changes, and improved debugging and error-handling capabilities. The release also adds a new RFC-conform OAuth 2.0 plugin, universal app link handling for your app, and updates to Felgo Plugins, Android 14 Support, and Qt Creator 14 for the latest Google Play Store requirements. Read on to learn more about the release.
The post What is Qt and how does it change HMI industrial automation? appeared first on Spyrosoft.
The post Squish Qt tips & tricks: Working with objects without unique properties appeared first on Spyrosoft.
Whether your product is in its early stages or ready to launch, the iPhone platform is a compelling option to […]
In the previous posts of this series (if you’ve missed them: parts 1, 2, 3, and 4), we have learned about relocation and trivial relocation.
We have explored what relocation means, what trivial relocation means, and how it can be used to optimize the implementation of certain data structures, such as the reallocation of a vector-like container (std::vector, QVector and so on).
Furthermore, we have explored how trivial relocation is connected to move assignments and how some types may be trivially relocatable for move construction but not for assignments. This property can be used to further optimize other operations, such as swaps, erasure, and swap-based algorithms such as std::sort or std::rotate.
In this blog post, we’ll have a look at trivial relocation from a “Standard C++” point of view.
That’s probably a question we should have asked as soon as we started this journey. Of course, the answer is no, it is not allowed!
Remember how trivial relocation works: we use memcpy a source object(‘s representation) into some storage, and claim that operation realizes the equivalent of move-constructing the source into that storage, plus destroying the source.
The problem is that one can’t just put data into some storage and pretend that an object exists in there. This is only allowed for a specific set of types, such as trivially copyable types. (Note that if a type is trivially copyable, then Qt automatically considers it trivially relocatable.)
However, as we have discussed, many interesting types (QString, std::vector, std::unique_ptr, …) are not trivially copyable, but they would still benefit from trivial relocatability.
Qt simply ignores the Standard wording and just uses trivial relocatability because it makes our code faster, reduces template bloat, reduces compilation times, and so on. I call this “Undefined Behavior That Works In Practice”: yes, the operation is illegal, but so are many others. Qt is in good company here; many other popular libraries employ the same “illegal” optimization — to name a few: Folly, EASTL, BSL, and possibly others; a survey is available here.
A few years ago, P1144 (“std::is_trivially_relocatable”) emerged. This proposal (which, by the way, has reached its eleventh revision at the time of this writing) was inspired by the work in many existing libraries (including Qt) and introduced the necessary language and library facilities in order to give well-defined semantics to relocation and trivial relocation. Despite its many iterations, it never made it over the “finish line”, voted into C++.
Some time after P1144’s initial revision, P2786 (“Trivial Relocatability For C++26”) was proposed. This was an alternative design, w.r.t. P1144, with different relocation semantics and a different set of enablers.
P1144 and P2786 “competed” for a little while; then, during the ISO C++ meeting in Tokyo (March 2024), the Evolution Working Group voted to adopt P2786 for C++26.
That’s great news, right?
Well, not really. After some analysis, it turned out that P2786’s design is limiting and not user-friendly, to the point that there have been serious concerns that existing libraries may not make use of it at all. In particular, P2786’s relocation semantics do not match Qt’s. With my Qt hat on, it soon became clear that we could not have used P2786 in Qt to replace our own implementation of trivial relocation. This fact raised some serious concerns.
For this reason, I co-authored a “petition paper” (P3236), together with many other library authors, asking EWG to reconsider its decision of going ahead with P2786.
I also have analyzed in detail the problems that I have with P2786’s design in a separate paper (P3233). I don’t want to go through the complete list of issues in a blog post — please read the paper and send feedback.
In June 2024, during the ISO C++ meeting in St. Louis, I presented P3233 to EWG. If you’re interested and/or want a summary of the issues I raised, check out the slides that I used during my presentation.
P3278R0 (“Analysis of interaction between relocation, assignment, and swap”) was also presented, making many of the remarks that I’ve also made in my paper; I consider a good sign that different authors independently reached the same conclusions.
Eventually, EWG voted to take P2786 back, given the issues raised. I consider it a victory in the face of the danger of standardizing something that does not match the current practices. Still, I understand the frustration at pouring a tremendous amount of work into getting a feature into C++, only to get asked to go back to the drawing board.
I hope I can find the time in the next few months to work more on trivial relocation, and present more papers at the next ISO C++ meeting in Wrocław. I think the goal to have trivial relocatability in C++26 is doable; we “just” have to iron out the details.
Regarding the two competing proposals (P1144 versus P2786), in P3236 I actually make an argument that they should be “merged”: each one has some design characteristics that are missing in the other.
Therefore, the trivial relocation story is anything but over. Stay tuned for more updates, hopefully in not the too distant future.
In the meanwhile, thank you for reading so far.
Overview about all installments:
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 and Trivial Relocation (Part 5) appeared first on KDAB.
Let’s say we’re working on a QML project that involves a TextEdit
.
There’s some text in it:
here is some text
We want to select part of this text and hit ctrl+B to make it bold:
here is some text
In Qt Widgets, this is trivial, but not so much in QML – we can get font.bold
of the entire TextEdit
, but not of just the text in the selection. We have to implement formattable selections manually.
To do this, there are two approaches we’ll look at:
TextEdit
as a property. This way we can make use of QTextDocument
and QTextCursor
to actually set text properties within the selection area. This more closely follows the patterns expected in Qt.
In Qt 6.7, the TextEdit
QML element does have a cursorSelection
property that works in this way, and by dissecting its implementation, we can write a pseudo-backport for other Qt versions.
Before we do this, let’s take a look at the hacky QML/JS solution.
We start by focusing on just making ctrl+B bold shortcuts work:
TextEdit {
id: txtEdit
anchors.fill: parent
selectByMouse: true
textFormat: TextEdit.RichText
}
Shortcut {
sequence: StandardKey.Bold
onActivated: {
if (txtEdit.selectedText.length > 0)
{
const start = txtEdit.selectionStart
const end = txtEdit.selectionEnd
let sel = txtEdit.getFormattedText(start, end)
.split("<!--StartFragment-->")[1]
.split("<!--EndFragment-->")[0]
txtEdit.remove(start, end)
if (sel.includes("font-weight:600;"))
sel = sel.replace("font-weight:600;", "")
else
sel = "<b>" + sel + "</b>"
txtEdit.insert(txtEdit.cursorPosition, sel)
txtEdit.select(start, end)
}
}
}
Notice that we actually remove and replace the selected text, and reselect the insertion manually.
We can set up similar shortcuts for italics and underline trivially, but what if we want to set font properties of only the text in the selected area?
To keep things simple, let’s see what happens if we want to set just the font family and size:
FontDialog {
id: fontDlg
}
Shortcut {
id: fontShortcut
property string sel: ""
property int start: 0
property int end: 0
sequence: StandardKey.Find
onActivated: {
if (txtEdit.selectedText.length > 0)
{
start = txtEdit.selectionStart
end = txtEdit.selectionEnd
sel = txtEdit.getFormattedText(start, end)
.split("<!--StartFragment-->")[1]
.split("<!--EndFragment-->")[0]
fontDlg.open()
}
}
}
Connections {
target: fontDlg
function onAccepted() {
txtEdit.remove(fontShortcut.start, fontShortcut.end)
if (fontShortcut.sel.includes("font-family:")) {
let fontToReplace = fontShortcut.sel.split("font-family:'")[1].split("';")[0]
fontShortcut.sel = fontShortcut.sel.replace(fontToReplace, fontDlg.font.family)
} else {
fontShortcut.sel = "<span style=\"font-family: '"
+ fontDlg.font.family + "'; font-size:"
+ (fontDlg.font.pixelSize ? fontDlg.font.pixelSize
: fontDlg.font.pointSize)
+ "\">" + fontShortcut.sel + "</span>"
}
txtEdit.insert(txtEdit.cursorPosition, fontShortcut.sel)
txtEdit.select(fontShortcut.start, fontShortcut.end)
}
}
If we start messing with other font style properties like italic
, bold
, spacing
, etc., we will end up with almost unreadably nasty string manipulation here.
This solution is overall hacky, as we replace HTML-formatted text from a snipped out section. It would be more Qt-idiomatic to retrieve QFont
info from a selection and set the properties without editing raw rich text. Furthermore, it’s better to do as much logic as possible in C++ rather than with JavaScript in QML.
cursorSelection
in Qt 6.7 QML
Let’s take a look at the cursorSelection
property of QtQuick TextEdit
in Qt 6.7.
By looking at its property declaration in qquicktextedit_p.h
, the type of cursorSelection
is QQuickTextSelection
.
This type is very basic. It has four read/write properties.
Here is the header qquicktextselection_p.h
:
class Q_QUICK_EXPORT QQuickTextSelection : public QObject
{
Q_OBJECT
Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged FINAL)
Q_PROPERTY(QFont font READ font WRITE setFont NOTIFY fontChanged FINAL)
Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged FINAL)
Q_PROPERTY(Qt::Alignment alignment READ alignment WRITE setAlignment NOTIFY alignmentChanged FINAL)
QML_ANONYMOUS
QML_ADDED_IN_VERSION(6, 7)
public:
explicit QQuickTextSelection(QObject *parent = nullptr);
QString text() const;
void setText(const QString &text);
QFont font() const;
void setFont(const QFont &font);
QColor color() const;
void setColor(QColor color);
Qt::Alignment alignment() const;
void setAlignment(Qt::Alignment align);
Q_SIGNALS:
void textChanged();
void fontChanged();
void colorChanged();
void alignmentChanged();
private:
QTextCursor cursor() const;
void updateFromCharFormat(const QTextCharFormat &fmt);
void updateFromBlockFormat();
private:
QTextCursor m_cursor;
QTextCharFormat m_charFormat;
QTextBlockFormat m_blockFormat;
QQuickTextDocument *m_doc = nullptr;
QQuickTextControl *m_control = nullptr;
};
Notice we’ve got these private data members:
QTextCursor m_cursor;
QTextCharFormat m_charFormat;
QTextBlockFormat m_blockFormat;
QQuickTextDocument *m_doc = nullptr;
QQuickTextControl *m_control = nullptr;
The m_doc
and m_control
are retrieved from the TextEdit
which parents the selection object. The object is always constructed by a QQuickTextEdit
, so in the constructor, the parent is cast to one using qmlobject_cast
. Then we set these two fields.
QQuickTextSelection::QQuickTextSelection(QObject *parent)
: QObject(parent)
{
// When QQuickTextEdit creates its cursorSelection, it passes itself as the parent
if (auto *textEdit = qmlobject_cast<QQuickTextEdit *>(parent)) {
m_doc = textEdit->textDocument();
m_control = QQuickTextEditPrivate::get(textEdit)->control;
// ...
// ...
Now what are m_charFormat
and m_blockFormat
?
Text documents are composed of a list of text blocks, which can be paragraphs, lists, tables, images, etc. Thus, a block format represents an individual block’s alignment formatting. Char format contains formatting information at the character level, like font family, weight, style, size, color, and so forth.
To initialize these, we need to get the cursor from the text control.
QTextCursor QQuickTextSelection::cursor() const
{
if (m_control)
return m_control->textCursor();
return m_cursor;
}
The cursor will give us a char format and a block format, which we use to get the font / color / alignment at the cursor’s location.
QFont QQuickTextSelection::font() const
{
return cursor().charFormat().font();
}
// ...
QColor QQuickTextSelection::color() const
{
return cursor().charFormat().foreground().color();
}
// ...
Qt::Alignment QQuickTextSelection::alignment() const
{
return cursor().blockFormat().alignment();
}
currentCharFormatChanged
is emitted by QQuickTextControl
when the cursor moves or the document’s contents change. If this format is indeed different from the fields of the selection object, we must update them and emit the selection’s signals, just as we would in setters. Since we keep track of block alignment too, we have to do the same when the cursor moves and block format is different.
QQuickTextSelection::QQuickTextSelection(QObject *parent)
: QObject(parent)
{
// When QQuickTextEdit creates its cursorSelection, it passes itself as the parent
if (auto *textEdit = qmlobject_cast<QQuickTextEdit *>(parent)) {
m_doc = textEdit->textDocument();
m_control = QQuickTextEditPrivate::get(textEdit)->control;
connect(m_control, &QQuickTextControl::currentCharFormatChanged,
this, &QQuickTextSelection::updateFromCharFormat);
connect(m_control, &QQuickTextControl::cursorPositionChanged,
this, &QQuickTextSelection::updateFromBlockFormat);
}
}
// ...
// ...
// ...
inline void QQuickTextSelection::updateFromCharFormat(const QTextCharFormat &fmt)
{
if (fmt.font() != m_charFormat.font())
emit fontChanged();
if (fmt.foreground().color() != m_charFormat.foreground().color())
emit colorChanged();
m_charFormat = fmt;
}
inline void QQuickTextSelection::updateFromBlockFormat()
{
QTextBlockFormat fmt = cursor().blockFormat();
if (fmt.alignment() != m_blockFormat.alignment())
emit alignmentChanged();
m_blockFormat = fmt;
}
Here are the setters for the properties, which use the cursor to access and mutate the character or block properties at its position.
void QQuickTextSelection::setText(const QString &text)
{
auto cur = cursor();
if (cur.selectedText() == text)
return;
cur.insertText(text);
emit textChanged();
}
// ...
void QQuickTextSelection::setFont(const QFont &font)
{
auto cur = cursor();
if (cur.selection().isEmpty())
cur.select(QTextCursor::WordUnderCursor);
if (font == cur.charFormat().font())
return;
QTextCharFormat fmt;
fmt.setFont(font);
cur.mergeCharFormat(fmt);
emit fontChanged();
}
// ...
void QQuickTextSelection::setColor(QColor color)
{
auto cur = cursor();
if (cur.selection().isEmpty())
cur.select(QTextCursor::WordUnderCursor);
if (color == cur.charFormat().foreground().color())
return;
QTextCharFormat fmt;
fmt.setForeground(color);
cur.mergeCharFormat(fmt);
emit colorChanged();
}
// ...
void QQuickTextSelection::setAlignment(Qt::Alignment align)
{
if (align == alignment())
return;
QTextBlockFormat format;
format.setAlignment(align);
cursor().mergeBlockFormat(format);
emit alignmentChanged();
}
Now, we want to do something like this in our code. The issue is that this implementation resides in the Qt source code itself, and cursorSelection
is a property of QQuickTextEdit
. If we want to do something like this without changing Qt source code, we have to use attached properties.
Using CursorSelection
as an attached property for a TextEdit
in QML might look something like this:
Item {
// ...
// ...
// ...
Shortcut {
// ctrl+B to toggle bold / not bold for selection
sequence: StandardKey.Bold
onActivated: {
txtEdit.CursorSelection.font = Qt.font({
bold: txtEdit.CursorSelection.font.bold !== true
})
}
}
TextEdit {
id: txtEdit
// ...
CursorSelection.font {
bold: false
italic: false
underline: false
}
}
}
To create our own attached property, we have to create two classes: CursorSelectionAttached
and CursorSelection
.
CursorSelectionAttached
will contain the implementation of the selection, while CursorSelection
serves as the attaching type, using the qmlAttachedProperties()
method to expose the signals and properties of an instance of CursorSelectionAttached
to the parent to which it is attached.
CursorSelection
also needs the QML_ATTACHED()
macro in its header declaration, and we must specify that it has an attached property with the macro QML_DECLARE_TYPEINFO()
outside the class scope.
Thus, CursorSelection
will just look like this:
// CursorSelection.h
class CursorSelection : public QObject
{
Q_OBJECT
QML_ATTACHED(CursorSelectionAttached)
QML_ELEMENT
public:
static CursorSelectionAttached *qmlAttachedProperties(QObject *object);
};
QML_DECLARE_TYPEINFO(CursorSelection, QML_HAS_ATTACHED_PROPERTIES)
Where the entire implementation is just this function definition:
// CursorSelection.cpp
CursorSelectionAttached *CursorSelection::qmlAttachedProperties(QObject *object)
{
if (auto *textEdit = qobject_cast<QQuickTextEdit *>(object))
return new CursorSelectionAttached(textEdit);
return nullptr;
}
Notice that we perform the qobject_cast
here and forward the result as the parent of the attached object. This way we only construct an attached object if we can cast the parent object to a TextEdit
.
Now, let’s see how CursorSelectionAttached
should be implemented. We begin with the constructor:
// we know that parent will be a QQuickTextEdit *
CursorSelectionAttached::CursorSelectionAttached(QQuickTextEdit *parent) noexcept
: QObject(parent)
, mEdit(parent) // this is the TextEdit we are attached to
{
// make sure the QTextDocument exists
const auto *const quickDoc = mEdit->textDocument(); // QQuickTextDocument *
auto *doc = quickDoc->textDocument(); // QTextDocument *
Q_ASSERT(doc != nullptr);
// retrieve QTextCursor from the QTextDocument
mCursor = QTextCursor(doc);
// When deselecting, the cursor position and anchor are
// set to the TextEdit's cursor position
connect(mEdit, &QQuickTextEdit::selectedTextChanged,
this, &CursorSelectionAttached::moveAnchorIfDeselected);
connect(mEdit, &QQuickTextEdit::cursorPositionChanged,
this, &CursorSelectionAttached::updatePosition);
// if we set a format with no selection, we keep it in an optional
// then when new text is added, it will have this formatting
// for example, with no selection we press ctrl+B and then start
// typing. we expect the text to be bold.
connect(mEdit->textDocument()->textDocument(),
&QTextDocument::contentsChange,
this,
&CursorSelectionAttached::applyFormatToNewTextIfNeeded);
}
Note that we connect to these three slots:
moveAnchorIfDeselected
updatePosition
applyFormatToNewTextIfNeeded
Let’s investigate the purpose of these.
moveAnchorIfDeselected
is invoked when the TextEdit’s selected text changes. A QTextCursor
has an anchor, which controls selection area. If text is being selected, the anchor is fixed in place where the selection is started, and the cursor position moves independently of the anchor. The selection area is located between the two positions. When a cursor moves without selecting anything, the anchor is located at and moves along with the cursor position.
Thus, when a cursor’s position is moved, we need to know if the anchor should be moved with it.
Since we invoke moveAnchorIfDeselected
when the selected text changes, we know that if the selection is now empty, this means there was a selection that has been deselected. Thus, the cursor and anchor should be equal to one another.
void CursorSelectionAttached::moveAnchorIfDeselected()
{
if (mEdit->selectedText().isEmpty())
mCursor.setPosition(mEdit->cursorPosition(), QTextCursor::MoveAnchor);
}
updatePosition
is invoked when the TextEdit’s cursor position changes. Depending on the TextEdit’s selection start and end positions, there are a few ways the cursor could be updated.
If there is no selected area in the TextEdit, the cursor and anchor should move together. If a selection’s start and end position both change, we must move the cursor twice: once to the start position, with the anchor moving, and once to the end position, with the anchor fixed in place. If the selection area is being resized, for example by dragging or using Shift+ArrowKeys, the cursor should move with the anchor fixed in place.
void CursorSelectionAttached::updatePosition()
{
// if there's no selection, just move the cursor & anchor
if (mEdit->selectionEnd() == mEdit->selectionStart())
{
mCursor.setPosition(mEdit->cursorPosition(), QTextCursor::MoveAnchor);
}
// if both the start and end need to be updated:
// move cursor and anchor to selection start, and
// move cursor to selection end while keeping anchor at start
//
// we have to make sure the anchor is moved correctly so the
// whole selection matches up -- otherwise cursor selection
// start or end might be in the middle of the actual
// selection, wherever the anchor is
else if (mEdit->selectionStart() != mCursor.selectionStart() &&
mEdit->selectionEnd() != mCursor.selectionEnd())
{
mCursor.setPosition(mEdit->selectionStart(), QTextCursor::MoveAnchor);
mCursor.setPosition(mEdit->selectionEnd(), QTextCursor::KeepAnchor);
}
// these two cases are for selection dragging, only start or
// end will move, so anchor stays in place
else if (mEdit->selectionStart() != mCursor.selectionStart())
{
mCursor.setPosition(mEdit->selectionStart(), QTextCursor::KeepAnchor);
}
else if (mEdit->selectionEnd() != mCursor.selectionEnd())
{
mCursor.setPosition(mEdit->selectionEnd(), QTextCursor::KeepAnchor);
}
}
applyFormatToNewTextIfNeeded
is invoked when the contents of the text document change. This is because font properties might be set without an active selection. In this case, the expected behavior is for the characters added afterwards will have these properties.
For example, if the font family is changed with no selection, and we start typing, we expect our text to be in this new font. To do this, we need an optional
in which we can save a format to apply to new text if needed, or otherwise contains nullopt
. We will call it mOptFormat
. It can be set in property setters, which you will see later. For now, we just make sure to use it when the text document content changes and there exists a value in the optional.
void CursorSelectionAttached::applyFormatToNewTextIfNeeded(int from, int charsRemoved, int charsAdded)
{
if (charsAdded && mOptFormat)
{
mCursor.setPosition(mCursor.position() - 1, QTextCursor::KeepAnchor);
mCursor.mergeCharFormat(mOptFormat.value());
mOptFormat.reset();
}
}
Now, let’s take a look at the properties to expose to QML, and how they can be retrieved and set using the cursor. Like the QQuickTextSelection
implementation, we will have properties text
and font
. We can implement the others as well, but for the sake of brevity, we will just focus on these two.
Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged FINAL)
Q_PROPERTY(QFont font READ font WRITE setFont NOTIFY fontChanged FINAL)
We’ll need to declare and define these getters and setters, and declare the signals:
Getters:
[[nodiscard]] QString text() const;
[[nodiscard]] QFont font() const;
Setters:
void setText(const QString &text);
void setFont(const QFont &font);
Signals:
void textChanged();
void fontChanged();
The getter and setter implementations will look very similar to the previous implementations shown for QQuickTextSelection
, with some minor differences.
Getter implementations:
QString CursorSelectionAttached::text() const
{
return mCursor.selectedText();
}
QFont CursorSelectionAttached::font() const
{
// simply get the font at the cursor position using charFormat
auto ret = mCursor.charFormat().font();
// if the cursor is at the start of a selection, we need to take the font
// at the position right in front of it. otherwise, the font will refer to the
// character at the position right before the selection begins
if (mCursor.hasSelection() && mCursor.position() == mCursor.selectionStart())
{
auto cur = mCursor;
cur.setPosition(cur.position() + 1);
ret = cur.charFormat().font();
}
return ret;
}
Setter implementations:
void CursorSelectionAttached::setText(const QString &text)
{
if (mCursor.selectedText() == text)
return;
mCursor.insertText(text);
emit textChanged();
}
void CursorSelectionAttached::setFont(const QFont &font)
{
if (font == mCursor.charFormat().font())
return;
QTextCharFormat fmt = mCursor.charFormat();
fmt.setFont(font, QTextCharFormat::FontPropertiesSpecifiedOnly);
// when no selection, formatting must be set on the next insertion
if (mCursor.selection().isEmpty())
mOptFormat = fmt;
else
mCursor.mergeCharFormat(fmt);
emit fontChanged();
}
The only thing that needs to be done now is override the destructor, which can just be set to default:
~CursorSelectionAttached() override = default;
Now we have all the implementation we need to use the attached property. If we put the two classes in one header file, it will look like this:
#pragma once
#include <QObject>
#include <QTextCursor>
#include <QtQml>
#include <optional>
class QQuickTextEdit;
class CursorSelectionAttached : public QObject
{
Q_OBJECT
Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged FINAL)
Q_PROPERTY(QFont font READ font WRITE setFont NOTIFY fontChanged FINAL)
QML_ANONYMOUS
public:
explicit CursorSelectionAttached(QQuickTextEdit *parent) noexcept;
~CursorSelectionAttached() override = default;
[[nodiscard]] QString text() const;
[[nodiscard]] QFont font() const;
void setText(const QString &text);
void setFont(const QFont &font);
signals:
void textChanged();
void fontChanged();
private slots:
void moveAnchorIfDeselected();
void updatePosition();
void applyFormatToNewTextIfNeeded(int from, int charsRemoved, int charsAdded);
private:
QTextCursor mCursor;
QQuickTextEdit *mEdit;
std::optional<QTextCharFormat> mOptFormat;
};
class CursorSelection : public QObject
{
Q_OBJECT
QML_ATTACHED(CursorSelectionAttached)
QML_ELEMENT
public:
static CursorSelectionAttached *qmlAttachedProperties(QObject *object);
};
QML_DECLARE_TYPEINFO(CursorSelection, QML_HAS_ATTACHED_PROPERTIES)
With this header, an implementation file containing the definitions, and a call to qmlRegisterUncreatableType<CursorSelection>
in your main.cpp
, the attached property can be used in QML.
Though this is not a perfect backport, this code allows us to set font properties for selected text in QML in a nearly identical way to its implementation in Qt 6.7. This is especially useful to implement any kind of richtext editing in a QML application, where this functionality is severely lacking in any Qt version prior to 6.7. Hopefully this is a helpful guide to backporting features, implementing attached properties, and doing more sane text editing in QML apps.
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 Formatting Selected Text in QML appeared first on KDAB.
Welcome to another blog post in the series discussing a specific technical topic. Today, we will take on one form […]
An over-the-air (OTA) update capability is an increasingly critical part of any embedded product to close cybersecurity vulnerabilities, allow just-in-time product rollouts, stomp out bugs, and deliver new features. We’ve talked about some of the key structural elements that go into an embedded OTA architecture before. But what about the back end? Let’s address some of those considerations now.
The ideal of a constant Internet connection is more aspiration than reality for many embedded devices. Sporadic connections, costly cellular or roaming charges, and limited bandwidth are common hurdles. These conditions necessitate smart management of update payloads and robust retry strategies that can withstand interruptions, resuming where they left off without getting locked in a continually restarting update cycle.
There are other ways to manage spotty connections. Consider using less frequent update schedules or empower users to initiate updates. These strategies however have trade-offs, including the potential to miss critical security patches. One way to strike a balance is to implement updates as either optional or mandatory, or flag updates as mandatory only when critical, allowing users to pace out updates when embedded connectivity isn’t reliable.
When network access is very unreliable, or even just plain absent, then USB updates are indispensable for updating device software. These updates can also serve as effective emergency measures or for in-field support. While the process of downloading and preparing a USB update can often be beyond a normal user’s capability, it’s a critical fallback and useful tool for technical personnel.
Deciding between software as a service (SaaS) and self-hosted options for your OTA server is a decision that impacts not just the update experience but also compliance with industry and privacy regulations. While SaaS solutions can offer ease and reliability, certain scenarios may necessitate on-premise servers. If you do need to host an OTA server yourself, you’ll need to supply the server hardware and assign a maintenance team to manage it. But you may not have to build it all from scratch – you can still call in the experts with proven experience in setting up self-hosted OTA solutions.
SSL certificates are non-negotiable for genuine and secure OTA updates. They verify your company as the authentic source of updates. Choosing algorithms with the longest (comparatively equivalent) key lengths will extend the reliable lifespan of these certificates. However, remember that certificates do expire; having a game plan in place to deal with expired certificates will allow you to avoid the panic of an emergency scramble if it should happen unexpectedly.
Accurate timekeeping is also essential for validating SSL certificates. A functioning and accurate real-time clock that is regularly NTP/SNTP synchronized is critical. If timekeeping fails, your certificates won’t be validated properly, causing all sorts of issues. (We recommend reading our OTA best practice guide for advice on what to do proactively and reactively with invalidated or expired certificates.
Encrypted update payloads are imperative as a safeguard against reverse-engineering and content tampering. This is true for OTA updates as well as any USB or offline updates. Leveraging the strongest possible encryption keys that your device can handle will enhance security significantly.
The growing ‘right to repair’ movement and associated legislation imply that devices should support updates outside of your organization’s tightly controlled processes. This may mean that you need to provide a manual USB update to meet repair requirements without exposing systems to unauthorized OTA updates. To prevent your support team from struggling with amateur software updates, you’ll want to configure your device to set a flag when unauthorized software has been loaded. This status can be checked by support teams to invalidate support or warranty agreements.
By carefully navigating the critical aspects of OTA updates, such as choosing the right hosting option and managing SSL certificates and encryption protocols, your embedded systems can remain up-to-date and secure under any operating conditions. While this post introduces the issues involved in embedded-system updates, there is much more to consider for a comprehensive strategy. For a deeper exploration and best practices in managing an embedded product software update strategy, please visit our best practice guide, Updates Outside the App Store.
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 Behind the Scenes of Embedded Updates appeared first on KDAB.
OPC UA client code that relies on hardcoded NodeIds is brittle and often only works with a specific OPC UA server instance. This article shows the proper way to write robust and portable OPC UA client code.
Continue reading OPC UA: Programming against Type Descriptions at basysKom GmbH.
Scythe Studio has been on the market since 2020, delivering numerous projects for satisfied clients and navigating a significant learning […]
Hello! Today I have released new digital updates to my book Create GUI Applications with Python & Qt.
This update brings all versions up to date with the latest developments in PyQt6 & PySide6. As well as corrections and additions to existing chapters, there are new sections dealing with form layouts, built-in dialogs and developing Qt applications using a Model View Controller (MVC) architecture. The same corrections and additions have been made to the PyQt5 & PySide2 editions.
As always, if you've previously bought a copy of the book you get these updates for free! Just go to the downloads page and enter the email you used for the purchase.
You can buy the latest editions below --
If you bought the book elsewhere (in paperback or digital) you can register to get these updates too. Email your receipt to register@pythonguis.com
Enjoy!
If you’ve been following our blog, you’re likely aware of Rust’s growing presence in embedded systems. While Rust excels in safety-by-design, it’s also common to find it integrated with C++. This strategic approach leverages the strengths of both languages, including extensive C++ capabilities honed over the years in complex embedded systems. Let’s delve into some key concepts for integrating Rust and C++.
If you’re adding Rust to an existing C++ project, you need to start in the right place. Begin by oxidizing (that is, converting code to Rust) areas that are bug-prone, difficult to maintain, or with security vulnerabilities. These are where Rust can offer immediate improvements. Focus on modules that are self-contained, have clean interfaces, and are primarily procedural rather than object-oriented. For example, libraries that handle media or image processing can be prime candidates for rewriting in Rust as these are often vulnerable to memory safety issues. Parsers and input handling routines also stand to benefit from Rust’s guarantees of safety.
As your project scales, weigh the merits of maintaining a C++ core with Rust components versus a Rust-centric application with C++ libraries. For smaller, newer projects, starting with Rust may help you avoid the complexities of dealing with C foreign function interfaces (FFIs). This decision may hinge on your safety priorities: if your project’s core tenant is safety, then a Rust-centric approach may be preferable. Conversely, if safety is needed only in certain areas of C++ project, keeping the core in C++ could be more practical.
Another consideration is how your project handles multi-threading. Mixing threading and memory ownership between Rust and C++ is very complex and prone to mistakes. Depending on how your application uses threads, this may tilt the decision in the direction of either C++ or Rust as the main “host” application.
While Rust offers many advantages, particularly in safety, C++ has its own merits that shouldn’t be hastily dismissed. The decision to rewrite should be strategic, based on actual needs rather than a pursuit of language purity since the risk of introducing new bugs through rewriting well-tested and stable C++ code outweigh the benefits of a Rust rewrite. Time-tested C++ code, particularly in areas like signal processing or cryptography, might be best left as is. Such code is often highly optimized, stable, and less prone to memory-related issues. As the saying goes, if it’s not broken, don’t “fix” it.
Despite its growing ecosystem, Rust is still relatively young. Relying on packages maintained by small teams or single individuals carries inherent risks. Moreover, as Rust is still in a period of rapid language evolution, this could result in frequent updates, posing challenges for large-scale or long-lived projects. In certain scenarios, such as very large codebases, specific embedded support requirements, or projects with long development cycles, C++ may remain the more practical choice. It is wise to use C++ where stability and longevity are important, and Rust where safety is critical but some development fluidity is acceptable.
By combining the reliability of C++ with the safety of Rust, developers can engineer systems that endure while minimizing the risk of common programming pitfalls. If you’re interested in reading more about this topic, you’ll want to read our best practice guide on Rust/C++ integration, which was created in collaboration with Ferrous Systems co-founder Florian Gilcher.
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 The Smarter Way to Rust appeared first on KDAB.
Adhering to the single-responsibility principle makes it easier to identify and modify selected code without impacting the entire system.