Display an overlay above a Gstreamer video in a Qt / QWidgets app - qt

I'm using Qt and Gstreamer to create a video viewer. I'm using this simple code to show a video:
GstElement* MainWindow::addVideo(QWidget* parent, const int& x, const int& y, const int& width, const int& height) {
GstElement* pipeline = gst_pipeline_new ("xvoverlay");
GstElement* src = VideoView::makeElement("v4l2src", { {"device", "/dev/video0"} });
GstElement* queue = gst_element_factory_make ("videoconvert", nullptr);
GstElement* sink = gst_element_factory_make ("xvimagesink", nullptr);
if (sink == nullptr)
g_error ("Couldn't find a working video sink.");
gst_bin_add_many (GST_BIN (pipeline), src, queue, sink, nullptr);
gst_element_link_many (src, queue, sink, nullptr);
QWidget* videoView = new VidView();
videoView->setParent(parent);
videoView->move(x, y);
videoView->resize(width, height);
videoView->setStyleSheet("background-color: red");
videoView->show();
gst_video_overlay_set_window_handle (GST_VIDEO_OVERLAY (sink), videoView->winId());
gst_element_set_state (pipeline, GST_STATE_PLAYING);
GstStateChangeReturn sret = gst_element_set_state (pipeline, GST_STATE_PLAYING);
if (sret == GST_STATE_CHANGE_FAILURE) {
gst_element_set_state (pipeline, GST_STATE_NULL);
gst_object_unref (pipeline);
// Exit application
QTimer::singleShot(0, QApplication::activeWindow(), SLOT(quit()));
}
return pipeline;
}
It work well. But now I need to display some semi-transparent widgets above the video (for instance to have a title bar showing the name of the camera, or a popup above all the window). Here's what I get when showing a semi-transparent yellow QWidget partially overlaying the video:
As you can see, under the yellow widget the video view background is shown, not the video.
Here's the code used to create that second widget:
QWidget* test = new QWidget(m_mainContainer);
test->resize(100, 100);
test->move(150, 150);
test->setStyleSheet("background-color: #55ffff00;");
How can I fix this?
Note: I can't use the glimagesink, because it has performances problems when showing multiple videos (the app will be showing up to 8 4K videos).

Related

QToolButton with icon + text: How to center both?

