Создаем стиль интерфейса Qt на примере таблицы

в 15:55, , рубрики: c++, qt, Qt Software, интерфейсы, Программирование, стили, метки: , , , ,

Как известно, Qt предлагает разработчикам практически неограниченные возможности для создания интерфейсов. В большинстве случаев, если вы используйте стандартные виджеты, внешний вид вашего приложения (и элементов его интерфейса) будет соответствовать внешнему виду вашей операционной системы. Для этого в Qt есть система так называемых стилей — классов, ответственных за отрисовку стандартных элементов интерфейса. В этой статье мы попробуем создать свой стиль и применить его к простейшему приложению для того, чтобы получить красивую таблицу на основе виджета QTableWidget.

Теоретическая часть

Итак, начнем с теории. В Qt существует абстрактный класс QStyle, ответственный, как не сложно догадаться, за стилизацию приложения. От него унаследовано некоторое количество классов (QWindowStyle, QMacStyle и т.д.), которые представляют собой стили, эмулирующие стандартный внешний вид операционной системы. Мы будем делать свой стиль для приложения по их образу и подобию. Qt требует, чтобы пользовательские стили были унаследованы не напрямую от QStyle, а от одного из его классов-наследников. Документация рекомендует выбрать наиболее близкий к желаемому системный стиль, и изменить его «под себя». Мы же будем наследовать наш класс от не менее абстрактного, чем QStyle, класса QCommonStyle.

Как же происходит рисование элементов внутри стиля? Класс QStyle имеет набор функций, названия которых начинаются со слова draw:

  • drawComplexControl()
  • drawControl()
  • drawItemPixmap()
  • drawItemText()
  • drawPrimitive()

Каждая из них делает свою небольшую часть работы по отрисовке элементов.

drawComplexControl() используется для рисования составных виджетов, то есть таких, которые содержат в самих себе несколько других виджетов.
В качестве примера можно привести QSpinBox, который, как несложно догадаться, реализует обычный SpinBox и состоит из поля ввода и двух маленьких кнопок:
Создаем стиль интерфейса Qt на примере таблицы

drawControl() рисует стандартные виджеты вроде кнопок и полей ввода.

drawPrimitive() рисует так называемые примитивные элементы, например, рамки.

drawItemPixmap() и drawItemText() имеют вполне говорящие названия и делают именно то, что вы могли бы от них ожидать. Детальные описание всех функций, а также их аргументов, можно очень легко найти в документации Qt, поэтому я на этом останавливаться не буду.

В нашем примере, мы будем делать стиль для элемента QTableWidget, который представляет собой обыкновенную таблицу. Стили для остальных элементов делаются абсолютно аналогично.

Начинаем

Первым делом, создадим новый класс С++. При создании класса, Qt услужливо предлагает нам написать его название, а так же название класса, от которого мы хотим наследовать. Назовем его для простоты myStyle, и укажем наследование от QCommonStyle. После этого, Qt создаст нам пару файлов (.h и .cpp), в которых, для начала, мы хотим получить примерно такую заготовку:

myStyle.h

#include <QCommonStyle>

class myStyle : public QCommonStyle
{
	Q_OBJECT
public:
	explicit myStyle();
	
signals:
	
public slots:
	
};

myStyle.cpp

#include "mystyle.h"

myStyle::myStyle() :
	QCommonStyle()
{
}

Наша функция main() будет выглядеть следующим образом:

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();
}

Как видите, все что мы делаем — это устанавливаем наш стиль, создаем виджет и отображаем его. На данном этапе у нас есть только пустой стиль, унаследованный от QCommonStyle, поэтому наша таблица будет выглядеть… ну, скажем, не очень привлекательно:

Создаем стиль интерфейса Qt на примере таблицы

Рассмотрим подробнее из чего состоит таблица.
Создаем стиль интерфейса Qt на примере таблицы

Структура, в общем, довольно простая и понятная. Стоит разве что остановиться на секунду на заголовках. В Qt разделяют два вида заголовков: горизонтальный (тот, который сверху), и вертикальный (тот, который слева). Под «Областью заголовка» я подразумеваю всю область, на которой впоследствии будет отображен заголовок. Секция — это каждая конкретная ячейка заголовка. Незанятая область — это та часть заголовка, на которой секций нет (это случается, когда суммарный размер всех секций меньше размера таблицы).

Итак, вооружившись этими знаниями, можно стилизовать каждый элемент. Начнем с добавления в наш класс функций

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;

Начнем с функции drawControl(). Добавим в реализацию код:

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

Ключевой атрибут этой функции — это element, который указывает на тип того, что мы будем рисовать. Внутри switch'a мы будем добавлять по case'у для каждого из тех элементов, которые мы будем рисовать сами. Все остальные будут обрабатываться в default-секции с помощью аналогичной функции родительского класса.

Начнем с рамки, которая окружает нашу таблицу целиком. Я буду делать градиентную рамку. Верхняя и нижняя границы будут белыми, а вертикальные границы будут нарисованы градиентом: они будут белыми по краям и светло-серыми в середине.

