Smooth line in QGraphicsScene with Qt - qt

I need to draw curves with Qt: The user clicks on QGraphicsScene (via QGraphicsView) and straight lines are drawn between the points clicked on by the user. When the user finishes to draw straight lines (by clicking on the right button), the set of lines becomes a curve.
For that, I need to use the QPainterPath::cubicTo(...) method and add the path to the QGraphicsScene using QGraphicsScene::addPath(...).
The problem is that I don't know how to compute the parameter values that are passed to cubicTo(...).
For example, in the following picture, the user has drawn the two gray lines by clicking points A B and C. When she clicks the right button, I want to draw the red line using cubicTo(...):
My current code only draws gray lines because I have set c1, c2, d1 and d2 values to point positions clicked on by the user :
void Visuel::mousePressEvent(QMouseEvent *event)
{
int x = ((float)event->pos().x()/(float)this->rect().width())*(float)scene()->sceneRect().width();
int y = ((float)event->pos().y()/(float)this->rect().height())*(float)scene()->sceneRect().height();
qDebug() << x << y;
if(event->button() == Qt::LeftButton)
{
path->cubicTo(x,y,x,y,x,y);
}
if(event->button() == Qt::RightButton)
{
if(path == NULL)
{
path = new QPainterPath();
path->moveTo(x,y);
}
else
{
path->cubicTo(x,y,x,y,x,y);
scene()->addPath(*path,QPen(QColor(79, 106, 25)));
path = NULL;
}
}
}

Apparently control points c1, c2, d1, d2 are just coordinates of point B in your picture. In more general case point C lies on AB line and point D lies on BC line.
In Qt code this can be written as follows:
Visuel class should have the following properties:
QPoint A, B, C;
unsigned int count;
Which should be initialized in Visuel's constructor.
void Visuel::Visuel()
{
count = 0;
// Initialize other stuff as well
}
Then we are going to overload QGraphicsScene::mousePressEvent() to detect user's mouse events:
void Visuel::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
/* Ignore everything else except left button's press */
if (event->type() != QEvent::GraphicsSceneMousePress ||
event->button() != Qt::LeftButton) {
return;
}
switch (count) {
case 0:
A = event->scenePos();
break;
case 1: {
B = event->scenePos();
/* Draw AB line */
QPainterPath path(A);
path.lineTo(B);
scene()->addPath(path, Qt::grey);
}
break;
case 2: {
C = event->scenePos();
/* Draw BC line */
QPainterPath path(B);
path.lineTo(C);
scene()->addPath(path, Qt::grey);
/* Draw ABBC cubic Bezier curve */
QPainterPath path2(A);
path2.cubicTo(B, B, C);
scene()->addPath(path2, Qt::red);
}
break;
default:
break;
}
if (count >= 2) {
count = 0;
} else {
count++;
}
}

Related

Button for Creative Coding Asteroids project