I am using multiple QToolButtons in a custom QGridLayout widget. The buttons are set to display icon + text based on an assigned default QAction. The only issue is that the content (icon + text) is always left-aligned.
The content (icon + text, marked as a red box in the screenshot), should be center in the button (indicated by the blue box).
For most cases this is just fine, given that Qt automatically tries to render that button with the minimal size. However I am stretching the button to fit nicely into my QGridLayout.
QToolButton* pButton = new QToolButton(0);
pButton->addDefaultAction(pAction);
pButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
pButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
QGridLayout *pActionAreaLayout = new QGridLayout;
pActionAreaLayout->addWidget(pSomeOtherWidget, 0, 0, 1, 2);
pActionAreaLayout->addWidget(pButton , 1, 0, 1, 1);
Is there a way to force the content to be centered in the button?
PS: I found the following comment in another forum, which however seems quite invasive and is not really clear to me yet:
You can try doing the horizontal alignment using a stylesheet, but you probably have to implement a QStyle proxy and reimplement drawControl() for QStyle::CE_ToolButtonLabel
Or derive from QToolButton, overwrite paintEvent() and call the style for everything other than the label.
As I suggest in answer to you another question. https://stackoverflow.com/a/28630318/1917249 Do not use QToolButton, just use QPushButton, and add popup menu if needed.
Then you wont have different sizes of QToolButton and QPushButton widgets. And you will have centered icon and text.
Popupmenu can be easily added to QPushButton ( only small arrow wont be shown )
QPushButton *pushButton = new QPushButton(toolAction->icon(), "PushButton", window);
// window - widget where button is placed ( to get correct QMenu position )
QObject::connect(pushButton, &QPushButton::released, [window, pushButton, action](){
QMenu menu;
menu.addAction(action);
QPoint pos = window->mapToGlobal(pushButton3->pos());
pos += QPoint(0, pushButton->height());
menu.exec(pos);
});
Or you can subclass QPushButton and add Popup menu handling there. Much better then try to center text with icon in QToolButton or have in same size of QPushButton and QToolButton
For complex example please see my answer: https://stackoverflow.com/a/28630318/1917249
The following class does the job for me:
class CenteredToolButtonStyle : public QProxyStyle
{
Q_OBJECT
public:
CenteredToolButtonStyle(QToolButton* b, const QSize& sIcon);
virtual void drawItemPixmap(QPainter *painter, const QRect &rect, int, const QPixmap &pixmap) const
override { m_pic = pixmap; m_ny = rect.y(); Draw(painter); }
virtual void drawItemText(QPainter *painter, const QRect &rect, int flags, const QPalette &pal, bool enabled,
const QString &text, QPalette::ColorRole textRole = QPalette::NoRole) const override;
void Draw(QPainter *painter) const;
const QToolButton* B;
const QSize SICON;
mutable QString m_s;
mutable QPixmap m_pic;
mutable QRect m_r;
mutable int m_nf, m_ny;
mutable bool m_bEnabled;
mutable QPalette m_pal;
mutable QPalette::ColorRole m_textRole;
};
CenteredToolButtonStyle::CenteredToolButtonStyle(QToolButton* b, const QSize& sIcon)
: QProxyStyle(), B(b), SICON(sIcon), m_nf(0), m_bEnabled(true), m_ny(0)
{
b->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
setParent(b);
}
void CenteredToolButtonStyle::drawItemText(QPainter *painter, const QRect &rect, int flags, const QPalette &pal,
bool enabled, const QString &text, QPalette::ColorRole textRole/* = QPalette::NoRole*/) const
{
m_s = text;
m_r = rect;
m_nf = flags | Qt::AlignCenter;
m_bEnabled = enabled;
m_pal = pal;
m_textRole = textRole;
Draw(painter);
}
void CenteredToolButtonStyle::Draw(QPainter *painter) const
{
if (m_ny) {
if (m_r.y() != m_ny) return;
auto r = m_r;
r.adjust(-SICON.width() - 8, m_ny = 0, -itemTextRect(B->fontMetrics(), m_r, m_nf, m_bEnabled, m_s).width(), 0);
QProxyStyle::drawItemPixmap(painter, r, Qt::AlignCenter, m_pic);
}
QProxyStyle::drawItemText(painter, m_r, m_nf, m_pal, m_bEnabled, m_s, m_textRole);
}
Sample use:
foreach(auto b, ui.mainToolBar->findChildren<QToolButton*>())
b->setStyle(new CenteredToolButtonStyle(b, ui.mainToolBar->iconSize()));

How to monitor changes to an arbitrary widget?