Для этого добавим в функцию drawControl() следующий код:

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;
комментарий

Итак, что же здесь происходит. Сначала мы создаем линейный градиент, направленный сверху вниз. Затем мы задаем цвета для ключевых точек, которых у нас три: начало, конец и середина. Как я уже говорил, начало и конец мы делаем белыми, а середину чуть сероватой.

После этого мы создаем перо и устанавливаем в него наш градиент. Перо в Qt рисует с помощью кисти (QBrush), в роли которой будет выступать градиент. Наконец, функцией drawRect() мы рисуем нашу рамку.

Теперь приступим к заголовкам. Саму область заголовка (CE_Header) нам обрабатывать не нужно. Мы займемся секциями и пустой областью. С пустой областью все очень просто, мы закрасим ее однотонным серым цветом:

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->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 );
	}
}
комментарий

Итак, секция может быть в двух состояниях: активном и неактивном. Для того, чтобы определить это, мы проверяем ее флаги. Флаг State_Sunken показывает, что секция нажата, а флаг State_On показывает, что выбрана ячейка, принадлежащая столбцу (или строке) этой секции. Если хотя бы один из них установлен, мы закрашиваем секцию ровным оранжевым цветом, а также рисуем для нее целиковую границу для того, чтобы оранжевая секция не смотрелась грубо на фоне других (не выделенных) светло-серых частей заголовка.

Если секция неактивна, мы закрашиваем ее градиентом от белого до светло-серого цвета и рисуем границу. Я решил рисовать только вертикальные границы секций, поэтому все что мне нужно — это изобразить вертикальную полоску справа от каждой секции. Чтобы полоски не смотрелись грубо, мы нарисуем их градиентом почти так же, как и рамку всей таблицы: концы полосок будут более светлыми, а середина — более темной. Для этого мы перенастраиваем градиент и устанавливаем его в перо, которым будет рисоваться полоса.

На данном этапе мы получили довольно симпатичный заголовок. Однако при выделении наши ячейки по-прежнему закрашиваются довольно некрасивым цветом по умолчанию. К тому же, стандартная фокусная рамка тоже выглядит не слишком красиво.

За отрисовку ячеек отвечает атрибут CE_ItemViewItem. Добавим в него следующий код:

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);
	}
}
комментарий

Здесь нам пришлось сделать гораздо больше. Во-первых, за раскраску выделенных ячеек другим цветом отвечает элемент PE_PanelItemViewItem, который рисуется в функции drawPrimitive(). Поэтому мы должны вызвать эту функцию, передав туда имеющиеся у нас параметры. После этого мы преобразовываем указатель на опции с базового класса QStyleOption в нужный нам класс QStyleOptionViewItemV4. Это нужно, помимо прочего, для получения текста и области рисования этого текста.

Если ячейка, которую мы рисуем, является выделенной

if (option->state & QStyle::State_HasFocus)

то мы рисуем небольшую серую рамку вокруг всей ячейки.

После этого мы получаем размер области, на которой должен бын выведен текст, и делаем небольшой отступ, чтобы текст не появился вплотную к левому верхнему краю. Наконец, мы рисуем текст функцией drawText().

Поскольку при обработки ячеек мы вызываем функцию drawPrimitive для элемента PE_PanelItemViewItem, реализуем отрисовку этого элмента, чтобы выделенные ячейки раскрашивались в какой-нибудь более приятный цвет.

По аналогии с drawControl(), добавим в drawPrimitive() точно такой же switch, и сразу реализуем нужную нам подсветку ячеек:

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;
}

Наконец, изменим шрифт, которым будет выводиться текст. Добавим в функцию drawItemText() следующий код:

painter->setPen( QPen( QColor( 30, 30, 30 )));
painter->setFont(QFont("Consolas"));
painter->drawText(rect, text);

Итак, мы преобразили невзрачную табличку, и теперь она выглядит так:

Создаем стиль интерфейса Qt на примере таблицы

что на мой взгляд гораздо лучше того, что было изначально.

Преимущества данного подхода

Конечно, создавать стили для всего приложения таким образом — весьма трудоемкая задача. Стиль для одного элемента (как в этой статье) можно сделать очень быстро, но если вы хотите позаботится о всех виджетах (например если вы хотите распространять свой стиль, чтобы другие люди могли им воспользоваться), это может отнять достаточно много времени. Описание элементов, которые должны быть нарисованы для каждого отдельно взятого виджета, я, к примеру, не нашел, и мне пришлось определять их методом «научного тыка». Однако у стилизации таким образом есть ряд существенных преимуществ.

Во-первых, эти стили применяются ко всем элементам. Если у вас есть 300таблиц, то вам не нужно настраивать каждую вручную, стиль к приложению применяется в одну строку.

Во-вторых, стили, созданные таким образом, удобно менять. Это, пожалуй, лучший способ предоставить пользователю возможность изменить стиль приложения через настройки. К тому же, вы можете быть точно уверены, что созданный вами стиль будет каждый раз выглядеть одинаково.

Исходники целиком:

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;
	}
}

Автор: Singerofthefall

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js