QPainter DrawImage CenterAligned - qt

Is there any way to draw an image on QPainter center aligned? I see QPainter::drawText gives us this provision but drawImage does not. I have one source rect, target rect and an image. when the source size is small the image gets drawn on the left side of the page. I want it to be printed center aligned.

The painter doesn't have a size, but the device() it paints on does. You can use QRect(painter.device()->width(), painter.device()->height()) as the rectangle where you want to center your image in.
Then you'd paint the image centered like so:
QImage source;
QPainter painter(...);
...
QRect rect(source.rect());
QRect devRect(0, 0, painter.device()->width(), painter.device()->height());
rect.moveCenter(devRect.center());
painter.drawImage(rect.topLeft(), source);

I would try to do the following (please follow the source code comments):
The sample image that should be drawn
// The image to draw - blue rectangle 100x100.
QImage img(100, 100, QImage::Format_ARGB32);
img.fill(Qt::blue);
In the paint event handler
[..]
QRect source(0, 0, 100, 100);
QRect target(0, 0, 400, 400);
// Calculate the point, where the image should be displayed.
// The center of source rect. should be in the center of target rect.
int deltaX = target.width() - source.width();
int deltaY = target.height() - source.height();
// Just apply coordinates transformation to draw where we need.
painter.translate(deltaX / 2, deltaY / 2);
painter.drawImage(source, img);
Of course you should check whether source rectangle is smaller than the target before applying this approach. I omitted that code for simplicity reasons just to demonstrate how you can center your image.

I wanted to show a more complete example with a variable image size that stays within the bounds of the area provided to add to the other great answers.
void ImageView::paintEvent(QPaintEvent*)
{
if (this->imageBuffer.empty()){ return; }
double widgetWidth = this->width();
double widgetHeight = this->height();
QRectF target(0, 0, widgetWidth, widgetHeight);
QImage tempQImage = *this->imageBuffer.at(this->imageBuffer.count()-1);
tempQImage = tempQImage.scaled(rect().size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
double imageSizeWidth = static_cast<double>(tempQImage.width());
double imageSizeHeight = static_cast<double>(tempQImage.height());
QRectF source(0.0, 0.0, imageSizeWidth, imageSizeHeight);
int deltaX = 0;
int deltaY = 0;
if(source.width() < target.width())
deltaX = target.width() - source.width();
else
deltaX = source.width() - target.width();
if(source.height() < target.height())
deltaY = target.height() - source.height();
else
deltaY = source.height() - target.height();
QPainter painter(this);
painter.translate(deltaX / 2, deltaY / 2);
painter.drawImage(source, tempQImage);
}

Related

writing a gauge widget using 2 images in qt

I'm writing a gauge widget, using QT, that is constructed from 2 separate images, one as background and the other as Needle. I reimplement paintEvent function as follow:
void myGaugeWidget::paintEvent(QPaintEvent *pe)
{
QPainter painter(this);
QPixmap bkgImage(bkgImgPath);
painter.drawPixmap(0, 0, width(), height(), bkgImage);
const double thetaDeg = 30.0;
QPixmap needle(needles[i].imgPath);
int needleWidth = 200;
int needleHeight = 200;
int anchorX = 20;
int anchorY = 30;
const int centerX = width()/2;
const int centerY = height()/2;
QTransform rm = QTransform().translate(-anchorX,- anchorY).rotate(thetaDeg).translate(centerX,centerY);
needle = needle.transformed(rm);
painter.drawPixmap(0,0, needle);
}
this code rotates my needle correctly but its position is not correct.
can anybody help me?
thanks.
This most likely would depend on your images and widget size. I have tried your code and it seems to me that QTransform().translate() is not doing anything in a QPixmap. I tried to give extreme values for translate() and removed rotate() - the image does not move.
I already have have my own implementation for a gauge. This is with painter transformation instead of the image. My images are of dimensions:
Gauge Background: 252x252 (there is some external blurring effects around the circle boundaries, making the background image larger than it seems)
Needle: 7x72 ( the image dimensions wrap around the boundaries of the needle itself)
Needle roation center (with respect to the background): 126, 126 (divide background size by 2)
The needle image points upward
For this setup, here is my paintEvent() with some explanations:
void myGaugeWidget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
//draw the background which is same size as the widget.
painter.drawPixmap(0, 0, bg.width(), bg.height(), bg);
//Calculate the angle of rotation.
//The gauge I am using has a cutout angle of 120 degrees at the bottom (symmetric)
float needleAngle = -120/*offset for start rotation*/ + ((value-minValue)*240/*total sweep of the gauge*//(maxValue-minValue));
painter.save();
//translate the painter to the roation center and then perform the rotation
painter.translate(126, 126);
painter.rotate(needleAngle);
//translate the rotated canvas to adjust for the height of the needle.
//If you don't do this, your needle's tip will be at the rotation center
painter.translate(0, -72);
//draw the needle and adjust for the width with the x value
painter.drawPixmap(-needle.width()/2, 0, needle.width(), needle.height(), needle);
painter.restore();
}

How to size the texture to occupy only a portion of a QQuickItem UI

I have overriden updatePaintNode in the following way to draw an OpenGL texture on a QQuickItem derived class called MyQQuickItem here.
QSGNode *MyQQuickItem::updatePaintNode(QSGNode * oldNode, QQuickItem::UpdatePaintNodeData * /*updatePaintNodeData*/)
{
QSGSimpleTextureNode * textureNode = static_cast<QSGSimpleTextureNode *>(oldNode);
if (!textureNode) {
textureNode = new QSGSimpleTextureNode();
}
QSize size(800, 800);
// myTextureId is a GLuint here
textureNode.reset(window()->createTextureFromId(myTextureId, size));
textureNode->setTexture(my_texture);
textureNode->markDirty(QSGBasicGeometryNode::DirtyMaterial);
QSizeF myiewport = boundingRect().size();
qreal xOffset = 0;
qreal yOffset = 10;
textureNode->setRect(xOffset, yOffset, myViewport.width(), myViewport.height());
return textureNode;
}
This renders the texture content well but covers the whole of my MyQQuickItem UI.
How can reduce the bottom margin of the texture to say fit 80% of the height of MyQQuickItem.
I want to render the texture to a portion of MyQQuickItem & leave the rest blank or black? Is that possible within updatePaintNode.
Note that the texture size is not the UI window size here. My texture size is 800 by 800. Whereas the UI window size is different and depends on the screen.
I found the answer to this:
Changing myViewport.height() gives the required end in Y direction one wishes to set. Similarly, changing myViewport.width() gives the required end in X direction one wishes to set.
4 parameters in TextureNode's setRect can stretch & fit the texture in the way one wishes within a portion of the QQuickItem.

Maintaining relative child position after applying QGraphicsItem::ItemIgnoresTransformations