I am starting a QT5 application with a rather complex design based on Qt Widgets. It runs on Beagleboard with a touchscreen. I will have a rather weird local invention instead of the LCD display. It's a laser painting on acrylic plate. It has no driver yet. To actually update a screen I must create a screenshot of the window as bitmap, turn it to grayscale and feed to a proprietary library, which will handle the laser. It should look cute, when ready. Unfortunately, the laser blinks on update, so I cannot just make screenshots on timer, or it will be jerky like hell.
I need to run a function every time a meaningful update of GUI happens, while preferably ignore things like button being pressed and released. Is there some way to create a hook without subclassing every single Qt Widget I will use? The only way to do this I know is to override paintEvent of everything. I want a simpler solution.
Possible assumptions are: the application will be running under X server with dummy display, will be the only GUI app running. Some updates happen without user input.
The code below does it. It doesn't dig too deeply into the internals of Qt, it merely leverages the fact that backing store devices are usually QImages. It could be modified to accommodate OpenGL-based backing stores as well.
The WidgetMonitor class is used to monitor the widgets for content changes. An entire top-level window is monitored no matter which particular widget is passed to the monitor(QWidget*) method. You only need to call the monitor method for one widget in the window you intend to monitor - any widget will do. The changes are sent out as a QImage of window contents.
The implementation installs itself as an event filter in the target window widget and all of its children, and monitors the repaint events. It attempts to coalesce the repaint notifications by using the zero-length timer. The additions and removals of children are tracked automagically.
When you run the example, it creates two windows: a source window, and a destination window. They may be overlapped so you need to separate them. As you resize the source window, the size of the destination's rendition of it will also change appropriately. Any changes to the source children (time label, button state) propagate automatically to the destination.
In your application, the destination could be an object that takes the QImage contents, converts them to grayscale, resizes appropriately, and passes them to your device.
I do not quite understand how your laser device works if it can't gracefully handle updates. I presume that it is a raster-scanning laser that runs continuously in a loop that looks roughly like this:
while (1) {
for (line = 0; line < nLines; ++line) {
drawLine();
}
}
You need to modify this loop so that it works as follows:
newImage = true;
QImage localImage;
while (1) {
if (newImage) localImage = newImage;
for (line = 0; line < localImage.height(); ++line) {
drawLine(line, localImage);
}
}
You'd be flipping the newImage flag from the notification slot connected to the WidgetMonitor. You may well find out that leveraging QImage, and Qt's functionality in general, in your device driver code, will make it much easier to develop. Qt provides portable timers, threads, collections, etc. I presume that your "driver" is completely userspace, and communicates via a serial port or ethernet to the micro controller that actually controls the laser device.
If you will be writing a kernel driver for the laser device, then the interface would be probably very similar, except that you end up writing the image bitmap to an open device handle.
// https://github.com/KubaO/stackoverflown/tree/master/questions/surface-20737882
#include <QtWidgets>
#include <array>
const char kFiltered[] = "WidgetMonitor_filtered";
class WidgetMonitor : public QObject {
Q_OBJECT
QVector<QPointer<QWidget>> m_awake;
QBasicTimer m_timer;
int m_counter = 0;
void queue(QWidget *window) {
Q_ASSERT(window && window->isWindow());
if (!m_awake.contains(window)) m_awake << window;
if (!m_timer.isActive()) m_timer.start(0, this);
}
void filter(QObject *obj) {
if (obj->isWidgetType() && !obj->property(kFiltered).toBool()) {
obj->installEventFilter(this);
obj->setProperty(kFiltered, true);
}
}
void unfilter(QObject *obj) {
if (obj->isWidgetType() && obj->property(kFiltered).toBool()) {
obj->removeEventFilter(this);
obj->setProperty(kFiltered, false);
}
}
bool eventFilter(QObject *obj, QEvent *ev) override {
switch (ev->type()) {
case QEvent::Paint: {
if (!obj->isWidgetType()) break;
if (auto *window = static_cast<QWidget *>(obj)->window()) queue(window);
break;
}
case QEvent::ChildAdded: {
auto *cev = static_cast<QChildEvent *>(ev);
if (auto *child = qobject_cast<QWidget *>(cev->child())) monitor(child);
break;
}
default:
break;
}
return false;
}
void timerEvent(QTimerEvent *ev) override {
if (ev->timerId() != m_timer.timerId()) return;
qDebug() << "painting: " << m_counter++ << m_awake;
for (auto w : m_awake)
if (auto *img = dynamic_cast<QImage *>(w->backingStore()->paintDevice()))
emit newContents(*img, w);
m_awake.clear();
m_timer.stop();
}
public:
explicit WidgetMonitor(QObject *parent = nullptr) : QObject{parent} {}
explicit WidgetMonitor(QWidget *w, QObject *parent = nullptr) : QObject{parent} {
monitor(w);
}
Q_SLOT void monitor(QWidget *w) {
w = w->window();
if (!w) return;
filter(w);
for (auto *obj : w->findChildren<QWidget *>()) filter(obj);
queue(w);
}
Q_SLOT void unMonitor(QWidget *w) {
w = w->window();
if (!w) return;
unfilter(w);
for (auto *obj : w->findChildren<QWidget *>()) unfilter(obj);
m_awake.removeAll(w);
}
Q_SIGNAL void newContents(const QImage &, QWidget *w);
};
class TestWidget : public QWidget {
QVBoxLayout m_layout{this};
QLabel m_time;
QBasicTimer m_timer;
void timerEvent(QTimerEvent *ev) override {
if (ev->timerId() != m_timer.timerId()) return;
m_time.setText(QTime::currentTime().toString());
}
public:
explicit TestWidget(QWidget *parent = nullptr) : QWidget{parent} {
m_layout.addWidget(&m_time);
m_layout.addWidget(new QLabel{"Static Label"});
m_layout.addWidget(new QPushButton{"A Button"});
m_timer.start(1000, this);
}
};
int main(int argc, char **argv) {
QApplication app{argc, argv};
TestWidget src;
QLabel dst;
dst.setFrameShape(QFrame::Box);
for (auto *w : std::array<QWidget *, 2>{&dst, &src}) {
w->show();
w->raise();
}
QMetaObject::invokeMethod(&dst, [&] { dst.move(src.frameGeometry().topRight()); },
Qt::QueuedConnection);
WidgetMonitor mon(&src);
src.setWindowTitle("Source");
dst.setWindowTitle("Destination");
QObject::connect(&mon, &WidgetMonitor::newContents, [&](const QImage &img) {
dst.resize(img.size());
dst.setPixmap(QPixmap::fromImage(img));
});
return app.exec();
}
#include "main.moc"