I'm an undergrad and in one of my classes, we have this assignment to make the Asteroids game (you know, the retro one!) on Processing (which is basically a simplified Javascript program). I have the code for a button:
void setup()
{
size(1280, 720);
}
void draw()
{
background(0,0,0);
drawButton();
}
Boolean pointIsInRectangle(int left, int top, int right, int bottom, int pointX, int pointY)
{
if (pointX >= left
&& pointX <= right
&& pointY <= bottom
&& pointY >= top
)
return true;
else
return false;
}
void drawButton()
{
int left = 350;
int top = 145;
int right = 860;
int bottom = 220;
rectMode(CORNERS);
color background = color(0,0,0);
if (pointIsInRectangle(left,top,right,bottom,mouseX,mouseY))
{
background = color(255);
}
// draw outer rectangle
stroke(255);
fill(background);
rect(left,top,right,bottom);
// draw caption
fill(255);
textSize(100);
text(" ASTEROIDS", left,bottom);
}
and I have the preliminary code for the ship for the game, but I need the button to get to an "in between" page so that when the button is clicked, it leads to a new screen that says "click anywhere to play game" and when any point in the screen is clicked, the ship appears and asteroids begin appearing and the game begins. HOW DO I GET THE BUTTON TO LEAD TO A NEW PAGE, AND HOW DO I CREATE THAT PAGE? I really cannot figure it out. Crossing my fingers that someone will be able to give me some guidance!!!!!
The actual result I'm seeing is that nothing is happening when the button is clicked. This makes sense because I don't know how to add the next page that says Click to Play Game, so this is the issue I'm facing. The code I have so far can be found above.
This is not question about a button, but about a game engine with multiple scenes.
Your game is a collection of screens (scenes). Every scene have definition of visuals, definition of logic, and definition of switch for another scene.
Here is the minimal solution of Your problem.
1 - Define visuals of Your screens in .display() method,
2 - Define logic condition for next screen switch in stateCondition().
boolean mouseReleased;
GameScreen screen;
GameScreen splashScreen;
GameScreen inBetweenPage;
void setup() {
size(1280, 720);
GameScreen splashScreen = new SplashScreen();
GameScreen inBetweenPage = new InBetweenPage();
splashScreen.setNextScreen(inBetweenPage);
inBetweenPage.setNextScreen(splashScreen);
screen = splashScreen;
mouseReleased = false;
}
void draw() {
screen.display();
if (screen.stateCondition()) {
screen = screen.getNextScreen();
}
}
interface GameScreen {
/**
** screen visual definition
**/
void display();
/**
** screen change state condition
**/
boolean stateCondition();
void setNextScreen(GameScreen scr);
GameScreen getNextScreen();
}
class SplashScreen implements GameScreen {
GameScreen nextScreen;
int left = 350;
int top = 145;
int right = 860;
int bottom = 220;
void display() {
background(0, 0, 0);
rectMode(CORNERS);
color background = color(0, 0, 0);
if (pointIsInRectangle(left, top, right, bottom, mouseX, mouseY)) {
background = color(255, 255, 255, 160);
}
// draw outer rectangle
stroke(255);
fill(background);
rect(left, top, right, bottom);
// draw caption
fill(255);
textSize(100);
textAlign(CENTER, CENTER);
text("ASTEROIDS", (left+right)/2, (top+bottom)/2-16);
}
boolean stateCondition() {
if (mouseReleased && (mouseButton == LEFT)) {
mouseReleased = false;
return pointIsInRectangle(left, top, right, bottom, mouseX, mouseY);
}
return false;
}
GameScreen getNextScreen() {
return nextScreen;
}
void setNextScreen(GameScreen target) {
this.nextScreen = target;
}
}
class InBetweenPage implements GameScreen {
GameScreen nextScreen;
int left = 0;
int top = 0;
int right = width;
int bottom = height;
void display() {
background(0, 0, 0);
// draw caption
fill(255);
textSize(24);
textAlign(CENTER, CENTER);
text("< click anywhere to play game >", width/2, height/2);
}
boolean stateCondition() {
if (mouseReleased && (mouseButton == LEFT)) {
mouseReleased = false;
return pointIsInRectangle(left, top, right, bottom, mouseX, mouseY);
}
return false;
}
GameScreen getNextScreen() {
return nextScreen;
}
void setNextScreen(GameScreen target) {
this.nextScreen = target;
}
}
Boolean pointIsInRectangle(int left, int top, int right, int bottom, int pointX, int pointY) {
if (pointX >= left && pointX <= right && pointY <= bottom && pointY >= top) {
return true;
} else
return false;
}
void mouseReleased() {
mouseReleased = true;
}
There is a relatively simple solution to this problem, which would be to make some space for a special "splashscreen" state in your code. It would almost be ninja coding to implement it, although in this case as you are learning it's not taking shortcuts but more like climbing a new step in the learning stairwell. Here's a proof of concept which shows what I'm speaking about:
// this boolean keeps track of the current game state: splashscreen or not
boolean splashScreen = true;
void setup() {
size(600, 400);
}
void draw() {
background(0);
// if the game has yet to start, show the splashscreen
if (splashScreen) {
drawSplashScreen();
} else {
playGame();
}
}
// this method draws the splashScreen
// it could be coded in the 'draw()' method, but it's easier to read this way
void drawSplashScreen() {
textAlign(CENTER);
textSize(30);
fill(255);
text("THIS IS THE SPLASHSCREEN \n click anywhere to play the game", width/2, height/2);
}
// this method contains everything your game loop needs to work
void playGame() {
textAlign(CENTER);
textSize(40);
fill(200, 0, 200);
text("YOU ARE CURRENTLY \n PLAYING THE GAME", width/2, height/2);
}
void mouseClicked() {
if (splashScreen) {
splashScreen = !splashScreen;
}
}
Here you have 2 game states: the splash screen and the game itself, but you could implement more than just these two. There's a design pattern called Finite State Machine that would be just perfect for your needs. Although you already have everything you need to code your assignment, let me explain a little further:
A FSM let you determine the context which can lead to another context and limit some actions to it's own context. A good example of this is Mario in the original Super Mario Bros game: when he's small, getting a magic mushroom will transform him into Super Mario. When he's Super, getting a flower will transform him into Fire Mario. But while small, getting a flower will only make him into Super Mario, not Fire Mario (in the ooold first game at least). That's because each one of these states have rules, and he cannot just jump from one to the other without regard for these.
Your game's logic has it's own rules too: you have the first screen with the "start" button. When this button is clicked, there's a second state where it says "click anywhere to play the game". If the user clicks, then the game itself starts. That makes for 3 states (3 screens if you like) where every state has it's own set of rules - which we often call 'business rules'.
We could schematize this assignment like this:
And here's the skeleton code that would implement such a game, including a bonus rectangle collision detection method:
// the game states are as follow:
// 0 is welcome screen with button
// 1 is click anywhere screen
// 2 is the game itself
int gameState = 0;
void setup() {
size(600, 400);
}
void draw() {
background(0);
// let's use the right game state here
switch(gameState) {
case 0:
drawWelcomeScreen();
break;
case 1:
drawClickAnywhereScreen();
break;
case 2:
playGame();
break;
}
}
void drawWelcomeScreen() {
fill(0, 0, 100);
rect(100, 100, 400, 100);
textAlign(CENTER);
textSize(30);
fill(255);
text("Click here to play", 300, 150);
text("THIS IS THE WELCOME SCREEN", width/2, 50);
}
void drawClickAnywhereScreen() {
textAlign(CENTER);
textSize(30);
fill(255);
text("Click anywhere to play the game", width/2, height/2);
}
void playGame() {
textAlign(CENTER);
textSize(40);
fill(200, 0, 200);
text("YOU ARE CURRENTLY \n PLAYING THE GAME \n click anywhere to go \n back to the welcome screen", width/2, height/2);
}
void mouseClicked() {
// now this will be more complicated, because you'll want to deal with clicks differently depending on the game state
// which kinda answers the question as how we'll deal with this issue: same as in the 'draw()' method
switch(gameState) {
case 0:
// if the click's coordinates are in the rectangle's coordinates (use math here, or a collision method)
// (in fact, use math pretty much everywhere)
// (I hope you like math)
// anyway here's an old collision method I paste everywhere on SO, feel free to steal it and improve on it!
// I wrote it as a student to deal with pretty much the same stuff that you're going through
if (intersect(100, 100, 400, 100, mouseX, mouseY, 1, 1)) {
gameState = 1;
}
break;
case 1:
gameState = 2;
break;
case 2:
gameState = 0;
break;
}
}
// enter the xy coordinates, the width and the heigh of 2 rectangle shapes and it'll return true if they intersect
boolean intersect(float x1, float y1, float w1, float h1, float x2, float y2, float w2, float h2) {
boolean checkX = false;
boolean checkY = false;
if ( (x1<x2 && (x1+w1)>x2) || (x1<(x2+w2) && (x1+w1)>x2+w2) || (x1>x2 && (x1+w1)<(x2+w2)) ) {
checkX = true;
}
if ( (y1<y2 && (y1+h1)>y2) || (y1<(y2+h2) && (y1+h1)>y2+h2) || (y1>y2 && (y1+h1)<(y2+h2)) ) {
checkY = true;
}
return checkX && checkY;
}
I hope I'm not confusing you with all this material. I'll try and keep an eye out for any question you may have about these things.
Good luck and have fun!

