Rotate Certain QWidgets in Qt - c++

I need to rotate certain elements of the UI (not all). I have tried a couple of things already and none of them have provided the solution I require.
I attempted to use the QGraphicsProxyWidget and Graphics View, this worked well for if I wanted to rotate the entire UI, but provided no way to rotate only certain elements.
I attempted to promote the widget to a custom class that overrides the paintEvent. I am unable to get this way to work though. The following is my current code
#include <QPushButton>
#include <QPainter>
#include <QPaintEvent>
class QRotateButton : public QPushButton
{
Q_OBJECT
public:
QRotateButton (QWidget *parent = 0) {}
protected:
void paintEvent(QPaintEvent *event) override {
if (isDrawing)
return QRotateButton::paintEvent(event);
isDrawing = true;
QPixmap buttonPixmap = grab();
QPainter painter(this);
painter.rotate(90);
painter.drawPixmap(0, 0, height(), width(), buttonPixmap);
isDrawing = false;
}
private:
bool isDrawing = false;
};
This returns the following errors
QWidget::repaint: Recursive repaint detected
Segmentation fault
Thanks in advance
Update
Following the comments below I changed the code to call QPushButton::paintEvent(Event) instead of QRotateButton::paintEvent(Event)
The application does now open, where as it didn't before. But unfortunately no button is shown. Whenever the paintEvent gets invoked now, the following error messages appear in the console:
QWidget::repaint: Recursive repaint detected
QWidget::paintEngine: Should no longer be called
QPainter::begin: Paint device returned engine == 0, type: 1
QPainter::rotate: Painter not active

After playing around with this further, I managed to figure it out.
#include <QPushButton>
#include <QPainter>
#include <QPaintEvent>
class QRotateButton : public QPushButton
{
Q_OBJECT
public:
QRotateButton (QWidget *) {
//Set variables to defaults
_isDrawing = false;
_recursiveCounter = 0;
_recursiveMax = 1;
_rotationSetting = 0;
_rotationAngle = 0;
_rotationX = 0;
_rotationY = 0;
_rotationWidth = 0;
_rotationHeight = 0;
}
/* Set rotation setting */
void SetRotation(int setting) {
//Set rotation setting
_rotationSetting = setting;
//Call paintEvent
this->update();
}
protected:
void paintEvent(QPaintEvent *event) override {
//If its drawing then it just wants to do the normal paintEvent
if (_isDrawing)
{
QPushButton::paintEvent(event);
return;
}
//Setup the variables depending on setting
setupRotationVariables();
//Setup painter and rotate to angle
QPainter painter(this);
painter.rotate(_rotationAngle);
//Without this here, this function is called over and over,
//this stops that whilst maintaing the rotated image we want
if (_recursiveCounter > 0 && _requiresRotation)
{
_recursiveCounter--;
painter.drawPixmap(_rotationX, _rotationY, _drawing.width(), _drawing.height(), _drawing);
return;
}
//When rotating 90/270 degrees, we resize the image so that we don't stretch the image
//This is not required when flipping the image
if (_requiresRotation)
resize(_rotationWidth, _rotationHeight);
//Get the button image, this will call this function so we set _isDrawing to true whilst it's going on so meaning that it's does the base method and returns
_isDrawing = true;
_drawing = grab();
_isDrawing = false;
//Now we have the image, if rotation is being done resize the widget back to it's previous measurements
if (_requiresRotation)
resize(_rotationHeight, _rotationWidth);
//Draw the image to the picture
painter.drawPixmap(_rotationX, _rotationY, _drawing.width(), _drawing.height(), _drawing);
//Depending on if this has rotated or not, setup the catch the tailend of the rotation event
if (_requiresRotation)
_recursiveCounter = _recursiveMax;
else
_recursiveCounter = 0;
}
private:
/* Stops Recursive resizing when resizing is required */
int _recursiveCounter;
int _recursiveMax;
/* If the widget is currently drawing, stops certain recursive calls */
bool _isDrawing;
/* If the widget needs to rotated instead of flipped */
bool _requiresRotation;
/* Contains image of previous render of button, for use in recursive calls */
QPixmap _drawing;
/* Rotation Setting (0-3) defines rotation type. This is used since rotations should only be multiples of 90 */
int _rotationSetting;
/* Variables after rotation */
int _rotationAngle;
int _rotationX;
int _rotationY;
int _rotationWidth;
int _rotationHeight;
/* Fetch variables based on Rotation Setting */
void setupRotationVariables () {
//Figure out what rotation is required
switch (_rotationSetting)
{
case 0: //Up side up
_rotationAngle = 0;
_rotationWidth = width();
_rotationHeight = height();
_rotationX = 0;
_rotationY = 0;
_requiresRotation = false;
break;
case 1: //Right side up
_rotationAngle = 90;
_rotationWidth = height();
_rotationHeight = width();
_rotationX = 0;
_rotationY = -width();
_requiresRotation = true;
break;
case 2: //Down side up
_rotationAngle = 180;
_rotationWidth = width();
_rotationHeight = height();
_rotationX = -width();
_rotationY = -height();
_requiresRotation = false;
break;
case 3: //Left side up
_rotationAngle = 270;
_rotationWidth = height();
_rotationHeight = width();
_rotationX = -height();
_rotationY = 0;
_requiresRotation = true;
break;
default:
printf("INVALID ROTATION\n");
break;
}
}
};
Note
This will still throw QWidget::repaint: Recursive repaint detected every time paintEvent is called, but we expect this and it is handled in code. Whilst I would prefer it not show, I am unable to figure out any way to stop it.

Related

Gtkmm : Drawing with cairo