How to create a draggable (borderless and titleless) top level window in QT

I'd appreciate help creating a top-level window in Qt with the following characteristics. The window must be:
Borderless, titleless and lie on top of all other windows on the desktop (easy)
Draggable by clicking and dragging anywhere inside it (this what I need help with)
Constrained to the top border of the desktop while dragging (relatively easy)
Basically, I'm trying to collapse our QT application to a top-level icon on the top border of the desktop.
You'll find the answer to the first part in: Making a borderless window with for Qt, and the answer to the second part in Select & moving Qwidget in the screen.
Combining the two, and adding the last part is straightforward.
Here's how you could do it:
#include <QtGui>
class W: public QWidget
{
Q_OBJECT
Set up a borderless widget with a few buttons to lock/unlock and quit:
public:
W(QWidget *parent=0)
: QWidget(parent, Qt::FramelessWindowHint), locked(false)
{
QPushButton *lock = new QPushButton("Lock");
QPushButton *unlock = new QPushButton("Unlock");
QPushButton *quit = new QPushButton("&Quit");
connect(lock, SIGNAL(clicked()), this, SLOT(lock()));
connect(unlock, SIGNAL(clicked()), this, SLOT(unlock()));
connect(quit, SIGNAL(clicked()),
QApplication::instance(), SLOT(quit()));
QHBoxLayout *l = new QHBoxLayout;
l->addWidget(lock);
l->addWidget(unlock);
l->addWidget(quit);
setLayout(l);
}
public slots:
void lock() {
locked = true;
move(x(), 0); // move window to the top of the screen
}
void unlock() { locked = false; }
Do the mouse handling:
protected:
void mousePressEvent(QMouseEvent *evt)
{
oldPos = evt->globalPos();
}
void mouseMoveEvent(QMouseEvent *evt)
{
const QPoint delta = evt->globalPos() - oldPos;
if (locked)
// if locked, ignore delta on y axis, stay at the top
move(x()+delta.x(), y());
else
move(x()+delta.x(), y()+delta.y());
oldPos = evt->globalPos();
}
private:
bool locked;
QPoint oldPos;
};

Adding a QSizeGrip to the corner of a QLabel