How to erase a QGraphicsItem from QGraphicsView using QPushButton

I am building a major user interface but I am stuck on a problem and in order to shrink the problem I build a small working example that carries exactly the problem.
After the user creates a new .db file using the icon and saving for example to Desktop it is possible to load images(only in .png format for now) in the QGraphicsView and cliking on the checkbox to enable from Right: Drag to Right:Select it is possible to draw boxes on the image. With a right click inside the drawn box we can open a QDialog that asks to name the extracted image from the box. The image is stored in the QTableView as shown in the small working example below:
The problem:
I tried several ways to erase the box (and the related index on the QTableView of the related box) but none of them was successful.
With the use of the "Erase Square" QPushButton I am trying to erase the box. Every box can be re-activated by just left double-clicking within the interested box. See below what I am trying to achieve:
I draw the box on the QGraphicsView and right mouse click to capture the image from the box:
void MainWindow::mousePressEvent(QGraphicsSceneMouseEvent *event)
{
if(event->button() == Qt::RightButton)
{
this->setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, SIGNAL(customContextMenuRequested(const QPoint &)),
this, SLOT(ShowContextMenu(const QPoint &)));
}
}
void MainWindow::contextMenuEvent(QContextMenuEvent *event)
{
Q_UNUSED(event);
leftScene->clearSelection(); // Selections would also render to the file
leftScene->setSceneRect(leftScene->itemsBoundingRect()); // Re-shrink the scene to it's bounding contents
QImage image(leftScene->sceneRect().size().toSize(), QImage::Format_ARGB32); // Create the image with the exact size of the shrunk scene
image.fill(Qt::transparent); // Start all pixels transparent
QPainter painter(&image);
leftScene->render(&painter);
QImage subimage = image.copy(QRect(start.toPoint(), end.toPoint()));
clipSceneDialog d(this);
d.show();
d.setImage(subimage);
d.setHeaderImage(currentLeftImagePath);
d.setBoundingBox(QRect(start.toPoint(), end.toPoint()));
if(d.exec() == QDialog::Rejected) {
return;
} else {
//
}
Param result = d.getData();
Parameters* param = new Parameters(result);
mDatabaseLeftCamera->addItem(param);
mModelLeftCamera->select();
ui->tableView->show();
}
Draw as many boxes as the user wants:
void MainWindow::onRubberBandUpdate(const QRect &viewportRect,
const QPointF &fromScenePoint,
const QPointF &toScenePoint)
{
if(viewportRect.isNull() && fromScenePoint.isNull() && toScenePoint.isNull() && imageLoaded)
{
if(currentSelection >= 0)
selections[currentSelection]->setActiveState(false);
QRectF select;
select.setCoords(start.x(), start.y(), end.x(), end.y());
square = new Square(select);
square->setActiveState(true);
currentSelection = selections.size();
selections.append(square);
leftScene->addItem(square->getGraphics());
ui->graphicsView->show();
}
else
{
start = fromScenePoint;
end = toScenePoint;
}
}
After drawing several boxes the user decides to reactivate one of the boxes by double-cliking within the box:
void MainWindow::onSceneDoubleClick(QPointF point)
{
qDebug() << "Click!";
QList<QGraphicsItem*> foundItems = leftScene->items(point);
if(foundItems.size() > 0 && foundItems[0]->group() != nullptr)
{
qDebug() << "Found";
int i = 0;
for(i=0;i<selections.size();i++)
{
qDebug() << "Iterate";
if(selections[i]->getGraphics() == foundItems[0]->group())
{
qDebug() << "Target";
break;
}
}
if(currentSelection >= 0)
selections[currentSelection]->setActiveState(false);
currentSelection = i;
selections[currentSelection]->setActiveState(true);
}
}
The user decides to erase the re-activated square:
void MainWindow::on_eraseSquare_clicked()
{
clearSceneLeft();
}
void MainWindow::clearSceneLeft()
{
if (selections.size() > 0) {
qDeleteAll(selections);
selections.clear();
currentSelection = -1;
}
for(int p=0;p<shape.size();p++)
{
leftScene->removeItem(shape[p]);
delete shape[p];
}
shape.clear();
}
But this is not working and nothing happens. Additionally because the box corresponds to an SQL reference on the QTableView I dont know how that could be connected in the SQL class.
I have been trying for many days to solve this problem and am running out of ideas. Thanks for shedding light on this problem or point in the right direction.
If you would like to try the small example interface you can do it from here