I have a QGraphicsTextItem parented to a QGraphicsItem. I want the QGraphicsTextItem to always reside directly above the QGraphicsItem, but I also want the text to remain the same size when the scale factor goes below 1, i.e. the text remains the size it is at a scale factor of 1 even when the parent graphics item is scaled smaller. I have found that setting the QGraphicsItem::ItemIgnoresTransformations flag to true when the scale factor is below 1 does the trick for retaining the size.
But I can’t seem to find a way to get the position of the text to always remain above the QGraphicsItem. Is there a way to do this? I tried using deviceTransform () function, but the text still moved off of the QGraphicsItem as I scrolled out. What was worse is that some of the text items started “jiggling”, i.e. they started continuously changing their position ever so slightly, so that it looked like they were shaking. If this is the function I need to use, I guess I don’t know how to use it properly.
In the constructor of my QGraphicsItem I’ve added a QGraphicsTextItem:
fTextItem = new QGraphicsTextItem(getName(), this);
fTextItem->setFlag(QGraphicsItem::ItemIgnoresTransformations);
Here is code snippet from paint function of QGraphicsItem
qreal lod = painter->worldTransform().m22();
if(lod <= 1.0) {
fTextItem-setFlag(QGraphicsItem::ItemIgnoresTransformations);
fTextItem->setPos(fTextItem->deviceTransform(view-viewportTransform()).inverted().map(view->mapFromScene(mapToScene(0,0))));
} else {
fTextItem->setFlag(QGraphicsItem::ItemIgnoresTransformations, false);
fTextItem->setPos(0, 0);
}
My suggestion is to subclass QGraphicsSimpleTextItem in this manner:
class TextItem
: public QGraphicsSimpleTextItem
{
public:
TextItem(const QString &text)
: QGraphicsSimpleTextItem(text)
{
}
void paint(QPainter *painter,
const QStyleOptionGraphicsItem *option, QWidget *widget)
{
painter->translate(boundingRect().topLeft());
QGraphicsSimpleTextItem::paint(painter, option, widget);
painter->translate(-boundingRect().topLeft());
}
QRectF boundingRect() const
{
QRectF b = QGraphicsSimpleTextItem::boundingRect();
return QRectF(b.x()-b.width()/2.0, b.y()-b.height()/2.0,
b.width(), b.height());
}
};
QGraphicsSimpleTextItem *mText = new TextItem("Item");
scene()->addItem(mText);
mText->setFlag(QGraphicsItem::ItemIgnoresTransformations, true);
mText->setPos(itemToFollow->pos());
Disclaimer: this may be overkill for what you are trying to do. We had some additional restrictions in our project that made this solution the easiest for us.
We had to do something similar in a project, and it ended up being easiest for us to not use ItemIgnoresTransformations and instead roll our own transform. Here is the main function we use to create a translation-only (no scaling) transform for drawing an item at a specific location. You might be able to modify it for your usage.
static QTransform GenerateTranslationOnlyTransform(
const QTransform &original_transform,
const QPointF &target_point) {
// To draw the unscaled icons, we desire a transform with scaling factors
// of 1 and shearing factors of 0 and the appropriate translation such that
// our icon center ends up at the same point. According to the
// documentation, QTransform transforms a point in the plane to another
// point using the following formulas:
// x' = m11*x + m21*y + dx
// y' = m22*y + m12*x + dy
//
// For our new transform, m11 and m22 (scaling) are 1, and m21 and m12
// (shearing) are 0. Since we want x' and y' to be the same, we have the
// following equations:
// m11*x + m21*y + dx = x + dx[new]
// m22*y + m12*x + dy = y + dy[new]
//
// Thus,
// dx[new] = m11*x - x + m21*y + dx
// dy[new] = m22*y - y + m12*x + dy
qreal dx = original_transform.m11() * target_point.x()
- target_point.x()
+ original_transform.m21() * target_point.y()
+ original_transform.m31();
qreal dy = original_transform.m22() * target_point.y()
- target_point.y()
+ original_transform.m12() * target_point.x()
+ original_transform.m32();
return QTransform::fromTranslate(dx, dy);
}
To use, take the QPainter transform that is passed to the paint method and do something like:
painter->save();
painter->setTransform(GenerateTranslationOnlyTransform(painter->transform(),
some_point));
// Draw your item.
painter->restore();
I've found another solution, which does not involve messing with any transformations or by hand scaling/positioning. There is a hint in QGraphicsItem::ItemIgnoresTransformations flag description:
QGraphicsItem::ItemIgnoresTransformations
The item ignores inherited transformations (i.e., its position is
still anchored to its parent, but the parent or view rotation, zoom or
shear transformations are ignored). [...]
And that's the key! We need two items: a parent that will keep the relative position (without any flags set) and a child item that will do the drawing at parent's (0,0) point (with QGraphicsItem::ItemIgnoresTransformations flag set). Simple as that!
I've encapsulated this functionality into a single class - here is some code:
#include <QGraphicsItem>
#include <QPainter>
class SampleShape : public QGraphicsItem
{
private:
/* This class implements shape drawing */
class SampleShapeImpl : public QGraphicsItem
{
public:
SampleShapeImpl (qreal len, QGraphicsItem *parent = nullptr)
: QGraphicsItem(parent), m_len(len)
{
/* ignore transformations (!) */
setFlag(QGraphicsItem::ItemIgnoresTransformations);
}
QRectF boundingRect (void) const override
{
/* sample bounding rectangle */
return QRectF(-m_len, -m_len, m_len*2, m_len*2);
}
void paint (QPainter *painter, const QStyleOptionGraphicsItem *, QWidget *) override
{
/* draw a shape, (0,0) is an anchor */
painter->drawLine(0, -m_len, 0, m_len);
painter->drawLine(-m_len, 0, m_len, 0);
// ...
}
private:
qreal m_len; // sample shape parameter
};
public:
/* This is actually almost an empty class, you only need to set
* a position and pass any parameters to a SampleShapeImpl class.
*/
SampleShape (qreal x, qreal y, qreal len, QGraphicsItem *parent = nullptr)
: QGraphicsItem(parent), m_impl(len, this) // <-- IMPORTANT!!!
{
/* set position at (x, y), view transformations will apply */
setPos(x, y);
}
QRectF boundingRect (void) const override
{
return QRectF(); // it's just a point, no size
}
void paint (QPainter *, const QStyleOptionGraphicsItem *, QWidget *) override
{
// empty, drawing is done in SampleShapeImpl
}
private:
SampleShapeImpl m_impl;
};
Great answer by Dave Mateer! I had the problem that I wanted to define a different scale factor at different zoom levels. This is how I did it:
void MyGraphicsItem::paint(QPainter * painter, const QStyleOptionGraphicsItem* option, QWidget* widget)
{
//save painter for later operations
painter->save();
QTransform originalTransform = painter->transform();
QPointF originalCenter = rect().center();
qreal dx = originalTransform.m11() * originalCenter.x() + originalTransform.m21() * originalCenter.y() + originalTransform.m31();
qreal dy = originalTransform.m22() * originalCenter.y() + originalTransform.m12() * originalCenter.x() + originalTransform.m32();
//normally our target scale factor is 1, meaning the item has keeps its size, regardless of zoom
//we adjust the scale factor though when the item is smaller than one pixel in comparison to the background image
qreal factor = 1.0;
//check if scale factor if bigger that the item size, and thus it occupies less that a pixel in comparision to the background image
if (rect().width() < originalTransform.m11()) {
//calculate adjusted scale factor
factor = originalTransform.m11() / rect().width();
}
//adjust position according to scale factor
dx -= factor * originalCenter.x();
dy -= factor * originalCenter.y();
//set the new transform for painting
painter->setTransform(QTransform::fromScale(factor, factor) * QTransform::fromTranslate(dx, dy));
//now paint...
QGraphicsXYZItem::paint(painter, option, widget);
//restore original painter
painter->restore();
}
You do need to adjust the bounding rectangle too in that case:
QRectF MyGraphicsItem::boundingRect() const
{
QRectF rect = QGraphicsEllipseItem::boundingRect();
//this is a bit hackish, let me know if you know another way...
if (scene() != NULL && scene()->views().at(0) != NULL)
{
//get viewport transform
QTransform itemTransform = scene()->views().at(0)->transform();
QPointF originalCenter = rect.center();
//calculate back-projected original size of item
qreal realSizeX = rect.width() / itemTransform.m11();
qreal realSizeY = rect.height() / itemTransform.m11();
//check if scale factor is bigger that the item size, and thus it occupies less that a pixel in comparison
//to the background image and adjust size back to equivalent of 1 pixel
realSizeX = realSizeX < 1.0 ? 1.0 : realSizeX;
realSizeY = realSizeY < 1.0 ? 1.0 : realSizeY;
//set adjusted position and size according to scale factor
rect = QRectF(rect.center().x() - realSizeX / 2.0, rect.center().y() - realSizeY / 2.0, realSizeX, realSizeY);
}
return rect;
}
With this solution the item work very well in my case.
Adding to Dave Mateer's answer, I think it'd be helpful to add that in some scenario, you should also maintain proper bounding rectangle (as well as shape) of the object. For me, I need to modify boundingRect() a little too for proper object selection behavior. Remember that the bounding rect of the object will be scaled and transformed as usual if we do NOT use ItemIgnoresTransformations flag. So we also need to rescale the boundingRect to maintain the view independence effect.
To maintain the view-independent bounding rectangle turns out to be quite easy: just grab the scaling factor from deviceTransform(m_view->viewportTransform()).inverted().m11() and multiply this constant to your local coordinate bounding rectangle. For example:
qreal m = this->deviceTransform(m_view->viewportTransform()).inverted().m11();
return QRectF(m*(m_shapeX), m*(m_shapeY),
m*(m_shapeR), m*(m_shapeR));
here is a solution I devised of very moderate complexity :
1) Get the boundingRect() of the parent and map it to scene
2) take the minimum X and Y of this list of points, this is the real origin of your item, in scene coordinates
3) set the position of the child
In Pyside :
br = parent.mapToScene(parent.boundingRect())
realX = min([item.x() for item in br])
realY = min([item.y() for item in br])
child.setPos(parent.mapFromScene(realX, realY)) #modify according to need

