0

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?

Jack Berkhout
  • 47
  • 1
  • 7
  • Could you explain me better please – eyllanesc Apr 17 '17 at 23:00
  • Hi, I need to split this over multiple comments, due to message-length limitations. Suppose I draw one thick purple line (the first one) already. Then I start to draw a second line, we click on the start point first. Than drag the mouse. We see a thin dashed white line whose endpoint follows the mouse. – Jack Berkhout Apr 18 '17 at 10:16
  • So I draw it in white. Then to update its position, I first un-draw it (just using the background color), and draw it again on the new position using white color. When I drag the line over the first one, we can see we draw a lot of black lines over that one. Finally, when I click on the endpoint, the thick purple line will be drawn. The black line marks are still visible on the first line, which we crossed during moving the mouse after the first click. – Jack Berkhout Apr 18 '17 at 10:17
  • If we could use a second layer for the line construction phase, we would not end up with (black) marks in the first layer. Or if we used an XOR pen mode, it would reconstruct the underlying color again, by drawing the second time on the same position. I hope this explains it better. In VS on Windows, this is simple, but I use Linux, and in QT it is not simple, as it seems to lack XOR functionality in graphicsscene. – Jack Berkhout Apr 18 '17 at 10:17
  • I've made a modification of your code, check if it's what you want: https://github.com/eyllanesc/stackoverflow/tree/master/GraphicsScene – eyllanesc Apr 18 '17 at 14:46
  • Hi eyllanesc, thank you very much for your work! That works perfect! Incredible! Now figure how it works... – Jack Berkhout Apr 18 '17 at 22:07
  • What purple lines? And how should they appear? – eyllanesc Apr 18 '17 at 22:10
  • I figured it, and that works now. I changed the above comment, just within 5 minutes. After I added addLine(...), those lines appear, and there are no more black marks! Now, I'm studying the code. How does it work? – Jack Berkhout Apr 18 '17 at 22:15
  • I'm reading http://doc.qt.io/qt-4.8/qgraphicslineitem.html, I'm trying to study what you created. It's exactly what I'm looking for. Also Antialiasing works now. :-) – Jack Berkhout Apr 18 '17 at 22:26
  • https://github.com/JackBerkhout/Draw002 Is with the purple line, antialiasing enabled and your working modification. The purple line is to check if that works, and it does. – Jack Berkhout Apr 18 '17 at 22:39

1 Answers1

0

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:

  1. 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.

  2. 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:

  1. 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.

  2. 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.

  3. The leveraging of C++11.

  4. 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

  1. Another demo of interactive item creation.
  2. A more advanced example of status notifications.

screenshot of the example code

// 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"
Community
  • 1
  • 1
Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313