📜 ⬆️ ⬇️

Create a Qt interface style using a table as an example.

As you know, Qt offers developers virtually unlimited possibilities for creating interfaces. In most cases, if you use standard widgets, the appearance of your application (and its interface elements) will match the appearance of your operating system. To this end, Qt has a system of so-called styles - classes responsible for drawing standard interface elements. In this article we will try to create your own style and apply it to the simplest application in order to get a beautiful table based on the QTableWidget widget.

Theoretical part


So let's start with the theory. In Qt, there is an abstract class called QStyle , which is responsible, as it is not difficult to guess, for styling the application. It inherits a number of classes ( QWindowStyle , QMacStyle , etc.), which are styles that emulate the standard appearance of the operating system. We will make our own style for the application in their image and likeness. Qt requires that custom styles be inherited not directly from QStyle , but from one of its successor classes. Documentation recommends choosing the closest to the desired system style, and changing it “by itself”. We will inherit our class from no less abstract than QStyle , the QCommonStyle class.

How is the drawing of elements inside the style? The QStyle class has a set of functions whose names begin with the word draw :


')
Each of them does its own small part of the work on drawing elements.

drawComplexControl () is used to draw composite widgets, that is, those that contain several other widgets in themselves.
As an example, QSpinBox , which, as it is easy to guess, implements the usual SpinBox and consists of an input field and two small buttons:


drawControl () draws standard widgets like buttons and input fields.

drawPrimitive () draws so-called primitive elements, such as frames.

drawItemPixmap () and drawItemText () have completely speaking names and do exactly what you would expect from them. Detailed descriptions of all functions, as well as their arguments, can be very easily found in the Qt documentation, so I will not dwell on this.

In our example, we will create a style for the QTableWidget element, which is an ordinary table. Styles for the remaining elements are made absolutely similar.

Getting started


First of all, create a new C ++ class. When creating a class, Qt helpfully offers us to write its name, as well as the name of the class from which we want to inherit. For simplicity, let's call it myStyle , and specify the inheritance from QCommonStyle . After that, Qt will create us a couple of files (.h and .cpp), in which, for a start, we want to get something like this:

myStyle.h
#include <QCommonStyle> class myStyle : public QCommonStyle { Q_OBJECT public: explicit myStyle(); signals: public slots: }; 

myStyle.cpp
 #include "mystyle.h" myStyle::myStyle() : QCommonStyle() { } 



Our main () function will look like this:

 int main(int argc, char *argv[]) { QApplication::setStyle( new myStyle ); QApplication a(argc, argv); QTableWidget w(4,3); w.setGeometry(QRect(0,0,300,250)); w.show(); return a.exec(); } 


As you can see, all we do is set up our style, create a widget and display it. At this stage, we only have an empty style inherited from QCommonStyle , so our table will look like ... well, let's say, not very attractive:



Let us consider in more detail what the table consists of.


The structure is, in general, fairly simple and straightforward. It is worth what to stop for a second on headlines. In Qt, there are two kinds of headers: horizontal (the one on top) and vertical (the one on the left). By “Title Area” I mean the entire area on which the title will later be displayed. A section is each specific cell in the header. The unoccupied area is the part of the header where there are no sections (this happens when the total size of all sections is smaller than the size of the table).

So, armed with this knowledge, you can stylize each element. Start by adding functions to our class.
 void drawControl(ControlElement element, const QStyleOption *opt, QPainter *p, const QWidget *w) const; void drawItemText(QPainter *painter, const QRect &rect, int flags, const QPalette &pal, bool enabled, const QString &text, QPalette::ColorRole textRole) const; void drawPrimitive(PrimitiveElement pe, const QStyleOption *opt, QPainter *p, const QWidget *w) const; 


Let's start with the drawControl () function. Add to the implementation code:

 switch(element) { default: QCommonStyle::drawControl(element, opt, p, w); break; } 


The key attribute of this function is element , which indicates the type of what we are going to draw. Inside the switch 'a, we will add a case ' for each of those elements that we will draw ourselves. All the rest will be processed in the default section using a similar function of the parent class.