Using Gtkmm and Cairo, I want to be able to draw different shapes on photos. In the header bar of my window, I have two buttons representing shapes to draw (circle and rectangle). When you click one of them, you can draw its associated shape. Here is mt code:
MyWindow.cpp
#include "MyWindow.h"
MyWindow::MyWindow()
: circleButton("circle"),
rectangleButton("rectangle ") {
set_default_size(700, 700);
set_position(Gtk::WIN_POS_CENTER);
header.set_show_close_button(true);
header.pack_start(rectangleButton);
header.pack_start(circleButton);;
set_titlebar(header);
// Dwg is an instance of Drawing class
circleButton.signal_clicked().connect([&] {
Dwg.switch_to_circle();
});
rectangleButton.signal_clicked().connect([&] {
Dwg.switch_to_rectangle();
});
add(Dwg);
show_all();
}
Drawing.h
#ifndef DRAWING_H
#define DRAWING_H
#include <gtkmm.h>
#include <cairo/cairo.h>
class MyDrawing : public Gtk::Layout {
public:
MyDrawing();
~MyDrawing();
void switch_to_circle();
void switch_to_rectangle();
protected:
virtual bool draw_image(const Cairo::RefPtr<::Cairo::Context> &cr);
virtual bool draw_rectangle(const Cairo::RefPtr<::Cairo::Context> &cr);
virtual bool draw_circle(const Cairo::RefPtr<::Cairo::Context> &cr);
private:
Glib::RefPtr<Gdk::Pixbuf> pix;
double beginPoint_x, beginPoint_y, endPoint_x, endPoint_y, lineWidth,width,height;
bool isDrawRectangle;
};
#endif // DRAWING_H
Drawing.cpp
#include <iostream>
#include "MyDrawing.h"
#include <cairomm/context.h>
#include <cairomm/surface.h>
MyDrawing::MyDrawing()
: isDrawRectangle(true),
width(20),
height(20) {
pix = Gdk::Pixbuf::create_from_file("file.svg", 500, 500);
if (pix) {
this->signal_draw().connect(sigc::mem_fun(*this, &MyDrawing::draw_image));
}
add_events(Gdk::BUTTON1_MOTION_MASK | Gdk::BUTTON_PRESS_MASK);
signal_button_press_event().connect([&](GdkEventButton *e) {
this->beginPoint_x = e->x;
this->beginPoint_y = e->y;
if(isDrawRectangle) {
this->signal_draw().connect(sigc::mem_fun(*this, &MyDrawing::draw_rectangle));
queue_draw();
}
else {
this->signal_draw().connect(sigc::mem_fun(*this, &MyDrawing::draw_circle));
queue_draw();
}
return true;
});
signal_motion_notify_event().connect([&](GdkEventMotion *e) {
this->endPoint_x = e->x;
this->endPoint_y = e->y;
width = endPoint_x - beginPoint_x;
height = endPoint_y - beginPoint_y;
if(isDrawRectangle) {
this->signal_draw().connect(sigc::mem_fun(*this, &MyDrawing::draw_rectangle));
queue_draw();
}
else {
this->signal_draw().connect(sigc::mem_fun(*this, &MyDrawing::draw_circle));
queue_draw();
}
return true;
});
}
MyDrawing::~MyDrawing() = default;
bool MyDrawing::draw_image(const Cairo::RefPtr<::Cairo::Context> &cr) {
std::cout << "signal img" << std::endl;
if (pix) {
cr->save();
Gdk::Cairo::set_source_pixbuf(cr, pix, 100, 100);
cr->rectangle(0, 0, get_width(), get_height());
cr->fill();
cr->restore();
}
return false;
}
bool MyDrawing::draw_rectangle(const Cairo::RefPtr<::Cairo::Context> &cr) {
std::cout << "signal square" << std::endl;
cr->save();
cr->set_line_width(10);
cr->set_source_rgba(0., 0., 1., 1.);
cr->rectangle(beginPoint_x, beginPoint_y, width, height);
cr->stroke();
cr->save();
cr->restore();
return false;
}
bool MyDrawing::draw_circle(const Cairo::RefPtr<::Cairo::Context> &cr) {
std::cout << "signal square" << std::endl;
cr->save();
cr->set_line_width(10);
cr->set_source_rgba(0., 0., 1., 1.);
cr->arc(beginPoint_x, beginPoint_y, width, 0, 2 * M_PI);
cr->stroke();
cr->restore();
return false;
}
void MyDrawing::switch_to_circle() {
isDrawRectangle = false;
}
void MyDrawing::switch_to_rectangle() {
isDrawRectangle = true;
}
When I click another shape, the previous shape keeps being displayed on the drawing area and the new shape is drawn on it. On the other hand, when the signal is disconnected, the corresponding shape also disappears from the screen. How could I make sure the shapes keep being displayed?
I am not sure exactly what made you inherit from Gtk::Layout instead of using a standard Gtk::DrawingArea, but I created a simplified (and working) example using a design similar to yours.
The basic idea is that when the user is done drawing a shape (stops the drag and releases the mouse button), the following happens:
The current state of the window (in terms of what is drawn on it) is saved to a Gtk::Pixbuf.
That Gtk::PixBuf is painted on the window.
This means that in 1., the last drawn shaped is also saved in the buffer. When 2. happens, is repainted on the window and hence does not go away. Here is the code, which you will need to adapt a bit to your case. First, a draw helper:
class DrawHelper : public Gtk::Layout
{
public:
DrawHelper();
~DrawHelper();
private:
bool draw_image(const Cairo::RefPtr<::Cairo::Context>& p_context);
bool draw_rectangle(const Cairo::RefPtr<::Cairo::Context>& p_context);
bool add_current_shape(const Cairo::RefPtr<::Cairo::Context>& p_context);
Glib::RefPtr<Gdk::Pixbuf> m_buffer;
double m_startX;
double m_startY;
double m_endX;
double m_endY;
double m_width;
double m_height;
sigc::connection m_drawConnection;
};
which is responsible to do the actual drawing and handle connections. It is implemented like so:
DrawHelper::DrawHelper()
{
// Create a pixel buffer containing the background image:
m_buffer = Gdk::Pixbuf::create_from_file("file.svg", DEFAULT_WIDTH, DEFAULT_HEIGHT);
signal_draw().connect(sigc::mem_fun(*this, &DrawHelper::draw_image));
// Enable signals:
add_events(Gdk::BUTTON1_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK);
// Save initial pointer position when clicked:
signal_button_press_event().connect(
[this](GdkEventButton* p_event)
{
m_startX = p_event->x;
m_startY = p_event->y;
return true;
});
// Update rectangle when mouse is dragged:
signal_motion_notify_event().connect(
[this](GdkEventMotion* p_event)
{
m_endX = p_event->x;
m_endY = p_event->y;
m_width = m_endX - m_startX;
m_height = m_endY - m_startY;
signal_draw().connect(sigc::mem_fun(*this, &DrawHelper::draw_rectangle));
queue_draw();
return true;
});
// Change background so it includes the shape just drawn by
// the user:
signal_button_release_event().connect(
[this](GdkEventButton* p_event)
{
// Notice we save to connection to later disconnect it:
m_drawConnection = signal_draw().connect(sigc::mem_fun(*this, &DrawHelper::add_current_shape));
return true;
});
}
DrawHelper::~DrawHelper() = default;
bool DrawHelper::draw_image(const Cairo::RefPtr<::Cairo::Context>& p_context)
{
Gdk::Cairo::set_source_pixbuf(p_context, m_buffer, 0, 0);
p_context->paint();
return false;
}
bool DrawHelper::draw_rectangle(const Cairo::RefPtr<::Cairo::Context>& p_context)
{
p_context->save();
p_context->set_line_width(2);
p_context->rectangle(m_startX, m_startY, m_width, m_height);
p_context->stroke();
p_context->restore();
return false;
}
bool DrawHelper::add_current_shape(const Cairo::RefPtr<::Cairo::Context>& p_context)
{
// Save the current drawing, including the last drawn
// shape. This will become the new background (which will
// visually preserve the last drawn shape).
m_buffer = Gdk::Pixbuf::create(p_context->get_target(), 0, 0, DEFAULT_WIDTH, DEFAULT_HEIGHT);
Gdk::Cairo::set_source_pixbuf(p_context, m_buffer, 0, 0);
p_context->paint();
// We disconnect the signal because we do not want it
// to keep getting called:
m_drawConnection.disconnect();
return false;
}
Then, a window to hold this helper and display it to the user:
class MyWindow : public Gtk::Window
{
public:
MyWindow();
private:
DrawHelper m_drawHelper;
};
MyWindow::MyWindow()
{
set_default_size(DEFAULT_WIDTH, DEFAULT_HEIGHT);
// Add draw helper:
add(m_drawHelper);
// Show all widgets:
show_all();
}
Then, the main to run it:
#include <gtkmm.h>
#include <cairo/cairo.h>
#include <cairomm/context.h>
#include <cairomm/surface.h>
constexpr int DEFAULT_WIDTH = 500;
constexpr int DEFAULT_HEIGHT = 500;
// DrawHelper here ...
// MyWindow here ...
int main(int argc, char *argv[])
{
auto app = Gtk::Application::create(argc, argv, "org.gtkmm.examples.base");
MyWindow window;
return app->run(window);
}
That being said, I would recommend you use a classic Gtk::DrawingArea instead and overload the on_draw signal handler. This would make all of this easier to understand, and the online documentation would be of more help to you.
If you are still interested, I have another solution for you. Instead of saving the already drawn shape on the background image, you could save their parameters directly and redraw them. I have written an example program that does just this:
#include <memory>
#include <vector>
#include <gtkmm.h>
#include <cairo/cairo.h>
#include <cairomm/context.h>
#include <cairomm/surface.h>
constexpr int DEFAULT_WIDTH = 500;
constexpr int DEFAULT_HEIGHT = 500;
constexpr double LINE_WIDTH = 2.0;
// Free functions for drawing shapes:
namespace
{
void DrawRectangle(const Cairo::RefPtr<Cairo::Context>& p_context,
double p_startX,
double p_startY,
double p_width,
double p_height)
{
p_context->save();
p_context->set_line_width(LINE_WIDTH);
p_context->set_source_rgba(0, 0, 1, 1);
p_context->rectangle(p_startX, p_startY, p_width, p_height);
p_context->stroke();
p_context->restore();
}
void DrawCircle(const Cairo::RefPtr<Cairo::Context>& p_context,
double p_startX,
double p_startY,
double p_width)
{
p_context->save();
p_context->set_line_width(LINE_WIDTH);
p_context->set_source_rgba(0, 0, 1, 1);
p_context->arc(p_startX, p_startY, p_width, 0, 2 * M_PI);
p_context->stroke();
p_context->restore();
}
}
// Shape interface:
//
// A shape represents a 2D geometric shape a user can draw on the
// Drawing area. All shapes implement a 'Draw' method which is where
// the drawing logic resides.
class IShape
{
public:
virtual ~IShape() = default;
virtual void Draw(const Cairo::RefPtr<Cairo::Context>& p_context) = 0;
};
// Rectangle shape:
class Rectangle : public IShape
{
public:
Rectangle(double p_left, double p_up, double p_width, double p_height)
: m_left{p_left}
, m_up{p_up}
, m_width{p_width}
, m_height{p_height}
{}
void Draw(const Cairo::RefPtr<Cairo::Context>& p_context) override
{
DrawRectangle(p_context, m_left, m_up, m_width, m_height);
}
private:
double m_up;
double m_left;
double m_width;
double m_height;
};
// Circle shape:
class Circle : public IShape
{
public:
Circle(double p_cX, double p_cY, double p_radius)
: m_cX{p_cX}
, m_cY{p_cY}
, m_radius{p_radius}
{}
void Draw(const Cairo::RefPtr<Cairo::Context>& p_context) override
{
DrawCircle(p_context, m_cX, m_cY, m_radius);
}
private:
double m_cX;
double m_cY;
double m_radius;
};
// Draw helper:
//
// This class represents the widget onto which the user can drawn. Under
// the hood, this is a Gtk::Drawing area with some signal handlers defined
// to draw shapes on user action.
//
// All drawing occurs in the 'on_draw' method, and all signal handlers to
// is to handle the data (e.g positions, dimentsions, etc) for the 'on_draw'
// method to work appropriately.
//
// The 'SetCurrentShape' method can be used to tell the helper which shape
// to draw.
class DrawHelper : public Gtk::DrawingArea
{
public:
enum class Shape
{
None,
Rectangle,
Circle,
};
DrawHelper()
{
add_events(Gdk::BUTTON1_MOTION_MASK | Gdk::BUTTON_PRESS_MASK | Gdk::BUTTON_RELEASE_MASK);
// Click, drag and release signal handlers:
signal_button_press_event().connect( [this](GdkEventButton* p_event){return OnButtonPressed(p_event);} );
signal_motion_notify_event().connect( [this](GdkEventMotion* p_event){return OnMouseMotion(p_event);} );
signal_button_release_event().connect([this](GdkEventButton* p_event){return OnButtonReleased(p_event);});
}
void SetCurrentShape(Shape p_shape)
{
m_currentShape = p_shape;
}
private:
// All drawing occurs here and only here:
bool on_draw(const Cairo::RefPtr<Cairo::Context>& p_context) override
{
// Draw background:
if(!m_buffer)
{
m_buffer = Gdk::Pixbuf::create_from_file("file.svg", DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
Gdk::Cairo::set_source_pixbuf(p_context, m_buffer, 0, 0);
p_context->paint();
// Draw previously drawn shapes:
for(const auto& shape : m_alreadyDrawn)
{
shape->Draw(p_context);
}
// Draw current shape:
if(m_currentShape == Shape::Rectangle)
{
DrawRectangle(p_context, m_startX, m_startY, m_width, m_height);
}
if(m_currentShape == Shape::Circle)
{
DrawCircle(p_context, m_startX, m_startY, m_width);
}
return false;
}
bool OnButtonPressed(GdkEventButton* p_event)
{
m_startX = p_event->x;
m_startY = p_event->y;
return true;
}
bool OnMouseMotion(GdkEventMotion* p_event)
{
m_endX = p_event->x;
m_endY = p_event->y;
m_width = m_endX - m_startX;
m_height = m_endY - m_startY;
queue_draw();
return true;
}
bool OnButtonReleased(GdkEventButton* p_event)
{
if(m_currentShape == Shape::Rectangle)
{
m_alreadyDrawn.push_back(std::make_unique<Rectangle>(m_startX, m_startY, m_width, m_height));
}
if(m_currentShape == Shape::Circle)
{
m_alreadyDrawn.push_back(std::make_unique<Circle>(m_startX, m_startY, m_width));
}
return true;
}
Shape m_currentShape = Shape::None;
Glib::RefPtr<Gdk::Pixbuf> m_buffer;
double m_startX;
double m_startY;
double m_endX;
double m_endY;
double m_width;
double m_height;
std::vector<std::unique_ptr<IShape>> m_alreadyDrawn;
};
// Main window:
//
// This window holds all widgets. Through it, the user can pick a shape
// to draw and use the mouse to draw it.
class MyWindow : public Gtk::Window
{
public:
MyWindow()
: m_drawRectangleBtn{"Rectangle"}
, m_drawCircleBtn{"Circle"}
{
set_default_size(DEFAULT_WIDTH, DEFAULT_HEIGHT);
m_headerBar.set_show_close_button(true);
m_headerBar.pack_start(m_drawRectangleBtn);
m_headerBar.pack_start(m_drawCircleBtn);;
set_titlebar(m_headerBar);
add(m_drawArea);
m_drawRectangleBtn.signal_clicked().connect([this](){OnRectangleBtnClicked();});
m_drawCircleBtn.signal_clicked().connect([this](){OnCircleBtnClicked();});
show_all();
}
private:
Gtk::HeaderBar m_headerBar;
Gtk::Button m_drawRectangleBtn;
Gtk::Button m_drawCircleBtn;
DrawHelper m_drawArea;
void OnRectangleBtnClicked()
{
m_drawArea.SetCurrentShape(DrawHelper::Shape::Rectangle);
}
void OnCircleBtnClicked()
{
m_drawArea.SetCurrentShape(DrawHelper::Shape::Circle);
}
};
int main(int argc, char *argv[])
{
auto app = Gtk::Application::create(argc, argv, "org.gtkmm.examples.base");
MyWindow window;
return app->run(window);
}
Each time the user releases the mouse button, the drawn shape is saved (with the parameters at the time of the release) into an std::vector as an IShape, which has a Draw, method. This method can later be called to redraw the shape. Then, in the on_draw handler, all previously drawn shapes are redrawn, leaving them on the screen. Note that I have used a Gtk::DrawingArea here, which is more typical than your approach. I wanted to show you that alternative which, in my opinion, makes cleaner code (no messing around with the handler callbacks).
On a final note, possible enhancements are possible with this (there are more, theses are just some I was thinking about while writing this):
You could reduce performance costs by caching some stuff instead of redrawing everything every time.
You could reduce performance costs by using parameters in the calls to queue_draw so that the whole widget is not constantly redrawn (only the part that changed).
You could use a factory to create the shapes. This would decouple the shape creation from the rest of the code, which would only know the IShape interface. It would also make you program easier to maintain if you ever want to add shapes.

Changing ToolButtonStyle of QToolButtons dynamically depending upon the size of window

I have created a widget class (in C++) ToolTray which is basically few QToolButtons added in QHboxLayout. These buttons are used by the user for operations like Save, Open etc. Initially, the buttons are added with toolButtonStyle set to Qt::ToolButtonTextBesideIcon.
I want to change the toolButtonStyle to Qt::ToolButtonIconOnly in resizeEvent if the new size is not sufficient to display text and icons of QToolButton. See the picture below:
If the window is resized and if the new size is sufficient to display the text and icon of all the QToolButton, then toolButtonStyle should be changed back to Qt::ToolButtonTextBesideIcon.
I tried to achieve this with following code:
void ToolTray::resizeEvent(QResizeEvent *event)
{
int totalWidth = 0;
bool mode = runBtn_->toolButtonStyle() == Qt::ToolButtonStyle::ToolButtonIconOnly;
// Mode => True => For ICON Only
// Mode => False => For ICON + Text
for (auto btn: toolBtns_) {
if (btn->isVisible())
totalWidth += btn->size().width();
}
qDebug() << "Total Width: " << totalWidth ;
qDebug() << "Event Size: " << event->size() << " Old size " << event->oldSize();
if (event->oldSize().isEmpty()) // Ignore ResizeEvent for QSize(-1,-1)
return;
if (mode) { // Already Small
if (event->size().width() < preferedFullWidth_)
return;
for (auto btn: toolBtns_) {
if (btn == moreBtn_)
continue;
btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
}
return;
}
// The QToolButtons are Text Beside Icon
if (event->size().width() >= totalWidth)
return;
qDebug() << "Here";
for (auto btn: toolBtns_)
btn->setToolButtonStyle(Qt::ToolButtonIconOnly);
preferedFullWidth_ = totalWidth;
}
However, I am unable to achieve what I wanted:
Whenever I try to shrink the size of the window, the QToolButton's Text first start clipping and after some more size reduction, the ToolButtonStyle is changed to Qt::ToolButtonIconOnly. I don't want clipping to happen.
I also want to add some margin (or hysteresis) in my implementation. Like once the QToolButton is changed to Qt::ToolButtonIconOnly at a particular width, then the width should be greater than preferedFullWidth_ + certain margin to switch back to Qt::ToolButtonTextBesideIcon.
Toolbars seem to be a theme this week... :)
So you basically have the right idea but the tricky part is figuring out the required size. Unfortunately the QToolBar layout is completely private so we have to figure stuff out on our own (even though it inherits from QLayout you can't get an instance of it via QToolBar::layout()).
This implementation is fairly basic and will probably not handle all cases (I only tested with basic actions, no custom widgets or such), but it does work for that (tested on Windows and Linux with various styles).
ADDED: I realize you weren't asking for a QToolBar specifically, but that's what you essentially described... I'm not sure why re-invent that wheel (a QToolBar can be placed in any layout, doesn't have to be in a main window), but if you were to implement your own version I think a lot of this example would still apply. I would also personally almost always use QActions for triggering UI events (vs. buttons) since they can be assigned to any number of UI elements (toolbar, manu bar, context menu, window shortcuts, etc.).
Adding a margin/hysteresis is left as an exercise for the reader... :) I don't think it needs one, but you could pad m_expandedSize in initSizes() with some arbitrary margin.
CollapsingToolBar
#include <QtWidgets>
class CollapsingToolBar : public QToolBar
{
Q_OBJECT
public:
explicit CollapsingToolBar(QWidget *parent = nullptr) : CollapsingToolBar(QString(), parent) {}
explicit CollapsingToolBar(const QString &title, QWidget *parent = nullptr) :
QToolBar(title, parent)
{
initSizes();
// If icon sizes change we need to recalculate all the size hints, but we need to wait until the buttons have adjusted themselves, so we queue the update.
connect(this, &QToolBar::iconSizeChanged, [this](const QSize &) {
QMetaObject::invokeMethod(this, "recalcExpandedSize", Qt::QueuedConnection);
});
// The drag handle can mess up our sizing, update preferred size if it changes.
connect(this, &QToolBar::movableChanged, [this](bool movable) {
const int handleSz = style()->pixelMetric(QStyle::PM_ToolBarHandleExtent, nullptr, this);;
m_expandedSize = (movable ? m_expandedSize + handleSz : m_expandedSize - handleSz);
adjustForSize();
});
}
protected:
// Monitor action events to keep track of required size.
void actionEvent(QActionEvent *e) override
{
QToolBar::actionEvent(e);
int width = 0;
switch (e->type())
{
case QEvent::ActionAdded:
// Personal pet-peeve... optionally set buttons with menus to have instant popups instead of splits with the main button doing nothing.
//if (QToolButton *tb = qobject_cast<QToolButton *>(widgetForAction(e->action())))
// tb->setPopupMode(QToolButton::InstantPopup);
//Q_FALLTHROUGH;
case QEvent::ActionChanged:
width = widthForAction(e->action());
if (width <= 0)
return;
if (e->type() == QEvent::ActionAdded || !m_actionWidths.contains(e->action()))
m_expandedSize += width + m_spacing;
else
m_expandedSize = m_expandedSize - m_actionWidths.value(e->action()) + width;
m_actionWidths.insert(e->action(), width);
break;
case QEvent::ActionRemoved:
if (!m_actionWidths.contains(e->action()))
break;
width = m_actionWidths.value(e->action());
m_expandedSize -= width + m_spacing;
m_actionWidths.remove(e->action());
break;
default:
return;
}
adjustForSize();
}
bool event(QEvent *e) override
{
// Watch for style change
if (e->type() == QEvent::StyleChange)
recalcExpandedSize();
return QToolBar::event(e);
}
void resizeEvent(QResizeEvent *e) override
{
adjustForSize();
QToolBar::resizeEvent(e);
}
private slots:
// Here we do the actual switching of tool button style based on available width.
void adjustForSize()
{
int availableWidth = contentsRect().width();
if (!isVisible() || m_expandedSize <= 0 || availableWidth <= 0)
return;
switch (toolButtonStyle()) {
case Qt::ToolButtonIconOnly:
if (availableWidth > m_expandedSize)
setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
break;
case Qt::ToolButtonTextBesideIcon:
if (availableWidth <= m_expandedSize)
setToolButtonStyle(Qt::ToolButtonIconOnly);
break;
default:
break;
}
}
// Loops over all previously-added actions and re-calculates new size (eg. after icon size change)
void recalcExpandedSize()
{
if (m_actionWidths.isEmpty())
return;
initSizes();
int width = 0;
QHash<QAction *, int>::iterator it = m_actionWidths.begin();
for ( ; it != m_actionWidths.end(); ++it) {
width = widthForAction(it.key());
if (width <= 0)
continue;
m_expandedSize += width + m_spacing;
it.value() = width;
}
adjustForSize();
}
private:
void initSizes()
{
// Preload some sizes based on style settings.
// This is the spacing between items
m_spacing = style()->pixelMetric(QStyle::PM_ToolBarItemSpacing, nullptr, this);
// Size of a separator
m_separatorWidth = style()->pixelMetric(QStyle::PM_ToolBarSeparatorExtent, nullptr, this);
// The layout margins (we can't even get the private QToolBarLayout via layout() so we figure it out like it does)
m_expandedSize = (style()->pixelMetric(QStyle::PM_ToolBarItemMargin, nullptr, this) + style()->pixelMetric(QStyle::PM_ToolBarFrameWidth, nullptr, this)) * 2;
// And the size of the drag handle if we have one
if (isMovable())
m_expandedSize += style()->pixelMetric(QStyle::PM_ToolBarHandleExtent, nullptr, this);
}
int widthForAction(QAction *action) const
{
// Try to find how wide the action representation (widget/separator) is.
if (action->isSeparator())
return m_separatorWidth;
if (QToolButton *tb = qobject_cast<QToolButton *>(widgetForAction(action))) {
const Qt::ToolButtonStyle oldStyle = tb->toolButtonStyle();
// force the widest size
tb->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
const int width = tb->sizeHint().width();
tb->setToolButtonStyle(oldStyle);
return width;
}
if (const QWidget *w = widgetForAction(action))
return w->sizeHint().width();
return 0;
}
int m_expandedSize = -1; // The maximum size we need with all buttons expanded and allowing for margins/etc
int m_spacing = 0; // Layout spacing between items
int m_separatorWidth = 0; // Width of separators
QHash<QAction *, int> m_actionWidths; // Use this to track action additions/removals/changes
};
Test/demo
// An XPM icon ripped from QCommonStyle
static const char * const info_xpm[]={
"32 32 5 1",
". c None",
"c c #000000",
"* c #999999",
"a c #ffffff",
"b c #0000ff",
"...........********.............",
"........***aaaaaaaa***..........",
"......**aaaaaaaaaaaaaa**........",
".....*aaaaaaaaaaaaaaaaaa*.......",
"....*aaaaaaaabbbbaaaaaaaac......",
"...*aaaaaaaabbbbbbaaaaaaaac.....",
"..*aaaaaaaaabbbbbbaaaaaaaaac....",
".*aaaaaaaaaaabbbbaaaaaaaaaaac...",
".*aaaaaaaaaaaaaaaaaaaaaaaaaac*..",
"*aaaaaaaaaaaaaaaaaaaaaaaaaaaac*.",
"*aaaaaaaaaabbbbbbbaaaaaaaaaaac*.",
"*aaaaaaaaaaaabbbbbaaaaaaaaaaac**",
"*aaaaaaaaaaaabbbbbaaaaaaaaaaac**",
"*aaaaaaaaaaaabbbbbaaaaaaaaaaac**",
"*aaaaaaaaaaaabbbbbaaaaaaaaaaac**",
"*aaaaaaaaaaaabbbbbaaaaaaaaaaac**",
".*aaaaaaaaaaabbbbbaaaaaaaaaac***",
".*aaaaaaaaaaabbbbbaaaaaaaaaac***",
"..*aaaaaaaaaabbbbbaaaaaaaaac***.",
"...caaaaaaabbbbbbbbbaaaaaac****.",
"....caaaaaaaaaaaaaaaaaaaac****..",
".....caaaaaaaaaaaaaaaaaac****...",
"......ccaaaaaaaaaaaaaacc****....",
".......*cccaaaaaaaaccc*****.....",
"........***cccaaaac*******......",
"..........****caaac*****........",
".............*caaac**...........",
"...............caac**...........",
"................cac**...........",
".................cc**...........",
"..................***...........",
"...................**..........."};
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr)
: QMainWindow(parent)
{
QToolBar* tb = new CollapsingToolBar(this);
tb->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
tb->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea);
QIcon icon = QIcon(QPixmap(info_xpm));
for (int i=0; i < 6; ++i)
tb->addAction(icon, QStringLiteral("Action %1").arg(i));
addToolBar(tb);
show();
// Adding another action after show() may collapse all the actions if the new toolbar preferred width doesn't fit the window.
// Only an issue if the toolbar size hint was what determined the window width to begin with.
//tb->addAction(icon, QStringLiteral("Action After"));
// Test setting button style after showing (comment out the one above)
//tb->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
// Test changing icon size after showing.
//tb->setIconSize(QSize(48, 48));
// Try this too...
//tb->setMovable(false);
}
};
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
//QApplication::setStyle("Fusion");
//QApplication::setStyle("windows");
MainWindow w;
return app.exec();
}

