QGraphicsView level of detail handling - qt

I am working on a visualization SW which uses QGraphicsView and QGraphicsScene (Qt4.8, PyQt). I need LOD (level of detail) handling depending on zoom level. For this I create more QGraphicsScenes and I render the whole scene to each with different LOD level. (There is a QGraphicsScene for each LOD level).
I switch between these QGraphicsScenes depending on the zoom level. (QGraphicsView.setScene()).
The big problem is that the QGraphicsView's ScrollBars resets their positions when setScene() is called.
Here is a code snippet which tries to restore the ScrollBars values, but it doesn't work:
hsb = self.horizontalScrollBar()
vsb = self.verticalScrollBar()
hv, vv = hsb.value(), vsb.value()
self.lod = i
self.setScene(self.scenes[self.lod])
hsb.setValue(hv) # this simply doesn't work !!!!
vsb.setValue(vv)
Any idea to retain the positions of the ScrollBars? Or any idea for efficient LOD handling?
I could try to connect the ScrollBar's value changed signals into some custom slots which would disconnect from its own signal than re-set the ScrollBar's value from a saved one. That would be a very lame and ugly solution.

I suggest to use a single scene to manage different level of details.
You can get the current level of detail directly inside the QGraphicsItem::paint method and draw the item accordingly.
Example C++:
void MyItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
qreal levelOfDetail = QStyleOptionGraphicsItem::levelOfDetailFromTransform(painter->worldTransform());
//draw the item depending on the level of detail
}
Python
def paint(self, painter, option, widget):
levelOfDetail = QStyleOptionGraphicsItem.optionlevelOfDetailFromTransform(painter.worldTransform())
#draw the item depending on the level of detail
See QStyleOptionGraphicsItem::levelOfDetailFromTransform (for pyqt4 QStyleOptionGraphicsItem.levelOfDetailFromTransform )
For large scene, with a lot of items, consider to group items. You can check the level of details when the scene transform (or the view transform) changes and show/hide the group of items. See QGraphicsItemGroup.

Related

How can I mirror a movie and play it in reverse?

I have been playing with QMovie a bit trying to mirror a movie as well as play it in reverse.
For the mirroring bit I tried assigning a negative width to no avail. Since QImage does offer facilities for this I had hoped QMovie would do the same.
There don't seem to be any facilities in the QMovie object for those things, so I'm wondering if I could manipulate the QIODevice to the QMovie object instead to achieve this, but this is completely new territory for me and I don't see anything obvious in the documentation that would achieve mirroring or playing in reverse.
The code example to start with would be the same as on the PySide page:
label = QLabel()
movie = QMovie("animations/fire.gif")
label.setMovie(movie)
movie.start()
Any ideas would be greatly appreciated.
Thanks,
Frank
QMovie doesn't provide the methods to set the current image, so you have to use directly QImageReader to play it in reverse (using QImageReader::jumpToImage()). This is not very easy bacause the delay between a frame and the next can change, but however you can get it calling QImageReader::nextImageDelay().
To display the movie, you can implement your own widget to draw the movie as you want.
In the paintEvent() you can set a transformation to the painter to get the mirror effect and than draw the current image of the movie.
Example:
void MyWidget::paintEvent(QPaintEvent * event)
{
QPainter painter(this);
painter.scale(-1, 1); // x-axis mirror
//here maybe you must adjust the transformation to center and scale the movie.
painter.drawImage(0, 0, currentImage);
}
To play the movie you have to set a timer that change the current image.
Example:
//you must create a timer in the constructor and connect it to this slot
void MyWidget::timeoutSlot()
{
int currentImageIndex;
//here you have to compute the image index
imageReader.jumpToImage(currentImageIndex);
currentImage = imageReader.read(); //maybe you want to read it only if the index is changed
update();
}
Here you can find an example of widget subclass, with timer and painter transformation
See also QImageReader, QTimer, QWidget::paintEvent

Dragging multiple QGraphicsItems in different QGraphicsScenes for the same amount