Let's start with the frame that surrounds our entire table. I will make a gradient frame. The upper and lower borders will be white, and the vertical borders will be drawn with a gradient: they will be white at the edges and light gray in the middle.

To do this, add the following code to the drawControl () function:

 case CE_ShapedFrame: { //  QLinearGradient gradient(0, 0, 0, opt->rect.height()); gradient.setColorAt(0, QColor(255,255,255)); gradient.setColorAt(0.5, QColor(225,225,225)); gradient.setColorAt(1, QColor(255,255,255)); QPen pen; pen.setBrush( gradient ); p->setPen(pen); //  p->drawRect( 0,0,opt->rect.width()-1, opt->rect.height()-1); } break; 


comment
So, what is happening here. First we create a linear gradient from top to bottom. Then we set the colors for the key points, which we have three: the beginning, the end and the middle. As I already said, we make the beginning and the end white, and the middle is a little grayish.

After that we create a feather and set our gradient in it. The pen in Qt draws with a brush ( QBrush ), which will be a gradient. Finally, using the drawRect () function, we draw our frame.


Now proceed to the headlines. The header area itself ( CE_Header ) does not need to be processed. We will deal with sections and empty area. With an empty area, everything is very simple, we will paint it with a monochromatic gray:

 case CE_HeaderEmptyArea: p->fillRect( opt->rect, QBrush( QColor( 233, 233, 233 ) ) ); break; 


With sections it is necessary a little more difficult. As we know, if you click on the table header, either the entire row or the entire column is highlighted (depending on where you clicked). In this case, the header changes its appearance so that the user can see the change in its state. Usually the heading is either “pressed” or tinted. We want to keep this functionality. Moreover, it would be desirable that when selecting a cell or cells, the corresponding headers are also colored.

So, add the following code:

 case CE_HeaderSection: { //  if( opt->state & State_Sunken || opt->state & State_On ) { //    p->fillRect(opt->rect, QBrush( QColor(255,170,80) )); p->setPen( QPen( QColor( 170,170,170) ) ); //   p->drawRect(opt->rect.x(), opt->rect.y(),opt->rect.width()-1,opt->rect.height()-1); } else//  { //    QLinearGradient gradient(0, 0, 0, opt->rect.height()); gradient.setSpread(QGradient::PadSpread); gradient.setColorAt(0, QColor(255,255,255)); gradient.setColorAt(1, QColor(220,220,220)); //  p->fillRect(opt->rect, QBrush( gradient )); //     gradient.setColorAt(0, QColor(230,230,230)); gradient.setColorAt(0.5, QColor(175,175,175)); gradient.setColorAt(1, QColor(230,230,230)); QPen pen; pen.setStyle(Qt::SolidLine); pen.setBrush(gradient); p->setPen(pen); //   p->drawLine( opt->rect.width() + opt->rect.x() - 1, opt->rect.y() + 3, opt->rect.width() + opt->rect.x() - 1, opt->rect.height() + opt->rect.y() - 3 ); } } 


comment
So, the section can be in two states: active and inactive. In order to determine this, we check its flags. The State_Sunken flag indicates that the section is pressed, and the State_On flag indicates that the cell belonging to the column (or row) of this section is selected. If at least one of them is set, we paint over the section with an even orange color, and also draw a whole border for it so that the orange section does not look rough against the other (not selected) light gray parts of the header.

If the section is inactive, we paint over it with a gradient from white to light gray and draw a border. I decided to draw only the vertical borders of the sections, so all I need is to draw a vertical strip to the right of each section. To make the strips not look coarse, we will draw them with a gradient almost the same as the frame of the entire table: the ends of the strips will be lighter and the middle will be darker. To do this, we reconfigure the gradient and set it in the pen that will draw the strip.


At this stage, we got a pretty nice headline. However, when selected, our cells are still painted over with a rather ugly default color. In addition, the standard focal frame also does not look very nice.

The CE_ItemViewItem attribute is responsible for drawing the cells. Add the following code to it:

 case CE_ItemViewItem: { //  ,       drawPrimitive(PE_PanelItemViewItem, opt, p, w); //       //    ,      . const QStyleOptionViewItemV4 * option = qstyleoption_cast<const QStyleOptionViewItemV4 *>(opt); if( !option ) { //  -    ,     QCommonStyle::drawControl(element, opt, p, w); return; } //       . if (option->state & QStyle::State_HasFocus) { QPen pen(QColor( 170,170,170 )); p->save(); p->setPen(pen); p->drawRect(opt->rect.x(), opt->rect.y(), opt->rect.width()-1, opt->rect.height()-1); p->restore(); } //  ,      QRect textRect = subElementRect(SE_ItemViewItemText, option, w); // ,    ""    textRect.setX( textRect.x() + 5 ); textRect.setY( textRect.y() + 5 ); if( !option->text.isEmpty() ) { // . p->drawText(textRect, option->text); } } 


comment
Here we had to do much more. First, the PE_PanelItemViewItem element is responsible for coloring the selected cells with a different color, which is drawn in the drawPrimitive () function. Therefore, we must call this function by passing the parameters we have there. After that, we convert the pointer to the options from the QStyleOption base class to the QStyleOptionViewItemV4 class we need. This is needed, among other things, to get the text and drawing area of ​​this text.

If the cell we are drawing is selected
 if (option->state & QStyle::State_HasFocus) 
then we draw a small gray frame around the whole cell.

After that we get the size of the area where the text should have been displayed, and we indent a little so that the text does not appear close to the upper left edge. Finally, we draw the text with the drawText () function.


Since, when processing cells, we call the drawPrimitive function for the PE_PanelItemViewItem element, we implement the drawing of this element so that the selected cells are painted in some more pleasant color.

By analogy with drawControl () , we add exactly the same switch to drawPrimitive () , and immediately implement the needed cell highlighting:

 switch( pe ) { case PE_PanelItemViewItem: //  ,    ,   . if (const QStyleOptionViewItemV4 *option = qstyleoption_cast<const QStyleOptionViewItemV4 *>(opt)) if (option->state & QStyle::State_Selected) { p->fillRect(option->rect, QBrush( QColor( 220,220,220,100 ))); } break; default: QCommonStyle::drawPrimitive( pe, opt, p, w); break; } 


Finally, change the font that will display the text. Add the following code to the drawItemText () function:
 painter->setPen( QPen( QColor( 30, 30, 30 ))); painter->setFont(QFont("Consolas")); painter->drawText(rect, text); 


So, we have transformed the nondescript tablet, and now it looks like this:



which in my opinion is much better than what was originally.

The advantages of this approach



Of course, creating styles for the entire application in this way is a very time consuming task. The style for one element (as in this article) can be done very quickly, but if you want to take care of all the widgets (for example, if you want to distribute your style so that other people can use it), this can take a lot of time. The description of the elements that should be drawn for each individual widget, I, for example, did not find, and I had to determine them by the method of "scientific poking". However, styling thus has a number of significant advantages.

First, these styles apply to all elements. If you have 300 tables, then you do not need to configure each one manually, the style is applied to the application in one line.

Secondly, it is convenient to change the styles created in this way. This is perhaps the best way to give the user the opportunity to change the style of the application through the settings. In addition, you can be sure that your style will look the same every time.

Sources entirely:

myStyle.h
 #ifndef MYSTYLE_H #define MYSTYLE_H #include <QCommonStyle> #include <QtGui> class myStyle : public QCommonStyle { Q_OBJECT public: explicit myStyle(); void drawControl(ControlElement element, const QStyleOption *opt, QPainter *p, const QWidget *w) const; void drawItemText(QPainter *painter, const QRect &rect, int flags, const QPalette &pal, bool enabled, const QString &text, QPalette::ColorRole textRole) const; void drawPrimitive(PrimitiveElement pe, const QStyleOption *opt, QPainter *p, const QWidget *w) const; signals: public slots: }; #endif // MYSTYLE_H 

myStyle.cpp
 #include "mystyle.h" myStyle::myStyle() : QCommonStyle() { } void myStyle::drawControl(QStyle::ControlElement element, const QStyleOption *opt, QPainter *p, const QWidget *w) const { switch( element ) { case CE_ShapedFrame: { QLinearGradient gradient(0, 0, 0, opt->rect.height()); gradient.setColorAt(0, QColor(255,255,255)); gradient.setColorAt(0.5, QColor(225,225,225)); gradient.setColorAt(1, QColor(255,255,255)); QPen pen; pen.setBrush( gradient ); p->setPen(pen); p->drawRect( 0,0,opt->rect.width()-1, opt->rect.height()-1); } break; case CE_ItemViewItem: { drawPrimitive(PE_PanelItemViewItem, opt, p, w); const QStyleOptionViewItemV4 * option = qstyleoption_cast<const QStyleOptionViewItemV4 *>(opt); if( !option ) { QCommonStyle::drawControl(element, opt, p, w); return; } if (option->state & QStyle::State_HasFocus) { QPen pen(QColor( 170,170,170 )); p->save(); p->setPen(pen); p->drawRect(opt->rect.x(), opt->rect.y(), opt->rect.width()-1, opt->rect.height()-1); p->restore(); } QRect textRect = subElementRect(SE_ItemViewItemText, option, w); textRect.setX( textRect.x() + 5 ); textRect.setY( textRect.y() + 5 ); if( !option->text.isEmpty() ) { p->drawText(textRect, option->text); } } break; case CE_Header: QCommonStyle::drawControl(element, opt, p, w); break; case CE_HeaderEmptyArea: p->fillRect( opt->rect, QBrush( QColor( 233, 233, 233 ) ) ); break; case CE_HeaderSection: { if( opt->state & State_Sunken || opt->state & State_On ) { p->fillRect(opt->rect, QBrush( QColor(255,170,80) )); p->save(); p->setPen( QPen( QColor( 170,170,170) ) ); p->drawRect(opt->rect.x(), opt->rect.y(),opt->rect.width()-1,opt->rect.height()-1); p->restore(); } else { QLinearGradient gradient(0, 0, 0, opt->rect.height()); gradient.setSpread(QGradient::PadSpread); gradient.setColorAt(0, QColor(255,255,255)); gradient.setColorAt(1, QColor(220,220,220)); p->fillRect(opt->rect, QBrush( gradient )); gradient.setColorAt(0, QColor(230,230,230)); gradient.setColorAt(0.5, QColor(175,175,175)); gradient.setColorAt(1, QColor(230,230,230)); QPen pen; pen.setStyle(Qt::SolidLine); pen.setBrush(gradient); p->setPen(pen); p->drawLine( opt->rect.width() + opt->rect.x() - 1, opt->rect.y() + 3, opt->rect.width() + opt->rect.x() - 1, opt->rect.height() + opt->rect.y() - 3 ); } } break; default: QCommonStyle::drawControl(element, opt, p, w); break; } } void myStyle::drawItemText(QPainter *painter, const QRect &rect, int flags, const QPalette &pal, bool enabled, const QString &text, QPalette::ColorRole textRole) const { painter->setPen( QPen( QColor( 30, 30, 30 ))); painter->setFont(QFont("Consolas")); painter->drawText(rect, text); } void myStyle::drawPrimitive(QStyle::PrimitiveElement pe, const QStyleOption *opt, QPainter *p, const QWidget *w) const { switch( pe ) { case PE_PanelItemViewItem: if (const QStyleOptionViewItemV4 *option = qstyleoption_cast<const QStyleOptionViewItemV4 *>(opt)) if ((option->state & QStyle::State_Selected)) p->fillRect(option->rect, QBrush( QColor( 220,220,220,100 ))); break; default: QCommonStyle::drawPrimitive( pe, opt, p, w); break; } } 

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


All Articles