How do I clear certain lines/paths from a QWidget but not others, then redraw separate lines/paths on top? - qt

I'm trying to optimize line drawings on a QWidget. I basically have a grid in the background and drawings on top of the grid. Currently I'm redrawing the background and grid lines every time the paint event is called. This works fine if the grid lines are far enough apart so I don't have to draw that many lines, but if the scale gets changed, the lines must be redrawn at that new scale. Also, if the window is resized, then more of the grid is displayed, hurting the performance even more.
Here is the code for drawing the grid:
// draw grid
painter.fillRect(0,0,areaWidth, areaHeight, QColor(255,255,255));
painter.setPen(QPen(QBrush(QColor(240,240,255)), 1, Qt::SolidLine, Qt::FlatCap));
int numXLines = areaWidth/mSIToPixelScale + 1;
int numYLines = areaHeight/mSIToPixelScale + 1;
double width = areaWidth;
double height = areaHeight;
for (int x=0; x<numXLines;x++)
{
for (int y=0; y<numYLines; y++)
{
painter.drawLine(0,y*mSIToPixelScale,width, y*mSIToPixelScale);
painter.drawLine(x*mSIToPixelScale,0,x*mSIToPixelScale,height);
}
}
So when numXLines and numYLines in the above code reach higher values, the performance drops very hard, which makes sense. The grid will always have to be redrawn if the scale changes, but if the scale does not change, then only the drawing on top of the grid should change. How can I accomplish this?

The QWidget::paintEvent( QPaintEvent* aEvent ) is called by the framework not just when you want it. So if you want to remove some lines from the widget than you need draw them conditionally in your function.
For example:
if ( numXLines < 25 && numYLines < 25 )
{
// Draw only every second lines for example.
}
else
{
// Draw all lines.
}
But this is not the best way. Maybe you shall use larger steps between the grid lines if there too many of them.

I found where I went wrong, I was redrawing the y lines every x iteration. I fixed it by creating two separate for loops:
// add grid lines to a painter path
QPainterPath grid;
for (int x=0; x<numXLines;x++)
{
grid.moveTo(x*mSIToPixelScale, 0);
grid.lineTo(x*mSIToPixelScale, height);
}
for (int y=0; y<numYLines; y++)
{
grid.moveTo(0, y*mSIToPixelScale);
grid.lineTo(width,y*mSIToPixelScale);
}
painter.drawPath(grid);
painter.end();
Also, I think drawing onto a QImage first then drawing that image inside the paintEvent would make code more organized, so all your doing in the paintEvent is drawing from a high level.

Related

How to disable linear filtering for drawImage on canvas in javafx

I'm trying to draw scaled image on canvas in javafx. Using this code:
Image image = ...;
canvas.setWidth(scale * width);
canvas.setHeight(scale * height);
GraphicsContext gc = canvas.getGraphicsContext2D();
gc.drawImage(image, 0, 0, scale * width, scale * height);
// this gives same result
// gc.scale(scale, scale);
// gc.drawImage(editableImage, 0, 0, width, height);
It works really fast but makes blurred images like this:
This is not what I'd like to see. Instead I want to get this picture:
Which can be drawn by manually setting each pixel color with such code:
PixelReader reader = image.getPixelReader();
PixelWriter writer = gc.getPixelWriter();
for (int y = 0; y < scale * height; ++y) {
for (int x = 0; x < scale * width; ++x) {
writer.setArgb(x, y, reader.getArgb(x / scale, y / scale));
}
}
But I cannot use this approach as it's too slow. It took couple of seconds to draw 1Kb image scaled 8 times. So I ask if there's any way to disable this blurry effect for drawing on canvas?
UPD 10/07/2019:
Looks like the issue is fixed! Now GraphicsContext should have property "image smoothing" controlling this behavior.
INITIAL ANSWER
I guess I've found answer to my question. As this issue says that there's no way to specify filtering options in graphics context.
Description:
When drawing an image in a GraphicsContext using the drawImage()
method to enlarge a small image to a larger canvas, the image is being
interpolated (possibly using a bilinear or bicubic algorithm). But
there are times like when rendering color maps (temperature,
zooplancton, salinity, etc.) or some geographical data (population
concentration, etc.) where we want to have no interpolation at all
(ie: use the nearest neighbor algorithm instead) in order to represent
accurate data and shapes.
In Java2D, this is possible by setting the appropriate
RenderingHints.KEY_RENDERING on the Graphics2D at hand. Currently on
JavaFX's GraphicsContext there is no such way to specify how the image
is to be interpolated.
The same applies when shrinking images too.
This could be expanded to support a better form of smoothing for the
"smooth" value that is available in both Image and ImageView and that
does not seem to work very well currently (at least on Windows).
The issue was created in 2013 but it's still untouched so unlikely it will be resolved soon.

