PDA

View Full Version : Really strange problem with custom QGraphicsWidget



Tomasu
14th April 2010, 14:23
I'm making a pretty basic board game (based on a grid of squares) with Qt on top of a QGraphicsView, and I'm implementing the game board manually in its own QGraphicsWidgets (due to not being able to tell the view not to scale and translate a given widget), which means I need to handle all the rendering and logic myself. Which so far hasn't been that much trouble, but I'm stuck with one bug (likely in my own code) that I just haven't been able to solve.

Basically, when I use the mouse wheel to zoom, my game board will zoom in and out, but occasionally the squares will render a little too small but otherwise everything looks fine, no gaps between the squares, and the outline I draw right after the squares is the right size. Even the trace log says the squares are the right size, yet things aren't quite right regardless.

If someone could help me look through my problem I'd be greatfull. I've attached the files for my custom widget, its a bit of a mess, but is pretty straight forward for the most part.

I'm also interested in ideas on how to improve the internal structure of the widget if anyone has ideas.

append: Should have done this originally, but I'm pasting just the few functions (slightly cleaned up) that I think matter in this case from the sources attached:


void GameBoard::paint ( QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget */* widget */)
painter->save();
painter->translate(geometry().width()/2.0, geometry().height()/2.0);

int piece_size_tf = ceil(piece_size * zoom);

QPointF board_off = QPointF((qreal)board_w / 2.0 * piece_size_tf, (qreal)board_h / 2.0 * piece_size_tf);
QPointF board_pos = QPointF((qreal)board_w * piece_size_tf * x_scroll, (qreal)board_h * piece_size_tf * y_scroll) - board_off;
QSizeF board_size = QSizeF(piece_size_tf * (qreal)board_w, piece_size_tf * (qreal)board_h);

board_pos.setX(qRound(board_pos.x()));
board_pos.setY(qRound(board_pos.y()));

board_size.setWidth(qRound(board_size.width()));
board_size.setHeight(qRound(board_size.height()));

QRectF board_geometry = QRectF(board_pos, board_size);

for(int i = 0; i < board_h; i++)
{
for(int j = 0; j < board_w; j++)
{
QRectF geom = board[i][j]->getGeometry();
geom.setLeft(qRound(geom.left() * zoom));
geom.setTop(qRound(geom.top() * zoom));
geom.setRight(qRound(geom.right() * zoom));
geom.setBottom(qRound(geom.bottom() * zoom));

painter->save();

painter->translate(board_pos + geom.topLeft());
board[i][j]->paint(painter, QSize(geom.width(), geom.height()));
painter->restore();
}
}

painter->drawRect(board_geometry);
painter->restore();
}


void GameBoard::wheelEvent(QGraphicsSceneWheelEvent * event)
{
qDebug("GameBoard::wheelEvent: %i", event->delta());
qDebug("GameBoard::wheelEvent:before zoom:%f piece_size:%i", zoom, piece_size);

int delta = event->delta();

int piece_size_tf = qRound((qreal)piece_size * zoom);
if(delta < 0 && // zoom in, if board is smaller than the view, just ignore event
piece_size_tf * board_w < geometry().width() &&
piece_size_tf * board_h < geometry().height()) {
qDebug("zoom bestfit: %f", zoom);
return;
}

qreal best_fit_scale = qMin<qreal>(((qreal)geometry().width())/((qreal)board_w * (qreal)piece_size), ((qreal)geometry().height())/((qreal)board_h * (qreal)piece_size));
best_fit_scale = 0.1 * qRound( (best_fit_scale-0.05) * 10.0 );
if(best_fit_scale < 0.0 || best_fit_scale > 4) {
qDebug("scale: out of range: %f", best_fit_scale);
}

qDebug("zoom +- %f (%d)", delta/1200.0, delta);
if(delta > 0) { // zoom in, love these magic numbers.
if(zoom+(delta/1200.0) > 4.0) {
qDebug("can't zoom in, too close already");
zoom = 4.0;
}
else {
zoom += delta / 1200.0;
}
}
else { // zoom out
if(zoom+(delta/1200.0) < best_fit_scale) {
qDebug("can't zoom out, too far out");
zoom = best_fit_scale;
}
else {
zoom += delta / 1200.0;
}
}

update();
}


void GameBoardSquare::paint ( QPainter * painter, QSize size )
{
//qDebug("GameBoardSquare::paint!");
QRect rect = QRect(0, 0, size.width(), size.height());

//qDebug() << "GameBoardSquare::paint: " << pos() << size();
if(flags & Filled) {
if(flags & FilledS) {
image->paint(painter, rect, "s_square");
}
else {
image->paint(painter, rect, "o_square");
}
}
else {
image->paint(painter, rect, "empty_square");

if(flags & LetterSelectOHover) {
image->paint(painter, rect, "o_highlight");
}
else if(flags & LetterSelectSHover) {
image->paint(painter, rect, "s_highlight");
}
}
}

JohannesMunk
14th April 2010, 14:38
Hi there!

Sorry no time to look through >20kb of code right now.

QGraphicsItem::ItemIgnoresTransformations doesn't work for you?

Johannes

Tomasu
14th April 2010, 14:44
Hi there!

Sorry no time to look through >20kb of code right now.

QGraphicsItem::ItemIgnoresTransformations doesn't work for you?

JohannesIt doesn't ignore translations. So sadly it doesn't work for me.

Tomasu
14th April 2010, 15:15
I've updated my original post with the most (i think) relevant source to the problem.

Tomasu
15th April 2010, 12:44
I'm really stuck here. I've been trying to figure this out for the past few days now (along with other problems that got fixed), but this one little bug is annoying the crap out of me. Everything I can think of hasn't fixed it, its taunting me :o ;)

I can't figure it out. if someone could spare some time to take a look at this with me I'd appreciate it.

JohannesMunk
15th April 2010, 13:28
Hi there!

1. Why do you round your rect? Just use QRectF throughout. QPainter has overloaded methods for the floating point types.

2. If that doesn't solve it, can you provide a screenshot and a minimal compilable example, that reproduces your problem?

Johannes

Tomasu
15th April 2010, 14:16
Using floating point coordinates causes slight movement in the individual components in the game board and the board itself. They shift a pixel somewhat randomly. My original implementation used nothing but floats because it was convenient, but it caused slight rendering glitches when the board was scaled, resized and translated.

Here's a screenshot of two instances, the one in the bg is the way it should look, and the foreground one is how it can sometimes appear. Note that the black outline is drawn using the same variables in the same function as the squares, so if one is wrong, they both should be.

http://strangesoft.net/sosgame-qtclient/prototype18-bug1.png

A minimal example will take some time to put together. Right now the that is part of a larger client program, and I'd have to start a new skeleton QGraphicsView application to make the example. Probably get it posted later today.

Thanks

JohannesMunk
15th April 2010, 14:32
Hi!

I can't see why floating point values should produce rendering glitches.. the graphicsscene has to use floats internally..

Try this.. Then the squares geometry is really based upon the same rect!


void GameBoard::paint ( QPainter * painter, const QStyleOptionGraphicsItem * option, QWidget */* widget */)
painter->save();
painter->translate(geometry().width()/2.0, geometry().height()/2.0);

//int piece_size_tf = ceil(piece_size * zoom);
double piece_size_tf = piece_size * zoom;

QPointF board_off = QPointF((qreal)board_w / 2.0 * piece_size_tf, (qreal)board_h / 2.0 * piece_size_tf);
QPointF board_pos = QPointF((qreal)board_w * piece_size_tf * x_scroll, (qreal)board_h * piece_size_tf * y_scroll) - board_off;
QSizeF board_size = QSizeF(piece_size_tf * (qreal)board_w, piece_size_tf * (qreal)board_h);

//board_pos.setX(qRound(board_pos.x()));
//board_pos.setY(qRound(board_pos.y()));

//board_size.setWidth(qRound(board_size.width()));
//board_size.setHeight(qRound(board_size.height()));

QRectF board_geometry = QRectF(board_pos, board_size);

for(int i = 0; i < board_h; i++)
{
for(int j = 0; j < board_w; j++)
{
//QRectF geom = board[i][j]->getGeometry();
//geom.setLeft(qRound(geom.left() * zoom));
//geom.setTop(qRound(geom.top() * zoom));
//geom.setRight(qRound(geom.right() * zoom));
//geom.setBottom(qRound(geom.bottom() * zoom));
QRectF geom(j*piece_size_tf,i*piece_size_tf,piece_size_tf ,piece_size_tf);

painter->save();

painter->translate(board_pos + geom.topLeft());
board[i][j]->paint(painter, QSizeF(geom.width(), geom.height()));
painter->restore();
}
}

painter->drawRect(board_geometry);
painter->restore();
}
HIH

Joh

JohannesMunk
15th April 2010, 14:44
When I step back twice.. I'm really not happy with your approach. Zooming, Scrolling ... all that can be handled entirely by the scene/view. By applying the right transformations..

Why are you doing it manually?

Johannes

Tomasu
15th April 2010, 14:58
I've tried to apply that, It does seem to get rid of the problem I was trying to fix, but it reintroduces the prior rendering glitches.

http://strangesoft.net/sosgame-qtclient/prototype18-bug2.png

And they appear at about the same times when the size bug would happen.

If it helps, the board is drawn using Svgs cached in QPixmaps, so no piece can even have a floating point size, so the board's over all geometry also can't have a fractional size. Using floating point math, subtle inacuracy is introduced, causing gaps. Or at least thats how I see it so far. And rounding the final calculated geometry fixes the gap issue, but introduces the size issue. Which is strange, because I have a printf outputing the Square's geometries in that inner paint loop, and they /look/ fine. They aren't a pixel off, and yet things are still drawing wrong.

Thanks again.

Tomasu
15th April 2010, 15:00
When I step back twice.. I'm really not happy with your approach. Zooming, Scrolling ... all that can be handled entirely by the scene/view. By applying the right transformations..

Why are you doing it manually?

Johannes

I plan to use QGraphicsWidgets to display user interface elements on top of the board, but I can't have those be translated or transformed. QGraphicsView doesn't allow me to make a QGraphicsItem/Widget that ignores both the translation and transform.

My first attempt at this widget just used QGV's own scaling and transforms. It worked fine for that, but I can't get item's to ignore the transform and translate.

JohannesMunk
15th April 2010, 15:15
Why don't you do it like this:



#ifndef GAMEBOARD_H
#define GAMEBOARD_H

#include <QtGui>

class CheckerBoardSQ : public QGraphicsItem
{
public:
CheckerBoardSQ(QGraphicsItem *parent = 0,QGraphicsScene *scene = 0) : QGraphicsItem(parent,scene) {}
static qreal size() {return 20;}
QRectF boundingRect() const
{
qreal penWidth = 1;
return QRectF(-penWidth/2,-penWidth/2,size() + penWidth,size() + penWidth);
}
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
painter->drawRoundedRect(0,0,size(),size(), 5, 5);
}
};

class CheckerBoard : public QGraphicsItem
{
public:
CheckerBoard(int n_x,int n_y,QGraphicsItem *parent = 0,QGraphicsScene *scene = 0) : QGraphicsItem(parent,scene)
{
m_n_x = n_x;m_n_y = n_y;
for (int i = 0;i < n_x;++i)
{
for (int j = 0;j < n_y;++j)
{
CheckerBoardSQ* sq = new CheckerBoardSQ(this);
sq->setPos(i*CheckerBoardSQ::size(),j*CheckerBoardSQ:: size());
}
}
}
QRectF boundingRect() const
{
qreal penWidth = 1;
return QRectF(-penWidth/2,-penWidth/2,m_n_x*CheckerBoardSQ::size() + penWidth,m_n_y*CheckerBoardSQ::size() + penWidth);
}
void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget)
{
painter->drawRect(0,0,m_n_x*CheckerBoardSQ::size(),m_n_y*Ch eckerBoardSQ::size());
}
private:
int m_n_x;
int m_n_y;
};

class MainWidget : public QWidget
{
public:
MainWidget() {
scene = new QGraphicsScene();
scene->addItem(new CheckerBoard(8,8));

view = new QGraphicsView(scene);

QVBoxLayout* vl = new QVBoxLayout();
vl->addWidget(view);

setLayout(vl);
}
void keyPressEvent (QKeyEvent * event)
{
if (event->text() == "+") {
view->scale(1.1,1.1);
event->accept();
}
if (event->text() == "-") {
view->scale(0.90,0.90);
event->accept();
}
if (event->text() == "#") {
view->resetTransform();
event->accept();
}
}

private:
QGraphicsScene* scene;
QGraphicsView* view;
};

#endif // GAMEBOARD_H

--------------------

#include <QtCore>
#include <QtGui>
#include "gameboard.h"

int main(int argc, char *argv[])
{
QApplication a(argc, argv);

MainWidget wdg;
wdg.show();

QObject::connect(&a, SIGNAL(lastWindowClosed()), &a, SLOT(quit()));

return a.exec();
}


Where is the problem with the translation?

Johannes

Tomasu
15th April 2010, 15:23
Why don't you do it like this:

Where is the problem with the translation?

Johannes

I should have probably explained better. This board can be any arbitrary size, and be scaled up and down arbitrarily, as well as scrolled in all directions so the players can see all the peices if the board is zoomed in. I don't use any primitive drawing functions for the squares, except for drawPixmap. The entire board is themable using Svg. And because Svg drawing is so slow, all Svg drawing is cached to pixmaps (much like KDE's plasma does it).

Once the board is working adequately I will be adding user interface elements to the QGV on top of the board that I do NOT want to have move or scale at all. QGV does not let you have an item that doesn't translate with the view, it only lets you tell it that items ignore scaling, so I can't use QGVs own translation and scaling. Also I don't want to use QGVs scaling as it just stretches the drawPixmap commands, rather than drawing a higher resolution version of the grapghics, which my code attempts to do.

So I'm stuck implementing my own translation and scaling because I can't use QGV's, lest it interfear with future objects, or cause the game to look "stretched", "fuzzy" or "pixelated" due to QGV/QPainter's own scaling.

JohannesMunk
15th April 2010, 15:32
Now we are talking! Sorry for taking so long to understand :->

Ok then: Why don't you just translate the board and leave the scene with the HUD-elements unchanged?

Johannes

Tomasu
15th April 2010, 15:57
Now we are talking! Sorry for taking so long to understand :->


It's ok, I didn't exactly explain the problem or situation very well. I'm not that good at asking for help at all. I don't do it a lot.



Ok then: Why don't you just translate the board and leave the scene with the HUD-elements unchanged?

Johannes

It would be pretty simple to get the board to just modify its own geometry on scaling and translation. But I'll still have to handle the scaling and drawing myself, so it doesn't really get me that much. And means I can't use the board widget's own size to calculate the "ideal board size" in the wheelEvent method. I can't really see an ideal way to handle all my "wants". either I have to reimplement a bunch of QGVs own features, or I have to deal with not getting my way. I don't like either extreme ;D

I had an implementation (its in svn some place) earlier that used a widget for each square inside the board widget, with a grid layout. And that was kindof tough to work with, as I want to add animations and hover efects to the squares. That implementation worked ok, but the quality of the gfx when scaling was a little iffy, I could forsee problems with fancier themes down the road.

Right now each square also expands when the mouse hovers over it using a QPropertyAnimation, so they smoothly animate as the mouse enters and leaves a square. That worked with QGWs for each square too, though it was a bit finicky.

JohannesMunk
15th April 2010, 16:14
Well I can't give you a straight advice here. But I would leave as much of the transformation as possible to the QGV. Mousewheel handling isn't really a problem, is it?

Concerning manual zoom: You can't have both: Integer aligned items and free zoomlevels. If you continue down that path, you should restrict the zooming factors values. And / or enable aliasing. But I would not round the items positions etc..

Maybe if you rephrase your question to a specific problem, maybe somebody else can help you out further!

Good luck!

Johannes

Tomasu
15th April 2010, 16:32
Well I can't give you a straight advice here. But I would leave as much of the transformation as possible to the QGV. Mousewheel handling isn't really a problem, is it?

Concerning manual zoom: You can't have both: Integer aligned items and free zoomlevels. If you continue down that path, you should restrict the zooming factors values. And / or enable aliasing. But I would not round the items positions etc..


Well the zooming isn't quite free. I have it limited to 0.1 steps.

As for rounding, I have to round the items size. The squares can't have fractional sizes, as QPixmaps can't have fractional sizes.



Maybe if you rephrase your question to a specific problem, maybe somebody else can help you out further!

Good luck!

JohannesThat is what I was trying to do orrignally. I need to fix the math some how in my code. The most odd bit about it is my debug traces give 100% correct values, yet the squares still draw too small occasionally. It makes no sense. It shouldn't be possible as far as I can see.

And with the code you provided earlier, the original problem is gone, but those gaps appear. If I can fix that I'd be happy. To be honest, my first try at that drawing code looked very similar to what you gave, using piece_size_tf * zoom instead of asking the square for its size. But that means I can't animate the squares on mouse over/out.

So I think I'd just like to get rid of the gaps that appear.