How to get Image pixel position loaded in QGraphicsView - Strange MapToScene() behaviour

I am originally loading image in QGraphicsView and using this method for basic zoom out and zoom in functionality.
However, I am unable to retrieve actual image pixel position using mapToScene functionality in eventFilter function of Graphics_view_zoom class. The below code produces behaviour exactly as windows photo viewer zooming only selected region.
MapToScene() returns same Point as mouse event position.
Here is the class which deals with zooming.
#include "Graphics_view_zoom.h"
#include <QMouseEvent>
#include <QApplication>
#include <QScrollBar>
#include <qmath.h>
Graphics_view_zoom::Graphics_view_zoom(QGraphicsView* view)
: QObject(view), _view(view)
{
_view->viewport()->installEventFilter(this);
_view->setMouseTracking(true);
_modifiers = Qt::ControlModifier;
_zoom_factor_base = 1.0015;
}
void Graphics_view_zoom::gentle_zoom(double factor) {
_view->scale(factor, factor);
_view->centerOn(target_scene_pos);
QPointF delta_viewport_pos = target_viewport_pos - QPointF(_view->viewport()->width() / 2.0,
_view->viewport()->height() / 2.0);
QPointF viewport_center = _view->mapFromScene(target_scene_pos) - delta_viewport_pos;
_view->centerOn(_view->mapToScene(viewport_center.toPoint()));
emit zoomed();
}
void Graphics_view_zoom::set_modifiers(Qt::KeyboardModifiers modifiers) {
_modifiers = modifiers;
}
void Graphics_view_zoom::set_zoom_factor_base(double value) {
_zoom_factor_base = value;
}
bool Graphics_view_zoom::eventFilter(QObject *object, QEvent *event) {
if (event->type() == QEvent::MouseMove) {
QMouseEvent* mouse_event = static_cast<QMouseEvent*>(event);
QPointF delta = target_viewport_pos - mouse_event->pos();
// Here I want to get absolute image coordinates
if (qAbs(delta.x()) > 5 || qAbs(delta.y()) > 5) {
target_viewport_pos = mouse_event->pos();
target_scene_pos = _view->mapToScene(mouse_event->pos());
}
} else if (event->type() == QEvent::Wheel) {
QWheelEvent* wheel_event = static_cast<QWheelEvent*>(event);
if (QApplication::keyboardModifiers() == _modifiers) {
if (wheel_event->orientation() == Qt::Vertical) {
double angle = wheel_event->angleDelta().y();
double factor = qPow(_zoom_factor_base, angle);
gentle_zoom(factor);
return true;
}
}
}
Q_UNUSED(object)
return false;
In mainwindow.cpp,
I am creating object of this class and loading an image as below:
m_GraphicsScene = new QGraphicsScene();
pixmapItem = new QGraphicsPixmapItem();
m_GraphicsScene->addItem(multiview[i].pixmapItem);
view_wrapper = new Graphics_view_zoom(ui->GraphicsView);
ui->GraphicsView->setScene(multiview[i].m_GraphicsScene);
pixmapItem->setPixmap(QPixmap::fromImage("img.jpg"));
multiview[view].m_GraphicsView->fitInView(QRectF(0,0,640,320),Qt::KeepAspectRatio);
Can anyone help with how do I achieve this ?
Keep in mind that the scaling you use only scales the scene, not the items. Given this, the position of the pixel can be obtained, so the algorithm is:
Obtain the mouse position with respect to the QGraphicsView
Transform that position with respect to the scene using mapToScene
Convert the coordinate with respect to the scene in relation to the item using mapFromScene of the QGraphicsItem.
Considering the above, I have implemented the following example:
#include <QtWidgets>
#include <random>
static QPixmap create_image(const QSize & size){
QImage image(size, QImage::Format_ARGB32);
image.fill(Qt::blue);
std::random_device rd;
std::mt19937_64 rng(rd());
std::uniform_int_distribution<int> uni(0, 255);
for(int i=0; i< image.width(); ++i)
for(int j=0; j < image.height(); ++j)
image.setPixelColor(QPoint(i, j), QColor(uni(rng), uni(rng), uni(rng)));
return QPixmap::fromImage(image);
}
class GraphicsView : public QGraphicsView
{
Q_OBJECT
Q_PROPERTY(Qt::KeyboardModifiers modifiers READ modifiers WRITE setModifiers)
public:
GraphicsView(QWidget *parent=nullptr): QGraphicsView(parent){
setScene(new QGraphicsScene);
setModifiers(Qt::ControlModifier);
auto item = scene()->addPixmap(create_image(QSize(100, 100)));
item->setShapeMode(QGraphicsPixmapItem::BoundingRectShape);
item->setPos(40, 40);
fitInView(QRectF(0, 0, 640, 320),Qt::KeepAspectRatio);
resize(640, 480);
}
void setModifiers(const Qt::KeyboardModifiers &modifiers){
m_modifiers = modifiers;
}
Qt::KeyboardModifiers modifiers() const{
return m_modifiers;
}
signals:
void pixelChanged(const QPoint &);
protected:
void mousePressEvent(QMouseEvent *event) override{
if(QGraphicsPixmapItem *item = qgraphicsitem_cast<QGraphicsPixmapItem *>(itemAt(event->pos()))){
QPointF p = item->mapFromScene(mapToScene(event->pos()));
QPoint pixel_pos = p.toPoint();
emit pixelChanged(pixel_pos);
}
QGraphicsView::mousePressEvent(event);
}
void wheelEvent(QWheelEvent *event) override{
if(event->modifiers() == m_modifiers){
double angle = event->orientation() == Qt::Vertical ? event->angleDelta().y(): event->angleDelta().x();
double factor = qPow(base, angle);
applyZoom(factor, event->pos());
}
}
private:
void applyZoom(double factor, const QPoint & fixedViewPos)
{
QPointF fixedScenePos = mapToScene(fixedViewPos);
centerOn(fixedScenePos);
scale(factor, factor);
QPointF delta = mapToScene(fixedViewPos) - mapToScene(viewport()->rect().center());
centerOn(fixedScenePos - delta);
}
Qt::KeyboardModifiers m_modifiers;
const double base = 1.0015;
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
GraphicsView *view = new GraphicsView;
QLabel *label = new QLabel;
QObject::connect(view, &GraphicsView::pixelChanged, label, [label](const QPoint & p){
label->setText(QString("(%1, %2)").arg(p.x()).arg(p.y()));
});
label->setAlignment(Qt::AlignCenter);
QWidget w;
QVBoxLayout *lay = new QVBoxLayout(&w);
lay->addWidget(view);
lay->addWidget(label);
w.show();
return a.exec();
}
#include "main.moc"
It may be better to use a custom graphics scene subclassed from QGraphicsScene as this makes extracting the necessary coordinates much simpler. The only snag is you have to have the QGraphicsPixmapItem::pos available in the custom QGraphicsScene class - I have included a full working example which uses Graphics_view_zoom.h and Graphics_view_zoom.cpp from the linked question. The position of the QGraphicsPixmapItem is passed to a member of the QGraphicsScene subclass, Frame, in order to make the necessary correction.
#include <QPixmap>
#include <QGraphicsPixmapItem>
#include <QGraphicsTextItem>
#include <QGraphicsScene>
#include <QGraphicsSceneMouseEvent>
#include <QGraphicsView>
#include <qfont.h>
#include "Graphics_view_zoom.h"
class Frame : public QGraphicsScene {
Q_OBJECT
public:
QGraphicsTextItem * coords;
QPointF pic_tl;
Frame::Frame(QWidget* parent)
: QGraphicsScene(parent) {
coords = new QGraphicsTextItem();
coords->setZValue(1);
coords->setFlag(QGraphicsItem::ItemIgnoresTransformations, true);
addItem(coords);
}
void Frame::tl(QPointF p) {
pic_tl = p;
}
protected:
void Frame::mouseMoveEvent(QGraphicsSceneMouseEvent* event) {
QPointF pos = event->scenePos();
coords->setPlainText("(" + QString("%1").arg(int(pos.x() - pic_tl.x())) + ", "
+ QString("%1").arg(int(pos.y() - pic_tl.y())) + ")");
coords->setPos(pos);
coords->adjustSize();
}
};
int main(int argc, char *argv[]) {
QApplication a(argc, argv);
QMainWindow* main = new QMainWindow();
QGraphicsView* GraphicsView = new QGraphicsView(main);
Graphics_view_zoom* view_wrapper = new Graphics_view_zoom(GraphicsView);
Frame* frame = new Frame(main);
QGraphicsPixmapItem* pixmapItem = new QGraphicsPixmapItem();
frame->addItem(pixmapItem);
GraphicsView->setScene(frame);
// Loads a 497x326 pixel test image
pixmapItem->setPixmap(QPixmap(":/StackOverflow/test"));
// small offset to ensure it works for pictures which are not at
// (0,0). Larger offsets produce the same result but require manual
// adjustments of the view, I have neglected those for brevity as
// they are not in the scope of the question.
pixmapItem->setPos(-20, 20);
frame->tl(pixmapItem->pos());
GraphicsView->fitInView(QRectF(0, 0, 640, 320), Qt::KeepAspectRatio);
GraphicsView->centerOn(pixmapItem->pos());
main->resize(1920, 1080);
main->show();
GraphicsView->resize(main->width(), main->height());
return a.exec();
}
This will display the image coordinates of the pixel under the mouse relative to (0,0) at the top left corner.
The mouse is not visible in these screenshots but in the first it is in exactly the upper left corner and the second it is in exactly the lower right corner. If these coordinates are needed inside the Graphics_view_zoom object then you simply have to scope the Frame instance appropriately, or pass the value as needed.
Note - the exact coordinates displayed may not precisely represent the position of the mouse in this example since they are cast to ints for demonstration, but the floating point values can be easily accessed since QGraphicsSceneMoveEvent::scenePos() returns a QPointF. Additionally, note that in running this demonstration there may be some (hopefully very small) variation on where the mouse appears to be relative to it's 'actual' position - I recommend using Qt::CrossCursor to allay this. For example on my system the default cursor is off by about a pixel for certain areas on my smaller display, this is also affected by the zoom level - higher zoom will produce more accurate results, less zoom will be less accurate.

How to refresh QGraphicsView to show changes in the QGraphicsScene's background

I have a custom QGraphicsView and QGraphicsScene. Inside QGraphicsScene I have overriden void drawBackground(QPainter *painter, const QRectF &rect) and based on a boolean flag I want to toggle a grid on and off. I tried calling clear() or calling the painter's eraseRect(sceneRect()) inside my function but it didn't work. So after doing some reading I guess it wasn't supposed to work since after changing the scene you need to refresh the view. That's why I'm emitting a signal called signalUpdateViewport()
void Scene::drawBackground(QPainter *painter, const QRectF &rect) {
if(this->gridEnabled) {
// Draw grid
}
else {
// Erase using the painter
painter->eraseRect(sceneRect());
// or by calling
//clear();
}
// Trigger refresh of view
emit signalUpdateViewport();
QGraphicsScene::drawBackground(painter, rect);
}
which is then captured by my view:
void View::slotUpdateViewport() {
this->viewport()->update();
}
Needless to say this didn't work. With doesn't work I mean that the results (be it a refresh from inside the scene or inside the view) are made visible only when changing the widget for example triggering a resize event.
How do I properly refresh the view to my scene to display the changed that I have made in the scene's background?
The code:
scene.h
#ifndef SCENE_HPP
#define SCENE_HPP
#include <QGraphicsScene>
class View;
class Scene : public QGraphicsScene
{
Q_OBJECT
Q_ENUMS(Mode)
Q_ENUMS(ItemType)
public:
enum Mode { Default, Insert, Select };
enum ItemType { None, ConnectionCurve, ConnectionLine, Node };
Scene(QObject* parent = Q_NULLPTR);
~Scene();
void setMode(Mode mode, ItemType itemType);
signals:
void signalCursorCoords(int x, int y);
void signalSceneModeChanged(Scene::Mode sceneMode);
void signalSceneItemTypeChanged(Scene::ItemType sceneItemType);
void signalGridDisplayChanged(bool gridEnabled);
void signalUpdateViewport();
public slots:
void slotSetSceneMode(Scene::Mode sceneMode);
void slotSetSceneItemType(Scene::ItemType sceneItemType);
void slotSetGridStep(int gridStep);
void slotToggleGrid(bool flag);
private:
Mode sceneMode;
ItemType sceneItemType;
bool gridEnabled;
int gridStep;
void makeItemsControllable(bool areControllable);
double round(double val);
protected:
void mousePressEvent(QGraphicsSceneMouseEvent *event);
void mouseMoveEvent(QGraphicsSceneMouseEvent *event);
void mouseReleaseEvent(QGraphicsSceneMouseEvent *event);
void keyPressEvent(QKeyEvent *event);
void drawBackground(QPainter *painter, const QRectF &rect);
};
Q_DECLARE_METATYPE(Scene::Mode)
Q_DECLARE_METATYPE(Scene::ItemType)
#endif // SCENE_HPP
scene.cpp
#include <QGraphicsItem>
#include <QGraphicsView>
#include <QGraphicsSceneMouseEvent>
#include <QPainter>
#include <QRectF>
#include <QKeyEvent>
#include <QtDebug>
#include "scene.h"
Scene::Scene(QObject* parent)
: QGraphicsScene (parent),
sceneMode(Default),
sceneItemType(None),
gridEnabled(true),
gridStep(30)
{
}
Scene::~Scene()
{
}
void Scene::setMode(Mode mode, ItemType itemType)
{
this->sceneMode = mode;
this->sceneItemType = itemType;
QGraphicsView::DragMode vMode = QGraphicsView::NoDrag;
switch(mode) {
case Insert:
{
makeItemsControllable(false);
vMode = QGraphicsView::NoDrag;
break;
}
case Select:
{
makeItemsControllable(true);
vMode = QGraphicsView::RubberBandDrag;
break;
}
case Default:
{
makeItemsControllable(false);
vMode = QGraphicsView::NoDrag;
break;
}
}
QGraphicsView* mView = views().at(0);
if(mView) {
mView->setDragMode(vMode);
}
}
void Scene::slotSetSceneMode(Scene::Mode sceneMode)
{
this->sceneMode = sceneMode;
qDebug() << "SM" << (int)this->sceneMode;
emit signalSceneModeChanged(this->sceneMode);
}
void Scene::slotSetSceneItemType(Scene::ItemType sceneItemType)
{
this->sceneItemType = sceneItemType;
qDebug() << "SIT:" << (int)this->sceneItemType;
emit signalSceneItemTypeChanged(this->sceneItemType);
}
void Scene::slotSetGridStep(int gridStep)
{
this->gridStep = gridStep;
}
void Scene::slotToggleGrid(bool flag)
{
this->gridEnabled = flag;
invalidate(sceneRect(), BackgroundLayer);
qDebug() << "Grid " << (this->gridEnabled ? "enabled" : "disabled");
update(sceneRect());
emit signalGridDisplayChanged(this->gridEnabled);
}
void Scene::makeItemsControllable(bool areControllable)
{
foreach(QGraphicsItem* item, items()) {
if(/*item->type() == QCVN_Node_Top::Type
||*/ item->type() == QGraphicsLineItem::Type
|| item->type() == QGraphicsPathItem::Type) {
item->setFlag(QGraphicsItem::ItemIsMovable, areControllable);
item->setFlag(QGraphicsItem::ItemIsSelectable, areControllable);
}
}
}
double Scene::round(double val)
{
int tmp = int(val) + this->gridStep/2;
tmp -= tmp % this->gridStep;
return double(tmp);
}
void Scene::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
QGraphicsScene::mousePressEvent(event);
}
void Scene::mouseMoveEvent(QGraphicsSceneMouseEvent *event)
{
emit signalCursorCoords(int(event->scenePos().x()), int(event->scenePos().y()));
QGraphicsScene::mouseMoveEvent(event);
}
void Scene::mouseReleaseEvent(QGraphicsSceneMouseEvent *event)
{
QGraphicsScene::mouseReleaseEvent(event);
}
void Scene::keyPressEvent(QKeyEvent* event)
{
if(event->key() == Qt::Key_M) {
slotSetSceneMode(static_cast<Mode>((int(this->sceneMode) + 1) % 3));
}
else if(event->key() == Qt::Key_G) {
slotToggleGrid(!this->gridEnabled);
}
QGraphicsScene::keyPressEvent(event);
}
void Scene::drawBackground(QPainter *painter, const QRectF &rect)
{
// FIXME Clearing and drawing the grid happens only when scene size or something else changes
if(this->gridEnabled) {
painter->setPen(QPen(QColor(200, 200, 255, 125)));
// draw horizontal grid
double start = round(rect.top());
if (start > rect.top()) {
start -= this->gridStep;
}
for (double y = start - this->gridStep; y < rect.bottom(); ) {
y += this->gridStep;
painter->drawLine(int(rect.left()), int(y), int(rect.right()), int(y));
}
// now draw vertical grid
start = round(rect.left());
if (start > rect.left()) {
start -= this->gridStep;
}
for (double x = start - this->gridStep; x < rect.right(); x += this->gridStep) {
painter->drawLine(int(x), int(rect.top()), int(x), int(rect.bottom()));
}
}
else {
// Erase whole scene's background
painter->eraseRect(sceneRect());
}
slotToggleGrid(this->gridEnabled);
QGraphicsScene::drawBackground(painter, rect);
}
The View currently doesn't contain anything - all is default. The setting of my Scene and View instance inside my QMainWindow is as follows:
void App::initViewer()
{
this->scene = new Scene(this);
this->view = new View(this->scene, this);
this->viewport = new QGLWidget(QGLFormat(QGL::SampleBuffers), this->view);
this->view->setRenderHints(QPainter::Antialiasing);
this->view->setViewport(this->viewport);
this->view->setViewportUpdateMode(QGraphicsView::FullViewportUpdate);
this->view->setCacheMode(QGraphicsView::CacheBackground);
setCentralWidget(this->view);
}
EDIT: I tried calling invalidate(sceneRect(), BackgroundLayer) before the update(), clear() or whatever I tried to trigger a refresh.
I also tried QGraphicsScene::update() from within the scene but it didn't work Passing both no argument to the function call and then passing sceneRect() didn't result in anything different then what I've described above.
Found the issue - I forgot to set the size of the scene's rectangle:
this->scene->setSceneRect(QRectF(QPointF(-1000, 1000), QSizeF(2000, 2000)));
I actually found the problem by deciding to print the size of the QRectF returned by the sceneRect() call and when I looked at the output I saw 0, 0 so basically I was indeed triggering the update but on a scene with the area of 0 which (obviously) would result in nothing.
Another thing that I tested and worked was to remove the background caching of my view.
When you change your grid settings, whether it's on or off (or color, etc.), you need to call QGraphicsScene::update. That's also a slot, so you can connect a signal to it if you want. You can also specify the exposed area; if you don't, then it uses a default of everything. For a grid, that's probably what you want.
You don't need to clear the grid. The update call ensures that the updated area gets cleared, and then you either paint on it if you want the grid, or don't paint on it if the grid shouldn't be there.
Sometimes when the view/scene refuse to run update() directly, processing the app events before the update() fixes it, like so:
qApp->processEvents();
update();