I have my own classes (MyGraphicsScene, MyGraphicsView, MyGraphicsItem) derived from QGraphicsScene, QGraphicsView and QGraphicsItem.
In my main window, I then create nine (9) instances of MyGraphicsScene, shown through nine instances of MyGraphicsView. All nine MyGraphicsScenes contain pointers to each other.
How can I drag an instance of MyGraphicsItem in one MyGraphicsScene, and then automatically drag certain MyGraphicsItem instances (in the eight remaining MyGraphicsScenes) for the same amount/distance/vector?
My first idea was to reimplement MyGraphicsItem::itemChange (with change == QGraphicsItem::ItemPositionChange) and then call setPos() for remaining instances of MyGraphicsItem (contained within other MyGraphicsScenes). However this won't work because I will get infinite recursion (setPos() would trigger itemChange() as well, including for the originating MyGraphicsItem).
Any other ideas from experienced Qt-ers?
I do know for a fact you can use the same QGraphicsScene in multiple views. So if you're just trying to have multiple views of the same thing, call MyGraphicsView all with the same scene.
If you define derived class for QGraphicsItem (I think you already have, right?) and reimplement the mouseMoveEvent so it calls the move method to all 9 objects.
Remember that you have at your disposal all the attributes of QMouseEvent, e.g. buttons(), pos().
void MyGraphicsItem::mouseMoveEvent( QMouseEvent *event )
{
if (event->buttons() & Qt::LeftButton) {
QPoint pos = event->pos();
// ...
}
}
Hope that helped!

Is there any easy way to copy QPainter state?

I want to implment push_group/pop_group of cairo with QPainter, but QPainter resets all its state while begin() with a new painterDevice, so I have to save/revert all state manually.
Yes, just check out QPainter::save() and QPainter::restore().
If you want to save/restore between the lifespan of multiple QPainters, you have to do it manually. You could just create a class PainterState that encapsulates the painter state (pen, brush, transform, etc.), and then store a QStack<PainterState>.
There is a QPainterState class, but it is for internal use only, and I think it's only for use with a single QPainter. See the source ("qpainter_p.h") if you're interested in the QPainterState members (too many to copy here).
When constructing the QPainter object, you can draw it to a QPicture. Then it can be reloaded when needed and painted out to the real QPaintDevice.
QPicture picture;
QPainter painterQueued;
painterQueued.begin(&picture); // paint in picture
painterQueued.drawEllipse(10,20, 80,70); // draw an ellipse
painterQueued.end(); // painting done
QImage myImage;
QPainter painterTarget;
painterTarget.begin(&myImage); // paint in myImage
painterTarget.drawPicture(0, 0, picture); // draw the picture at (0,0)
painterTarget.end(); // painting done
You could queue up many QPicture objects in a list, stack, etc, and replay them when needed.

Qt paint on a popup window with data from database

In Qt GUI application, I made a dialog containing a table. When I double click a row in the table, I want:
a popup window to show up;
get points data according to that row from a database;
plot those points on the popup window.
I've done the fetch function of points data in a database.cpp. But according to the rule, the plotting function has to be in dialog.cpp, in a void Dialog::paintEvent(QPaintEvent *event) function. Can I do the plotting function lineTo() in that database.cpp data fetch function?
You can paint in a QPixmap from anywhere, and pass that pixmap to the popup dialog to be displayed inside a QLabel or painted by the paintEvent function.
You can also use QPolygonF which has the advantage of being more cleanly scalable.
Look at the function generatePixmap in that article (Qt Quaterly), then use QLabel::setPixmap to assign the pixmap to the label.

Best way to create a long line (or cross line) cursor in Qt GraphicsView