Zooming to point of interest

I have the following variables:
Point of interest which is the position(x,y) in pixels of the place
to focus.
Screen width,height which are the dimensions of the window.
Zoom level which sets the zoom level of the camera.
And this is the code I have so far.
void Zoom(int pointOfInterestX,int pointOfInterstY,int screenWidth,
int screenHeight,int zoomLevel)
{
glScalef(1,1,1);
glTranslatef( (pointOfInterestX/2) - (screenWidth/2), (pointOfInterestY/2) - (screenHeight/2),0);
glScalef(zoomLevel,zoomLevel,1);
}
And I want to do zoom in/out but keep the point of interest in the middle of the screen. but so far all of my attempts have failed.
You can start the rendering of your frame like this:
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
GLdouble left = (0 - pointOfInterestX) / zoomLevel + pointOfInterestX;
GLdouble right = (WindowW - pointOfInterestX) / zoomLevel + pointOfInterestX;
GLdouble bottom = (WindowH - pointOfInterestY) / zoomLevel + pointOfInterestY;
GLdouble top = (0 - pointOfInterestY) / zoomLevel + pointOfInterestY;
glOrtho(left, right, bottom, top, -1, 1);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();

HowTo stick QDialog to Screen Borders like Skype do?

A long time ago I tried to find method how to stick QDialog window to screen borders for my small projects like Skype windows do it, but I failed. May be I was looking this code not in the right place, so now I'm looking the solution here, on stack! :)
So, does any one have a deal with some kind of such code, links, samples?
In my opinion, we have to reimplement QDialog moveEvent function, like below, but that code does not working:
void CDialog::moveEvent(QMoveEvent * event) {
QRect wndRect;
int leftTaskbar = 0, rightTaskbar = 0, topTaskbar = 0, bottomTaskbar = 0;
// int top = 0, left = 0, right = 0, bottom = 0;
wndRect = this->frameGeometry();
// Screen resolution
int screenWidth = QApplication::desktop()->width();
int screenHeight = QApplication::desktop()->height();
int wndWidth = wndRect.right() - wndRect.left();
int wndHeight = wndRect.bottom() - wndRect.top();
int posX = event->pos().x();
int posY = event->pos().y();
// Snap to screen border
// Left border
if (posX >= -m_nXOffset + leftTaskbar &&
posX <= leftTaskbar + m_nXOffset) {
//left = leftTaskbar;
this->move(leftTaskbar, posY);
return;
}
// Top border
if (posY >= -m_nYOffset &&
posY <= topTaskbar + m_nYOffset) {
//top = topTaskbar;
this->move(posX, topTaskbar);
return;
}
// Right border
if (posX + wndWidth <= screenWidth - rightTaskbar + m_nXOffset &&
posX + wndWidth >= screenWidth - rightTaskbar - m_nXOffset) {
//right = screenWidth - rightTaskbar - wndWidth;
this->move(screenWidth - rightTaskbar - wndWidth, posY);
return;
}
// Bottom border
if (posY + wndHeight <= screenHeight - bottomTaskbar + m_nYOffset &&
posY + wndHeight >= screenHeight - bottomTaskbar - m_nYOffset) {
//bottom = screenHeight - bottomTaskbar - wndHeight;
this->move(posX, screenHeight - bottomTaskbar - wndHeight);
return;
}
QDialog::moveEvent(event);
}
Thanks.
As you thought you can achieve this in the moveEvent function.
I guess the following code do the trick but since I have nothing to test here I will write some pseudo code:
First get the available screen area:
const QRect screen = QApplication::availableGeometry(this);
// This get the screen rect where you can drag a dialog
Then get the position of your dialog relative to the desktop (if your dialog is a child of an other widget, you need to transform coordinates from widget relative to desktop relative):
const QRect dialog = geometry();
// Do here transformation
Now test if dialog is near screen border
if( abs(dialog.left()-screen.left() < OFFSET )
move(screen.left(), dialog.top();
else if( abs(dialog.top()-screen.top() < OFFSET )
move(dialog.left(), screen.top() )
// etc. for the 2 other cases
Let me know if it works
In the pos property description from the QWidget documentation, there is the following warning about moving a window inside the move event handling method.
Warning: Calling move() or setGeometry() inside moveEvent() can
lead to infinite recursion.
That said, there is no proper way to stick the dialog window inside the screen border.
Note :
The behavior you observed in KDE comes from the Window Manager. Actually, the Window Manager is the one that arranges the application windows (like dialog boxes) to show them on the screen. The KDE Window Manager has an option to make all application windows (called client) stick to the border.

Resources