📜 ⬆️ ⬇️

qt-items - a new framework, or an attempt to find a theory of everything

As you know, physicists have long tried to find the Theory of Everything, within which one could explain all known interactions in nature. The tendency to generalization is inherent not only to physicists, but also to mathematicians, and programmers. The ability of a smaller number of entities to explain and predict a wide range of phenomena is very valuable. For programmers, various APIs and frameworks act as theories. Some of them solve narrowly specialized problems, and some claim to be universal theories. An example of the latter is Qt, a universal framework designed primarily for GUI development.

Next, I’ll tell you what I don’t like in Qt and how it can be made even more versatile, powerful and convenient for work.

Demo video (better to watch in HD):
')


Qt, like many other GUI frameworks, has evolved from simple to complex. First, simple widgets were created, then more complex and complex ones. Appeared Model / View framework, to display data in a tabular or tree view. Appeared Graphics Items framework to display a set of graphic elements. All of these frameworks have different APIs and are incompatible with each other. In fact, we have three independent and almost overlapping theories within the framework of one big one. When I need to develop a new visual element, then I have to choose in which of the three frameworks I am going to use it and use the corresponding API. Thus, I cannot create an element that could be used as a separate widget, and embedded in the table cells, and used in the nodes of the graphic scene.

Qt develops under the slogan - Write once, run anythere. This may be true for writing final applications, but for expansion and customization of the library itself this is not the case.

Let's think about how widgets should be arranged in order for the Qt library to become truly unified and powerful.

Consider different widgets (checkbox, table, tree and graphic scene) and try to find something in them in common. Information in them is grouped into cells (Items). A checkbox consists of a single cell, a table is made up of rows and columns of cells; in the scene, the cells are nodes. Thus, it can be said that all widgets display cells, only their number and location in space are specific for different types of widgets. Let's say that the widget displays some space of cells ( Space ). For simple widgets, the cell space is trivial to a SpaceItem , and consists of a single cell. For a table, you can create a SpaceGrid that describes how cells are organized into rows and columns. For the graphic scene, we have SpaceScene , where cells can be placed as you like.

What do all the spaces have in common, what can be distinguished into the base class?
So far, two things can be distinguished:
  1. Return the total size of the space (usually the bounding box of all cells)
  2. Return the location of the cell according to its ItemID


class Space { virtual QSize size() const = 0; virtual QRect itemRect(ItemID item) const = 0; }; 

Let's now take a close look at the cells themselves. For clarity, we will study the following table:



The cells also have some structure. For example, the checkbox consists of a small square with a check mark and text. In a table, cells can be very complex (contain text, pictures, links, as in my video example). Note that for a table, as a rule, the cells in one column have the same structure. Therefore, it is easier for us to describe not every cell, but a whole set. A range of cells can be different, for example, all RangeAll cells, cells from the RangeColumn column, cells from the RangeRow row, cells from even-numbered RangeOddRow rows, etc. What kind of interface can be distinguished for the base class Range ? The interface is simple and concise - to answer the question whether some cell is in the Range or not:

 class Range { virtual bool hasItem(ItemID item) const = 0; }; 

After we have decided on a subset of cells, we need to specify what type of information in these cells we want to display. The view class will be responsible for displaying the smallest and indivisible piece of information. For example, ViewCheck can display a checkbox icon, ViewText displays a string of text, etc.

So far, the base View class should only be able to draw information in a cell:

 class View { virtual void draw(QPainter* painter, ItemID item, QRect rect) const = 0; }; 

The question arises, where does ViewCheck know that he needs to draw the icon on the left in the cell, and ViewText knows that he needs to draw text after the checkbox icon? To do this, we will create another "dwarf" class Layout . This class can place a View inside a cell. For example, LayoutLeft will place View at the left edge of the cell, LayoutRight at the right, and LayoutClient will occupy the entire space of the cell. Here is the basic interface:

 class Layout { virtual void doLayout(ItemID item, View view, QRect& itemRect, QRect& viewRect) const = 0; }; 

The doLayout function changes the itemRect and viewRect parameters so that the view is positioned inside the item cell. For example, LayoutLeft requests the size required by the view to display information in the cell, and bites the necessary space from itemRect. As you can see, one more function is required from the View interface - size:

 class View { virtual void draw(QPainter* painter, ItemID item, QRect rect) const = 0; virtual QSize size(ItemID item) const = 0; }; 

As a result, in order to describe what and how we want to display in the cells of a certain space, we need to enumerate triples of objects tuple <Range, View, Layout>. I called this triple ItemSchema . Completely our Space class looks like this:

 class Space { virtual QSize size() const = 0; virtual QRect itemRect(ItemID item) const = 0; QVector<ItemSchema> schemas; }; 

Here is a vivid example (the signatures are a bit outdated, but I think the main idea is clear):



Creating different heirs of the Range , View and Layout classes, and combining them in various ways, we have rich possibilities for customizing any cell space and, thus, any widget. For example, having created the ViewRating class, which displays an estimate in the form of asterisks, I can use it both as a separate widget, in table cells, and in elements of a graphic scene.

This architecture has the cooperation of programmers. Someone can write their own type of cell space, which lays out the cells in some special way. Someone will write a View that displays specific data. And these programmers can take advantage of each other’s work. This is not a complete list of my implementations of the View class, they are easy to create and use (the implementation is literally a few lines of code):
  1. ViewButton - draws a button;
  2. ViewCheck - draws the checkbox icon;
  3. ViewColor - fills the area with a specific color;
  4. ViewEnumText - draws text from a limited list;
  5. ViewImage , ViewPixmap , ViewStyleStandardPixmap - draw images;
  6. ViewLink - draws text links;
  7. ViewAlternateBackground - draws across the stripes;
  8. ViewProgressLabel , ViewProgressBox - draw a progress bar or percents;
  9. ViewRadio - draws the radio button icon;
  10. ViewRating - draws assessment icons;
  11. ViewSelection - draws selected cells;
  12. ViewText - draws text;
  13. ViewTextFont - changes the font of the following text;
  14. ViewVisible - shows or hides another View;


Go ahead. As a rule, the widget does not display the whole space of cells, but only the visible part. The Space class is convenient for describing cell space, but bad for drawing cells in some limited visible area. Let's define a special class to display the sub-region of the CacheSpace space:

 class CacheSpace { // reference to items space Space space; // visible area QRect window; // draw cached items void draw(QPainter* painter) const; // visit all cached items virtual void visit(Visitor visitor) = 0; }; 


Each specific heir from CacheSpace ( CacheGrid , CacheScene , etc.) stores a set of cached CacheItem cells differently (but optimally for a given type of space). Therefore, we will allocate the visit function in the base class, which visits all cached cells. Using it, it is easy to implement the draw function — you just need to visit all the cached cells and call their own draw function.

As the name implies, CacheItem stores all the information needed to display a specific cell:

 class CacheItem { ItemID item; QRect itemRect; QVector<CacheView> views; void draw(QPainter* painter) const; }; 

Here, the draw function is also very simple - in a loop, call draw on the CacheView class, which is responsible for drawing the smallest and indivisible piece of information inside the cell.

 class CacheView { View view; QRect viewRect; void draw(QPainter* painter, ItemID item) const; }; 


Thus, the widget needs to have CacheSpace and use it to draw the contents of its cell space:

 class Widget { // space of items Space space; // cache of visible area of space CacheSpace cacheSpace; void paintEvent(QPaintEvent *event) override; void resizeEvent(QResizeEvent *event) override; }; 

In the resizeEvent handler, we change the visible region of the cacheSpace.window object, and in the paintEvent handler, we draw its contents cacheSpace.draw ().

As you can see, the hierarchy of CacheSpace-> CacheItem-> CacheView objects allows us to “see” the entire visual structure of the widget with maximum details. We can access any smallest and indivisible piece of information, going down from the CacheSpace level to the level of a separate CacheItem cell and, further, going through individual CacheView inside the cell.

This ability, to present any widget as a hierarchy of CacheSpace-> CacheItem-> CacheView, gives us great control and introspection of the widget.

For example, we can implement a single access interface to any of our widgets from the automated testing system. The automatic GUI testing system usually queries the necessary area in the widget and then acts on this area with the mouse, simulating the user's actions. We can provide such a system with the most detailed "map" of areas that can be affected.

Another example is the animations presented in the video example. We can not only see what our widget consists of, but also affect its component parts. For example, you can change the location of any objects in the hierarchy (CacheSpace-> CacheItem-> CacheView) in time or draw them with translucency. Thus, you can collect a whole library of animations that can be applied to any widget and to any space of cells.

As a result, I want to once again list in which areas you can customize this library:
  1. Space - you can create your own cell space types
  2. CacheSpace - you can create new types of display spaces, for example, implement CacheSpaceCourusel - display a list of cells in the form of carousel
  3. View - create new types of visualizations for cells
  4. Animation - create new animations


This note is a continuation of the previous two: here and here . The qt-items project is an implementation of the ideas from these notes.

There are still a lot of ideas and tasks for further development, so stay in touch.

Source: https://habr.com/ru/post/255573/


All Articles