I have a QwtPlot view containing many QwtPlotCurve and I want to highlight/mignify (currently simply trying to change the color) of the closest point to the mouse position (because I'll display some info about this point of measurement when user will press the mouse button, and I'd like him to know what point is the current "target").
So I use a QwtPlotPicker to get mouse position and then I setup an extra QwtPlotCurve curve with this single point ("target") to be drawn with a different color on top of the others.
It works, but the only way I could make this work is by calling QwtPlot::replot() which is heavy to be called every time the mouse is being moved (as I may have many thousands of point being plotted).
I'd like to only repaint the area where previously highlighted point was (to restore default display) and then only repaint the area where newly highlighted point is. But when I do, this (call repaint(QRect) rather than replot()), nothing happens (no point is highlighted), however, if I deactivate the window, I see the point gets highlighted, so it looks like repaint does some piece of job but not enough for the end user to see it...
Note that I disabled Qwt backing store features.
Here is my MCVE:
widget.h:
#include <QDialog>
class QLabel;
class QwtPlotCurve;
class QwtPlot;
class Dialog : public QDialog
{
Q_OBJECT
public:
Dialog();
public slots:
void onHovered( const QPointF& pt );
private:
std::vector<QwtPlotCurve*> curves;
QwtPlotCurve* highlight;
std::tuple<QwtPlotCurve*,int,QRect> highlighted;
QLabel* closestLabel;
QwtPlot* plot;
};
widget.cpp:
#include "widget.h"
#include <QVBoxLayout>
#include <QLabel>
#include <qwt_plot.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_picker.h>
#include <qwt_plot_canvas.h>
#include <qwt_picker_machine.h>
#include <sstream>
Dialog::Dialog()
{
setLayout( new QVBoxLayout() );
plot = new QwtPlot(this);
layout()->addWidget( plot );
layout()->addWidget( closestLabel = new QLabel( this ) );
for ( int i = 0; i != 5; ++i )
{
QwtPlotCurve* curve = new QwtPlotCurve();
QVector<double> x, y;
for ( int i = 0; i != 10; ++i )
{
x.push_back( std::rand() );
y.push_back( std::rand() );
}
curve->setSamples( x, y );
curve->setStyle( QwtPlotCurve::Dots );
curve->setPen( Qt::black, 5 );
curve->attach(plot);
curves.push_back( curve );
}
highlight = new QwtPlotCurve();
highlight->setSamples( {}, {} );
highlight->setStyle( QwtPlotCurve::Dots );
highlight->setPen( Qt::red, 5 );
highlight->attach(plot);
QwtPlotCanvas* canvas = dynamic_cast<QwtPlotCanvas*>( plot->canvas() );
if ( canvas )
canvas->setPaintAttribute( QwtPlotCanvas::BackingStore, false );
plot->replot();
QwtPlotPicker* picker = new QwtPlotPicker( plot->canvas() );
picker->setStateMachine(new QwtPickerTrackerMachine());
connect(picker, SIGNAL(moved(const QPointF&)), this, SLOT(onHovered(const QPointF&)));
}
// inspired from QwtPlotCurve::closestPoint
int closestPoint( QwtPlotCurve& curve, const QPoint &pos, double *dist )
{
const size_t numSamples = curve.dataSize();
if ( curve.plot() == NULL || numSamples <= 0 )
return -1;
const QwtSeriesData<QPointF> *series = curve.data();
const QwtScaleMap xMap = curve.plot()->canvasMap( curve.xAxis() );
const QwtScaleMap yMap = curve.plot()->canvasMap( curve.yAxis() );
const double xPos = xMap.transform( pos.x() );
const double yPos = yMap.transform( pos.y() );
int index = -1;
double dmin = DBL_MAX;
for ( uint i = 0; i < numSamples; i++ )
{
const QPointF sample = series->sample( i );
const double cx = xMap.transform( sample.x() ) - xPos;
const double cy = yMap.transform( sample.y() ) - yPos;
const double dist = sqrt( pow(cx,2) + pow(cy,2) );
if ( dist < dmin )
{
index = i;
dmin = dist;
}
}
if ( dist )
*dist = dmin;
return index;
}
void Dialog::onHovered( const QPointF& pt )
{
// mouse moved!
QwtPlotCurve* closest = NULL;
int closestIndex = -1;
double minDist = DBL_MAX;
for ( auto curve : curves )
{
double dist;
int index = closestPoint( *curve, pt.toPoint(), &dist );
if ( dist < minDist )
{
minDist = dist;
closestIndex = index;
closest = curve;
}
}
if ( !closest )
return;
std::stringstream str;
QPointF closestPoint = closest->sample(closestIndex);
str << "Closest point is " << closestPoint.rx() << "," << closestPoint.ry();
closestLabel->setText( str.str().c_str() );
if ( std::get<0>( highlighted ) == closest &&
std::get<1>( highlighted ) == closestIndex )
{
// highlighted point is unchanged
return;
}
else
{
// highlighted point changed
const QwtScaleMap xMap = plot->canvasMap( QwtPlot::xBottom );
const QwtScaleMap yMap = plot->canvasMap( QwtPlot::yLeft );
const int rectSize = highlight->pen().width() * 2;
const int x = xMap.transform( closestPoint.rx() );
const int y = xMap.transform( closestPoint.ry() );
const QRect cr = plot->canvas()->contentsRect();
highlight->setSamples( { closestPoint.rx() }, { closestPoint.ry() } );
QRect smallCR( x - rectSize/2, y - rectSize/2, rectSize, rectSize );
std::tuple<QwtPlotCurve*,int,QRect> newHighlighted{ closest, closestIndex, smallCR };
QwtPlotCanvas* canvas = dynamic_cast<QwtPlotCanvas*>( plot->canvas() );
if ( canvas )
{
if ( std::get<2>( highlighted ) != QRect() )
{
// repaint previously highlighted area:
canvas->repaint( std::get<2>( highlighted ) );
}
// repaint newly highlighted area:
canvas->repaint( std::get<2>( newHighlighted ) );
// if you replace lines above by this one, it works!
//canvas->replot();
}
highlighted = newHighlighted;
}
}
main.cpp:
#include <QApplication>
#include "widget.h"
int main( int argc, char* argv[] )
{
QApplication app( argc, argv );
Dialog dlg;
dlg.show();
return app.exec();
}
Edit:
If I replace highlight = new QwtPlotCurve(); by highlight = new MyCurve(); with MyCurve defined as:
class MyCurve : public QwtPlotCurve
{
public:
void drawSeries( QPainter *painter,
const QwtScaleMap &xMap, const QwtScaleMap &yMap,
const QRectF &canvasRect, int from, int to ) const override
{
static int i = 0;
if ( dataSize() != 0 )
std::cout << "PAINTING " << i++ << std::endl;
QwtPlotCurve::drawSeries( painter, xMap, yMap, canvasRect, from, to );
}
};
Then I see that the console show a new "PAINTING" when each canvas->repaint are called, howevere the red point does not become visible. Now if I move another window over mine (or press Alt), a new "PAINTING" is reported and this time the closest point becomes red. So as I mentioned, the method looks good but not enough to have the view be repainted as expected...
You should use QwtPlotDirectPainter, it is designed to do exactly what you want:
QwtPlotDirectPainter offers an API to paint subsets ( f.e all
additions points ) without erasing/repainting the plot canvas.
You can se it being used in the "event_filter" example of Qwt:
// Hightlight the selected point
void CanvasPicker::showCursor( bool showIt )
{
if ( !d_selectedCurve )
return;
QwtSymbol *symbol = const_cast<QwtSymbol *>( d_selectedCurve->symbol() );
const QBrush brush = symbol->brush();
if ( showIt )
symbol->setBrush( symbol->brush().color().dark( 180 ) );
QwtPlotDirectPainter directPainter;
directPainter.drawSeries( d_selectedCurve, d_selectedPoint, d_selectedPoint );
if ( showIt )
symbol->setBrush( brush ); // reset brush
}
Depending on the showIt parameter, this function will either draw the point as "selected" or redraw it in its original/unselected style.
You can see how it is used in the select() function:
void CanvasPicker::select( const QPoint &pos )
{
[...]
showCursor( false ); // Mark the previously selected point as deselected
d_selectedCurve = NULL;
d_selectedPoint = -1;
if ( curve && dist < 10 ) // 10 pixels tolerance
{
d_selectedCurve = curve;
d_selectedPoint = index;
showCursor( true ); // Mark the new point as selected.
}
}
In you case, I believe you could directly use the CanvasPicker class and just do some fine tuning like calling select() on QEvent::MouseMove instead of QEvent::MouseButtonPress.
Related
So I wanted to add a texture to a map, but the problem with it is that I can't quite get why it doesn't get to the correct place while zooming with different zoom sizes.
I'm normally trying to achieve setting the texture position on the background to my position, keeping myself centered into the frame: for example my texture size is 1500x1600 and I'm located at X140, Y590 in that picture ( yes, the coordinates are retrieved correctly as I've checked with the console ), zooming in with some value and scaling the texture and setting it's position to where I'm at.
The code is the following:
if (!areTexturesInit) {
InitiateTextures();
areTexturesInit = true;
}
wxBitmap bit(imageTest);
wxPaintDC dc(this);
double zoomSize = 0.9; // here I'm applying the zooming proportions ( 0.1 - bigger size of the texture, 0.9 - more zoomed in )
this->SetSize(wxSize(386, 386)); // size of the main frame
backgroundSize.x = GetSize().x; // get the size of the main frame
backgroundSize.y = GetSize().y;
middlePoint.x = (backgroundSize.x / 2); // calculate the middle point of the frame
middlePoint.y = (backgroundSize.y / 2);
mapSizeX = 25600 / -zoomSize; // scale vs zoom size
mapSizeY = 25600 / zoomSize;
Vector3 myPosition;
GetPlayerPosition(&myPosition); // gets my location
float TextureCoordinateX = middlePoint.x + (myPosition.x / mapSizeX) * backgroundSize.x;
float TextureCoordinateY = middlePoint.y - (myPosition.y / mapSizeY) * backgroundSize.y;
dc.DrawBitmap(bit, TextureCoordinateX, TextureCoordinateY);
Vector3 myPosOnMap = PositionToMapPosition(myPosition, myPosition); // calculates my position on the map vs mapSizeX and Y & rotates vector
dc.SetPen(wxPen(wxColor(255, 0, 0), 4));
dc.DrawRectangle(wxRect(myPosOnMap.x, myPosOnMap.y, 2, 2)); // draws me on the map with a red square
The problem is that I think I've messed up the zooming part somewhere.
I've attached some demos so you can see what I'm talking about:
"zoomSize" of 0.9:
"zoomSize" of 0.67 - which kind of works, but I need to change it to different zoomSizes, there being the problem:
Panning and zooming a dc is surprisingly complicated. It requires working with 3 separate coordinate systems and it's really easy to accidentally work in the wrong coordinate system.
Here's an example I wrote a while ago that shows how to do the calculations that allow a dc to be pan and zoomed.
It sounds like you're not interested in the pan part, so you can ignore all the stuff that allows a user to set their own pan. However, it's still necessary to to use a pan vector just for the zoom in order to center the zoom at the correct location.
// For compilers that support precompilation, includes "wx/wx.h".
#include "wx/wxprec.h"
#ifdef __BORLANDC__
#pragma hdrstop
#endif
// for all others, include the necessary headers (this file is usually all you
// need because it includes almost all "standard" wxWidgets headers)
#ifndef WX_PRECOMP
#include "wx/wx.h"
#endif
#include <wx/graphics.h>
#include <wx/dcbuffer.h>
class PanAndZoomCanvas:public wxWindow
{
public:
PanAndZoomCanvas(wxWindow *parent,
wxWindowID id = wxID_ANY,
const wxPoint &pos=wxDefaultPosition,
const wxSize &size=wxDefaultSize,
long style=0,
const wxString &name="PanAndZoomCanvas");
wxRect2DDouble GetUntransformedRect() const;
protected:
void DoDrawCanvas(wxGraphicsContext*);
private:
void OnPaint(wxPaintEvent&);
void OnMouseWheel(wxMouseEvent&);
void OnLeftDown(wxMouseEvent&);
void OnMotion(wxMouseEvent&);
void OnLeftUp(wxMouseEvent&);
void OnCaptureLost(wxMouseCaptureLostEvent&);
void ProcessPan(const wxPoint&,bool);
void FinishPan(bool);
int m_zoomFactor;
wxPoint2DDouble m_panVector;
wxPoint2DDouble m_inProgressPanVector;
wxPoint m_inProgressPanStartPoint;
bool m_panInProgress;
};
PanAndZoomCanvas::PanAndZoomCanvas(wxWindow *parent, wxWindowID id,
const wxPoint &pos, const wxSize &size,
long style, const wxString &name)
:wxWindow(parent, id, pos, size, style, name)
{
Bind(wxEVT_PAINT,&PanAndZoomCanvas::OnPaint,this);
Bind(wxEVT_MOUSEWHEEL,&PanAndZoomCanvas::OnMouseWheel,this);
Bind(wxEVT_LEFT_DOWN,&PanAndZoomCanvas::OnLeftDown,this);
SetBackgroundStyle(wxBG_STYLE_PAINT);
m_zoomFactor = 100;
m_panVector = wxPoint2DDouble(0,0);
m_inProgressPanStartPoint = wxPoint(0,0);
m_inProgressPanVector = wxPoint2DDouble(0,0);
m_panInProgress = false;
}
void PanAndZoomCanvas::DoDrawCanvas(wxGraphicsContext* gc)
{
gc->SetPen(*wxBLACK_PEN);
wxGraphicsPath path = gc->CreatePath();
path.MoveToPoint(100,100);
path.AddLineToPoint(300,100);
path.AddLineToPoint(300,300);
path.CloseSubpath();
gc->StrokePath(path);
}
void PanAndZoomCanvas::OnPaint(wxPaintEvent& WXUNUSED(event))
{
wxAutoBufferedPaintDC dc(this);
dc.Clear();
wxGraphicsContext* gc = wxGraphicsContext::Create(dc);
if ( gc )
{
double a = m_zoomFactor / 100.0;
wxPoint2DDouble totalPan = m_panVector + m_inProgressPanVector;
gc->Translate(-totalPan.m_x, -totalPan.m_y);
gc->Scale(a, a);
DoDrawCanvas(gc);
delete gc;
}
}
void PanAndZoomCanvas::OnMouseWheel(wxMouseEvent& event)
{
if ( m_panInProgress )
{
FinishPan(false);
}
int rot = event.GetWheelRotation();
int delta = event.GetWheelDelta();
int oldZoom = m_zoomFactor;
m_zoomFactor += 10*(rot/delta);
if ( m_zoomFactor<10 )
{
m_zoomFactor = 10;
}
if ( m_zoomFactor>800)
{
m_zoomFactor = 800;
}
double a = oldZoom / 100.0;
double b = m_zoomFactor / 100.0;
// Set the panVector so that the point below the cursor in the new
// scaled/panned cooresponds to the same point that is currently below it.
wxPoint2DDouble uvPoint = event.GetPosition();
wxPoint2DDouble stPoint = uvPoint + m_panVector;
wxPoint2DDouble xypoint = stPoint/a;
wxPoint2DDouble newSTPoint = b * xypoint;
m_panVector = newSTPoint - uvPoint;
Refresh();
}
void PanAndZoomCanvas::ProcessPan(const wxPoint& pt, bool refresh)
{
m_inProgressPanVector = m_inProgressPanStartPoint - pt;
if ( refresh )
{
Refresh();
}
}
void PanAndZoomCanvas::FinishPan(bool refresh)
{
if ( m_panInProgress )
{
SetCursor(wxNullCursor);
if ( HasCapture() )
{
ReleaseMouse();
}
Unbind(wxEVT_LEFT_UP, &PanAndZoomCanvas::OnLeftUp, this);
Unbind(wxEVT_MOTION, &PanAndZoomCanvas::OnMotion, this);
Unbind(wxEVT_MOUSE_CAPTURE_LOST, &PanAndZoomCanvas::OnCaptureLost, this);
m_panVector += m_inProgressPanVector;
m_inProgressPanVector = wxPoint2DDouble(0,0);
m_panInProgress = false;
if ( refresh )
{
Refresh();
}
}
}
wxRect2DDouble PanAndZoomCanvas::GetUntransformedRect() const
{
double a = m_zoomFactor / 100.0;
wxSize sz = GetSize();
wxPoint2DDouble zero = m_panVector/a;
return wxRect2DDouble(zero.m_x, zero.m_y, sz.GetWidth()/a, sz.GetHeight()/a);
}
void PanAndZoomCanvas::OnLeftDown(wxMouseEvent& event)
{
wxCursor cursor(wxCURSOR_HAND);
SetCursor(cursor);
m_inProgressPanStartPoint = event.GetPosition();
m_inProgressPanVector = wxPoint2DDouble(0,0);
m_panInProgress = true;
Bind(wxEVT_LEFT_UP, &PanAndZoomCanvas::OnLeftUp, this);
Bind(wxEVT_MOTION, &PanAndZoomCanvas::OnMotion, this);
Bind(wxEVT_MOUSE_CAPTURE_LOST, &PanAndZoomCanvas::OnCaptureLost, this);
CaptureMouse();
}
void PanAndZoomCanvas::OnMotion(wxMouseEvent& event)
{
ProcessPan(event.GetPosition(), true);
}
void PanAndZoomCanvas::OnLeftUp(wxMouseEvent& event)
{
ProcessPan(event.GetPosition(), false);
FinishPan(true);
}
void PanAndZoomCanvas::OnCaptureLost(wxMouseCaptureLostEvent&)
{
FinishPan(true);
}
class MyFrame : public wxFrame
{
public:
MyFrame(wxWindow* parent, int id = wxID_ANY, wxString title = "Demo",
wxPoint pos = wxDefaultPosition, wxSize size = wxDefaultSize,
int style = wxDEFAULT_FRAME_STYLE );
};
MyFrame::MyFrame( wxWindow* parent, int id, wxString title, wxPoint pos
, wxSize size, int style )
:wxFrame( parent, id, title, pos, size, style )
{
PanAndZoomCanvas* canvas = new PanAndZoomCanvas(this);
}
class myApp : public wxApp
{
public:
virtual bool OnInit()
{
MyFrame* frame = new MyFrame(NULL);
frame->Show();
return true;
}
};
wxIMPLEMENT_APP(myApp);
On windows, this looks like this:
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.
I'm running the sample code below with Qt5.11.0 on OSX 10.13.6 (also RHEL 7.6, where the problem also occurs, but not as ugly as on OSX). The test program displays a custom model in a QTableView, with indexWidgets set for several of the columns:
#include <QtCore/QDebug>
#include <QtCore/QAbstractItemModel>
#include <QtWidgets/QApplication>
#include <QtWidgets/QMainWindow>
#include <QtWidgets/QHBoxLayout>
#include <QtWidgets/QHeaderView>
#include <QtWidgets/QTableView>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QRadioButton>
class AModel : public QAbstractItemModel
{
public:
int rowCount( const QModelIndex& parent = QModelIndex() ) const override {
return 5;
};
int columnCount( const QModelIndex& parent = QModelIndex() ) const override {
return 5;
};
QModelIndex parent( const QModelIndex& index ) const override {
return QModelIndex();
};
QModelIndex index( int row, int column, const QModelIndex& parent = QModelIndex() ) const override {
if( ( ! parent.isValid() ) &&
row >= 0 && row < 5 &&
column >= 0 && column < 5 ) {
return createIndex( row, column );
} else {
return QModelIndex();
}
};
QVariant data( const QModelIndex& index, int role = Qt::DisplayRole ) const override {
QVariant qval;
if( index.column() >= 1 && index.column() < 4 ) { return QVariant(); }
switch( role ) {
case Qt::DisplayRole:
qval = QString( "%1,%2" ).arg( index.row() ).arg( index.column() );
break;
default:
qval = QVariant();
break;
}
return qval;
};
};
class AWidget : public QWidget
{
public:
AWidget( QWidget* parent ) : QWidget( parent ) {
QHBoxLayout* l = new QHBoxLayout();
this->setLayout( l );
QRadioButton* save = new QRadioButton( "Save" );
QRadioButton* del = new QRadioButton( "Delete" );
l->addWidget( save );
l->addWidget( del );
};
};
int
main( int argc, char *argv[] ) {
QApplication app( argc, argv );
QMainWindow* mw = new QMainWindow();
AModel* model = new AModel();
QTableView* view = new QTableView();
view->setModel( model );
// view->verticalHeader()->setDefaultSectionSize( 15 );
for( int irow = 0; irow < model->rowCount(); irow++ ) {
QPushButton* pb = new QPushButton( "Mogrify", mw );
QRadioButton* rb = new QRadioButton( "Choose", mw );
AWidget* aw = new AWidget( mw );
QObject::connect( pb, &QPushButton::clicked, [irow](){ qDebug() << "Mogrifying " << irow; } );
QObject::connect( rb, &QRadioButton::clicked, [irow](){ qDebug() << "Choosing " << irow; } );
view->setIndexWidget( model->index( irow, 1 ), pb );
view->setIndexWidget( model->index( irow, 2 ), rb );
view->setIndexWidget( model->index( irow, 3 ), aw );
}
view->resizeColumnsToContents();
mw->setCentralWidget( view );
mw->show();
return app.exec();
}
If I just run this as shown above, the result comes out with all the table-embedded widgets having enough space:
If however I uncomment the call to setDefaultSectionSize() in the code above, the table-embedded widgets do not size themselves the way I'd wish. The QPushButton is cut off at the bottom, the QRadioButton is crammed in with little padding, and the custom composite widget doesn't show up at all:
I've tried all manner of QSizeHint experiments, subclassing, and internet searching to get these embedded widgets to size themselves according to the space available in the table cells, so far to no avail. How do I make these embedded indexWidgets paint themselves so they fit into the cell space provided in the QTableView, when I'm telling the QTableView how big its cells should be?
The problem is not in the QTableView but in your custom widget. The custom widget has a layout that must have margins equal to 0.
class AWidget : public QWidget
{
public:
AWidget( QWidget* parent=nullptr) :
QWidget( parent )
{
QHBoxLayout* l = new QHBoxLayout(this);
l->setContentsMargins(0, 0, 0, 0); // <----
QRadioButton* save = new QRadioButton( "Save" );
QRadioButton* del = new QRadioButton( "Delete" );
l->addWidget( save );
l->addWidget( del );
};
};
I have an object that I want to be able to move. I want it to move only if i click within in, then drag my mouse to a place, then upon release, it starts moving towards it. so if i click inside it, I can move my mouse anywhere as much as I want while holding mouse button, but only when I release does it start to move.
Currently, the best ive been able to do is make the object follow (a couple seconds behind btw, NOT on the mouse position) as long as im holding the mouse button and moving it. it doesnt matter where I start the click from, as long as I click and move, the object moves towards it as long as im holding the mouse button. any other attempts leave the object staying still/not moving at all.
void mousemotion(int x, int yc){
globals.mouse_x = x;
globals.mouse_y = HEIGHT - yc;
}
and
int main(int argc, char** argv){
glutInit(&argc, argv);
....
//glutMouseFunc(processMouse);
glutMotionFunc(mousemotion);
are the only mouse functions/callbacks that are currently being used to allow the result above. I have tried things like adding a glutMouseFunc callback but from changing the state parameter in it produces bad results. E.G:
//glutMouseFunc callback
void processMouse(int button, int state, int x, int yc){
if ( state == GLUT_UP){
globals.centre_x = globals.mouse_x;
globals.centre_y = globals.mouse_y;
}
GLUT_DOWN doesnt change the main behaviour, but when the object is in motion, and I just click once, the object will snap to the position it was going to. GLUT_UP just makes it so once I release the mouse, the object will immediately snap to the position it was going. These behaviours make sense as to they are behaving the way they are, but I cant manipulate it to work the way I want to. I also made a function to check if a point is inside the object but I dont know where its applicable
bool inside(int x, int y){
if(x >= globals.centre_x - 20
&& x <= globals.centre_x +20
&& y >= globals.centre_y - 20
&& y <= globals.centre_y+ 20){
return true;
}
else
return false;
}
pressumably it would be used inside once of the mouse functions, using the x and y mouse coordinates as parameters.
all the drag and drop examples Ive seen online involve immediate dragging of the object, i.e. click on object and object follows the exact x,y mouse coordinates as you move it around, but I want to make it so only when I let go of the mouse will the object start to move.
Any Help appreciated. let me know if i can clarify anything. thanks
I do not use GLUT but Based on these:
GLUT docs
GLUT mouse button down
To detect mouse event type You need to do something like this:
//glutMouseFunc callback
int state0l=GLUT_UP; // last left mouse buttons state
int state0r=GLUT_UP; // last right mouse buttons state
void processMouse(int button, int state1, int x, int y)
{
if (button == GLUT_LEFT_BUTTON)
{
// decode and handle the mouse events by type
if ((state0l == GLUT_UP )&&(state1 == GLUT_DOWN)) // mouse down (click)
{
// here do your stuff
}
if ((state0l == GLUT_DOWN)&&(state1 == GLUT_DOWN)) // mouse move while clicked
{
// here do your stuff
}
if ((state0l == GLUT_DOWN)&&(state1 == GLUT_UP )) // mouse up (release)
{
// here do your stuff
}
if ((state0l == GLUT_UP )&&(state1 == GLUT_UP )) // mouse move without buttons
{
// here do your stuff
}
// store actual buttons state for next time
state0l = state1;
}
if (button == GLUT_RIGHT_BUTTON)
{
// decode and handle the mouse events by type
if ((state0r == GLUT_UP )&&(state1 == GLUT_DOWN)) // mouse down (click)
{
// here do your stuff
}
if ((state0r == GLUT_DOWN)&&(state1 == GLUT_DOWN)) // mouse move while clicked
{
// here do your stuff
}
if ((state0r == GLUT_DOWN)&&(state1 == GLUT_UP )) // mouse up (release)
{
// here do your stuff
}
if ((state0r == GLUT_UP )&&(state1 == GLUT_UP )) // mouse move without buttons
{
// here do your stuff
}
// store actual buttons state for next time
state0r = state1;
}
}
As you can see I just inspect last and actual state of the buttons to detect 4 possibilities +/- the same as in the link I gave you before:
simple low level Drag&Drop example in C++
I just have q0,q1 instead of the state0,state1 when you inspect the methods like: editor::mov,add_kruh,add_stvorec,... they all use this same technique (but only those events they use of coarse).
Do selection detection in your mouse callback on button-down
Set up animation parameters (start & end position + duration) in your mouse callback on button-up
Interpolate animation positions in a timer callback & update selected position
All together (right-click to add rectangles):
#include <GL/glut.h>
// https://glm.g-truc.net/
#include <glm/glm.hpp>
#include <vector>
struct Rect
{
glm::vec2 pos;
glm::vec2 dim;
bool IsInside( glm::vec2 point ) const
{
const bool inX = pos.x < point.x && point.x < pos.x + dim.x;
const bool inY = pos.y < point.y && point.y < pos.y + dim.y;
return inX && inY;
}
};
// rect list & selection
std::vector< Rect > rects;
int selected = -1;
// animation state
int timeBeg = -1;
int timeEnd = -1;
glm::vec2 src;
glm::vec2 dst;
void mouse( int button, int state, int x, int y )
{
if( GLUT_RIGHT_BUTTON == button )
{
// add rect
if( GLUT_UP == state )
{
rects.push_back( Rect{ glm::vec2( x, y ), glm::vec2( 60, 60 ) } );
glutPostRedisplay();
}
return;
}
if( GLUT_LEFT_BUTTON == button && ( timeBeg < 0 || timeEnd < 0 ) )
{
// select rect
if( GLUT_DOWN == state )
{
for( size_t i = 0; i < rects.size(); ++i )
{
if( !rects[i].IsInside( glm::vec2( x, y ) ) )
continue;
selected = i;
glutPostRedisplay();
return;
}
}
// finish select
if( GLUT_UP == state && selected >= 0 )
{
timeBeg = glutGet( GLUT_ELAPSED_TIME );
timeEnd = timeBeg + 1000;
src = rects[ selected ].pos;
dst = glm::vec2( x, y );
}
return;
}
}
void timer( int value )
{
glutTimerFunc( 16, timer, 0 );
// don't repaint if we aren't animating
if( timeBeg < 0 || timeEnd < 0 || selected < 0 )
return;
const int timeCur = glutGet( GLUT_ELAPSED_TIME );
if( timeCur > timeEnd )
{
// animation done
timeBeg = -1;
timeEnd = -1;
selected = -1;
glutPostRedisplay();
return;
}
float pct = ( timeCur - timeBeg ) / static_cast< float >( timeEnd - timeBeg );
rects[ selected ].pos = glm::mix( src, dst, pct );
glutPostRedisplay();
}
void display()
{
glClearColor( 0, 0, 0, 1 );
glClear(GL_COLOR_BUFFER_BIT);
glMatrixMode( GL_PROJECTION );
glLoadIdentity();
double w = glutGet( GLUT_WINDOW_WIDTH );
double h = glutGet( GLUT_WINDOW_HEIGHT );
glOrtho( 0, w, h, 0, -1, 1 );
glMatrixMode( GL_MODELVIEW );
glLoadIdentity();
for( size_t i = 0; i < rects.size(); ++i )
{
if( selected == i )
glColor3ub( 255, 0, 0 );
else
glColor3ub( 255, 255, 255 );
const Rect& rect = rects[i];
glRectf( rect.pos.x, rect.pos.y, rect.pos.x + rect.dim.x, rect.pos.y + rect.dim.y );
}
glutSwapBuffers();
}
int main( int argc, char** argv )
{
glutInit( &argc, argv );
glutInitWindowSize( 800, 600 );
glutInitDisplayMode( GLUT_RGBA | GLUT_DOUBLE );
glutCreateWindow( "GLUT" );
glutMouseFunc( mouse );
glutTimerFunc( 0, timer, 0 );
glutDisplayFunc( display );
glutMainLoop();
return 0;
}
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.