QGraphicsItem leaves artifacts when changing boundingRect

My LineItem inheriting from QGraphicsLineItem can change its pen width.
I have created a boundingRect that uses the QGraphicsLineItem::boundingRect adjusted by pads that get calculated based on pen width and arrows. It works.
void LineItem::calculateStuff() // called on any change including pen width
{
qreal padLeft, padRight, padT;
padLeft = 0.5 * m_pen.width(); // if no arrows
padT = padLeft;
padRight = padLeft;
m_boundingRect = QGraphicsLineItem::boundingRect().adjusted(-padLeft, -padT, padRight, padT);
update();
}
QRectF LineItem::boundingRect() const
{
return m_boundingRect;
}
QPainterPath LineItem::shape() const
{
QPainterPath p;
p.addRect(m_boundingRect);
return p;
}
There is only one artifact that I get:
if I increase the pen width, then decrease it, I get traces:
these of course disappear as soon as i move mouse or any action (I had a hard time getting the screen shots)
As pretty as they are (seriously I consider them a "feature :-) ) - I am trying to eliminate them. I tried to remember previous bounding rectangle, and update the item with the previous bounding rectangle - i thought that was what the option was for - but it didn't work.
QRectF oldRect = selectedItem->boundingRect();
item->setItemPenWidth(p);
selectedItem->update(oldRect);
selectedItem->update();
My viewport has
setViewportUpdateMode(BoundingRectViewportUpdate);
If I change to
setViewportUpdateMode(FullViewportUpdate);
I don't get artifacts - but I think this will impact performance which is a major constraint.
How can I fix these artifacts - that only occur in that specific situation, decreasing pen width / decreasing bounding rect of line, without impacting performance ?
Simple fix... I had to add
prepareGeometryChange();
in my calculateStuff() function.
I have not seen any changes from this before, it is the first time I change my boundingRect that it does not update seamlessly.

Don't repaint instantly after setVisble in Qt

I'm using QPushButton in my mineSweeping game.
After changing from easy mode to hard mode, the number of QPushButton is supposed to change from 9x9 to 30x16.
So, I add QPushButton with the largest number(which is of hard mode) to GridLayout in constructor of MainWindow.
btnArr = new QPushButton[HARD_WIDTH * HARD_HEIGHT]; // member element
int index = 0;
for (int i = 0; i < HARD_HEIGHT; ++i) {
for (int j = 0; j < HARD_WIDTH; ++j) {
ui->mainGrid->addWidget(&btnArr[index], i, j, 1, 1,
Qt::AlignCenter);
++index;
}
}
Then if the user change mode(e.g.: easy mode to hard mode), resetBtn(HARD_WIDTH, HARD_HEIGHT); will be called.
void MainWindow::resetBtn(const int width, const int height)
{
int index = 0;
for (int i = 0; i < HARD_HEIGHT; ++i) {
for (int j = 0; j < HARD_WIDTH; ++j) {
if (j < width && i < height) {
btnArr[index].setVisible(true);
} else {
btnArr[index].setVisible(false);
}
++index;
}
}
}
The problem is that it seems the widget repaints each time setVisible is called. So in the hard mode case, it will be called 30x16 times, which caused strange effect like this:
So how can I set the widget not repaint during this loop?
Thanks in advance.
I think that you are trying to solve the wrong problem. You shouldn't be updating the widgets like this. If you necessarily want to, then hiding the parent widget of the layout before the change and showing it again afterwards should work.
A better approach is to use QStackedWidget and have all the boards prepared initially. Switching to a different board is then simply a matter of switching the active widget.
The total number of QPushButton is really big: 30x16 = 480!!! I don't use to make people change their programming logic, but in this case I think that using QPushButtons is not the better approach. The layout must have a really bad time trying to move the objects as they are added, and perhaps you are reaching some internal limit in refresh time for the layout to be repainted.
What I would have done is a custom widget with a custom paintEvent method. There you can divide its width and height in the number of columns and rows that you wish and paint the cells with pixmaps as the game is played.
For the mouse interaction, the best would have been to override the mousePressEvent with a custom logic that calculates the mouse position in the grid and calls the corresponding methods or emits signals indicating the position of the event. Not very hard to code. You can also use the event->buttons() method to know which mouse button was pressed and emit different signals if you wish.
I don't use to answer telling that it is better to change your whole program, but in this case I think you are going "the hard way". I know this is not the kind of answer you are looking for, but consider this possibility.
You could try calling setUpdatesEnabled(false) on the parent widget before doing those "massive" changes, and re-enable it once all is done.
Maybe I'm wrong but as far as I know Qt doesn't render the widget right after setVisible() is called. Rendering happens as a result of a 'render' event, except if you call render() manually.
From the official Qt doc (http://qt-project.org/doc/qt-4.8/qwidget.html#paintEvent):
Qt also tries to speed up painting by merging multiple paint events
into one. When update() is called several times or the window system
sends several paint events, Qt merges these events into one event with
a larger region (see QRegion::united()). The repaint() function does
not permit this optimization, so we suggest using update() whenever
possible.
My instincts tell me that it's not a painting problem rather a layouting (not enough space to present every button in 'hard mode').
Also I think you shouldn't use Qt::AlignCenter when you add your buttons to the layout, it will try to centerize every button in the layout. You should rather centerize the parent widget of the layout (if you don't have one create one and centerize it) and set size-policies correctly (QWidget setSizePolicy).
But as #Mat suggested if this really is a painting problem you can use setUpdatesEnabled(false/true) (if setUpdatesEnabled solves your problem please accept #Mat 's solution)
Try to enabling/disabling instead of visible/invisible:
void MainWindow::resetBtn(const int width, const int height)
{
int index = 0;
for (int i = 0; i < HARD_HEIGHT; i++)
for (int j = 0; j < HARD_WIDTH; j++)
btnArr[index++].setEnabled(j < width && i < height);
}

Should i re-draw SurfaceLayer on every frame?

I've create simple example: background surface layer and 10 small "dots" on it (10 surface layers 10x10 px each filled with color via fillRect()). Paint method simply moves the dots around periodically:
private SurfaceLayer background;
private List<Layer> dots = new ArrayList<Layer>();
#Override
public void init()
{
background = graphics().createSurfaceLayer(graphics().width(), graphics().height());
background.surface().setFillColor(Color.rgb(100, 100, 100));
background.surface().fillRect(0, 0, graphics().width(), graphics().height());
graphics().rootLayer().add(background);
for (int i = 0; i < 10; i++)
{
SurfaceLayer dot = graphics().createSurfaceLayer(10, 10);
dot.surface().clear();
dot.surface().setFillColor(Color.rgb(250, 250, 250));
dot.surface().fillRect(0, 0, 10, 10);
dot.setDepth(1);
dot.setTranslation(random()*graphics().width(), random()*graphics().height());
dots.add(dot);
graphics().rootLayer().add(dot);
}
}
#Override
public void paint(float alpha)
{
for (Layer dot : dots)
{
if (random() > 0.999)
{
dot.setTranslation(random()*graphics().width(), random()*graphics().height());
}
}
}
Somehow java version draws all dots while html and android version draw only 1.
Manual doesn't clearly say if i should re-draw all these dots in every paint() call. And as far as i understood SurfaceLayer is meant for cases when you do not modify layer on every frame (so same buffer can be reused?), but this doesn't work.
So can you guys help me with correct SurfaceLayer usage? If i just filled a rectangular on SurfaceLayer - would it ramin on this layer forever or should i fill it in each paint call? If yes - is this different from ImmeadiateLayer?
You don't need to redraw a surface layer on every call to paint. As you have shown, you draw it only when preparing it, and the texture into which you've draw will be rendered every frame without further action on your part.
If the Android and HTML backend are not drawing all of your surface layers, there must be a bug. I'll try to reproduce your test and see if it works for me.
One note: creating a giant surface the size of the screen and drawing a solid color into it is a huge waste of texture memory. Just create an ImmediateLayer that calls fillRect() on every frame, which is far more efficient than creating a massive screen-covering texture.

How to remove/clear previously drawn lines before redrawing new lines in Qt?

I am drawing few lines using paintEvent(QPaintEvent * event). Sometime later if I want to draw new lines then the previously drawn lines are not cleared/removed. How could I draw the new lines only, by removing/clearing old lines. Is there any property to clear the previously drawn lines.Please let me know.
void QGraphWidget::paintEvent(QPaintEvent * event)
{
const QRect & rect = event->rect();
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
drawLines(painter, rect);//calling painter.drawLine() inside
}
Thanks...
In my opinion, the correct approach is to clear the area before drawing new lines. This can be achieved either by using the autoFillBackground property as proposed by Stephen Chu, or by calling manually the QPainter::eraseRect() before drawing your lines in the QGraphWidget::paintEvent method.
void QGraphWidget::paintEvent(QPaintEvent * event)
{
const QRect & rect = event->rect();
QPainter painter(this);
painter.eraseRect(rect);
painter.setRenderHint(QPainter::Antialiasing);
drawLines(painter, rect);
}
Another option is to draw "negatively" the lines from the previous call to QGraphWidget::paintEvent. If you store the coordinate of your old lines, you might first draw lines using the background brush and then draw your new lines using the foreground brush. See code sample that might fit into you drawLines method. Note that if you draw something else in your widget, drawing the lines negatively might erase some of the other graphics which is why the first approach, erasing all the GraphWidget area, is better.
// save the current brush
QBrush currentBrush = painter.brush();
// draw the old line with the background brush
painter.setBrush(background());
painter.drawLine(oldLine);
// draw the new line with the current brush
painter.setBrush(currentBrush);
painter.drawLine(newLine);
You want to fill your widget with window background color before redraw. Set autoFillBackground to true after you create the widget and Qt will handle this for you
I don't think there is a specific call to remove the line. If you're repainting the entire area each time paintEvent() function is called you shouldn't see previous lines. If you're not repainting the area, you'll have to draw over the line yourself. This code for instance is drawing a line in a different position each time the method is invoked:
QPainter painter(this);
painter.setBrush(QBrush(Qt::red));
painter.drawRect(rect());
painter.setPen(QPen(Qt::yellow));
static int i = 0;
painter.drawLine(QPointF(i, i), QPointF(i, height() - i));
i++;
but "previous lines" are cleared completely. If you want to keep those you'll have to repaint only a specific area or you'll have to repaint those.
This is one way to delete whole line from QT.
me->setFocus();
int pos;
QTextCursor tc= me->textCursor();
pos=tc.columnNumber();
tc.select(QTextCursor::LineUnderCursor);
QString str=tc.selectedText();
tc.removeSelectedText();
tc.movePosition(QTextCursor::NextBlock,QTextCursor::MoveAnchor);
tc.insertText(str);
tc.insertBlock();
tc.movePosition(QTextCursor::PreviousBlock,QTextCursor::MoveAnchor);
tc.movePosition(QTextCursor::StartOfLine,QTextCursor::MoveAnchor);
me->setTextCursor(tc);
return true;

Resources