The easy way to create the long cross line cursor (as long as viewport) is create a cross line graphicsItem, when mouse moved, set the item's pos property.
But this way will be very slow when the scene is complex, because it should update the whole viewport to update the cursor's pos.
The another easy way is setCursor(QCursor(..)),use a QPixmap to define the long cross line, this way will very fast , but the cursor will exceed the viewport rect.
Is there another way to show a long cross line cursor fastly?
Thanks very much!
If I understand correctly, you want to draw an horizontal line and a vertical line, crossing at the cursor position, and being as large as the viewport is.
A poosible solution would be to reimplement QGraphicsScene::drawForeground() to draw the two lines with the painter.
The problem is that the scene doesn't know about the mouse position. This means the view will have to track it and inform the scene when the mouse position has changed.
To do that, you'll have to create your own GraphicsScene (inheriting QGraphicsScene) and your own GraphicsView (inheriting QGraphicsView).
On your GraphicsView constructor, you'll have to start mouse tracking. This will make your you receive a mouseMoveEvent each time the mouse moves inside the view :
GraphicsViewTrack::GraphicsViewTrack(QWidget* parent) : QGraphicsView(parent) {
setMouseTracking(true);
}
void GraphicsViewTrack::mouseMoveEvent(QMouseEvent* pEvent) {
QPointF MousePos = this->mapToScene(pEvent->pos());
emit mousePosChanged(MousePos.toPoint());
}
As you can see in the code snippet above, the view is emitting a signal (mousePosChanged) to which the scene will be connected. This signal contains the mouse position, converted to the scene's coordinates.
Now, on the scene side, you have to add a slot which will be called when the mouse position changed, store the new mouse position in a member variable and reimplement QGraphicsScene::drawForeground() :
void GraphicsSceneCross::drawForeground(QPainter* painter, const QRectF& rect) {
QRectF SceneRect = this->sceneRect();
painter->setPen(QPen(Qt::black, 1));
painter->drawLine(SceneRect.left(), m_MousePos.y(), SceneRect.right(), m_MousePos.y());
painter->drawLine(m_MousePos.x(), SceneRect.top(), m_MousePos.x(), SceneRect.bottom());
}
void GraphicsSceneCross::onMouseChanged(QPoint NewMousePos) {
m_MousePos = NewMousePos; // Store the mouse position in a member variable
invalidate(); // Tells the scene it should be redrawn
}
The last thing to do is connect the GraphicsView's signal to the GraphicsScene slot.
I'll let you check if this solution is acceptable performance wise.
Based on Jerome answer and using python I created this code in my QGraphicsScene subclass:
def drawForeground(self, painter, rect):
if self.guidesEnabled:
painter.setClipRect(rect)
painter.setPen(self.guidePen)
painter.drawLine(self.coords.x(), rect.top(), self.coords.x(), rect.bottom())
painter.drawLine(rect.left(), self.coords.y(), rect.right(), self.coords.y())
def mouseMoveEvent(self, event):
self.coords = event.scenePos()
self.invalidate()
It should be straighforward for you to write its appropiate C++ code. Note that I take advantage of the rect argument passed by Qt Api framework and I clip the painter to that
area, since it's the visible area to be drawn.
I also cache the pen object, since I realized in other experiments that creating objects while painting will penalty performace and doing it so you also give the user the chance to set a custom pen in your program options.
I have found a way to do this!
I develops under Windows system, so can use lower GDI api jumping out of Qt's painting system.
The detail is get the HDC of the QGraphicsView's viewPort. Then in the QMouseEvent of QGraphicsView use "MoveToEx" and "LineTo" drawing two lines on the viewport,then I should do is erase the "old" cursor, It's easy to do this using "setROP2(HDC dc,R2_NOT)",then draw the old Cursor stored again.
This method doesn't enter the QPainter system, so the GraphicsItems under the cursor will not be repaint.
To solve the filker problem when the mouse moves fast, I don't use "double buffer". I used QTimer to update the cursor at CPU's idle. The detail is in QMouseEvent ,don't update the cursor at time ,but store the position to a list, When CPU idle, draw the cursor at the list of positions
I wish this will help others who meets the same problem with me.
Thanks Jérôme, who gave me useful tip of QGraphicsScene.

Resources