I try to create a tiled base map by using Qt GUI:
I have two classes: one to set my tile images that inherits from QGraphicPixmapItem, one to load my map from a text file that is a 2D array composed of the numbers 1, 0 and 2, and add it to a scene:
But my program quits unexpectedly and I do not know where is the break:
my tile class:
class mapTile: public QGraphicsPixmapItem
{
public:
enum Tile {DOOR, GRASS, FIRE};
mapTile(): QGraphicsPixmapItem(),currentTile(GRASS){
syncBitmap();
}
Tile getTile() const {return currentTile;}
void setTile(Tile newTile){
if(currentTile!= newTile){
currentTile=newTile;
syncBitmap();
}
}
private:
void syncBitmap() {//Set my image to my tile
switch(currentTile) {
case DOOR:
image->setPixmap(QPixmap(":/mpa/castledoors.png"));
case GRASS:
image->setPixmap(QPixmap(":/map/grass3_blur.jpg"));
case FIRE:
image->setPixmap(QPixmap(":/map/feu1/png"));
}
}
Tile currentTile;
QGraphicsPixmapItem *image;
};
my class map:
Map.h:
class Map
{
public:
static const int TILE_SIZE=20; //value of the tile in pixels!!!->20X20=a tile
void paintMap(QGraphicsScene *scene);
Map();
private:
static const int WIDTH= 13;// width of the grid cell
static const int HEIGHT= 9;//height of the grid cell
//my grid cell map !!!!
int array[WIDTH][HEIGHT];
mapTile *tile; //my tile
};
And Map.cpp
/*Create a default Map*/
Map::Map()
{
QFile myfile("path to my file");
if (!myfile.open(QIODevice::ReadOnly | QIODevice::Text))
{
QMessageBox::information(0, "error", myfile.errorString());
}
QTextStream in(&myfile);
QString line ;
QStringList fields;
int i=0;
int j=0;
while (!in.atEnd()) {
//Fill my array with my list--> convert Qtring into Int
for (i=0; i<HEIGHT; i++ ) {
line = in.readLine();
fields = line.split(" ");
for(j=0;j<WIDTH;j++)
{
//cout<<fields[j].toInt();
array[i][j] = fields[j].toInt();
}
}
}
}
//Paint my map with my tile
void Map::paintMap(QGraphicsScene *scene){
int i=0, j=0;
tile= new mapTile();
for (i=0; i<HEIGHT; i++){
for(j=0; j<WIDTH; j++){
switch(array[i][j]){
case 0:
tile->setTile(mapTile::GRASS);
tile->setPos(i,j);
scene->addItem(tile);
j+=TILE_SIZE;
case 1:
tile->setTile(mapTile::FIRE);
tile->setPos(i,j);
scene->addItem(tile);
j+=TILE_SIZE;
case 2:
tile->setTile(mapTile::DOOR);
tile->setPos(i,j);
scene->addItem(tile);
j+=TILE_SIZE;
}
i+=TILE_SIZE;//
}
}
}
and finally my mainwindow (I directly give the file.cpp):
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
myMap= new Map;
scene= new QGraphicsScene(this);
myMap->paintMap(scene);
ui->graphicsView->setScene(scene);
}
MainWindow::~MainWindow()
{
delete ui;
}
Sorry guys for the long post but I am stuck!
Any ideas on what I missed?
Well, there are at least five problems in your code:
You can't share the same myTile instance when adding them to the scene. You need to create a new one each time you're adding a tile to the scene. At the moment you're creating only one instance at the start of Map::paintMap.
Your myTile class inherits from QGraphicsPixmapItem, however it also has a QGraphicsPixmapItem *image; member which it leaves uninitialized (so it's a roaming pointer), but then it uses it in image->setPixmap(QPixmap(":/mpa/castledoors.png")); which will crash. Instead, you'll want to just call setPixmap(QPixmap(":/mpa/castledoors.png")) (calling the function of your superclass).
In the above item you may have noticed that you misspelled "map" as "mpa", though this will not be the reason of your crash.
In mapTile::syncPixmap as well as in Map::paintMap, you forgot the break; statements in the switch. Without those, all tiles would appear like fire tiles.
You're adding your tile size to the i and j iterators, as if they were the pixel coordinates, but at the same time you're using them as array indexes. You need to either use separate variables for the pixel coordinates or multiply i and j by the TILE_SIZE when calling setPos instead.
Good luck!
I have finally found solutions to my problems:
I added a scene to my function paintMap for adding each item to my scene and created a QGraphicScene* getScene() that returns the scene then I just call it my mainWindow by just writing
ui->graphicsView->setScene(myMap->getScene())
Moreover in my mapTile class I don't use anymore the function setTile(). I removed it, then changed my QGraphicPixmapItem *image by a QPixmap image and in my function syncbitmap() I did
this->setPixmap(image.scaled(TILE_SIZE,TILE_SIZE));at the end of my switch! Why? Because my tile is already a QGraphicsPixmapItem (inheritance) so I just have to set a pixmap to it! And I put it at the end of the switch because if I set the image before, the image will never change because it will be already set! And finally in the constructor I removed currentTile(GRASS), added a Tile variable in my constructor as an argument and just put currentTile= name of your variable in the constructor and then called the syncBitmap function. So in paintMap() you just have to do:
tile= new mapTile(mapTile::GRASS);
tile->setPos(j*TILE_SIZE,i*TILE_SIZE);
scene->addItem(tile);
in each case! VoilĂ !
I hope that will help a newbie like me, at least to understand the logic behind! I am not sure that is the best way to do but this way works for me! :)
Related
Let's say I have a class with two Gtk::Containers like so:
class Example : public Gtk::VBox
{
public:
Example();
virtual ~Example();
private:
Gtk::HBox m_c1; // First container
Gtk::Grid m_c2; // Second container
};
Constructing an Example is implemented this way:
Example::Example()
{
const int nbRows{6};
const int nbColumns{7};
for(int col{0}; col < nbColumns; ++col)
{
Gtk::Button* btn {new Gtk::Button("A")};
m_c1.pack_start(*btn);
}
for(int row{0}; row < nbRows; ++row)
{
for(int col{0}; col < nbColumns; ++col)
{
Gtk::Button* btn {new Gtk::Button("A")};
m_c2.attach(*btn, col, row, 1, 1);
}
}
// Layout setup:
pack_start(m_c1);
pack_start(m_c2);
show_all();
}
What I want to do is to make sure the child widgets in m_c2 are the same size as the child widget in m_c1, to ensure visual consistency between the two containers. Otherwise, it looks like this:
Just to make sure, I do not want m_c1 to be made a Gtk::Grid.
Here's what I have tried so far: I used the get_child_at() method available from the Gtk::HBox m_c1 to get a reference to its first child. I then called get_width() and get_height() on that child. The idea was to feed m_c2's child widgets these dimensions. The problem is that the returned reference is a nullptr.
From some posts I have read, it seems my widgets may no yet be realized, and that would explain my difficulties. How could I achieve this?
You need to set each button in the grid to expand horizontally. Then all the sizing will take care of itself.
I have started to learn Qt, and try to improve my basic C++ skills.
In GraphicsScene, I try to draw a line by using the mouse (mouse events).
When I start drawing a line in GraphicsScene, a thin dashed line is drawn from the origin, where I clicked first to the current mouse position and moves with the mouse, before the second point is clicked. To erase it, I draw it in black. If I hover over already draw lines, you will see the black drawn lines on them. To undraw it without leaving marks, an XOR operation on GraphicsScene would come in handy, or if I could draw in a different layer and not touching the other layer could be handy. But I couldn't yet figure how to do it. The example is on https://github.com/JackBerkhout/QT_Draw001
In line.cpp is the function setLineP2(int x, int y), which draws and erases that thin dashed line.
Can anybody help me with this, please?
The major misconception is thinking of a QGraphicsScene as some sort of a bitmap: it's not! It is a collection of items that can render themselves, and a spatial index for them. In a scene, if you wish to delete/hide something, you must not overpaint it - instead simply delete/hide the item in question as desired. The scene will handle all the details - that's what it's for
You must forget about GDI-anything at this point. You're not painting on the raw DC here. Even when using raw GDI, you do not want to paint on the window's DC as that flickers, you should paint on a bitmap and blit the bitmap to the window.
For example, your eraseScene method adds a rectangle on top of the scene, wasting memory and resources as all the previous items are retained (you can iterate through them), whereas all it should do is to clear the scene (or its equivalent):
void MainWindow::eraseScreen(void)
{
[...]
scene->addRect(0, 0, width()+1000, height()+1000, pen, brush);
}
vs. the correct:
void MainWindow::eraseScreen(void)
{
scene->clear();
}
Below is a complete example that approximates what you presumably meant to do in your code. It is 120 lines long. It was somewhat hard to figure out what exactly you meant to do as your code is so convoluted - it's useful to describe the exact behavior in simple terms in the question.
The example uses QPainterPath to keep a list of MoveTo and LineTo elements that a QPainterPathItem renders. It also uses a QGraphicsLineItem to display the transient line.
The MyScene::PathUpdater is used to enclose the context where a path is modified, and ensure that proper pre- and post-conditions are maintained. Namely:
Since QPainterPath is implicitly shared, you should clear the path held by QGraphicsPathItem to avoid an unnecessary implicit copy. That's the precondition necessary before modifying m_path.
After m_path has been modified, the path item must be updated, as well as a new status emitted.
The following other points are worth noting:
Holding the members by value leads to a notable absence of any memory management code (!) - the compiler does it all for us. You won't find a single new or delete anywhere. They are not necessary, and we're paying no additional cost for not doing this manually. Modern C++ should look exactly like this.
The clear split between the display MainWindow and MyScene. The MainWindow knows nothing about the specifics of MyScene, and vice-versa. The code within main acts as an adapter between the two.
The leveraging of C++11.
Succinct style necessary for SO test cases and examples: for learning it's best to keep it all in one file to easily see all the parts of the code. It's only 120 lines, vs. more than twice that if split across files. Our brains leverage the locality of reference. By splitting the code you're making it harder for yourself to comprehend.
See also
Another demo of interactive item creation.
A more advanced example of status notifications.
// https://github.com/KubaO/stackoverflown/tree/master/questions/scene-polygon-7727656
#include <QtWidgets>
class MainWindow : public QWidget
{
Q_OBJECT
QGridLayout m_layout{this};
QPushButton m_new{"New"};
QPushButton m_erase{"Erase All"};
QLabel m_label;
QGraphicsView m_view;
public:
MainWindow() {
m_layout.addWidget(&m_new, 0, 0);
m_layout.addWidget(&m_erase, 0, 1);
m_layout.addWidget(&m_label, 0, 2);
m_layout.addWidget(&m_view, 1, 0, 1, 3);
m_view.setBackgroundBrush(Qt::black);
m_view.setAlignment(Qt::AlignBottom | Qt::AlignLeft);
m_view.scale(1, -1);
connect(&m_new, &QPushButton::clicked, this, &MainWindow::newItem);
connect(&m_erase, &QPushButton::clicked, this, &MainWindow::clearScene);
}
void setScene(QGraphicsScene * scene) {
m_view.setScene(scene);
}
Q_SIGNAL void newItem();
Q_SIGNAL void clearScene();
Q_SLOT void setText(const QString & text) { m_label.setText(text); }
};
class MyScene : public QGraphicsScene {
Q_OBJECT
public:
struct Status {
int paths;
int elements;
};
private:
bool m_newItem = {};
Status m_status = {0, 0};
QPainterPath m_path;
QGraphicsPathItem m_pathItem;
QGraphicsLineItem m_lineItem;
struct PathUpdater {
Q_DISABLE_COPY(PathUpdater)
MyScene & s;
PathUpdater(MyScene & scene) : s(scene) {
s.m_pathItem.setPath({}); // avoid a copy-on-write
}
~PathUpdater() {
s.m_pathItem.setPath(s.m_path);
s.m_status = {0, s.m_path.elementCount()};
for (auto i = 0; i < s.m_status.elements; ++i) {
auto element = s.m_path.elementAt(i);
if (element.type == QPainterPath::MoveToElement)
s.m_status.paths++;
}
emit s.statusChanged(s.m_status);
}
};
void mousePressEvent(QGraphicsSceneMouseEvent *event) override {
PathUpdater updater(*this);
auto pos = event->scenePos();
m_lineItem.setLine(0, 0, pos.x(), pos.y());
m_lineItem.setVisible(true);
if (m_path.elementCount() == 0 || m_newItem)
m_path.moveTo(pos);
m_path.lineTo(pos.x()+1,pos.y()+1); // otherwise lineTo is a NOP
m_newItem = {};
}
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) override {
PathUpdater updater(*this);
auto pos = event->scenePos();
m_lineItem.setLine(0, 0, pos.x(), pos.y());
m_path.setElementPositionAt(m_path.elementCount()-1, pos.x(), pos.y());
}
void mouseReleaseEvent(QGraphicsSceneMouseEvent *) override {
m_lineItem.setVisible(false);
}
public:
MyScene() {
addItem(&m_pathItem);
addItem(&m_lineItem);
m_pathItem.setPen({Qt::red});
m_pathItem.setBrush(Qt::NoBrush);
m_lineItem.setPen({Qt::white});
m_lineItem.setVisible(false);
}
Q_SLOT void clear() {
PathUpdater updater(*this);
m_path = {};
}
Q_SLOT void newItem() {
m_newItem = true;
}
Q_SIGNAL void statusChanged(const MyScene::Status &);
Status status() const { return m_status; }
};
int main(int argc, char *argv[])
{
using Q = QObject;
QApplication app{argc, argv};
MainWindow w;
MyScene scene;
w.setMinimumSize(600, 600);
w.setScene(&scene);
Q::connect(&w, &MainWindow::clearScene, &scene, &MyScene::clear);
Q::connect(&w, &MainWindow::newItem, &scene, &MyScene::newItem);
auto onStatus = [&](const MyScene::Status & s){
w.setText(QStringLiteral("Paths: %1 Elements: %2").arg(s.paths).arg(s.elements));
};
Q::connect(&scene, &MyScene::statusChanged, onStatus);
onStatus(scene.status());
w.show();
return app.exec();
}
#include "main.moc"
I am trying to create items in one panel and add to a second panel. In the second panel I want them to be movable (and have context menu). The AddItem button should add the item from the RenderArea, to the existing list of items in the CollectionView (which may already have been moved)
In the code below, the ShapeView::addItem() is supposed to create a copy of the item from the RenderArea (where it can change shape, color etc but is not movable, starts at (0,0)), place it in the CollectionView, where it is movable. The RenderArea holds one item - once added to CollectionView, the RenderArea item should reset.
What is happening... I can't seem to be able to separate the item from the two classes.
When I add item, even if the items in
CollectionView have moved, their position resets to 0,0 (or whatever the initial position was in the RenderArea; but the adding
works properly, the individual item properties are correct, like shape and color; also - CollectionView items move, RenderArea item
doesn't).
I am posting all the relevant code:
class Item : public QGraphicsItem
{
Item() { setFlag(ItemIsMovable, false); }
Item::Item(Item ©Item) { // copy constructor
setFlag(ItemIsMovable);
setPos(copyItem.pos()); // doesn't seem to work
QRectF boundingRect() const { return QRectF(-35, -30, 35, 20); }
void Item::paint(QPainter *painter, const QStyleOptionGraphicsItem */*option*/, QWidget */*widget*/) {
painter->drawRect(boundingRect()); }
protected:
void mouseMoveEvent(QGraphicsSceneMouseEvent *event) {
QGraphicsItem::mouseMoveEvent(event);
}
};
class CollectionView : public QGraphicsView
{
Q_OBJECT
public:
CollectionView(QWidget *parent = 0);
void update();
QList<Item*> *m_items;
};
CollectionView::CollectionView(QWidget *parent)
: QGraphicsView(parent)
{
QGraphicsScene *s = new QGraphicsScene(this);
s->setSceneRect(-width()/2, -height()/2, width(), height());
setScene(s);
setViewportUpdateMode(BoundingRectViewportUpdate);
m_items = new QList<Item*>();
scene()->clear();
}
void CollectionView::update()
{
scene()->clear();
for(int i = 0; i< m_items->size(); i++)
{
Item* item = new Item(*m_items->at(i));
item->setPos(m_items->at(i)->p); // doesn't seem to work
scene()->addItem(item);
}
viewport()->update();
}
class RenderArea : public QGraphicsView
{
Q_OBJECT
public:
RenderArea(QWidget *parent = 0);
public:
Item* item;
};
RenderArea::RenderArea(QWidget *parent)
: QGraphicsView(parent)
{
QGraphicsScene *s = new QGraphicsScene(this);
s->setSceneRect(-width()/2, -height()/2, width(), height());
setScene(s);
setViewportUpdateMode(BoundingRectViewportUpdate);
item = new Item();
s->addItem(item);
}
// this is the boss/coordinator class
class ShapeView : public QGraphicsView
{
Q_OBJECT
public:
ShapeView(QWidget *parent = 0);
CollectionView *collectionView;
private slots: // more slots corresponding to more controls
void addItem();
private:
QPushButton *addButton;
RenderArea *renderArea;
};
ShapeView::ShapeView(QWidget *parent)
: QGraphicsView(parent)
{
collectionView = new CollectionView(parent);
renderArea = new RenderArea(this);
addButton = new QPushButton(tr("Add Item"));
connect(addButton, SIGNAL(clicked()), this, SLOT(addItem()));
}
void ShapeView::addItem()
{
Item* item = new Item(*renderArea->item);
collectionView->m_items->append(item);
collectionView->update();
// place a new item on renderarea
renderArea->item = new Item();
renderArea->scene()->clear();
renderArea->scene()->addItem(renderArea->item);
renderArea->viewport()->update();
}
Something is wrong in either the way I copy the item, I just don't know what. I see no reason why, when adding items, the CollectionView items all snap to the (0,0) position. Perhaps the issue is in the copy constructor but I can't figure what is wrong.
I hope someone can help give me a hint
Update: It seems that the problem is in the CollectionView::update() function... adding a 'delete' at the end of the loop removes the item from the collection... Or in the ShapeView::addItem() function... Am I creating copy of the item or just reusing it ?
But how do I create a copy of the item ?
My assumption was that there was a connection between the items I paint on QGraphicsScene and their representation (as pointers to the objects). Wrong... In fact I misunderstood what the pointer was pointing at...
I updated the position of the items in my list after mouse move and everything is fine.
void CollectionView::mouseMoveEvent(QMouseEvent *event)
{
Item *currentItem = (Item*)itemAt(event->pos().x(), event->pos().y());
if(!currentItem) return;
QGraphicsView::mouseMoveEvent(event);
m_items->at(currentItem->z)->setPos(currentItem->pos());
}
The above fixes the code shown. But it is not the best fix.
The better solution to the problem - remove my list of items completely, since QGraphicsScene already holds a copy. (there was one benefit of handling my own list of items - bringForward and sendBackward for items was easier to implement since my z values were equal to item index)
I've done all of the layouts and have most of the code written even. But, I'm stuck in two places.
1) I'm not quite sure how to set up the timer. Am I using it correctly in the gridwindow class? And, am I used the timer functions/signals/slots correctly with the other gridwindow functions.
2) In GridWindow's timerFired() function, I'm having trouble checking/creating the vector-vectors. I wrote out in the comments in that function exactly what I am trying to do.
Any help would be much appreciated.
main.cpp
// Main file for running the grid window application.
#include <QApplication>
#include "gridwindow.h"
//#include "timerwindow.h"
#include <stdexcept>
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
void Welcome(); // Welcome Function - Prints upon running program; outputs program name, student name/id, class section.
void Rules(); // Rules Function: Prints the rules for Conway's Game of Life.
using namespace std;
// A simple main method to create the window class and then pop it up on the screen.
int main(int argc, char *argv[])
{
Welcome(); // Calls Welcome function to print student/assignment info.
Rules(); // Prints Conway's Game Rules.
QApplication app(argc, argv); // Creates the overall windowed application.
int rows = 25, cols = 35; //The number of rows & columns in the game grid.
GridWindow widget(NULL,rows,cols); // Creates the actual window (for the grid).
widget.show(); // Shows the window on the screen.
return app.exec(); // Goes into visual loop; starts executing GUI.
}
// Welcome Function: Prints my name/id, my class number, the assignment, and the program name.
void Welcome()
{
cout << endl;
cout << "-------------------------------------------------------------------------------------------------" << endl;
cout << "Name/ID - Gabe Audick #7681539807" << endl;
cout << "Class/Assignment - CSCI-102 Disccusion 29915: Homework Assignment #4" << endl;
cout << "-------------------------------------------------------------------------------------------------" << endl << endl;
}
// Rules Function: Prints the rules for Conway's Game of Life.
void Rules()
{
cout << "Welcome to Conway's Game of Life." << endl;
cout << "Game Rules:" << endl;
cout << "\t 1) Any living cell with fewer than two living neighbours dies, as if caused by underpopulation." << endl;
cout << "\t 2) Any live cell with more than three live neighbours dies, as if by overcrowding." << endl;
cout << "\t 3) Any live cell with two or three live neighbours lives on to the next generation." << endl;
cout << "\t 4) Any dead cell with exactly three live neighbours becomes a live cell." << endl << endl;
cout << "Enjoy." << endl << endl;
}
gridcell.h
// A header file for a class representing a single cell in a grid of cells.
#ifndef GRIDCELL_H_
#define GRIDCELL_H_
#include <QPalette>
#include <QColor>
#include <QPushButton>
#include <Qt>
#include <QWidget>
#include <QFrame>
#include <QHBoxLayout>
#include <iostream>
// An enum representing the two different states a cell can have.
enum CellType
{
DEAD, // DEAD = Dead Cell. --> Color = White.
LIVE // LIVE = Living Cell. ---> Color = White.
};
/*
Class: GridCell.
A class representing a single cell in a grid. Each cell is implemented
as a QT QFrame that contains a single QPushButton. The button is sized
so that it takes up the entire frame. Each cell also keeps track of what
type of cell it is based on the CellType enum.
*/
class GridCell : public QFrame
{
Q_OBJECT // Macro allowing us to have signals & slots on this object.
private:
QPushButton* button; // The button inside the cell that gives its clickability.
CellType type; // The type of cell (DEAD or LIVE.)
public slots:
void handleClick(); // Callback for handling a click on the current cell.
void setType(CellType type); // Cell type mutator. Calls the "redrawCell" function.
signals:
void typeChanged(CellType type); // Signal to notify listeners when the cell type has changed.
public:
GridCell(QWidget *parent = NULL); // Constructor for creating a cell. Takes parent widget or default parent to NULL.
virtual ~GridCell(); // Destructor.
void redrawCell(); // Redraws cell: Sets new type/color.
CellType getType() const; //Simple getter for the cell type.
private:
Qt::GlobalColor getColorForCellType(); // Helper method. Returns color that cell should be based from its value.
};
#endif
gridcell.cpp
#include <iostream>
#include "gridcell.h"
#include "utility.h"
using namespace std;
// Constructor: Creates a grid cell.
GridCell::GridCell(QWidget *parent)
: QFrame(parent)
{
this->type = DEAD; // Default: Cell is DEAD (white).
setFrameStyle(QFrame::Box); // Set the frame style. This is what gives each box its black border.
this->button = new QPushButton(this); //Creates button that fills entirety of each grid cell.
this->button->setSizePolicy(QSizePolicy::Expanding,QSizePolicy::Expanding); // Expands button to fill space.
this->button->setMinimumSize(19,19); //width,height // Min height and width of button.
QHBoxLayout *layout = new QHBoxLayout(); //Creates a simple layout to hold our button and add the button to it.
layout->addWidget(this->button);
setLayout(layout);
layout->setStretchFactor(this->button,1); // Lets the buttons expand all the way to the edges of the current frame with no space leftover
layout->setContentsMargins(0,0,0,0);
layout->setSpacing(0);
connect(this->button,SIGNAL(clicked()),this,SLOT(handleClick())); // Connects clicked signal with handleClick slot.
redrawCell(); // Calls function to redraw (set new type for) the cell.
}
// Basic destructor.
GridCell::~GridCell()
{
delete this->button;
}
// Accessor for the cell type.
CellType GridCell::getType() const
{
return(this->type);
}
// Mutator for the cell type. Also has the side effect of causing the cell to be redrawn on the GUI.
void GridCell::setType(CellType type)
{
this->type = type;
redrawCell();
}
// Handler slot for button clicks. This method is called whenever the user clicks on this cell in the grid.
void GridCell::handleClick()
{ // When clicked on...
if(this->type == DEAD) // If type is DEAD (white), change to LIVE (black).
type = LIVE;
else
type = DEAD; // If type is LIVE (black), change to DEAD (white).
setType(type); // Sets new type (color). setType Calls redrawCell() to recolor.
}
// Method to check cell type and return the color of that type.
Qt::GlobalColor GridCell::getColorForCellType()
{
switch(this->type)
{
default:
case DEAD:
return Qt::white;
case LIVE:
return Qt::black;
}
}
// Helper method. Forces current cell to be redrawn on the GUI. Called whenever the setType method is invoked.
void GridCell::redrawCell()
{
Qt::GlobalColor gc = getColorForCellType(); //Find out what color this cell should be.
this->button->setPalette(QPalette(gc,gc)); //Force the button in the cell to be the proper color.
this->button->setAutoFillBackground(true);
this->button->setFlat(true); //Force QT to NOT draw the borders on the button
}
gridwindow.h
// A header file for a QT window that holds a grid of cells.
#ifndef GRIDWINDOW_H_
#define GRIDWINDOW_H_
#include <vector>
#include <QWidget>
#include <QTimer>
#include <QGridLayout>
#include <QLabel>
#include <QApplication>
#include "gridcell.h"
/*
class GridWindow:
This is the class representing the whole window that comes up when this program runs.
It contains a header section with a title, a middle section of MxN cells and a bottom section with buttons.
*/
class GridWindow : public QWidget
{
Q_OBJECT // Macro to allow this object to have signals & slots.
private:
std::vector<std::vector<GridCell*> > cells; // A 2D vector containing pointers to all the cells in the grid.
QLabel *title; // A pointer to the Title text on the window.
QTimer *timer; // Creates timer object.
public slots:
void handleClear(); // Handler function for clicking the Clear button.
void handleStart(); // Handler function for clicking the Start button.
void handlePause(); // Handler function for clicking the Pause button.
void timerFired(); // Method called whenever timer fires.
public:
GridWindow(QWidget *parent = NULL,int rows=3,int cols=3); // Constructor.
virtual ~GridWindow(); // Destructor.
std::vector<std::vector<GridCell*> >& getCells(); // Accessor for the array of grid cells.
private:
QHBoxLayout* setupHeader(); // Helper function to construct the GUI header.
QGridLayout* setupGrid(int rows,int cols); // Helper function to constructor the GUI's grid.
QHBoxLayout* setupButtonRow(); // Helper function to setup the row of buttons at the bottom.
};
#endif
gridwindow.cpp
#include <iostream>
#include "gridwindow.h"
using namespace std;
// Constructor for window. It constructs the three portions of the GUI and lays them out vertically.
GridWindow::GridWindow(QWidget *parent,int rows,int cols)
: QWidget(parent)
{
QHBoxLayout *header = setupHeader(); // Setup the title at the top.
QGridLayout *grid = setupGrid(rows,cols); // Setup the grid of colored cells in the middle.
QHBoxLayout *buttonRow = setupButtonRow(); // Setup the row of buttons across the bottom.
QVBoxLayout *layout = new QVBoxLayout(); // Puts everything together.
layout->addLayout(header);
layout->addLayout(grid);
layout->addLayout(buttonRow);
setLayout(layout);
}
// Destructor.
GridWindow::~GridWindow()
{
delete title;
}
// Builds header section of the GUI.
QHBoxLayout* GridWindow::setupHeader()
{
QHBoxLayout *header = new QHBoxLayout(); // Creates horizontal box.
header->setAlignment(Qt::AlignHCenter);
this->title = new QLabel("CONWAY'S GAME OF LIFE",this); // Creates big, bold, centered label (title): "Conway's Game of Life."
this->title->setAlignment(Qt::AlignHCenter);
this->title->setFont(QFont("Arial", 32, QFont::Bold));
header->addWidget(this->title); // Adds widget to layout.
return header; // Returns header to grid window.
}
// Builds the grid of cells. This method populates the grid's 2D array of GridCells with MxN cells.
QGridLayout* GridWindow::setupGrid(int rows,int cols)
{
QGridLayout *grid = new QGridLayout(); // Creates grid layout.
grid->setHorizontalSpacing(0); // No empty spaces. Cells should be contiguous.
grid->setVerticalSpacing(0);
grid->setSpacing(0);
grid->setAlignment(Qt::AlignHCenter);
for(int i=0; i < rows; i++) //Each row is a vector of grid cells.
{
std::vector<GridCell*> row; // Creates new vector for current row.
cells.push_back(row);
for(int j=0; j < cols; j++)
{
GridCell *cell = new GridCell(); // Creates and adds new cell to row.
cells.at(i).push_back(cell);
grid->addWidget(cell,i,j); // Adds to cell to grid layout. Column expands vertically.
grid->setColumnStretch(j,1);
}
grid->setRowStretch(i,1); // Sets row expansion horizontally.
}
return grid; // Returns grid.
}
// Builds footer section of the GUI.
QHBoxLayout* GridWindow::setupButtonRow()
{
QHBoxLayout *buttonRow = new QHBoxLayout(); // Creates horizontal box for buttons.
buttonRow->setAlignment(Qt::AlignHCenter);
// Clear Button - Clears cell; sets them all to DEAD/white.
QPushButton *clearButton = new QPushButton("CLEAR");
clearButton->setFixedSize(100,25);
connect(clearButton, SIGNAL(clicked()), this, SLOT(handleClear()));
buttonRow->addWidget(clearButton);
// Start Button - Starts game when user clicks. Or, resumes game after being paused.
QPushButton *startButton = new QPushButton("START/RESUME");
startButton->setFixedSize(100,25);
connect(startButton, SIGNAL(clicked()), this, SLOT(handleStart()));
buttonRow->addWidget(startButton);
// Pause Button - Pauses simulation of game.
QPushButton *pauseButton = new QPushButton("PAUSE");
pauseButton->setFixedSize(100,25);
connect(pauseButton, SIGNAL(clicked()), this, SLOT(handlePause()));
buttonRow->addWidget(pauseButton);
// Quit Button - Exits program.
QPushButton *quitButton = new QPushButton("EXIT");
quitButton->setFixedSize(100,25);
connect(quitButton, SIGNAL(clicked()), qApp, SLOT(quit()));
buttonRow->addWidget(quitButton);
return buttonRow; // Returns bottom of layout.
}
/*
SLOT method for handling clicks on the "clear" button.
Receives "clicked" signals on the "Clear" button and sets all cells to DEAD.
*/
void GridWindow::handleClear()
{
for(unsigned int row=0; row < cells.size(); row++) // Loops through current rows' cells.
{
for(unsigned int col=0; col < cells[row].size(); col++)
{
GridCell *cell = cells[row][col]; // Grab the current cell & set its value to dead.
cell->setType(DEAD);
}
}
}
/*
SLOT method for handling clicks on the "start" button.
Receives "clicked" signals on the "start" button and begins game simulation.
*/
void GridWindow::handleStart()
{
this->timer = new QTimer(this); // Creates new timer.
connect(this->timer, SIGNAL(timeout()), this, SLOT(timerFired())); // Connect "timerFired" method class to the "timeout" signal fired by the timer.
this->timer->start(500); // Timer to fire every 500 milliseconds.
}
/*
SLOT method for handling clicks on the "pause" button.
Receives "clicked" signals on the "pause" button and stops the game simulation.
*/
void GridWindow::handlePause()
{
this->timer->stop(); // Stops the timer.
delete this->timer; // Deletes timer.
}
// Accessor method - Gets the 2D vector of grid cells.
std::vector<std::vector<GridCell*> >& GridWindow::getCells()
{
return this->cells;
}
void GridWindow::timerFired()
{
// I'm not sure how to write this code.
// I want to take the original vector-vector, and also make a new, empty vector-vector of the same size.
// I would then go through the code below with the original vector, and apply the rules to the new vector-vector.
// Finally, I would make the new vector-vecotr the original vector-vector. (That would be one step in the simulation.)
cout << cells[1][2];
/*
for (unsigned int m = 0; m < original.size(); m++)
{
for (unsigned int n = 0; n < original.at(m).size(); n++)
{
unsigned int neighbors = 0; //Begin counting number of neighbors.
if (original[m-1][n-1].getType() == LIVE) // If a cell next to [i][j] is LIVE, add one to the neighbor count.
neighbors += 1;
if (original[m-1][n].getType() == LIVE)
neighbors += 1;
if (original[m-1][n+1].getType() == LIVE)
neighbors += 1;
if (original[m][n-1].getType() == LIVE)
neighbors += 1;
if (original[m][n+1].getType() == LIVE)
neighbors += 1;
if (original[m+1][n-1].getType() == LIVE)
neighbors += 1;
if (original[m+1][n].getType() == LIVE)
neighbors += 1;
if (original[m+1][n+1].getType() == LIVE)
neighbors += 1;
if (original[m][n].getType() == LIVE && neighbors < 2) // Apply game rules to cells: Create new, updated grid with the roundtwo vector.
roundtwo[m][n].setType(LIVE);
else if (original[m][n].getType() == LIVE && neighbors > 3)
roundtwo[m][n].setType(DEAD);
else if (original[m][n].getType() == LIVE && (neighbors == 2 || neighbors == 3))
roundtwo[m][n].setType(LIVE);
else if (original[m][n].getType() == DEAD && neighbors == 3)
roundtwo[m][n].setType(LIVE);
}
}*/
}
It looks like the timer is set up correctly to me.
For the timerFired() function, I would not create a temporary matrix of GridCell objects, but just a temporary matrix of flags that indicate whether the cell is live or not. These flags are really all that changes in your function, so just temporarily store the new flags, then set them on the original grid cells to save all of the extra memory and allocation time required for creating a temporary matrix of cells. Here is an example:
//Store flags that represent whether the new grid cells will be live or not
vector< vector<bool> > is_live(cells.size());
for(int m=0; m<cells.size(); m++)
{
is_live.at(m).resize(cells.at(m).size());
for(int n=0; n<cells.at(m).size(); n++)
{
//count neighbors
unsigned int neighbors = 0;
for(int i=-1; i<=1; i++)
{
for(int j=-1; j<=1; j++)
{
neighbors += static_cast<int>(
cells[m+i][n+j]->getType() == LIVE);
}
}
//we counted the current cell when counting the neighbors so
//subtract it back off if needed.
neighbors -= static_cast<int>(cells[m][n]->getType() == LIVE);
//Set the type to the original value
is_live[m][n] = cells[m][n]->getType() == LIVE;
//change it based on the neighbor count.
//Some of your logic around here seemed repetitive so I
//did it differently. You may want to change it back
//if you had a specific purpose for the way you did it
is_live[m][n] = (is_live[m][n] && neighbors <= 3) ||
(!is_live[m][n] && neighbors == 3);
}
}
//Set the cell types based on the is_live flags.
for(int m=0; m<cells.size(); m++)
{
for(int n=0; n<cells.at(m).size(); n++)
cells[m][n]->setType(is_live[m][n] ? LIVE : DEAD);
}
Note: I did not compile or test this so there are no guarantees.
Mission: Draw two lines with different color on one graph with automatic cliping, by adding points bit by bit.
So, what am I doing. Create class GraphWidget, inherited from QGraphicsView. Create member of QGraphicsScene. Create 2 QPainterPath instances, and add them to graphicsScene.
Then, I eventually call graphWidget.Redraw(), where call for QPainterPath.lineTo() for both instances. And I expect appearance of that lines of graphics view, but it doesn't.
I tired from reading Qt's doc and forums. What am I doing wrong?
We need to know more, what does not happen? Does the window appear at all? Are the lines not drawn? In the meantime try out this sample code if you want :) Edit: updated to show updating.
#include ...
class QUpdatingPathItem : public QGraphicsPathItem {
void advance(int phase) {
if (phase == 0)
return;
int x = abs(rand()) % 100;
int y = abs(rand()) % 100;
QPainterPath p = path();
p.lineTo(x, y);
setPath(p);
}
};
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QGraphicsScene s;
QGraphicsView v(&s);
QUpdatingPathItem item;
item.setPen(QPen(QColor("red")));
s.addItem(&item);
v.show();
QTimer *timer = new QTimer(&s);
timer->connect(timer, SIGNAL(timeout()), &s, SLOT(advance()));
timer->start(1000);
return a.exec();
}
You should get something like this:
The path in any QGraphicsPathItem can of course be updated later. You might want to keep the original painter path somewhere to avoid performance hit caused by all the path copying (I'm not sure if QPainterPath is implicitly shared...)
QPainterPath p = gPath.path();
p.lineTo(0, 42);
gPath.setPath(p);
Animation
It seems that you're trying to do some sort of animation/on-the-fly updating. There is entire framework for this in Qt. In the simplest form you can subclass QGraphicsPathItem, reimplement its advance() slot to automatically fetch next point from motion. The only thing left to do then would be calling s.advance() with the required frequency.
http://doc.trolltech.com/4.5/qgraphicsscene.html#advance
Evan Teran, sorry for that comment.
// Constructor:
GraphWidget::GraphWidget( QWidget *parent ) :
QGraphicsView(parent),
bounds(0, 0, 0, 0)
{
setScene(&scene);
QPen board_pen(QColor(255, 0, 0));
QPen nature_pen(QColor(0, 0, 255));
nature_path_item = scene.addPath( board_path, board_pen );
board_path_item = scene.addPath( nature_path, nature_pen );
}
// Eventually called func:
void GraphWidget::Redraw() {
if(motion) {
double nature[6];
double board[6];
// Get coords:
motion->getNature(nature);
motion->getBoard(board);
if(nature_path.elementCount() == 0) {
nature_path.moveTo( nature[0], nature[1] );
} else {
nature_path.lineTo( nature[0], nature[1] );
}
if(board_path.elementCount() == 0) {
board_path.moveTo( board[0], board[1] );
} else {
board_path.lineTo( board[0], board[1] );
}
}
}