How can i change order of widget in layout by drag and drop with the mouse?

I make my own class from QWidget with redefine of paintEvent(), mousePressEvent(), mouseReleaseEvent() and mouseMoveEvent(). All that methods for move widgets over other widget (yellow).
When i create my widgets in a layout, it looks like this:
But when I move black widget to the bottom and red to the top like this:
and resize window, all widgets refresh to their align positions:
But i want, when i move one widget higher then another, the widgets should align in layout in new places, like this:
Which function i should redefine to do it?
P.S.
There is a piece of code, that can move widgets positions inside layout (change their indexes), but i don't know how find out their (x,y) position to calculate new indexes in layout. I think, that i can do it in resizeEvent().
But it when it event was emitted, positions already changed to old. (like before moveing on 1 picture), and i need positions after moveing (like on secon picture). How can i get position of widget before it will be aligned?
or How can i change order of widget in layout by drag and drop with the mouse?
I write my own widget, then redefine following methods: mouseReleaseEvent(), paintEvent(), mousePressEvent(), mouseMoveEvent(). In mousePressEvent() I hold old X and Y positions and mouse position on figure. Then in mouseMoveEvent() i calculate if minimum distance of mouse move is riched and move widget to new position (it not moves widget index in layout). After it, if emitted mouseReleaseEvent() i just calculate new index of moving widget and change and update parent layout. If widget moves less then it height, then layout just updates without changing widget index.
void SimpleWidget::mouseMoveEvent(QMouseEvent *event)
{
if (!(event->buttons() & Qt::LeftButton))
return;
if (!IsMinimumDistanceRiched(event))
{
return;
}
int y = event->globalY() - mouseClickY + oldY;
int BottomBorder = parentWidget->geometry().height() - this->geometry().height();
if(y < 0) y = 0;
else if(y > BottomBorder) y = BottomBorder;
move(oldX, y);
}
void SimpleWidget::mousePressEvent(QMouseEvent *event)
{
if (event->buttons() & Qt::LeftButton)
dragStartPosition = event->pos();
oldX = this->geometry().x();
oldY = this->geometry().y();
mouseClickX = event->globalX();
mouseClickY = event->globalY();
}
bool SimpleWidget::IsMinimumDistanceRiched(QMouseEvent *event)
{
return (event->pos() - dragStartPosition).manhattanLength() >= QApplication::startDragDistance();
}
bool SimpleWidget::moveInLayout(QWidget *widget, MoveDirection direction)
{
QVBoxLayout* myLayout = qobject_cast<QVBoxLayout*>(widget->parentWidget()->layout());
const int index = myLayout->indexOf(widget);
if (direction == MoveUp && index == 0)
{
return false;
}
if (direction == MoveDown && index == myLayout->count()-1 )
{
return false;
}
const int newIndex = direction == MoveUp ? index - 1 : index + 1;
myLayout->removeWidget(widget);
myLayout->insertWidget(newIndex , widget);
return true;
}
void SimpleWidget::paintEvent(QPaintEvent *)
{
QStyleOption o;
o.initFrom(this);
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &o, &p, this);
}
void SimpleWidget::mouseReleaseEvent(QMouseEvent *)
{
int y = geometry().y();
MoveDirection direct;
int offset;
if(oldY > y)
{
offset = oldY - y;
direct = MoveUp;
}
else if(oldY < y)
{
offset = y - oldY;
direct = MoveDown;
}
int count = offset/height();
for(int i = 0; i < count; i++)
{
moveInLayout(this, direct);
}
update();
QVBoxLayout* myLayout = qobject_cast<QVBoxLayout*>(this->parentWidget->layout());
myLayout->update();
this->saveGeometry();
}

