Writing our own QTableView from scratch

So there was a Qt framework and for the last 10 years almost nothing has changed in it. And one person wanted to write his QTableView with the functionality he needed, namely, he wanted to display cells in several rows in one line. He also wanted to stretch one of the cells across the width of the other two, etc. (well, like in 1C for example).

I searched and searched for a ready-made example on the Internet and couldn’t find it. And then one day he thought to look at how the QTableView itself was made inside and he felt bad from the number of lines of code, more than one thousand there.

And the man took patience with him and walked according to the code, he walked for a long time, and on about the 5th day he realized that he had already passed here. And he turned around and looked at his path and it was revealed to him that he was walking in a circle. And his whole path appeared to him and he remembered all the roads and the kingdom of knowledge opened up to him and he understood what he wanted.

Now to the point: you need to create a template for the layout of sections. It’s essentially like a chessboard, only now a line can have 2,3, etc. row. And now one cell can be placed at 2,3, etc. squares of the chessboard both horizontally and vertically.

Now we have to somehow call what is what. The line now has horizontal and vertical rows. We will not confuse vertical rows with columns. Let’s leave the concept of a column with the same meaning as in the data model, that is, a column is a field number (in a select request).

Now the field number can be indicated in several cells in the template with only one limitation – strictly in a rectangular area. That is, not just anywhere, but compactly within the visual rectangle and without gaps.

And then it turned out that for the required rendering we override the method painEvent class QTableView and painEvent class QHeaderView and it turns out that it’s not at all difficult to draw like this:

So the point is simple in rendering the QTableView. Namely: through drawCell We draw each cell separately, passing the coordinates of its rectangle, data and drawing style. Then we draw grid lines between the cells.

To know the coordinates, width and height of each cell, four new containers are used. They are logically stored in a horizontal header.

QTableView works in conjunction with the QHeaderView header classes. There are two headers: horizontal and vertical.

Headers are important; it is by their geometry (location of sections) that we determine the location (geometry) of the cells of the table itself (plus the offset to a specific row along Y). Also in headers we change the width of the columns or the height of the rows. In essence, this is a convenient framework (template) for the geometry of the cells.

We’ll have to make our own headers. And now we have to make a separate horizontal and vertical header, because all this versatility of Qt does not suit us.

Horizontal header (QpHorHeaderView) this will be the frame for the arrangement of cells. Just as before, QTableView will access the horizontal QHeaderView to obtain information (where to draw the cell) to obtain the QRect of the cell, plus will add an offset by y to draw on a specific line.

For greater compatibility, we will keep the vertical header as such, but essentially it will take all the cell geometry data from the horizontal header.

I must say a little that creating your own QpTableView follows the principle of trying to maintain compatibility with the original QTableView functionality, that is, the class methods remain almost the same.

But it is logical to remove part of the functionality associated with merging cells (span), and you can also remove the functionality reverse sections, that is, displaying in reverse order, since this is not really relevant for us. It is not yet relevant to hide sections and we will also remove this functionality. Why hide a section when you can just initialize a new section template.

You need to understand that drawing headers and the table itself are separate independent operations. Each class has a virtual method overridden for this purpose. paintEvent.

Note: very peculiar width and height of QRect in Qt. It turns out that if we see this through qDebug(): QRect(0.0 101×101) and it seems that the width (and height) are 101. But in fact, this actually means the width (or height) is 100px.
And 101 is the number of pixels, that is, the number from 0 to 100, and this is equal to 101 pieces.

In terms of time, creating my QTableView and two QHeaderViews took approximately 4 working weeks. Since we removed span functionality, and it was highly integrated, we had to restore the work of almost all the broken functionality, in particular, interactively changing the width of columns and the height of rows (rows) with the mouse, and the selection (cells, columns, rows) also broke.

I had to understand the Qt code in some detail. For example, a cell frame in a table is drawn explicitly using drawLinebut the frame in the header is not drawn explicitly, these are background gaps between the sections.

Drawing when selecting a cell(s) does not result in drawing the entire table, but only those cells whose image has changed at least a little visually (a change in the background is also a visual change).

Regarding delegates, everything is simple: the cell rectangle is passed to the delegate and then the delegate himself draws everything in the cell as he needs. This is about comboboxes, all sorts of checkboxes, etc. It’s interesting where the delegate rendering is triggered – this is (“strangely enough”) the cell selection event and the method setSelection.

It should also probably be noted that the QTableView and QHeaderView classes both inherit from QAbstractItemView (each on his own, of course). The QAbstractItemView class inherits from QAbstractScrollArea.

Here it should be noted that the above classes are not completely abstract; they also implement part of the functionality. And more importantly, part of the functionality is implemented in their private satellites like QAbstractItemViewPrivate. This means that we will have to collect our classes as part of the Qt source code (gui branch), because the methods of private classes do not stick out into the libraries, which is the meaning laid down by Qt-names.

In fact, we are completely rewriting the QTableView and QHeaderView classes from scratch.

Therefore, we decided to name our classes with the prefix Qpso that it is clear and visual. That is, we will have classes like QpTableView, QpHorHeaderView, QpVertHeaderView. The files themselves will be called qp_tableview.h/.cpp, that is, we will also add an underscore. The underscore makes our files stand out well in the pile of Qt sources.

Yes, now about assembling our functionality as part of the Qt source code. But it doesn’t work any other way, that is, you can do purely open inheritance from QAbstractItemView, it will compile, but during assembly the linker will not find the methods QAbstratItemViewPrivate because they Not are marked as exportable in Qt libraries. As a result, you need to at least edit the Qt file header qabstractitemview_p.h and as a result, you will have to rebuild the gui source branch again. That is, you will have to rebuild the source code at least once.

This is a very unpleasant problem for Qt beginners. If anyone knows how to make your own class, inherited from QAbstractItemView, so that you can freely distribute it without having to go into the Qt source code, I will be immensely grateful…

On the other hand, once you build from source, you understand that the process of adding your functionality inside, debugging, and assembling is not at all difficult operations.

After we have drawn our table for the first time with a new cell layout template, the fun begins, namely:

  1. Creating Delegates

  2. Scrolling

  3. Changing the row width by dragging the row border to the right or left (up, down) with the mouse.

  4. Selecting a cell, selecting columns, rows, selecting an arbitrary sector, etc.

And all this kills a huge amount of time, as it forces you to thoroughly understand the work of rendering. But it’s worth it, because subsequently unnecessary questions about why something is drawn incorrectly no longer arise.

The most interesting stages of drawing the table and headers occur when interactively changing the column width or row height (in a horizontal header) by dragging the edge of the column (or row) with the mouse. When dragging the edge of a column (row), we see how the table changes and choose a visual option that is pleasant for us, but this means that the drawing occurs constantly as the mouse moves. A timer is used here because in the event moveMouseEvent You cannot immediately draw a table or header. It is more correct to set the timer and when moveMouseEvent completes successfully (and possibly several times) draw the table based on the timer event, that is, after some reasonable time.

So the train started moving and the first beta version of our QpTableView/QpHorHeaderView/QpVertHeaderView class set was released.

A short introductory video on the new features: QpTableView.

Tomorrow we will post it on GitHub and to our Chinese comrades on Gitee.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *