Ribbon? Это просто! или Работаем с каскадными таблицами стилей (CSS) в Qt

    Некоторое время назад в одном из обсуждений я упомянул о том, что контрол «a-la ribbon» (который был использован в MS Office 2007 для организации меню) легко и непринуждённо реализуется средствами Qt.

    Я не хочу спорить о том, удобен ribbon или нет (сам я больше склоняюсь ко второму мнению). Но на его примере можно отлично раскрыть возможности каскадных таблиц стилей для Qt (которые были представлены в Qt 4.2), чем я и займусь. Сразу прошу прощения: я не дизайнер, поэтому с точки зрения эстетики мой QRibbon скорее всего не дотянет до своего собрата от МС, но дизайнеры в МС, полагаю, в своей области превосходят меня на 2 головы, да и человеко-часов, думаю, там было затрачено в слегка побольше. Я же всего лишь демонстрирую общий принцип и базовые возможности.

    Так как я сейчас изучаю язык Python, то для демонстрации был выбран именно он, но для C++ всё делается абсолютно так же. Заранее прошу прощения: Python я только-только изучаю, поэтому код может быть полон корявостей, так что прошу больно не пинать :)

    Итак, начнём!

    Создаём скелет


    Возьмём стандартный виджет QTabWidget в качестве основы, т.к. он сам тут напрашивается. Страницы будем делать из виджетов, имеющих горизонтальный лэйаут (QHBoxLayout), последний элемент которого — QSpacerItem (не знаю, как назвать его по-русски, но это такая невидимая фиговина, которая заполняет свободное пространство :)).

    #!/usr/bin/python
    
    from PyQt4 import Qt, uic
    import sys
    
    # Class that represents a ribbon page
    class Tab(Qt.QWidget) :
    	def __init__(self) :
    		Qt.QWidget.__init__(self)
    
    		# create the spacer and the layout and set it as a widget main layout
    		self.spacer = Qt.QSpacerItem(10, 10, Qt.QSizePolicy.Expanding)
    		self.layout = Qt.QHBoxLayout()
    		self.layout.addSpacerItem(self.spacer)
    		self.setLayout(self.layout)
    
    # Class that represents a ribbon
    class QRibbon(Qt.QTabWidget) :
    	def __init__ (self) :
    		Qt.QTabWidget.__init__(self)
    		self.resize(450, 170)
    
    		# storage for tabs
    		self.tabs = dict()
    
    	def addTab(self, tabName) :
    		# check if tab with this name already exists
    		if not tabName in self.tabs :
    			newTab = Tab()
    			self.tabs[tabName] = newTab
    			Qt.QTabWidget.addTab(self, newTab, tabName)
    
    	def addPane(self, tabName, pane) :
    		# check if tab with this name exists
    		if tabName in self.tabs :
    			tab = self.tabs[tabName]
    			tab.layout.insertWidget(tab.layout.count() - 1, pane)
    
    if __name__ == "__main__" :
    	app = Qt.QApplication(sys.argv)
    	ribbon = QRibbon()
    
    	# add a couple of tabs
    	ribbon.addTab(ribbon.tr('Home'))
    	ribbon.addTab(ribbon.tr('Insert'))
    	
    	ribbon.show()
    	
    	app.exec_()
    


    Теперь открываем QtDesigner и рисуем панели, которые мы будем добавлять на «страницы» нашего риббона. Воспроизводить панель попиксельно в мои планы не входит, я просто покажу, как можно обыграть разные кнопки и раскрасить их с помощью таблицы стилей. Первую панель сделаем такой:

    На ней будет большая кнопка Paste и 2 кнопки поменьше: Cut и Copy.

    Вторая панель будет панелью шрифтов. Разместим на ней комбо-боксы для выбора семейства шрифта и его размера, а также «западающие» кнопки Bold, Italic и Underline:


    Каждую из панелей мы сделаем отдельным виджетом (отдельным классом). Этот класс будет загружать свой ui-файл (который мы рисуем в дизайнере) и обрабатывать сигналы от контролов (эту часть я пропущу, как не относящуюся непосредственно к нашей теме).

    Итак, добавим базовый класс панели (пока что он не несёт никакой нагрузки, но в дальнейшем через него можно будет задавать панелям некоторые общие свойства, как, например, стиль) и 2 класса панелей:

    class Pane(Qt.QWidget) :
    	def __init__(self) :
    		Qt.QWidget.__init__(self)
    
    class ClipboardPane(Pane) :
    	def __init__(self) :
    		Pane.__init__(self)
    		uiClass, qtBaseClass = uic.loadUiType('edit.ui')
    		self.ui = uiClass()
    		self.ui.setupUi(self)
    
    		# set icons for buttons
    		self.ui.pasteBtn.setIcon(Qt.QIcon('paste.png'))
    		self.ui.pasteBtn.setIconSize(Qt.QSize(48, 48))
    		self.ui.cutBtn.setIcon(Qt.QIcon('cut.png'))
    		self.ui.copyBtn.setIcon(Qt.QIcon('copy.png'))
    
    class FontPane(Pane) :
    	def __init__(self) :
    		Pane.__init__(self)
    		uiClass, qtBaseClass = uic.loadUiType('font.ui')
    		self.ui = uiClass()
    		self.ui.setupUi(self)
    


    Ну и в главной функции добавим наши панели на страницу «Home»:

    if __name__ == "__main__" :
    	...
    	# add two panes to "Home" page
    	ribbon.addPane(ribbon.tr('Home'), ClipboardPane())
    	ribbon.addPane(ribbon.tr('Home'), FontPane())
    	...
    	app.exec_()
    


    В результате получаем вот такое чудо:


    «Стоп!» — думаю себе, — «что-то тут не так»....


    Очертания вроде проглядывают, но риббон явно «не торт»… Спокойствие, только спокойствие, подходим к самому интересному!
    Итак, создадим вспомогательную функцию, читающую и возвращающую содержимое файла (будем использовать её для чтения таблиц стилей из файлов) и добавим в конструкторы классов QRibbon и Pane строчку, читающую соответствующий файл стиля:
    def readStyleSheet(fileName) :
    	css = Qt.QString()
    	file = Qt.QFile(fileName)
    	if file.open(Qt.QIODevice.ReadOnly) :
    		css = Qt.QString(file.readAll())
    		file.close()
    	return css
    
    class QRibbon(Qt.QTabWidget) :
    	def __init__ (self) :
    		...
    		self.setStyleSheet(readStyleSheet('qribbon.qss'))
    
    class FontPane(Pane) :
    	def __init__(self) :
    		...
    		self.setStyleSheet(readStyleSheet('page.qss'))
    


    Думаю, будет лучше, если кнопки Cut, Copy и Paste сделать плоскими. Идём в QtDesigner и выставляем им соответствующий атрибут. Ну а теперь займёмся самым интересным: создаём CSS, которая преобразит наш риббон до неузнаваемости!

    «Раскрась сам»


    Сначала займёмся таблицей стилей для основного окна (qribbon.qss). Синтаксис 1-в-1 скопирован с CSS, поэтому я буду давать лишь общие комментарии, ибо код говорит сам за себя:

    Установим цвет фона виджетов:
    QWidget {
    	background-color: #d0d9f0;
    }


    Сделаем верхнюю границу панелей более симпатичной:
    QTabWidget::pane {
    	border-top: 2px solid qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #90a0e0, stop:1.0 #303070);
    }

    Тут требуется небольшое пояснение. При помощи первых 4-х параметров задаётся направление: из левого-верхнего угла в левый нижний, т.е. вертикально вниз. Далее идут контрольные точки (принимают значения от 0 до 1) и их цвета. В данном случае у нас простой градиент: идёт от нуля до единицы без дополнительных промежуточных точек.

    Зададим небольшой отступ для табов:
    QTabWidget::tab-bar {
    	left: 30px;
    }


    Зададим отступы для каждого из табов в таб-баре, а также изменим цвет текста и сделаем табам круглые уголки:
    QTabBar::tab {
    	padding: 5px 15px 3px 15px;
    	margin-top: 10px;
    	color: #303070;
    	border-top-left-radius: 4px;
    	border-top-right-radius: 4px;
    }


    Изменим цвет таба при наведении курсора:
    QTabBar::tab:hover {
    	background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
    		stop: 0 #fffaff, stop: 0.4 #fff0c0,
    		stop: 0.5 #fff0c0, stop: 1.0 #d0d9f0);
    	border: 1px solid #a4a063;
    }

    Здесь градиент чуть сложнее: он имеет 2 промежуточные «контрольные точки»: 0.4 и 0.5.

    Зададим неактивному табу стиль нижней границы, совпадающий с верхней границей панелей (см. выше):
    QTabBar::tab:!selected {
    	border-bottom: 2px solid qlineargradient(x1:0, y1:0, x2:0, y2:1, stop:0 #90a0e0, stop:1 #303070);
    }


    Изменим стиль активного таба и уберём его нижнюю границу:
    QTabBar::tab:selected {
    	background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
    		stop: 0 #f0f0ff, stop: 0.4 #f4f4ff,
    		stop: 0.5 #e7e7ff, stop: 1.0 #d0d9f0);
    	border: 1px solid #808090;
    	border-bottom: solid 0px;
    }


    Слегка изменим стиль кнопок:
    QPushButton {
    	background-color: 
    		qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #f6fcff, stop: 1.0 #a0b0d0);
    	border: 1px solid #a0a0b0;
    	border-radius: 3px;
    }


    Изменим цвет «утопленных» кнопок:
    QPushButton:checked {
    background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #f0eeaa, stop: 1 #eeaa88);
    }


    Спрячем «плоские» кнопки. Пусть будет видно только содержимое:
    QPushButton:flat {
    	border: none;
    	background: none;
    }


    «Оживим» кнопки в момент нажатия:
    QPushButton:pressed {
    	background-color: #e0e3ff;
    }


    Посмотрим что получилось:


    Хм, вроде становится получше, правда же? Осталось придать немного шарма панелькам с контролами. Создадим файл page.qss:

    
    QFrame {
    	background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #f6fcff, stop: 1.0 #a0b0d0);
    	border: 1px solid #8080a0;
    	border-top-left-radius: 4px;
    	border-top-right-radius: 4px;
    }
    
    QLabel {
    	color: #303070;
    	background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #c6cccf, stop: 1.0 #90a0b0);
    	border-top-left-radius: 0px;
    	border-top-right-radius: 0px;
    	border-bottom-left-radius: 4px;
    	border-bottom-right-radius: 4px;
    	border-top: 0px;
    }


    Вот и всё.
    Теперь вспомним то, как выглядел наш риббон до того, как мы применили стили и сравниваем с тем, как он выглядит теперь:



    Неплохо, правда? :) (комбо-боксы, разумеется, тоже кастомизуются, но это я оставлю в качестве домашнего задания)

    За подробной документацией по использованию стилей в Qt можно обращаться по адресу doc.trolltech.com/4.2/stylesheet.html
    Единственное пожелаение тем, кто воспользуется данным вводным руководством и будет использовать стили в своих программах: помещайте стили во внешние файлы. В этом случае даже если вы слегка переусердствуете — пользователь сможет потюнить UI без необходимости разбирать программный код.

    Архив с исходниками тут.

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 43

    • UFO just landed and posted this here
        +6
        Цитирую: «Сразу прошу прощения: я не дизайнер». Ребят, я сам люблю правду матку в глаза рубить, но не до такой же степени. Тут даже у меня, с моей репутацией грубияна (прошлое грузчика не спрячешь даже под смокингом), хватило бы такта промолчать о таких несущественных мелочах :) По крайней мере не в первом комменте об этом писать, который к тому же усердно плюсуется.

        Цитирую дальше: «Заранее прошу прощения: Python я только-только изучаю». Поскольку дальше ещё и реализацию неполную ругают. И тоже плюсуется. Нет, я понимаю, если бы человек строил из себя кого-то большего, чем он есть. Но ведь совсем наоборот :)

        Кстати, неоднократно замечал — зачастую о новом лучше всего рассказывает новичок, который делится наисвежайшим опытом. Потому что старички уже забыли, что им в самом начале было непонятно, каким путём они дошли к вершинам, впечатления притупились…

        Поэтому не прессуйте зазря. Ещё одна цитата: «прошу больно не пинать». Нет, я понимаю, что вы (все) небольно пинаете, от доброты душевной, для пользы дела… Но может быть совсем не стоит? А? :)

        Особенно что потоков благодарности я не заметил, и на этом фоне любой пинок выделяется. Не пресекайте на корню благое начинание как этого автора, так и других потенциальных, которые сейчас ещё не писатели, а читатели.
        • UFO just landed and posted this here
            +2
            Судя по последней фразе, вы ещё очень молоды. Это не «обоснованная критика», просто констатация факта (моё предположение подтвердилось профайлом).

            Надеюсь, на своей серебрянной свадьбе вы не скажете супруге про слегка пересоленный праздничный ужин (в рамках «обоснованной критики»), а с наичестнейшими глазами соврёте, что всё было божественно, и сами в это поверите :)
              +1
              Как я понял, человек делает не дизайн, а показывает какие возможности есть у Ribbon.
                +1
                А еще точнее не у Ribbon, а у QT+python+qss.
                  +1
                  Ну да, спасибо.

                  А вы тут про дизайн лепите, когда тема другая.
                    +1
                    А ещё точнее, у Qt+qss, Python не играет роли, на С++ всё будет точно так же.
                • UFO just landed and posted this here
                    0
                    Констатация факта — это я про ваш возраст. Я сначала предположил, что это юношеский максимализм (ваша фраза «обоснованная критика не может быть во вред»), а потом проверил через профайл, и убедился, что моё предположение подтвердилось, т.е. является фактом (профайл ведь соответствует действительности? — риторический вопрос).

                    Да я что, и не думал сердиться, я даже из минусов тут ваши комменты вытаскивал по мере сил, когда их туда загнали. Поскольку ничего злого в них действительно нет. Но… Просто постепенно к вам будет приходить понимание (я искренне надеюсь), что и обоснованная критика, и правда-матка — всё хорошо в меру и к месту, с учётом контекста и последствий. Типа так, вот :)
                    • UFO just landed and posted this here
              +1
              >Лучше бы поменять картинки «стало» и «было» для наглядности.

              Да, наверное так будет лучше. Поменял.
              0
              Довольно-таки познавательно. Спасибо :)
                0
                Начиная с Qt 4.3 каскадные таблицы стилей можно применять и в Mac OS X.
                  0
                  Не дочитал.
                  Таки не поддерживается вообще на Mac OS X.
                    0
                    Warning: Qt style sheets are currently not supported for Mac OS X and custom QStyle subclasses. We plan to address this in some future release.

                    в QMacStyle не поддерживается. Можно попробовать запустить программу с параметрами "-style plastique", например. Точно сказать не могу, мака нет под рукой.
                      0
                      Ммм… прочитал доку к 4.3, а сам скопипастил кусок из 4.4
                      В 4.3 по-другому несколько пишут:
                      Warning: Qt style sheets are currently not supported for QMacStyle (the default style on Mac OS X) and custom QStyle subclasses. We plan to address this in some future release.


                      В общем проверить нужно с не-мак стилем.
                  0
                  а можно ссылку на действующий пример бросить, так имхо будет нагляднее.
                  • UFO just landed and posted this here
                      +1
                      Как демонстрация стилей Qt — хорошо.

                      Если же рассматривать конкретно риббон, то до нормальной реализации еще как до Луны.
                      Не хватает таких замечательных вещей, как автоматический лейаут элементов в группах, автоматическое изменение их размера при необходимости (когда не помещаются), дроп-даун галерей, сворачивания, меню, Quick Access тулбара, etc.

                      На самом деле, на сайте MS есть целый мануал по проектированию ribbon-интерфейса. Так что если всерьез задумаетесь реализовывать, рекомендую ознакомиться :)
                        0
                        Не дадите ссылочку? Последнее время работаю над ribbon интерфейсом для одной программы а информации по нему кот наплакал.
                          0
                          www.microsoftio.com/officeUI/evaluation/2007_Microsoft_Office_Fluent_UI_Design_Guidelines_-_Evaluation.pdf
                          Уверены, что хотите с нуля это создавать? :)
                          Не лучше ли воспользоваться готовым решением?
                            0
                            Уже пользуемся dev.express. Но эти компоненты похоже не на 100% совпадают со спецификацией (замечено путем сравнения с office 2007). Во и хочется почитать видение самого микрософта, чтобы определиться с отличиями.
                              0
                              У меня есть вопрос, возможно он может показаться несколько странным:
                              1) Насколько легально использовать концепцию интерфейса Ribbon?
                              2) И насколько легально «воспользоваться готовым решением», т.е. документацией Микрософта на эту тему?
                                0
                                1) Вполне легально, иначе бы не было кучи коммерческих контролов.

                                2) Вообще под «готовым решением» я предполагал соответствующий набор классов, а не просто документацию :) Тоже вполне легально.
                                Например, сам MS предоставляет бесплатные Ribbon контролы для MFC (в составе MSVC 2008 Feature Pack) и WPF (пока в стадии беты, но уже вполне юзабелен).
                                Бесплатный Ribbon для Windows.Forms есть как сторонняя разработка на CodeProject-е.
                                Помимо этого, как уже было сказано, есть много коммерческих решений.
                                  0
                                  Ответ на мой вопрос есть у Микрософта Office UI Licensing Program. Т.е. использовать в своих продуктах можно, но для этого нужно получить соответствующую лицензию (бесплатную). Но в лицензии сказано, что компания может и не дать такую лицензия, для разработок с конкурирующим функционалом.
                                  Судя по The Gotcha on that MS License on the Office 2007 «Giveaway» эта лицензия не совместима например, с GPL. Микрософт подала заявку на патентование этой технологии, и в случае удачи, маловероятно, что можно будет скажем написать свою библиотеку, реализующего схожий функционал. Однако, патент могут и не дать, т.к. схожие черты имеются и других, более ранних свободных продуктах KDE to sue MS over Ribbon GUI?.
                                  Из всего этого, имхо, использовать для своей свободной разработки «MS Office Fluent UI Design Guidelines» нельзя.
                                  Может быть кто разрешит мои сомнения?
                                  • UFO just landed and posted this here
                            0
                            >автоматическое изменение их размера при необходимости (когда не помещаются)
                            Возможно неверно понял, но динамическая компоновка виджетов (а, в частности — изменение их размера) в линуксе всегда и так работала.
                              +1
                              Там не просто подгонка размера, там различные стратегии по переключению режима в зависимости от размера.
                              То есть вот были, например, три большие кнопки с текстом. Потом им стало не хватать места, они делаются тремя маленькими с текстом и лейаутятся в столбик. Потом, когда места снова не хватает, может отключаться текст, и остаются только иконки.
                              При это можно указывать, какие кнопки можно, а какие нельзя так изменять, etc.
                              0
                              >Как демонстрация стилей Qt — хорошо.

                              В этом и заключалась цель.

                              >Если же рассматривать конкретно риббон, то до нормальной реализации еще как до Луны.

                              Я даже обработчики событий не цеплял, какой может быть разговор о «нормальной реализации»? И потратил на это дело 2 человеко-вечера. Будете продолжать сравнивать? :)
                                0
                                Не буду.
                                Раз цель достигнута, то смысла в этом нет. :)
                              +5
                              Для Java Swing тоже есть Ribbon — выглядит симпатично:
                                0
                                Под Делфи тоже видел готовые коммерческие компоненты
                                  0
                                  Developer Express — выпускает и под дельфи и под еще несколько сред.
                                  –5
                                  Вауу!!!
                                    0
                                    Спасибо большое за статью, обязательно воспользуюсь ею ;)
                                      0
                                      Благодарю за статью!
                                        +1
                                        Спасибо! По-моему у вас отличный код :) Чувствуется опыт C++ :)
                                        Разве что я делаю немного другой импорт:
                                        from PyQt4.QtCore import *
                                        from PyQt4.QtGui import *

                                        И тогда не надо лишний раз использовать Qt.* (что удобно, так как имена всех классов начинаются на Q). Сам Qt при этом тоже будет доступен (чтобы брать из него константы).
                                          +1
                                          >По-моему у вас отличный код :) Чувствуется опыт C++ :)

                                          Чего я действительно опасаюсь, так это как раз того, что слишком сильно «будет чувствоваться С++» :)))
                                          Как там говорилось? «Писать на Фортране можно на любом языке». Что-то вроде этого %)

                                          За совет по импортам — спасибо!
                                          0
                                          Спасибо! Плюс вам в карму, а пост — в избранное. Сам я C++ разработчик, но возможности CSS в Qt как-то совершенно упускал из виду. Буду пользоваться.
                                            0
                                            Картинки похерелись :( ФРР
                                              0
                                              Вечером постараюсь поправить.
                                                +1
                                                Поправил.

                                              Only users with full accounts can post comments. Log in, please.