Qt: algorithm to rotate and resize a qgraphicslineitem present on a qgraphicsscene

Can someone please share the algorithm to rotate and resize a QGraphicsLineItem present on a QGraphicsScene? I wish to be able to click on either end of the line and rotate it while the opposite end automatically becomes the anchor point for rotation.
I have tried the solution given below and it works brilliantly!!. The solution is for line rotation only. This solution is provided to me when i posted the question at other Qt platform. All the credit goes to original author who provided the solution to me.
void Line::mousePressEvent( QGraphicsSceneMouseEvent * event ){
const QPointF pos = event->pos();
const qreal l1 = QLineF( pos, this->line().p1() ).length();
const qreal l2 = QLineF( pos, this->line().p2() ).length();
const qreal threshold = 3.5;
if( l1 < l2 and l1 < threshold ){
_dragIndex = 1;
} else if ( l2 < l1 and l2 < threshold ){
_dragIndex = 0;
} else{
_dragIndex = -1;
}
event->setAccepted( _dragIndex != -1 );
}
void Line::mouseMoveEvent( QGraphicsSceneMouseEvent * event ){
if( _dragIndex != -1 ){
const QPointF anchor = _dragIndex == 0 ? this->line().p1() : this->line().p2();
QLineF ma = QLineF(anchor,event->pos());
ma.setLength( line().length() );
const QPointF rotated = anchor + QPointF( ma.dx(), ma.dy() );
this->setLine( _dragIndex == 0 ? QLineF(anchor,rotated) : QLineF(rotated,anchor) );
}
}
void Line::mouseReleaseEvent( QGraphicsSceneMouseEvent * event ){
_dragIndex = -1;
QGraphicsLineItem::mouseReleaseEvent(event);
}
1- Overload mouseMoveEvent of your scene. Make sure you are in the initial state, no clicks, nothing. If position is close enough to the ends, set your flag as such.
// scene.cpp
void mouseMoveEvent(QMouseEvent *e){
if(moveToRotate){
lineItem->rotate(flag, e->pos());
...
return;
}
if(isClose(e->pos(), lineItem->firstEnd)
flag = 1;
else if(isClose(e->pos(), lineItem->secondEnd)
flag = 2;
else
flag = 0;
return;
}
2- Overload mouseClickEvent of the scene. If flag is set (1 or 2), get your anchor point. You can overload paint method of lineItem to highlight the end point. Also set firstClick flag.
void mouseClickEvent(QMouseEvent *e){ // firstClick happened
if(flag !=0){ //process click
firstClick = true;
paintAnchor = true;
moveToRotate = true;
}
else // neglect click
return;
return;
}
// myCustomLineItemclassThatInheritsQGraphicsLineItem.cpp
void paint(... ){
if(paintAnchor){
...
}
}
There are obvious errors, but the general flow should be as such. You may need a signal to connect the boolean to the paint event of another class. You may need to overload mouseReleaseEvent to properly set moveToRotate. And, of course rotate and isClose methods are yours to be written -maybe even exist in the API.
Just construct the main flowchart, it will help you a lot.

x11 limit mouse movements

I try to lock mouse cursor movements to the left half of the screen. I have following screen setup:
On the left side is a Qt window of size 1120x1080, on the right side a GL window of size 800x1080.
I use Openbox window manager under Ubuntu 12.10. The window layout stays fixed.
I need to restrict mouse movement to the Qt window.
To get the mouse to stay in the window, enable mouse movement with:
setMouseTracking(true);
and override void QWidget::mouseMovement( QMouseEvent * event )
void TheWindow::mouseMoveEvent ( QMouseEvent * event )
{
// get window size without frame
QRect s = geometry();
// get current cursor position
int x = event->globalX();
int y = event->globalY();
bool reset = false;
// Check cursor position relative to window
if (event->x() < 0)
{
x -= event->x();
reset = true;
}
else if (event->x() >= s.width())
{
x += s.width() - event->x() - 1;
reset = true;
}
if (event->y() < 0)
{
y -= event->y();
reset = true;
}
else if (event->y() >= s.height())
{
y += s.height() - event->y() - 1;
reset = true;
}
// if reset needed move cursor
if (reset) QCursor::setPos(x,y);
}
this involving QGraphicsItem::itemChange(). If you have an item which you want to restrict to a certain area then reimplement itemChange() for that item and monitor QGraphicsItem::ItemPositionHasChanged changes to see whether the items wants to be placed outside your area of interest and prevent that by returning a position from inside that area. for example:
QVariant QGraphicsItem::itemChange(GraphicsItemChange change, const QVariant &value)
{
switch (change) {
case ItemPositionHasChanged:
if(x() < -200 || y() < -200 || x() > 200 || y() > 200)
setPos(0, 0);
graph->itemMoved();
break;
default:
break;
};
return QGraphicsItem::itemChange(change, value);
}

Resources