C++ rapidly refresh desktop

I am trying to make a program currently that outputs a polygon to the desktop for a simple animation. The problem I am currently running into is that the animation gets an "onion" effect because the desktop isn't refreshing. I have searched for a method to refresh the desktop however because it's an animation, none of the solutions can refresh it fast enough. Below is an example of my code:
#include <iostream>
#include <Windows.h>
#include <math.h>
#include <Shlobj.h>
int main() {
//start ambrose
POINT amby[5];
POINT pos;
/* hide console window */
ShowWindow(FindWindowA("ConsoleWindowClass", NULL), false);
/* Calling GetDC with argument 0 retrieves the desktop's DC */
HDC hDC_Desktop = GetDC(0);
//This is just an example of what I am doing
for (int i = 0; i < 10000; i++) {
pos.x = 600+sin(double(i)/50)*200;
pos.y = 500+cos(double(i)/50)*200;
amby[0].x = -10+pos.x;
amby[0].y = -10+pos.y;
amby[1].x = -50+pos.x;
amby[1].y = -50+pos.y;
amby[2].x = 50+pos.x;
amby[2].y = -50+pos.y;
Polygon(hDC_Desktop,amby, 3);
Sleep(10);
}
//The method I was trying before that didn't work VVVVV
//LPITEMIDLIST pidl;
//SHGetSpecialFolderLocation(NULL,CSIDL_DESKTOP,&pidl);
//SHChangeNotify(SHCNE_ASSOCCHANGED,SHCNF_IDLIST,pidl,0);
return 0;
}
Thanks
Edit
I have tried using invalidateRect as such:
...
for (int i = 0; i < 10000; i++) {
pos.x = 600+sin(double(i)/50)*200;
pos.y = 500+cos(double(i)/50)*200;
amby[0].x = -10+pos.x;
amby[0].y = -10+pos.y;
amby[1].x = -50+pos.x;
amby[1].y = -50+pos.y;
amby[2].x = 50+pos.x;
amby[2].y = -50+pos.y;
Polygon(hDC_Desktop,amby, 3);
InvalidateRect(GetDesktopWindow(),NULL, true);
Sleep(10);
}
...
I am wondering if there is anyway to call WM_ERASEBKGND or WM_DISPLAYCHANGE to force a change. Does anyone know if there is a way to call these?
I am not sure what you are trying to achieve. Let me just answer to problem of onion effect. A quick and dirty solution to erase what was drawn in the previous iteration could be to draw using XOR mode but the solution has a few downsides, like flicker and color could be arbitrary. A proper solution that would address both the downsides would be to do all the drawing in a memory DC and BitBlt the same to the screen.
Code for the quick and dirty solution would be -
SetROP2(hDC_Desktop,R2_XORPEN);
//This is just an example of what I am doing
for (int i = 0; i < 100; i++)
{
if(i!=0)
{
pos.x = 600+sin(double(i-1)/50)*200;
pos.y = 500+cos(double(i-1)/50)*200;
amby[0].x = -10+pos.x;
amby[0].y = -10+pos.y;
amby[1].x = -50+pos.x;
amby[1].y = -50+pos.y;
amby[2].x = 50+pos.x;
amby[2].y = -50+pos.y;
Polygon(hDC_Desktop,amby, 3);
}
pos.x = 600+sin(double(i)/50)*200;
pos.y = 500+cos(double(i)/50)*200;
amby[0].x = -10+pos.x;
amby[0].y = -10+pos.y;
amby[1].x = -50+pos.x;
amby[1].y = -50+pos.y;
amby[2].x = 50+pos.x;
amby[2].y = -50+pos.y;
Polygon(hDC_Desktop,amby, 3);
Sleep(10);
}
There's an easy solution, and that's to not actually draw on the desktop. Instead, create a transparent full-screen window. Since it's transparent, any pixel that you don't draw will show the desktop underneath. Hence, only your polygon pixels will hide the underlying desktop.
As a result, the desktop window never needs to be invalidated or repainted etc.
Why don't you use a transparent wnd.
class COverlayWnd : public CWnd
{
DECLARE_DYNAMIC(COverlayWnd)
public:
COverlayWnd();
virtual ~COverlayWnd();
protected:
afx_msg void OnPaint();
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
DECLARE_MESSAGE_MAP()
};
// OverlayWnd.cpp : implementation file
//
The implementation. Just move the window if you want animations to run all over the desktop.
#include "stdafx.h"
// COverlayWnd
IMPLEMENT_DYNAMIC(COverlayWnd, CWnd)
COverlayWnd::COverlayWnd()
{
}
COverlayWnd::~COverlayWnd()
{
}
BEGIN_MESSAGE_MAP(COverlayWnd, CWnd)
ON_WM_PAINT()
ON_WM_CREATE()
END_MESSAGE_MAP()
void COverlayWnd::OnPaint()
{
CPaintDC dc(this);
CRect rect;
GetClientRect( &rect );
dc.FillSolidRect(&rect, RGB(1,1,1));
//paint other stuff that don't have RGB(1,1,1)
}
int COverlayWnd::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CWnd::OnCreate(lpCreateStruct) == -1)
return -1;
BOOL bRet = 0;
bRet = ModifyStyleEx(0,WS_EX_LAYERED|WS_EX_TRANSPARENT);
bRet = ModifyStyle(DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU,0);
bRet = ModifyStyle(WS_POPUP,0);
bRet = SetLayeredWindowAttributes(RGB(1,1,1),0,LWA_COLORKEY);
//the RGB(1,1,1) is the transparent color
ASSERT(bRet);
//this->EnableWindow(FALSE);
return 0;
}