Zooming in a QGraphicsScene makes my drawings disappear sometimes - c++

I have a QGraphicsScene where I add a QPixmap composed of 4 images and the borders of each image.
I create a new QPixmap with the total size and then use a QPainter to draw each sub-image in the appropiate place in the bigger pixmap. After one sub-image is done, inmediately draw its borders (this may not be optimal but for now I don't mind).
Once the "final" pixmap is finished, I insert directly to the scene with
scene->addPixmap( total )
Here's the code for the pixmap composition:
QPixmap pixFromCube( PanoramicImages* lim ) const
{
const QSize img_size = getImageSize( lim );
const QSize pano_size( img_size.width() * 4, img_size.height() );
QPixmap toret( pano_size );
if( !toret.isNull() ) {
QPainter painter( &toret );
painter.setRenderHint( QPainter::Antialiasing );
int x( 0 );
QPixmap pix = lim->getCamera1Image();
if( !pix.isNull() ) {
painter.drawPixmap( 0, 0, pix.width(), pix.height(), pix );
drawPixBorder( painter, pix.rect() );
}
x += img_size.width();
pix = lim->getCamera2Image();
if( !pix.isNull() ) {
painter.drawPixmap( x, 0, pix.width(), pix.height(), pix );
drawPixBorder( painter, QRectF( x, 0, pix.width(),
pix.height() ) )
;
}
x += img_size.width();
pix = lim->getCamera3Image();
if( !pix.isNull() ) {
painter.drawPixmap( x, 0, pix.width(), pix.height(), pix );
drawPixBorder( painter, QRectF( x, 0, pix.width(),
pix.height() ) )
;
}
x += img_size.width();
pix = lim->getCamera4Image();
if( !pix.isNull() ) {
painter.drawPixmap( x, 0, pix.width(), pix.height(), pix );
drawPixBorder( painter, QRectF( x, 0, pix.width(),
pix.height() ) )
;
}
}
return toret;
}
And
void drawPixBorder( QPainter& painter, const QRectF rect ) const
{
const QBrush oldBrush = painter.brush();
const QPen oldPen = painter.pen();
QColor color( Qt::blue );
if( timer.isActive() ) {
color = Qt::green;
} else {
color = Qt::red;
}
const QBrush brush( color );
QPen pen( brush, 22 );
const QPointF points[ 5 ] = {
rect.topLeft(),
rect.topRight(),
rect.bottomRight(),
rect.bottomLeft(),
rect.topLeft()
};
painter.setBrush( brush );
painter.setPen( pen );
painter.drawPolyline( points, sizeof( points ) / sizeof( points[ 0 ] ) );
painter.setBrush( oldBrush );
painter.setPen( oldPen );
}
Here's the final pixmap when it's loaded for the first time:
And here after a few zoom-outs:
As you can see, at the right some of the borders are missing. When zooming back again to the inital position, the borders are displayed. If I use a smaller width for the lines (say, 5), the borders disappear sooner.
I've been reading other questions here and in the Qt Forums and tried some suggestions like:
pen.setCosmetic( true );
or
painter.setRenderHint( QPainter::NonCosmeticDefaultPen, false);
or:
painter.setRenderHint( QPainter::Antialiasing );
setting the pen width directly to 0
pen.setWidth( 0 )
and combinations.
Neither of them prevented the borders to disappear and using a bigger width just delays the problem.
Is there a way to show always the borders regardless of the zoom level?
Thanks to #Robert for his help. As he has stated in his answer, the solution was to draw directly in the scene, instead of doing it in the pixmap and then adding it.
For drawing in the scene, I decided to use a QPainterPath:
int x( 0 );
QPainterPath rectPath;
for( unsigned int i( 0 ); i < 4; ++i ) {
rectPath.addRect( QRectF( x, 0, width, height ) );
x += width;
}
QColor color( Qt::blue );
if( timer.isActive() ) {
color = Qt::green;
} else {
color = Qt::red;
}
scene->addPath( rectPath, QPen( color ) );

It is because the painter you are using to create the pixmap does not know anything about the transformations/scale of the graphic scene... A possible solutions would be to draw the rectangles within the scene and not directly to the pixmap.

Related

QwtPlotSpectrogram with log scales

I want to put logarithmic a scale next to spectrogram. I want the displayed image to be the same as for the linear data. The code for the version with linear scales looks like this:
#include <QApplication>
#include <QMainWindow>
#include <qwt_plot.h>
#include <qwt_plot_spectrogram.h>
#include <qwt_matrix_raster_data.h>
#include <qwt_color_map.h>
#include <qwt_scale_engine.h>
int main( int argc, char* argv[] ) {
QApplication app( argc, argv );
QMainWindow wnd;
QVector<double> heat_values( 100 * 100 );
for( int n = 0; n < 100 * 100; ++n ) {
heat_values[n] = ( n % 100 ) + n / 100;
};
QwtPlotSpectrogram heat;
auto heat_data = std::make_unique<QwtMatrixRasterData>();
heat_data->setValueMatrix( heat_values, 100 );
heat_data->setResampleMode(
QwtMatrixRasterData::ResampleMode::NearestNeighbour );
heat_data->setInterval( Qt::XAxis, QwtInterval( 0, 100.0 ) );
heat_data->setInterval( Qt::YAxis, QwtInterval( 0, 100.0 ) );
heat_data->setInterval( Qt::ZAxis, QwtInterval( 0, 200.0 ) );
heat.setDisplayMode( QwtPlotSpectrogram::DisplayMode::ImageMode, true );
heat.setColorMap( new QwtLinearColorMap( Qt::white, Qt::black ) );
heat.setData( heat_data.release() );
QwtPlot p;
p.setAutoDelete( false );
heat.attach( &p );
p.repaint();
wnd.setCentralWidget( &p );
wnd.resize( 400, 300 );
wnd.show();
return QApplication::exec();
}
and produces the expected result.
However, I want the same image but with different scales, for example logarithmic scales from 1 to 101. But after I change the scales like this:
p.setAxisScaleEngine( QwtPlot::yLeft, new QwtLogScaleEngine() );
p.setAxisScale( QwtPlot::yLeft, 1.0, 101.0 );
p.setAxisScaleEngine( QwtPlot::xBottom, new QwtLogScaleEngine() );
p.setAxisScale( QwtPlot::xBottom, 1.0, 101.0 );
then the spectrogram is all messed up.
Does anyone know how to just change the displayed scale?
msvc 2017, x64, qwt 6.1.4, qt 5.12.2
Edit:
I can get half way there by defining my own RasterData and mapping the coordinates back into bins, but it's still missing the inverse transformation, so the displayed data is a 'log' version of the original.
class RasterData : public QwtRasterData
{
public:
double value( double const x, double const y ) const override {
int const ix = std::min<int>( std::max<int>( 0, x ), m_cols-1 );
int const iy = std::min<int>( std::max<int>( 0, y ), m_cols-1 );
return m_values[iy * m_cols + ix];
}
void setValueMatrix( QVector<double> const& values, int const cols ) {
m_values = values;
m_cols = cols;
}
private:
QVector<double> m_values;
int m_cols;
};
then result then looks like this:
But essentially I want to avoid all of these tranformations. I want it to just transform the image data passed in via setValueMatrix into an image using the set color map and stretch that image to fit the plot.
The best way I found to make this work is by deriving from QwtPlotSpectrogram and changing the transformation to linear for the call to draw.
class PlotSpectrogram : public QwtPlotSpectrogram {
public:
void draw(
QPainter* painter,
QwtScaleMap const& xMap,
QwtScaleMap const & yMap,
QRectF const& canvasRect ) const override {
QwtScaleMap xMapLin( xMap );
QwtScaleMap yMapLin( yMap );
auto const xi = data()->interval( Qt::XAxis );
auto const yi = data()->interval( Qt::YAxis );
auto const dx = xMapLin.transform( xMap.s1() );
xMapLin.setScaleInterval( xi.minValue(), xi.maxValue() );
auto const dy = yMapLin.transform( yMap.s2() );
yMapLin.setScaleInterval( yi.minValue(), yi.maxValue() );
xMapLin.setTransformation( new QwtNullTransform() );
yMapLin.setTransformation( new QwtNullTransform() );
QwtPlotSpectrogram::draw(
painter, xMapLin, yMapLin, canvasRect.translated( dx, -dy ) );
}
};
With main altered for a scale log scale from 20..50 and using PlotSpectrogram
PlotSpectrogram heat;
auto heat_data = std::make_unique<QwtMatrixRasterData>();
heat_data->setValueMatrix( heat_values, 100 );
heat_data->setInterval( Qt::XAxis, QwtInterval( 0, 100.0 ) );
heat_data->setInterval( Qt::YAxis, QwtInterval( 0, 100.0 ) );
heat_data->setInterval( Qt::ZAxis, QwtInterval( 0, 200.0 ) );
heat.setDisplayMode( QwtPlotSpectrogram::DisplayMode::ImageMode, true );
heat.setColorMap( new QwtLinearColorMap( Qt::white, Qt::black ) );
heat.setData( heat_data.release() );
QwtPlot p;
p.setAxisScaleEngine( QwtPlot::yLeft, new QwtLogScaleEngine() );
p.setAxisScale( QwtPlot::yLeft, 20.0, 50.0 );
p.setAxisScaleEngine( QwtPlot::xBottom, new QwtLogScaleEngine() );
p.setAxisScale( QwtPlot::xBottom, 20.0, 50.0 );
p.setAutoDelete( false );
heat.attach( &p );
I then get the desired output
QwtPlotMatrixRasterData is not working with non linear scales !
When using QwtRasterData instead everything will work out of the box with any type of scales.

qt - QPainter::rotate with non-perpendicular angles causes wiggly lines

I'm trying to draw text on a widget at an angle which is non-perpendicular, i.e. between 0 and 90. Drawing the text itself is no issue, but the resulting text is very wiggly/unevenly drawn.
In the picture below, I'm drawing to lines of text at a 45 degree angle. The first line of text is many underscores ("_____"), and second line of text is "Multithreading". Underscores are drawn here instead of a line just to highlight the issue.
As you can see, the first line obviously shows the text is not evenly drawn. This is more subtle in the second line, but still visible.
Drawing at perpendicular angles (0, 90, 180, etc.) does not cause this effect. Any reason why this is happening?
I'm working on Windows 10 with Qt 5.7.0.
Minimal code example:
void MyWidget::paintEvent( QPaintEvent * /* event */ )
{
QFont font;
font.setPointSize( 16 );
font.setStyleStrategy( QFont::StyleStrategy::PreferAntialias );
setFont( font );
QImage image( size(), QImage::Format_ARGB32_Premultiplied );
QPainter imagePainter( &image );
imagePainter.initFrom( this );
imagePainter.setFont( font() );
imagePainter.setRenderHint( QPainter::Antialiasing, true );
imagePainter.eraseRect( rect() );
// Set logical origo in the middle of the image
m_window = QRect(
- width() / 2, // x
- height() / 2, // y
width(), // w
height() // h
);
imagePainter.setWindow( m_window );
m_viewport = QRect(
0, // x
0, // y
width(), // w
height() // h
);
imagePainter.setViewport( m_viewport );
draw( imagePainter );
imagePainter.end();
QPainter widgetPainter( this );
widgetPainter.drawImage( 0, 0, image );
}
void MyWidget::draw( QPainter & painter )
{
painter.save();
// Rotate anti-clockwise
painter.rotate( -m_degrees );
painter.drawText( m_window.top(), 0, tr( "Multithreads" ) );
painter.drawText( m_window.top(), 15, tr( "__________" ) );
painter.restore();
}
I found a workaround from this Qt bug ticket. In short, the fix is to draw the text as a QPainterPath rather than as text.
Example of the fix is
// Do this
QPainterPath glyphPath;
glyphPath.addText( x, y, painter.font(), text );
painter.fillPath( glyphPath, painter.pen().color() );
// instead of this
painter.drawText( x, y, text );
EDIT:
The difference can be seen below.
Before:
After:

Qwt - Can we plot an Area chart?

Does anyone know if we can plot an area chart using the Qwt library on Qt creator ? If yes, could someone help me with a sample program ?
Based on the screenshots and the listing of examples, I would look at the source for cpuplot.
./qwt-6.1.2/examples>ls
animation curvdemo1 examples.pro radio simpleplot sysinfo
barchart dials friedberg rasterview sinusplot tvplot
bode distrowatch itemeditor realtime spectrogram
controls event_filter legends refreshtest stockchart
cpuplot examples.pri oscilloscope scatterplot stylesheets
./qwt-6.1.2/examples/cpuplot>ls
cpupiemarker.cpp cpuplot.cpp cpuplot.pro cpustat.h
cpupiemarker.h cpuplot.h cpustat.cpp
And
cpuplot.cpp
#include <qapplication.h>
#include <qlayout.h>
#include <qlabel.h>
#include <qpainter.h>
#include <qwt_plot_layout.h>
#include <qwt_plot_curve.h>
#include <qwt_scale_draw.h>
#include <qwt_scale_widget.h>
#include <qwt_legend.h>
#include <qwt_legend_label.h>
#include <qwt_plot_canvas.h>
#include "cpupiemarker.h"
#include "cpuplot.h"
class TimeScaleDraw: public QwtScaleDraw
{
public:
TimeScaleDraw( const QTime &base ):
baseTime( base )
{
}
virtual QwtText label( double v ) const
{
QTime upTime = baseTime.addSecs( static_cast<int>( v ) );
return upTime.toString();
}
private:
QTime baseTime;
};
class Background: public QwtPlotItem
{
public:
Background()
{
setZ( 0.0 );
}
virtual int rtti() const
{
return QwtPlotItem::Rtti_PlotUserItem;
}
virtual void draw( QPainter *painter,
const QwtScaleMap &, const QwtScaleMap &yMap,
const QRectF &canvasRect ) const
{
QColor c( Qt::white );
QRectF r = canvasRect;
for ( int i = 100; i > 0; i -= 10 )
{
r.setBottom( yMap.transform( i - 10 ) );
r.setTop( yMap.transform( i ) );
painter->fillRect( r, c );
c = c.dark( 110 );
}
}
};
class CpuCurve: public QwtPlotCurve
{
public:
CpuCurve( const QString &title ):
QwtPlotCurve( title )
{
setRenderHint( QwtPlotItem::RenderAntialiased );
}
void setColor( const QColor &color )
{
QColor c = color;
c.setAlpha( 150 );
setPen( QPen( Qt::NoPen ) );
setBrush( c );
}
};
CpuPlot::CpuPlot( QWidget *parent ):
QwtPlot( parent ),
dataCount( 0 )
{
setAutoReplot( false );
QwtPlotCanvas *canvas = new QwtPlotCanvas();
canvas->setBorderRadius( 10 );
setCanvas( canvas );
plotLayout()->setAlignCanvasToScales( true );
QwtLegend *legend = new QwtLegend;
legend->setDefaultItemMode( QwtLegendData::Checkable );
insertLegend( legend, QwtPlot::RightLegend );
setAxisTitle( QwtPlot::xBottom, " System Uptime [h:m:s]" );
setAxisScaleDraw( QwtPlot::xBottom,
new TimeScaleDraw( cpuStat.upTime() ) );
setAxisScale( QwtPlot::xBottom, 0, HISTORY );
setAxisLabelRotation( QwtPlot::xBottom, -50.0 );
setAxisLabelAlignment( QwtPlot::xBottom, Qt::AlignLeft | Qt::AlignBottom );
/*
In situations, when there is a label at the most right position of the
scale, additional space is needed to display the overlapping part
of the label would be taken by reducing the width of scale and canvas.
To avoid this "jumping canvas" effect, we add a permanent margin.
We don't need to do the same for the left border, because there
is enough space for the overlapping label below the left scale.
*/
QwtScaleWidget *scaleWidget = axisWidget( QwtPlot::xBottom );
const int fmh = QFontMetrics( scaleWidget->font() ).height();
scaleWidget->setMinBorderDist( 0, fmh / 2 );
setAxisTitle( QwtPlot::yLeft, "Cpu Usage [%]" );
setAxisScale( QwtPlot::yLeft, 0, 100 );
Background *bg = new Background();
bg->attach( this );
CpuPieMarker *pie = new CpuPieMarker();
pie->attach( this );
CpuCurve *curve;
curve = new CpuCurve( "System" );
curve->setColor( Qt::red );
curve->attach( this );
data[System].curve = curve;
curve = new CpuCurve( "User" );
curve->setColor( Qt::blue );
curve->setZ( curve->z() - 1 );
curve->attach( this );
data[User].curve = curve;
curve = new CpuCurve( "Total" );
curve->setColor( Qt::black );
curve->setZ( curve->z() - 2 );
curve->attach( this );
data[Total].curve = curve;
curve = new CpuCurve( "Idle" );
curve->setColor( Qt::darkCyan );
curve->setZ( curve->z() - 3 );
curve->attach( this );
data[Idle].curve = curve;
showCurve( data[System].curve, true );
showCurve( data[User].curve, true );
showCurve( data[Total].curve, false );
showCurve( data[Idle].curve, false );
for ( int i = 0; i < HISTORY; i++ )
timeData[HISTORY - 1 - i] = i;
( void )startTimer( 1000 ); // 1 second
connect( legend, SIGNAL( checked( const QVariant &, bool, int ) ),
SLOT( legendChecked( const QVariant &, bool ) ) );
}
void CpuPlot::timerEvent( QTimerEvent * )
{
for ( int i = dataCount; i > 0; i-- )
{
for ( int c = 0; c < NCpuData; c++ )
{
if ( i < HISTORY )
data[c].data[i] = data[c].data[i-1];
}
}
cpuStat.statistic( data[User].data[0], data[System].data[0] );
data[Total].data[0] = data[User].data[0] + data[System].data[0];
data[Idle].data[0] = 100.0 - data[Total].data[0];
if ( dataCount < HISTORY )
dataCount++;
for ( int j = 0; j < HISTORY; j++ )
timeData[j]++;
setAxisScale( QwtPlot::xBottom,
timeData[HISTORY - 1], timeData[0] );
for ( int c = 0; c < NCpuData; c++ )
{
data[c].curve->setRawSamples(
timeData, data[c].data, dataCount );
}
replot();
}
void CpuPlot::legendChecked( const QVariant &itemInfo, bool on )
{
QwtPlotItem *plotItem = infoToItem( itemInfo );
if ( plotItem )
showCurve( plotItem, on );
}
void CpuPlot::showCurve( QwtPlotItem *item, bool on )
{
item->setVisible( on );
QwtLegend *lgd = qobject_cast<QwtLegend *>( legend() );
QList<QWidget *> legendWidgets =
lgd->legendWidgets( itemToInfo( item ) );
if ( legendWidgets.size() == 1 )
{
QwtLegendLabel *legendLabel =
qobject_cast<QwtLegendLabel *>( legendWidgets[0] );
if ( legendLabel )
legendLabel->setChecked( on );
}
replot();
}
int main( int argc, char **argv )
{
QApplication a( argc, argv );
QWidget vBox;
vBox.setWindowTitle( "Cpu Plot" );
CpuPlot *plot = new CpuPlot( &vBox );
plot->setTitle( "History" );
const int margin = 5;
plot->setContentsMargins( margin, margin, margin, margin );
QString info( "Press the legend to en/disable a curve" );
QLabel *label = new QLabel( info, &vBox );
QVBoxLayout *layout = new QVBoxLayout( &vBox );
layout->addWidget( plot );
layout->addWidget( label );
vBox.resize( 600, 400 );
vBox.show();
return a.exec();
}
Hope that helps.

QtWidget Disable Margin for overlay widget

My situation: I have a grid layout with n*n widgets inside. Additionally I place an overlay widget in the grid with position 0,0 and span n,n.
Evertyhing fine with this, but there is a weird margin and I don't know what causes it...
Has anyone an idea how i can prevent this? I think I'm missing something trivial...
SudokuFieldWidget::SudokuFieldWidget(QWidget *parent) : QFrame(parent)
{
...
m_layout = new QGridLayout( this );
m_layout->setSpacing( 0 );
m_layout->setMargin( 1 );
this->initCells( true );
this->setLayout( m_layout );
m_markerOverlay = new SudokuMarkerOverlayWidget( this );
m_layout->addWidget( m_markerOverlay, 0, 0, m_fieldSize, m_fieldSize );
}
SudokuMarkerOverlayWidget::SudokuMarkerOverlayWidget(QWidget* parent) : QWidget(parent)
{
setAttribute(Qt::WA_NoSystemBackground);
setAttribute(Qt::WA_TransparentForMouseEvents);
...
}
void SudokuMarkerOverlayWidget::paintEvent(QPaintEvent*)
{
QPainter painter(this);
painter.fillRect( rect(), QColor( 255, 0, 0, 128 ) );
}
Had an error in my paint-Routine for SudokuFieldWidget which caused this misbehaviour... m_markerOverlay->setGeometry( this->geometry() );

save the current items on qwidget as image

I'm trying draw some rhombuses with random colors in a QWidget. And i want to save the current QWidget as image. I use such code to do this:
QPixmap pixmap(this->size());
this->render(&pixmap);
pixmap.save("test.png");
The problem is that the render() seems to call paintEvent again, and the paintEvent will draw the rhombuses with new random colors, so that i always get a different Image saved compared to the image displayed. Can someone tell me how to save the current QWidget? Thanks in advance.
Code for drawing rhombuses:
void Dialog::paintEvent(QPaintEvent *e) {
QPainter painter(this);
QRect background(0,0,this->geometry().width(),this->geometry().height());
painter.setBrush( QBrush( Qt::white ) );
painter.setPen( Qt::NoPen );
//QBrush bbrush(Qt::black,Qt::SolidPattern);
painter.drawRect(background);
int width = this->geometry().width();
int height = this->geometry().height();
//draw rectangles
int rec_size=64;
int rows=0;
int cols=0;
rows=floor((double)height/(double)rec_size);
cols=floor((double)width/(double)rec_size);
QPointF points[4]; // QRect rec(0,0,rec_size,rec_size);
for (int i=0;i<floor(rows);i++){
for (int j=0;j<floor(cols);j++){
painter.setBrush( QBrush( colors[rand() % color_size] ) );
//QPainter painter(this);
points[0] = QPointF(rec_size*(j),rec_size*(i+0.5));
points[1] = QPointF(rec_size*(j+0.5),rec_size*(i));
points[2] = QPointF(rec_size*(j+1),rec_size*(i+0.5));
points[3] = QPointF(rec_size*(j+0.5),rec_size*(i+1));
painter.drawPolygon(points, 4);
}
}
painter.end();
}
You can have a class member variable of boolean type to check in the paintEvent whether a random color should be used. Also a variable to save the index of the last color used is necessary:
bool isRandom;
int lastColor;
The paintEvent should be like :
void Dialog::paintEvent(QPaintEvent *e) {
...
if(isRandom)
{
lastColor = rand() % color_size;
painter.setBrush( QBrush( colors[lastColor] ) );
}
else
painter.setBrush( QBrush( colors[lastColor] ) );
...
}
The variable has true when drawing the widget regularly. When you want to save it's image, assign the variable to false, save the image and assign it to true again :
isRandom = false;
QPixmap pixmap(this->size());
this->render(&pixmap);
pixmap.save("test.png");
isRandom = true;