I'm attempting to produce a widget that consists of a text display that can be resized by the user grabbing the lower right corner. So far I've been able to generate this:
I've applied a red background to the layout to make it more obvious what's going on. I've used the following code to generate this:
m_sizeGrip = new QSizeGrip( this );
m_layout = new QHBoxLayout( this );
m_label = new QLabel( this );
m_layout->setContentsMargins( QMargins() );
m_layout->setSpacing( 0 );
m_layout->addWidget( m_label );
m_layout->addWidget( m_sizeGrip, 0, Qt::AlignBottom | Qt::AlignRight );
setWindowFlags( Qt::SubWindow );
Basically, it's a horizontal layout with the label and grip added to it, which is then installed on a QWidget. My problem is that I'd like the grip to be on the lower right corner of the label, rather than the parent widget. I'd also like to make it invisible while keeping it enabled.
Or perhaps I'm going about this the wrong way. My ultimate goal is to have a textual display widget that can be resized by the user either horizontally or vertically, but doesn't have a visible grip that would obscure the text. Am I already on the right track with the code above, or is there a better way to achieve this?
You could create a custom QLabel for that. The idea would be to track mouse move events (which by default only fire when a mouse button is pressed), and resize based on how much the mouse traveled since the last event.
This allows you to control exactly how to display the "gripper" (if at all) and what shape it should have. You can constrain the resizing to vertical or horizontal (or not).
Here's a demo of how you could do that (resizes both ways). Warning: this might wreak havoc in your layout.
#include <QtGui>
class GripLabel: public QLabel
{
Q_OBJECT
public:
GripLabel(QString const& title, QWidget* parent = 0)
: QLabel(title, parent),
resizing(false),
gripSize(10, 10)
{
// Prevent the widget from disappearing altogether
// Bare minimum would be gripSize
setMinimumSize(100, 30);
}
QSize sizeHint() const
{
return minimumSize();
}
protected:
bool mouseInGrip(QPoint mousePos)
{
// "handle" is in the lower right hand corner
return ((mousePos.x() > (width() - gripSize.width()))
&& (mousePos.y() > (height() - gripSize.height())));
}
void mousePressEvent(QMouseEvent *e)
{
// Check if we hit the grip handle
if (mouseInGrip(e->pos())) {
oldPos = e->pos();
resizing = true;
} else {
resizing = false;
}
}
void mouseMoveEvent(QMouseEvent *e)
{
if (resizing) {
// adapt the widget size based on mouse movement
QPoint delta = e->pos() - oldPos;
oldPos = e->pos();
setMinimumSize(width()+delta.x(), height()+delta.y());
updateGeometry();
}
}
void paintEvent(QPaintEvent *e)
{
QLabel::paintEvent(e);
QPainter p(this);
p.setPen(Qt::red);
p.drawRect(width()-gripSize.width(), height()-gripSize.height(),
gripSize.width(), gripSize.height());
}
private:
bool resizing;
QSize gripSize;
QPoint oldPos;
};
Sample main:
#include "griplabel.h"
int main(int argc, char **argv)
{
QApplication app(argc, argv);
QWidget *w = new QWidget;
QPushButton *b = new QPushButton("button");
GripLabel *l = new GripLabel("Hello");
QHBoxLayout *y = new QHBoxLayout;
y->addWidget(b);
y->addWidget(l);
y->setSizeConstraint(QLayout::SetFixedSize);
w->setLayout(y);
w->show();
return app.exec();
}

Paint a QGraphicsItem to a QImage without needing a scene/view

So here's what I'm trying to do - Using a custom QGraphicsItem, I have my QPainter setup to paint into a QImage, which I then save to a file (or just keep the QImage in memory until I need it).
The issue I've found is that QGraphicsItem::paint() is only called if the QGraphcsItem belongs to a scene, the scene belongs to a view, AND the view and scene are not hidden.
Here's the code outside my project for testing purposes:
MyQGfx Class
void MyQGfx::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
qDebug() << "begin of paint function";
QRectF rec = boundingRect();
QImage image(boundingRect().size().toSize(),
QImage::Format_ARGB32_Premultiplied);
image.fill(0);
// construct a dedicated offline painter for this image
QPainter imagePainter(&image);
imagePainter.translate(-boundingRect().topLeft());
// paint the item using imagePainter
imagePainter.setPen(Qt::blue);
imagePainter.setBrush(Qt::green);
imagePainter.drawEllipse(-50, -50, 100, 100);
imagePainter.end();
if(image.save("C://plot.jpg"))
{
qDebug() << "written";
}
else {
qDebug() << "not written";
}
}
MainWindow Class
....
QGraphicsView* view = new QGraphicsView(this);
QGraphicsScene* scene = new QGraphicsScene(this);
view->setScene(scene);
MyQGfx* gfx = new MyQGfx();
scene->addItem(gfx);
gfx->update();
....
This all works fine, but I don't want a view/scene necessary, as it would be displayed on the mainwindow - is there any way around this?
Can't you just create a custom method accepting a QPainter, one painting on a QImage and one on your item?

Resources